fix(divergence): keep claw releases on the table during multiball
Bug: with several balls in play the claw could still drop its ball into the shooter lane (terminal frame 18), sending it back to the launcher. Cause: the restriction to table releases (frames 1, 6, 7) is latched when the ball enters the claw's capture zone, from the ball count at that moment. Balls added afterwards (crazy mode, a release-ball spawn) did not affect a capture that had already chosen frame 18. Fix: while the claw holds a ball and is heading for frame 18, update_claw checks the live ball count every timer callback. With more than one ball (the held one included) it re-targets the claw to a random table frame using the same restricted draw as a multiball capture. The claw animates one frame per callback, so it simply turns around towards the new target. Single-ball captures, and multiball captures that were already restricted at entry, behave exactly as before; the extra random draw only happens in the situation that used to misbehave. Test Plan: - cargo test claw_never_drops_into_the_shooter_lane_with_several_balls: a frame-18 capture gains a second ball mid-ride and must release at frame 1, 6, or 7, onto the table. - A release-mode stress run (40 two-player crazy-mode games) counted 254 claw releases and none into the shooter lane with several balls.
This commit is contained in:
1 parent
0e8121d027
commit
17a7b3c328
3 files changed
+39
No files matched your search
@@ -34,6 +34,8 @@ and this project adheres to
|
|||||||
- The release-ball effect frees the ball parked in the special hole even when
|
- The release-ball effect frees the ball parked in the special hole even when
|
||||||
several balls are already in play; the released ball keeps ownership of the
|
several balls are already in play; the released ball keeps ownership of the
|
||||||
hole until it rolls out.
|
hole until it rolls out.
|
||||||
|
- The claw never drops a ball into the shooter lane while more than one ball is
|
||||||
|
in play, including balls that arrive after the capture began.
|
||||||
|
|
||||||
## [1.2.1] - 2026-09-01
|
## [1.2.1] - 2026-09-01
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ differences follow from treating every ball alike:
|
|||||||
- The release-ball effect (effect seven) frees a parked special-hole ball
|
- The release-ball effect (effect seven) frees a parked special-hole ball
|
||||||
whenever one is parked, not only when exactly one ball is in play. The
|
whenever one is parked, not only when exactly one ball is in play. The
|
||||||
released ball owns the hole's contact under its own slot number.
|
released ball owns the hole's contact under its own slot number.
|
||||||
|
- The claw re-targets a shooter-lane release (terminal frame 18) to a table
|
||||||
|
release whenever more than one ball is in play during its ride, not only
|
||||||
|
when the capture began in multiball.
|
||||||
- High-score entries carry a `crazy` flag that both local storage and the
|
- High-score entries carry a `crazy` flag that both local storage and the
|
||||||
shared service persist; older entries default to normal scores.
|
shared service persist; older entries default to normal scores.
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
|
|||||||
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
||||||
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
||||||
const CLAW_TABLE_TERMINAL_FRAMES: [u8; 3] = [1, 6, 7];
|
const CLAW_TABLE_TERMINAL_FRAMES: [u8; 3] = [1, 6, 7];
|
||||||
|
/// Claw terminal frame that drops the released ball into the shooter lane.
|
||||||
|
const CLAW_SHOOTER_LANE_FRAME: u8 = 18;
|
||||||
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_wrap)]
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
@@ -2269,6 +2271,14 @@ impl Game {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.claw.frame_accumulator += dt;
|
self.claw.frame_accumulator += dt;
|
||||||
|
// Never send a ball down the shooter lane while other balls are in
|
||||||
|
// play, even when more balls arrived after the capture began.
|
||||||
|
if self.claw.ball_suspended
|
||||||
|
&& self.claw.target_frame == CLAW_SHOOTER_LANE_FRAME
|
||||||
|
&& self.balls.len() > 1
|
||||||
|
{
|
||||||
|
self.claw.target_frame = self.next_claw_terminal_frame(true);
|
||||||
|
}
|
||||||
while self.claw.active && self.claw.frame_accumulator >= self.claw_frame_seconds {
|
while self.claw.active && self.claw.frame_accumulator >= self.claw_frame_seconds {
|
||||||
self.claw.frame_accumulator -= self.claw_frame_seconds;
|
self.claw.frame_accumulator -= self.claw_frame_seconds;
|
||||||
self.advance_claw(events);
|
self.advance_claw(events);
|
||||||
@@ -2791,6 +2801,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claw_never_drops_into_the_shooter_lane_with_several_balls() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.begin_claw_scenario(CLAW_SHOOTER_LANE_FRAME);
|
||||||
|
assert_eq!(game.claw.target_frame, CLAW_SHOOTER_LANE_FRAME);
|
||||||
|
// A second ball arrives while the claw is already on its way.
|
||||||
|
game.balls.push(Ball {
|
||||||
|
position: vec2(160.0, 300.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for _ in 0..80 {
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
if events.contains(&Event::ClawRelease) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
|
assert!(CLAW_TABLE_TERMINAL_FRAMES.contains(&game.claw.frame));
|
||||||
|
assert!(!game.balls[0].in_launcher);
|
||||||
|
assert!(game.balls[0].position.x < 300.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_count_is_bounded() {
|
fn player_count_is_bounded() {
|
||||||
assert_eq!(Game::new(0).players.len(), 1);
|
assert_eq!(Game::new(0).players.len(), 1);
|
||||||
|
|||||||
Reference in new issue
Block a user