test(divergence): cover converging claw approaches
A follow-up review questioned whether both multiball slots could enter the Greifarm deep zone before either established capture ownership. The zone is less than 16 pixels wide while ball contact begins at 17 pixels, so two non-overlapping balls cannot fit there together. Document that invariant and exercise the real sequential timer from cardinal approaches at two distances and speeds, asserting that every multiball-latched capture retains its entry owner. Test Plan: - broader diagnostic sweep -- passed (574 captures, no owner mismatch) - `just --justfile tdkpin-rs/justfile test` -- passed (152 game, 8 service tests) - `just --justfile tdkpin-rs/justfile clippy` -- passed - `cargo +nightly fmt --manifest-path tdkpin-rs/Cargo.toml --all -- --check` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -153,6 +153,8 @@ pub struct Claw {
|
||||
pub bank: ClawSpriteBank,
|
||||
pub ball_suspended: bool,
|
||||
captured_slot: Option<ClawBallSlot>,
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user