diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 6a6a177..f260333 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -153,6 +153,8 @@ pub struct Claw { pub bank: ClawSpriteBank, pub ball_suspended: bool, captured_slot: Option, + // The deep zone is less than 16 px wide while ball contact is 17 px, so + // only one physically non-overlapping ball can own a pending entry. capture_entry: Option<(ClawBallSlot, bool)>, target_frame: u8, frame_accumulator: f32, @@ -4723,6 +4725,83 @@ mod tests { ); } + #[test] + fn nonoverlapping_two_ball_claw_approaches_keep_capture_ownership() { + // Exercise the sequential solver from non-overlapping cardinal + // approaches around the sub-8 px deep zone. Ball contact begins at + // 17 px, so a second ball must be resolved before both can settle. + let capture_center = CLAW_TRIGGER_CENTER - vec2(0.0, 2.0); + let directions = [ + vec2(1.0, 0.0), + vec2(0.0, 1.0), + vec2(-1.0, 0.0), + vec2(0.0, -1.0), + ]; + let radii = [10.0, 18.0]; + let speeds = [100.0, 300.0]; + let mut captures = 0; + + for first_direction in directions { + for second_direction in directions { + for first_radius in radii { + for second_radius in radii { + let first_position = capture_center + first_direction * first_radius; + let second_position = capture_center + second_direction * second_radius; + if first_position.distance(second_position) < 17.0 { + continue; + } + + for first_speed in speeds { + for second_speed in speeds { + let mut game = Game::new_with_seed(1, 7); + game.ball = Ball { + position: first_position, + velocity: (capture_center - first_position).normalize() + * first_speed, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }; + game.secondary_ball = Some(Ball { + position: second_position, + velocity: (capture_center - second_position).normalize() + * second_speed, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }); + game.object_active.fill(false); + game.score_mode = ScoreMode::Multiball; + let mut events = Vec::new(); + + for _ in 0..100 { + game.timer_tick(0.01, 1, &mut events); + if game.claw.active { + let Some((owner, true)) = game.claw.capture_entry else { + break; + }; + captures += 1; + assert_eq!( + Some(owner), + game.claw.captured_slot, + "positions {first_position:?}/{second_position:?}, speeds {first_speed}/{second_speed}" + ); + break; + } + if game.secondary_ball.is_none() || game.finished { + break; + } + } + } + } + } + } + } + } + + assert!(captures > 0, "the sweep must exercise claw captures"); + } + #[test] fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() { let mut game = Game::new_with_seed(1, 7);