Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Architecture Review: ComboMultiplier device (new type) #1324

Closed
avanwinkle opened this issue Apr 1, 2019 · 4 comments
Closed

Architecture Review: ComboMultiplier device (new type) #1324

avanwinkle opened this issue Apr 1, 2019 · 4 comments

Comments

@avanwinkle
Copy link
Collaborator

avanwinkle commented Apr 1, 2019

I'm looking into replicating a scoring feature I've seen in the wild, and haven't found a good way to do it with native MPF functionality. The feature is a global per-shot multiplier that applies to any scoring awards triggered by that shot.

As seen in a number of recent Stern titles, each lane has a starting 1X multiplier. Hitting a lane increases the multiplier (some titles increase the hit lane, other titles increase all other lanes), up to a maximum amount of 5-8X. After a timeout, all lanes reset back to 1X. Any scoring triggered by a lane—whether a hurryup during a mode, collecting a mystery award, or building a jackpot value during multiball—any number awarded by a lane is multiplied by its multiplier.

Proposal: a ComboMultiplier device type

My thought would be to create a new device type that can configure an arbitrary number of (stackable) multipliers. In order for the multipliers to be universal, they would have to be attached to Switches instead of Shots. My initial idea is that switches attached to ComboMultipliers would track their own current multiplier(s) and pass those in their active and inactive events. Shots and ShotGroups that triggered hits from those switches would also pass the multiplier value(s) in the hit events.

Example Config

combo_multipliers:
  hyperdrive:
    # Each of these switches is part of the combo group
    switches: s_left_orbit, s_inner_loop, s_left_lane, s_right_lane, s_right_orbit
    # All switches start and reset to this multiplier
    starting_value: 1.0
    # When a switch is hit, how much does *it's* multiplier increase?
    increment_hit: 0.0
    # When a switch is hit, how much does the *previously hit switch's* multiplier increase?
    increment_previous: 0.5
    # When a switch is hit, how much do all the other (not it, not prev) switches increase?
    increment_others: 1.0
    # If no switch is hit for a certain time, reset them all
    timeout: 10s
    # Force a reset when need be
    reset_events: mode_base_started

shot_groups:
  some_modes_group:
    shots: sh_left_orbit, sh_inner_loop, sh_left_lane, sh_right_lane, sh_right_orbit

variable_player:
  some_modes_group_lit_hit:
    score:
      int: 3000 * combo_multiplier_hyperdrive

In this scenario, when the ComboMultiplier device initializes it finds all of the Switch devices and attaches itself. The Switch has a public method to attach a combo multiplier, which causes the Switch to define self.combo_multipliers = {} (if None) and sets self.combo_multipliers[(combo_multiplier_name)] to the default value.

Option 1: Coordination via Events

ComboMultiplier tracking hits
The ComboMultiplier listens to the hit events of all its switches and when one is hit, it broadcasts a combo_multiplier_(name)_increment event with kwargs for each switch and its increment amount. The ComboMultiplier internally tracks the current and previous switches, so it can set increment values per-switch.

Switches increasing their multiplier
Switches that were attached to the ComboMultiplier are listening for the combo_multiplier_(name)_increment event, and look to see if their switch name is one of the kwargs. If so, the switch increases its internal self.combo_multipliers[(name)] value by the kwarg value.

Option 2: Coordination via Callback

ComboMultiplier tracking hits
When the ComboMultiplier attaches itself to a Switch, it provides a callback method that the Switch stores. When the Switch's self.hit() event is called, the switch calls any ComboMultiplier callbacks it's attached to and passes itself as the argument. The ComboMultiplier tracks the identity of the hit Switch and increments accordingly.

Switches increasing their multiplier
Switches have a public method to increment/reset their multiplier value for a given ComboMultiplier name. Each ComboMultiplier device has a list containing the instances of its Switches. When a hit occurs, the ComboMultiplier iterates through each Switch in the list and calls its increment method, passing the multiplier's name and the increment/reset value for that particular switch.

