From 71e5450a84edf48fd17d60eb9ac15644b91431f1 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 31 Aug 2026 23:27:15 +0200 Subject: [PATCH] fix(divergence): scope claw latch to captured slot The Greifarm entry restriction was still shared across both multiball slots. An unrelated upper-playfield ball could clear the entering ball's latch during its own record-89 scan, while a shallow aborted entry could retain a stale choice. Store the latch owner with the capture-time restriction, permit only that slot to clear it when leaving the deep capture zone, and migrate the owner when a held secondary ball becomes primary. Test Plan: - `just --justfile tdkpin-rs/justfile test` -- passed (151 game, 8 service tests) - `just --justfile tdkpin-rs/justfile clippy` -- passed - `just --justfile tdkpin-rs/justfile build-production` -- passed - `just --justfile tdkpin-rs/justfile web-build` -- passed - `cargo +nightly fmt --manifest-path tdkpin-rs/Cargo.toml --all -- --check` -- passed - `rumdl check --flavor commonmark tdkpin-rs/CHANGELOG.md tdkpin-rs/AGENTS.md` -- passed - `git diff --cached --check` -- passed --- tdkpin-rs/CHANGELOG.md | 5 +- tdkpin-rs/src/game.rs | 181 ++++++++++++++++++++++++++++++----- tdkpin-rs/web/tdkpin-rs.wasm | Bin 1332496 -> 1332707 bytes 3 files changed, 161 insertions(+), 25 deletions(-) diff --git a/tdkpin-rs/CHANGELOG.md b/tdkpin-rs/CHANGELOG.md index 175f221..2739218 100644 --- a/tdkpin-rs/CHANGELOG.md +++ b/tdkpin-rs/CHANGELOG.md @@ -14,8 +14,9 @@ and this project adheres to latch the two-ball capture rule at each entry, exclude the launcher release while that capture is active, and let later single-ball re-entries use all four original release choices. -- Preserve that entry decision through the complete pull phase and migrate the - Greifarm contact owner when a held secondary ball becomes the primary ball. +- Preserve that entry decision through the complete pull phase, scope it to the + entering slot until that ball exits the capture zone, and migrate Greifarm + state when a held secondary ball becomes the primary ball. ## [1.2.0] - 2026-08-31 diff --git a/tdkpin-rs/src/game.rs b/tdkpin-rs/src/game.rs index 8156e3b..6a6a177 100644 --- a/tdkpin-rs/src/game.rs +++ b/tdkpin-rs/src/game.rs @@ -136,6 +136,16 @@ enum ClawBallSlot { Secondary, } +impl ClawBallSlot { + const fn from_ball_number(ball_number: u16) -> Self { + if ball_number == 2 { + Self::Secondary + } else { + Self::Primary + } + } +} + #[derive(Clone, Copy, Debug)] pub struct Claw { pub active: bool, @@ -143,7 +153,7 @@ pub struct Claw { pub bank: ClawSpriteBank, pub ball_suspended: bool, captured_slot: Option, - capture_restricted: Option, + capture_entry: Option<(ClawBallSlot, bool)>, target_frame: u8, frame_accumulator: f32, } @@ -156,7 +166,7 @@ impl Default for Claw { bank: ClawSpriteBank::Opening, ball_suspended: false, captured_slot: None, - capture_restricted: None, + capture_entry: None, target_frame: 10, frame_accumulator: 0.0, } @@ -1045,8 +1055,8 @@ impl Game { events, ) { CaptureStep::Complete => { - let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); + let terminal_frame = self + .next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1595,8 +1605,8 @@ impl Game { ) { CaptureStep::Holding | CaptureStep::Outside => {} CaptureStep::Complete => { - let terminal_frame = - self.next_claw_terminal_frame(self.claw.capture_restricted == Some(true)); + let terminal_frame = self + .next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number)); self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events); action = BallAction::Suspend; } @@ -1664,7 +1674,7 @@ impl Game { && ball.capture_age == 0 && self.record_contacts[usize::from(record_id)] == 0 { - self.claw.capture_restricted = None; + self.clear_claw_capture_entry_for(ball_number); } return CaptureStep::Outside; } @@ -1683,11 +1693,12 @@ impl Game { && contact_allowed && !(record_id == 148 && self.special_hole_gate == SpecialHoleGate::Suppressed) { - if record_id == 89 && ball.capture_age == 0 && self.claw.capture_restricted.is_none() { + if record_id == 89 && ball.capture_age == 0 && self.claw.capture_entry.is_none() { // Latch the restriction when the ball first enters the deep // capture zone, before the pull/hold phase can outlive the // other active ball. - self.claw.capture_restricted = Some(ball_count > 1); + self.claw.capture_entry = + Some((ClawBallSlot::from_ball_number(ball_number), ball_count > 1)); } if ball.capture_age < 300 { let mut velocity = MilliVec::from_velocity_per_second(ball.velocity); @@ -1743,12 +1754,12 @@ impl Game { return CaptureStep::Complete; } + if record_id == 89 && surface_distance >= -11_000 { + self.clear_claw_capture_entry_for(ball_number); + } if surface_distance > -11_000 && contact == ball_number { self.record_contacts[contact_index] = 0; ball.capture_age = 0; - if record_id == 89 { - self.claw.capture_restricted = None; - } } let velocity = MilliVec::from_velocity_per_second(ball.velocity); if surface_distance <= milli_distance(velocity) @@ -2126,9 +2137,27 @@ impl Game { self.claw.ball_suspended && self.claw.captured_slot == Some(slot) } + fn claw_capture_restricted_for(&self, ball_number: u16) -> bool { + self.claw.capture_entry == Some((ClawBallSlot::from_ball_number(ball_number), true)) + } + + fn clear_claw_capture_entry_for(&mut self, ball_number: u16) { + let slot = ClawBallSlot::from_ball_number(ball_number); + if self + .claw + .capture_entry + .is_some_and(|(owner, _)| owner == slot) + { + self.claw.capture_entry = None; + } + } + fn promote_claw_secondary_to_primary(&mut self) { if self.claw.captured_slot == Some(ClawBallSlot::Secondary) { self.claw.captured_slot = Some(ClawBallSlot::Primary); + if let Some((ClawBallSlot::Secondary, restricted)) = self.claw.capture_entry { + self.claw.capture_entry = Some((ClawBallSlot::Primary, restricted)); + } if self.record_contacts[89] == 2 { self.record_contacts[89] = 1; } @@ -2182,7 +2211,7 @@ impl Game { self.claw.bank = ClawSpriteBank::Closing; self.claw.ball_suspended = true; self.claw.captured_slot = Some(ClawBallSlot::Primary); - self.claw.capture_restricted = None; + self.claw.capture_entry = None; self.claw.frame_accumulator = 0.0; self.ball.velocity = Vec2::ZERO; self.ball.spin = Real48::ZERO; @@ -2205,11 +2234,7 @@ impl Game { self.claw.target_frame = terminal_frame; self.claw.bank = ClawSpriteBank::Closing; self.claw.ball_suspended = true; - self.claw.captured_slot = Some(if ball_number == 2 { - ClawBallSlot::Secondary - } else { - ClawBallSlot::Primary - }); + self.claw.captured_slot = Some(ClawBallSlot::from_ball_number(ball_number)); self.claw.frame_accumulator = 0.0; ball.velocity = Vec2::ZERO; ball.spin = Real48::ZERO; @@ -4513,7 +4538,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert_eq!( game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), @@ -4583,7 +4608,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); game.ball.velocity = Vec2::ZERO; game.ball.capture_age = 300; @@ -4592,10 +4617,112 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert!(matches!(game.claw.target_frame, 1 | 6 | 7)); } + #[test] + fn other_multiball_slot_cannot_clear_the_claw_entry_latch() { + let mut game = Game::new_with_seed(1, 7); + game.ball.in_launcher = false; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + game.secondary_ball = Some(Ball { + position: vec2(210.0, 100.0), + velocity: Vec2::ZERO, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }); + game.object_active.fill(false); + game.score_mode = ScoreMode::Multiball; + let mut events = Vec::new(); + + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + + let mut secondary = game.secondary_ball.take().expect("secondary ball"); + let secondary_position = MilliVec::from_position(secondary.position); + assert_eq!( + game.check_sensor_objects_for_ball( + &mut secondary, + 2, + 2, + secondary_position, + MilliVec::default(), + &mut events, + ) + .action, + BallAction::Keep + ); + game.secondary_ball = Some(secondary); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + } + + #[test] + fn shallow_claw_exit_clears_only_the_owning_entry_latch() { + let mut game = Game::new_with_seed(1, 7); + game.ball.in_launcher = false; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + game.secondary_ball = Some(Ball { + position: vec2(210.0, 240.0), + velocity: Vec2::ZERO, + in_launcher: false, + spin: Real48::ZERO, + capture_age: 0, + }); + game.object_active.fill(false); + game.score_mode = ScoreMode::Multiball; + let mut events = Vec::new(); + + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); + + game.ball.position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0); + game.ball.velocity = Vec2::ZERO; + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(game.ball.position), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!(game.claw.capture_entry, None); + + game.secondary_ball = None; + game.score_mode = ScoreMode::Normal; + game.ball.position = CLAW_TRIGGER_CENTER; + game.ball.velocity = vec2(100.0, 0.0); + assert_eq!( + game.check_sensor_objects( + MilliVec::from_position(CLAW_TRIGGER_CENTER), + MilliVec::default(), + &mut events, + ), + BallAction::Keep + ); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Primary, false)) + ); + } + #[test] fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() { let mut game = Game::new_with_seed(1, 7); @@ -4737,11 +4864,16 @@ mod tests { ); game.secondary_ball = Some(secondary); assert_eq!(game.record_contacts[89], 2); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Secondary, true)) + ); game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events); assert!(game.secondary_ball.is_none()); assert!(game.primary_ball_suspended()); assert_eq!(game.record_contacts[89], 1); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); assert_eq!(game.score_mode, ScoreMode::Normal); assert!(events.contains(&Event::Drain)); } @@ -4784,7 +4916,7 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend ); - assert_eq!(game.claw.capture_restricted, Some(true)); + assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true))); game.secondary_ball = None; game.score_mode = ScoreMode::Normal; @@ -4808,7 +4940,10 @@ mod tests { game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Keep ); - assert_eq!(game.claw.capture_restricted, Some(false)); + assert_eq!( + game.claw.capture_entry, + Some((ClawBallSlot::Primary, false)) + ); assert_eq!( game.check_sensor_objects(claw_position, MilliVec::default(), &mut events), BallAction::Suspend diff --git a/tdkpin-rs/web/tdkpin-rs.wasm b/tdkpin-rs/web/tdkpin-rs.wasm index c9d0a06855937d9251f27141c855f8ed1393b4ec..7d8f9ce72e81bb53f8526f094f972c8d3975fb15 100755 GIT binary patch delta 4594 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 cuYF58fBTknf%Yxwg6&(V@ujKk z@9T`hjP;Cl%=L_Q?Ded*j5SR4jCG8)y!DK=tlLAq7(WQ}0)3vG?BZNAd8X6cB`4t!x zM4cHJ6dVOQrcbG2oFa-OuE3cEld`R5^c06k2`Dfq@c&^@5QR(5t!7MRY?;nn!&uAM zGQF{e(V4Mj`_URkV@Ads(|^`7YARk~S7ZU%eU)926+~ZSR}@uX&}W>)t*8Rx-<tRTviNMrys0i3^7(!5Ow^|Sg6Fz$IQgc$P7{h3IvcM zq+mh{KOTG!)fAa<|wOHJBVAP=VQz z$xVsXm6t(5!;!Jjm6uUL-IbSxhY=KDj8+Wyd7$D93hL9tTNw4HD_mscoZjBT=*Orz z{aOp7JfrILZ!L@`7&WKwZ)KEboG|@%E91*xO$A2B1jVDm~Pt5XuvpSdRjMQ4`a*p&)tlMJerP-g$j(0bC|LO z+NK-yFkX>|xDyoIx(t&zK*`>b$%4R!DjPWoS2E21f=(79JLEM@EoEx(wIYmDoZ0 zm=)MTYMFW1xE+^ly4b+rXySMSL<=-FHZ(9WfWv$ehoXoA1Jrel3OxFZ64R~v7@fqC z&1OWHJ$-T?qb1|5>1X>G#bvLtgWbsJR>;c;as((<6)UJaGPxBgs7+_=XB1}apDxG-65M%%Jsr`(#j8moyPhdO*$pd23-%MasXKa~%w}DY&y3RyKbHPgMkj7Ywe4HyGa53fYw|KWGAgJD%z^mXkx^08QC(5lkx@~UhY_5RK-oldI`1OJ zGmM(kZ!KbsV$|Gjx|p$$kxk99K1)$;`nD z#p&GM%sh;((3u%T zvlv^ZYx**qF}6%k@MQ*73^RS1n;BcC%ld%@L;RRC7+a_B^<$RgXc1;%tY@m_oPN)b zxoLWmKl46U0Z@h!;1y6{&}ZDl1}^nL98hxk&cq|Yt-$Hnv8vvW7aWi*pv0iW6?R@ zh3cgoSJW#oWUqDl&)BRa<5XYc^#4C|bB7aSvx1a@Oh=8Pl!By_LdSB)`eFqxM~Si` z#|Fki1!*UZj^&OGjKxI<92q=#nG~eJe&C4WWmk||uE@Z{%8tx2q!3u@G5XBa83^hBR8yAq!qZp zVZ{IrD;8K-ao`OrE_hfmV+$)Td|}1pfCw#4T%pB)2rV{HXn|`)!mQ2A*fQOy99%AEmNV-zwoad3&ODQ`XS-nq^EWnV>xNl@QGo-REVf(LGtXdW zoUr^-8#ALBq(R2)c#knlfmxdw*7Ra_d;$?@V1zctm>pk1L^>D+CQj$*U{+Rt!=+eU){mHgmLopfKKKL#+KC{Hz@fp!p~MBEz$FETK+AO7NzBsId;6LF6~HdT=cYgX z%vOx;)6FL^+c36IFPXq>$Jjo7^8{v7#`ftiCxGQtCNeK)Y@L2)B69&F*z78Ku(7y( zy8YTDW_d=&?&;qqGv_e2Pfwo0YzomTJ$=g*=H-kn(_^NB6X)cq%zBKi(+^E$Uc%W2 zjs^~a*6I1vnCCP0Oy{4@?9bReJ$^cK6l3r7UDKHr7<;GRp3dyW*fm{k2J?Gp3Ic^O zJ2Z^9tIuR!FU#1u{rWa$W>&^Y({Jx)E?~Se-DeN8iU6pU3{93G=C|ohd%*3bhuzEy zjBL|2_A=)(vTdKXm)RcFqPn(^*;t4bRA;e(nl9kLWfW+cF1?>wQ<4STJlMpn$P8)m zZej*^lz5q@$L?pAV*EC}Vn4It^vC;|jUXvbfstkUzWvOq(=84#>oB%#Pd&hVfRS

UzWovIxWp8g#3^C#Y4BQM146UpT47^DEc2))kK_q?$8h;`y1A{)2{I2PbWBHN=<=Gh+)RBa{ zriaJzDKoZAua4t;EqH(()zItH%j5Zq8Tq%rj^|TmQIzFG)f~jhz#xibnALRs6h3Du zCQb$hAqEBpRu*O^MkMKv9NT+R_>`C#_fKD+&Uc-0YkPYJUweB7e|vj|Kzn