fix(game): preserve rule state across player turns

The rewrite reset wheel holes, top targets, collision latches, active records,
selected effects, and multiball readiness on every drain. The reconstructed
turn-state code instead saves all 175 live record states for the outgoing player
and restores the incoming player's mirror.

Add a per-player rule snapshot and save/load it at normal turn transitions.
Keep transient ball, claw, tilt, and presentation state global while retaining
each player's mechanism progress and contact/activation state across later
turns and extra balls.

Test Plan:
- `cargo test --all-targets` -- passed, 58 tests
- `cargo clippy --all-targets -- -D warnings` -- passed
- `rumdl check tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md` -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-23 16:58:10 +02:00
parent 0c52100a7e
commit 64d41eb605
3 changed files with 83 additions and 7 deletions
+79 -6
View File
@@ -162,6 +162,7 @@ pub struct Player {
pub diamond_segments: u8,
pub double_score: bool,
pub media_level: u8,
rules: RuleState,
}
impl Default for Player {
@@ -176,6 +177,7 @@ impl Default for Player {
diamond_segments: 0,
double_score: false,
media_level: 0,
rules: RuleState::default(),
}
}
}
@@ -200,6 +202,29 @@ enum MultiballState {
Ready,
}
#[derive(Clone, Copy, Debug)]
struct RuleState {
wheel_holes: [bool; 5],
top_targets: [bool; 3],
trigger_contacts: [bool; 176],
object_active: [bool; 176],
target_effect: u8,
multiball_state: MultiballState,
}
impl Default for RuleState {
fn default() -> Self {
Self {
wheel_holes: [false; 5],
top_targets: [false; 3],
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
target_effect: 0,
multiball_state: MultiballState::Unavailable,
}
}
}
fn initial_object_activity() -> [bool; 176] {
let mut active = [true; 176];
active[0] = false;
@@ -1165,20 +1190,15 @@ impl Game {
} else {
player.balls = player.balls.saturating_sub(1);
}
self.save_current_rule_state();
events.push(Event::Drain);
events.push(Event::Sound(2008));
self.top_targets.fill(false);
self.wheel_holes.fill(false);
self.tilted = false;
self.tilt_counter = 0;
self.tilt_counter_accumulator = 0.0;
self.bumper_flash.fill(0.0);
self.wheel_animation = 0.0;
self.claw = Claw::default();
self.trigger_contacts.fill(false);
self.object_active = initial_object_activity();
self.target_effect = 0;
self.multiball_state = MultiballState::Unavailable;
self.secondary_ball = None;
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
@@ -1191,6 +1211,7 @@ impl Game {
for _ in 0..self.players.len() {
if self.players[next].balls > 0 || self.players[next].extra_balls > 0 {
self.current_player = next;
self.load_current_rule_state();
self.ball = Ball::default();
return;
}
@@ -1199,6 +1220,27 @@ impl Game {
self.finished = true;
}
fn save_current_rule_state(&mut self) {
self.players[self.current_player].rules = RuleState {
wheel_holes: self.wheel_holes,
top_targets: self.top_targets,
trigger_contacts: self.trigger_contacts,
object_active: self.object_active,
target_effect: self.target_effect,
multiball_state: self.multiball_state,
};
}
fn load_current_rule_state(&mut self) {
let state = self.players[self.current_player].rules;
self.wheel_holes = state.wheel_holes;
self.top_targets = state.top_targets;
self.trigger_contacts = state.trigger_contacts;
self.object_active = state.object_active;
self.target_effect = state.target_effect;
self.multiball_state = state.multiball_state;
}
fn live_wall_segment(&self, object_id: u8, resting: Segment) -> Segment {
let (start, end) = match object_id {
66 if self.flippers.left_raised => (vec2(98.0, 382.0), vec2(131.0, 369.0)),
@@ -1561,6 +1603,37 @@ mod tests {
assert!(game.ball.in_launcher);
}
#[test]
fn player_turns_save_and_restore_all_rule_record_state() {
let mut game = Game::new(2);
game.wheel_holes[2] = true;
game.top_targets[1] = true;
game.trigger_contacts[149] = true;
game.object_active[90] = false;
game.target_effect = 3;
game.multiball_state = MultiballState::Ready;
game.drain(&mut Vec::new());
assert_eq!(game.current_player, 1);
assert_eq!(game.wheel_holes, [false; 5]);
assert_eq!(game.top_targets, [false; 3]);
assert!(!game.trigger_contacts[149]);
assert!(game.object_active[90]);
assert_eq!(game.target_effect, 0);
assert_eq!(game.multiball_state, MultiballState::Unavailable);
game.wheel_holes[4] = true;
game.drain(&mut Vec::new());
assert_eq!(game.current_player, 0);
assert!(game.wheel_holes[2]);
assert!(!game.wheel_holes[4]);
assert!(game.top_targets[1]);
assert!(game.trigger_contacts[149]);
assert!(!game.object_active[90]);
assert_eq!(game.target_effect, 3);
assert_eq!(game.multiball_state, MultiballState::Ready);
}
#[test]
fn fast_ball_cannot_tunnel_through_the_top_rail() {
let mut game = Game::new(1);