Example Event Sequence

  1. Mode starts. The ComboMultiplier's reset_events triggers a reset call. All Switches are set to the starting_value.
  2. A Switch (s_left_orbit) is activated. It posts
    s_left_orbit_active Args={ combo_multiplier_hyperdrive: 1.0 }
  3. A Shot (sh_orbit) registers a hit from the switch. It posts
    sh_orbit_hit Args={ combo_multiplier_hyperdrive: 1.0 }
  4. A VariablePlayer awards points for the shot (or shot group) and includes combo_multiplier_hyperdrive in the dynamic value.
  5. The ComboMultiplier tracks that the activated switch was s_left_orbit. It passes no increment to that switch, but passes an increment of 1.0 to all the other switches in its configuration.
  6. The other four Switches set their internal combo multiplier value to 2.0
  7. A different switch (s_right_ramp) is activated. It posts
    s_right_ramp_active Args={ combo_multiplier_hyperdrive: 2.0 }
  8. A shot (sh_right_ramp) registers a hit from the switch. It posts
    sh_right_ramp_hit Args={ combo_multiplier_hyperdrive: 2.0 }
  9. A VariablePlayer awards 2X points for the shot
  10. The ComboMultiplier tracks that the hit switch was s_right_ramp. It passes no increment to that switch. It passes a 0.5 increment to s_left_orbit and a 1.0 increment to the other three switches.
  11. Ten seconds pass without a switch hit. The ComboMultiplier passes a reset value of 1.0 to all the switches in its configuration, and clears the "previous" switch value.
  12. All the Switches set their internal combo multiplier value to 1.0

Questions

  • Is there an existing mechanism or combo in MPF that will provide this functionality?
  • Is there overhead to passing a possibly large payload of kwargs on switch/shot/group hit events?
  • If using the Events approach, is this too many events?
  • If using the Callback approach, is it too obscure or behind-the-scenes?
  • Is there a better/smarter way to solve this?
@jabdoa2
Copy link
Collaborator

jabdoa2 commented Apr 2, 2019

Sounds like a good idea. I like it. The logic for when which counter increases seems to be complex. Is that a common thing? It looks like there is one counter per switch.

  • Is there an existing mechanism or combo in MPF that will provide this functionality?

I guess you build the same with counters and timers but it seems like a common element so this makes sense to me.

  • Is there overhead to passing a possibly large payload of kwargs on switch/shot/group hit events?

Not much. It is a dict which is passed by reference so assembling the dict should be most of the cost.

  • If using the Events approach, is this too many events?

Would be probably fine for most users. It might limit the number of switch hits on embedded/low-power devices such as the RPi Zero.

  • If using the Callback approach, is it too obscure or behind-the-scenes?

This would make it a core feature in MPF. Does it have to be that deeply integrated?

  • Is there a better/smarter way to solve this?

What about this: Instead of passing the combo value as argument you could also reference the device directly like this:

variable_player:
  some_modes_group_lit_hit:
    score:
      int: 3000 * device.combo_multiplierers.combo_multiplier_hyperdrive.value["your_switch_or_event"]

The syntax would become a bit more explicit but it would allow integration via events. It would even allow combos on non-switch events. Instead of device.combo_multiplierers.combo_multiplier_hyperdrive.value["your_switch_or_event"] we could also make this device.combo_multiplierers.combo_multiplier_hyperdrive.your_switch_or_event.

Would that work? It would be totally decoupled and maybe even a bit more universal. What do you think?

@avanwinkle
Copy link
Collaborator Author

Thanks for the feedback @jabdoa2 !

I made many attempts to try and access the multiplier value through the device at the time of scoring, but there were two issues that I couldn't get past.

  1. If the multiplier values change when a switch is hit, there's a possible race condition between increasing the value and multiplying the score against the previous value. A priority setting could help, but it would be difficult to ensure that every variable_player in every mode had the correct priority.

  2. By the time the variable_player gets to the score, we've lost all information about which switch was hit. If the score comes off a counter hit or a shot_group hit (like >90% of my scores do), there's no way to know whether it was the left_orbit in the shot group or the right_ramp in the shot group that caused the hit. If the left_orbit has a 1.5X multiplier and the right_ramp has a 6X multiplier... how can that be added?

These two challenges led me to the belief that event args were the way to go, because those could be captured by the original switch at the time of the hit, and passed along to the shots, shot_groups, logic_blocks, and event_players that might end up triggering a variable_player event.

Ideas?

@jabdoa2
Copy link
Collaborator

jabdoa2 commented Apr 4, 2019

Understood. I guess we should use the event approach (option 1). Args overhead should not be high. Additionally, we could later add the same for non-switch hits in case someone wants this.

@avanwinkle
Copy link
Collaborator Author

Closing this issue because we now include source in all scoring events (and actually any player variable change) from PR #1620. That change allows variable players to track scoring from specific modes and take behaviors accordingly, which I believe satisfies all the gaps raised by this issue. Hurray!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants