fix(divergence): replace a full wheel immediately during multiball
Bug: filling the fifth wheel hole with several balls in play awarded the transfer, multiplier, and extra ball, but did not run the wheel replacement. The original deferred the 281-frame panel until a later launcher reset, which with crazy-mode balls could be a long time, while the full wheel kept every hole closed. Requirement: a full wheel must be replaced with the same animation and scoring as in single-ball play. Balls still in play keep moving, with the wheel's outer border as a solid boundary and no hole captures until the replacement is done. Changes: - complete_lock_hole starts the panel for a multiball fill too (if one is not already running). The single-ball path is unchanged and still restarts it unconditionally, as in the original. Scoring was already identical and is untouched. - timer_tick only freezes physics during the panel while every ball waits in the launcher. That is always the case for the single-ball replacement (the locking ball was reset), so it is frozen exactly as before; during multiball the balls in play keep integrating. - While the panel runs, check_lock_holes captures nothing and retain_wheel_rim adds a circle around the wheel hub (record 134's center, 48 px ball-center radius, enclosing the five hole posts) with the hub's material. It only repels balls moving towards it, so a ball already inside rolls out instead of being caged. - Stress runs then showed balls trapped for good at the bottom of a hole pocket, cradled between the hub and a post, after another ball filled that hole or the panel closed it. The pocket bottom lies outside the hole's capture zone, so nothing ever moved them. A filled or closed hole now kicks such a ball (more than 4 px inside its rim) out of its center, radially away from the hub at 3 px/step, a path that clears both neighbouring posts. Balls arriving from outside are reflected by the rim before getting that deep. - A lock or special-hole capture that removes the last remaining ball of a multiball callback now resets it to the launcher (or finishes it when tilted), exactly like the single-ball capture, instead of ending the player's ball. Test Plan: - cargo test (164 tests), including full_wheel_during_multiball_replaces_it_while_balls_play_on (rim bounce, no captures, balls move, wheel cleared after the panel), filled_or_closed_lock_hole_kicks_out_a_ball_resting_in_its_pocket (the exact trapped position seen in stress runs), and the updated fifth_multiball_lock_starts_the_panel_and_caps_survivor_reaward. - A temporary release-mode stress run (not committed): 40 seeded two-player games with crazy mode, autoplay flippers, and nudges all finished; 62 wheel replacements happened during multiball; no invariant violations. - Manual: in crazy mode fill the fifth wheel hole; the replacement animation plays while the other balls keep moving and bounce off the wheel instead of dropping into its holes.
This commit is contained in:
1 parent
17a7b3c328
commit
158936577a
4 files changed
+222
-10
No files matched your search
@@ -36,6 +36,13 @@ and this project adheres to
|
|||||||
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
|
- 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.
|
in play, including balls that arrive after the capture began.
|
||||||
|
- Filling the fifth wheel hole during multiball replaces the wheel at once with
|
||||||
|
the normal animation and scoring instead of waiting for multiball to end.
|
||||||
|
Balls in play keep moving, the holes stay closed, and the wheel's outer rim
|
||||||
|
acts as a solid boundary until the replacement finishes. Locking the last
|
||||||
|
remaining ball returns it to the launcher as in single-ball play.
|
||||||
|
- A closed or filled lock hole kicks out a ball that came to rest in its pocket,
|
||||||
|
so it can no longer stay trapped next to the wheel hub.
|
||||||
|
|
||||||
## [1.2.1] - 2026-09-01
|
## [1.2.1] - 2026-09-01
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ differences follow from treating every ball alike:
|
|||||||
- The claw re-targets a shooter-lane release (terminal frame 18) to a table
|
- 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
|
release whenever more than one ball is in play during its ride, not only
|
||||||
when the capture began in multiball.
|
when the capture began in multiball.
|
||||||
|
- A full wheel during multiball starts the 281-frame replacement immediately
|
||||||
|
(the original deferred it to the next launcher reset). The table only freezes
|
||||||
|
while every ball waits in the launcher; otherwise balls keep moving, the lock
|
||||||
|
holes do not capture, and the wheel's outer rim (record 134's center, 48 px
|
||||||
|
ball contact radius) repels balls from outside.
|
||||||
|
- A ball resting inside a filled or closed lock hole's pocket is kicked out
|
||||||
|
from the hole center, radially away from the hub. A capture that removes
|
||||||
|
the last ball of a multiball callback resets it to the launcher like a
|
||||||
|
single-ball capture instead of ending the ball.
|
||||||
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,10 @@ decoded, build-ready subset; it does not replace that evidence archive.
|
|||||||
others remain and ends the player's ball only for the last one;
|
others remain and ends the player's ball only for the last one;
|
||||||
`remove_ball` keeps the claw and contact-record owners pointing at the
|
`remove_ball` keeps the claw and contact-record owners pointing at the
|
||||||
same physical ball; effect seven's ball joins after all slot passes;
|
same physical ball; effect seven's ball joins after all slot passes;
|
||||||
|
- wheel replacement: the 281-frame panel freezes the table only while every
|
||||||
|
ball waits in the launcher; during multiball the lock holes close, the
|
||||||
|
wheel rim (`retain_wheel_rim`) repels balls, and closed holes kick out a
|
||||||
|
ball resting in their pocket (`kick_out_of_lock_hole`);
|
||||||
- crazy mode (a modern addition): `add_crazy_balls` drops ten ordinary balls
|
- crazy mode (a modern addition): `add_crazy_balls` drops ten ordinary balls
|
||||||
and marks the current player, whose high score is labelled;
|
and marks the current player, whose high score is labelled;
|
||||||
- `geometry.rs`: segment/circle collision primitives;
|
- `geometry.rs`: segment/circle collision primitives;
|
||||||
|
|||||||
+202
-10
@@ -49,6 +49,16 @@ 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.
|
/// Claw terminal frame that drops the released ball into the shooter lane.
|
||||||
const CLAW_SHOOTER_LANE_FRAME: u8 = 18;
|
const CLAW_SHOOTER_LANE_FRAME: u8 = 18;
|
||||||
|
/// Wheel hub record 134; its center is also the center of the lock wheel.
|
||||||
|
const WHEEL_HUB_ID: u8 = 134;
|
||||||
|
/// Ball-center contact radius of the wheel's outer rim (about 40 px of
|
||||||
|
/// artwork plus the 8 px ball radius), so it encloses the five hole posts.
|
||||||
|
const WHEEL_RIM_CONTACT_RADIUS: f32 = 48.0;
|
||||||
|
/// How far (in millipixels) a ball must sit inside a closed lock hole's rim
|
||||||
|
/// before the hole kicks it out. Balls arriving from outside are reflected by
|
||||||
|
/// the rim before they get this deep.
|
||||||
|
const CLOSED_HOLE_KICKOUT_DEPTH_MILLI: i32 = 4_000;
|
||||||
|
const CLOSED_HOLE_KICKOUT_SPEED_MILLI: f32 = 3_000.0;
|
||||||
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)]
|
||||||
@@ -707,7 +717,9 @@ impl Game {
|
|||||||
if self.balls.len() < 2 {
|
if self.balls.len() < 2 {
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
}
|
}
|
||||||
if !panel_active {
|
// The single-ball wheel replacement runs with its ball parked in the
|
||||||
|
// launcher and freezes the table; balls still in play keep moving.
|
||||||
|
if !panel_active || self.balls.iter().any(|ball| !ball.in_launcher) {
|
||||||
self.advance_ball_slots(substeps, events);
|
self.advance_ball_slots(substeps, events);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,9 +1083,14 @@ impl Game {
|
|||||||
BallAction::Remove => {
|
BallAction::Remove => {
|
||||||
// A capture during multiball takes the ball off the table
|
// A capture during multiball takes the ball off the table
|
||||||
// without a drain. If the other balls already left during
|
// without a drain. If the other balls already left during
|
||||||
// this callback, it was the last ball after all.
|
// this callback it was the last ball, which returns to the
|
||||||
|
// launcher exactly like a single-ball capture.
|
||||||
if self.balls.len() == 1 {
|
if self.balls.len() == 1 {
|
||||||
return self.lose_ball(index, events);
|
if self.tilted {
|
||||||
|
return self.lose_ball(index, events);
|
||||||
|
}
|
||||||
|
self.reset_ball_to_launcher(index);
|
||||||
|
return BallStep::Stop;
|
||||||
}
|
}
|
||||||
self.remove_ball(index);
|
self.remove_ball(index);
|
||||||
return BallStep::Removed;
|
return BallStep::Removed;
|
||||||
@@ -1100,6 +1117,36 @@ impl Game {
|
|||||||
self.step_ball(pass, events) != BallStep::Continue
|
self.step_ball(pass, events) != BallStep::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// While the wheel is being replaced its holes are closed and the
|
||||||
|
/// wheel's outer rim repels balls arriving from outside.
|
||||||
|
fn retain_wheel_rim(
|
||||||
|
old_position: MilliVec,
|
||||||
|
predicted: MilliVec,
|
||||||
|
velocity: MilliVec,
|
||||||
|
best: &mut Option<(u8, bool, StaticCollisionCandidate)>,
|
||||||
|
) {
|
||||||
|
let Some(hub) = PASSIVE_CIRCLES
|
||||||
|
.iter()
|
||||||
|
.find(|circle| circle.id == WHEEL_HUB_ID)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(candidate) = circle_collision_candidate_at(
|
||||||
|
old_position,
|
||||||
|
predicted,
|
||||||
|
velocity,
|
||||||
|
hub.center,
|
||||||
|
WHEEL_RIM_CONTACT_RADIUS,
|
||||||
|
CollisionMaterial::circle(
|
||||||
|
f64::from(hub.normal_rebound),
|
||||||
|
f64::from(hub.tangent_coupling),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
retain_first_collision(best, (WHEEL_HUB_ID, false, candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn retain_static_range(
|
fn retain_static_range(
|
||||||
&self,
|
&self,
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
@@ -1182,6 +1229,9 @@ impl Game {
|
|||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.panel_frame.is_some() {
|
||||||
|
Self::retain_wheel_rim(old_position, predicted, *velocity, &mut best);
|
||||||
|
}
|
||||||
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
||||||
if old_position.y < 250_000 {
|
if old_position.y < 250_000 {
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
@@ -1680,6 +1730,13 @@ impl Game {
|
|||||||
let contact = self.record_contacts[contact_index];
|
let contact = self.record_contacts[contact_index];
|
||||||
let contact_allowed =
|
let contact_allowed =
|
||||||
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
|
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
|
||||||
|
if (129..=133).contains(&record_id)
|
||||||
|
&& contact == 99
|
||||||
|
&& surface_distance < -CLOSED_HOLE_KICKOUT_DEPTH_MILLI
|
||||||
|
{
|
||||||
|
kick_out_of_lock_hole(ball, center_view);
|
||||||
|
return CaptureStep::Outside;
|
||||||
|
}
|
||||||
|
|
||||||
if surface_distance < -11_000
|
if surface_distance < -11_000
|
||||||
&& contact_allowed
|
&& contact_allowed
|
||||||
@@ -1790,6 +1847,24 @@ impl Game {
|
|||||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) -> Option<BallAction> {
|
) -> Option<BallAction> {
|
||||||
|
if self.panel_frame.is_some() {
|
||||||
|
// The wheel is being replaced: no hole captures, and a ball that
|
||||||
|
// was resting in a hole's pocket is kicked back onto the table.
|
||||||
|
for sensor in LOCK_HOLES {
|
||||||
|
let center = MilliVec::from_position(sensor.center);
|
||||||
|
let current = MilliVec::from_position(ball.position);
|
||||||
|
let offset = MilliVec {
|
||||||
|
x: center.x.wrapping_sub(current.x),
|
||||||
|
y: center.y.wrapping_sub(current.y).wrapping_sub(2_000),
|
||||||
|
};
|
||||||
|
let radius = MilliVec::from_position(vec2(sensor.radius, 0.0)).x;
|
||||||
|
if milli_distance(offset) - radius < -CLOSED_HOLE_KICKOUT_DEPTH_MILLI {
|
||||||
|
kick_out_of_lock_hole(ball, sensor.center);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
||||||
match self.capture_record_step(
|
match self.capture_record_step(
|
||||||
ball,
|
ball,
|
||||||
@@ -1839,7 +1914,10 @@ impl Game {
|
|||||||
player.secondary_score = 0;
|
player.secondary_score = 0;
|
||||||
player.score_multiplier = player.score_multiplier.wrapping_add(1);
|
player.score_multiplier = player.score_multiplier.wrapping_add(1);
|
||||||
player.extra_balls = player.extra_balls.wrapping_add(1);
|
player.extra_balls = player.extra_balls.wrapping_add(1);
|
||||||
if ball_count == 1 {
|
// Deliberate original-game divergence: the original deferred the
|
||||||
|
// wheel replacement until multiball ended. It now starts at once,
|
||||||
|
// and balls still in play run against the closed wheel rim.
|
||||||
|
if ball_count == 1 || self.panel_frame.is_none() {
|
||||||
self.panel_frame = Some(0);
|
self.panel_frame = Some(0);
|
||||||
}
|
}
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
@@ -2544,6 +2622,28 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
|||||||
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deliberate original-game divergence: with several balls, one can come to
|
||||||
|
/// rest at the bottom of a lock hole's pocket, between the wheel hub and a
|
||||||
|
/// post, when another ball fills that hole or the wheel is being replaced.
|
||||||
|
/// The closed hole sends it out from its center, radially away from the hub,
|
||||||
|
/// which clears both neighbouring posts.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn kick_out_of_lock_hole(ball: &mut Ball, hole_center: Vec2) {
|
||||||
|
let hub = PASSIVE_CIRCLES
|
||||||
|
.iter()
|
||||||
|
.find(|circle| circle.id == WHEEL_HUB_ID)
|
||||||
|
.map_or(vec2(124.0, 76.0), |circle| circle.center);
|
||||||
|
let direction = (hole_center - hub).normalize_or_zero();
|
||||||
|
ball.position = hole_center;
|
||||||
|
ball.velocity = MilliVec {
|
||||||
|
x: (direction.x * CLOSED_HOLE_KICKOUT_SPEED_MILLI).round() as i32,
|
||||||
|
y: (direction.y * CLOSED_HOLE_KICKOUT_SPEED_MILLI).round() as i32,
|
||||||
|
}
|
||||||
|
.to_velocity_per_second();
|
||||||
|
ball.spin = Real48::ZERO;
|
||||||
|
ball.capture_age = 0;
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
|
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
|
||||||
let position = MilliVec::from_position(ball.position);
|
let position = MilliVec::from_position(ball.position);
|
||||||
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
@@ -2825,6 +2925,102 @@ mod tests {
|
|||||||
assert!(game.balls[0].position.x < 300.0);
|
assert!(game.balls[0].position.x < 300.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_wheel_during_multiball_replaces_it_while_balls_play_on() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.wheel_holes = [true, true, true, true, false];
|
||||||
|
game.record_contacts[129..=132].fill(99);
|
||||||
|
game.players[0].secondary_score = 150_000;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
assert_eq!(
|
||||||
|
game.complete_lock_hole(4, 2, &mut events),
|
||||||
|
BallAction::Remove
|
||||||
|
);
|
||||||
|
assert_eq!(game.panel_frame, Some(0));
|
||||||
|
assert_eq!(game.player().secondary_score, 0);
|
||||||
|
assert_eq!(game.player().score_multiplier, 2);
|
||||||
|
|
||||||
|
// A ball heading for the wheel bounces off its outer rim.
|
||||||
|
let hub = vec2(124.0, 76.0);
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: hub + vec2(52.0, 0.0),
|
||||||
|
velocity: MilliVec { x: -3_000, y: 0 }.to_velocity_per_second(),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let mut hit_rim = false;
|
||||||
|
for _ in 0..4 {
|
||||||
|
game.fixed_update(&mut events);
|
||||||
|
hit_rim |= game.last_collision_id == Some(WHEEL_HUB_ID);
|
||||||
|
}
|
||||||
|
assert!(hit_rim);
|
||||||
|
assert!(game.balls[0].velocity.x > 0.0);
|
||||||
|
|
||||||
|
// The holes do not capture while the wheel is replaced.
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: LOCK_HOLES[0].center,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
for _ in 0..20 {
|
||||||
|
game.fixed_update(&mut events);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
game.record_contacts[129..=133]
|
||||||
|
.iter()
|
||||||
|
.all(|contact| *contact != 2)
|
||||||
|
);
|
||||||
|
assert!(!game.balls[0].in_launcher);
|
||||||
|
|
||||||
|
// Balls keep moving during the animation, and it completes.
|
||||||
|
let mut panel = Game::new_with_seed(1, 7);
|
||||||
|
panel.panel_frame = Some(0);
|
||||||
|
panel.wheel_holes.fill(true);
|
||||||
|
panel.balls[0] = Ball {
|
||||||
|
position: vec2(160.0, 300.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let start = panel.balls[0].position;
|
||||||
|
panel.timer_tick(0.03, 3, &mut events);
|
||||||
|
assert_ne!(panel.balls[0].position, start);
|
||||||
|
for _ in 0..300 {
|
||||||
|
panel.timer_tick(0.03, 3, &mut events);
|
||||||
|
}
|
||||||
|
assert_eq!(panel.panel_frame, None);
|
||||||
|
assert_eq!(panel.wheel_holes, [false; 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filled_or_closed_lock_hole_kicks_out_a_ball_resting_in_its_pocket() {
|
||||||
|
// Found by multiball stress runs: the bottom of hole 131's pocket,
|
||||||
|
// cradled between the wheel hub and post 137.
|
||||||
|
let pocket = vec2(140.85, 65.2);
|
||||||
|
let hub = vec2(124.0, 76.0);
|
||||||
|
for closed_by_panel in [false, true] {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
if closed_by_panel {
|
||||||
|
game.panel_frame = Some(0);
|
||||||
|
} else {
|
||||||
|
game.record_contacts[131] = 99;
|
||||||
|
game.wheel_holes[2] = true;
|
||||||
|
}
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: pocket,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
for _ in 0..40 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
game.balls[0].position.distance(hub) > WHEEL_RIM_CONTACT_RADIUS,
|
||||||
|
"ball still in the wheel at {:?}",
|
||||||
|
game.balls[0].position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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);
|
||||||
@@ -5583,7 +5779,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fifth_multiball_lock_defers_panel_and_caps_survivor_reaward() {
|
fn fifth_multiball_lock_starts_the_panel_and_caps_survivor_reaward() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.wheel_holes = [true, true, true, true, false];
|
game.wheel_holes = [true, true, true, true, false];
|
||||||
game.record_contacts[129..=133].fill(2);
|
game.record_contacts[129..=133].fill(2);
|
||||||
@@ -5598,13 +5794,9 @@ mod tests {
|
|||||||
assert_eq!(game.player().score, 620_000);
|
assert_eq!(game.player().score, 620_000);
|
||||||
assert_eq!(game.player().secondary_score, 0);
|
assert_eq!(game.player().secondary_score, 0);
|
||||||
assert_eq!(game.player().score_multiplier, 2);
|
assert_eq!(game.player().score_multiplier, 2);
|
||||||
assert_eq!(game.panel_frame, None);
|
assert_eq!(game.panel_frame, Some(0));
|
||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
|
|
||||||
let mut reset_by_special_hole = game.clone();
|
|
||||||
reset_by_special_hole.reset_ball_to_launcher(0);
|
|
||||||
assert_eq!(reset_by_special_hole.panel_frame, Some(0));
|
|
||||||
|
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.complete_lock_hole(4, 1, &mut events),
|
game.complete_lock_hole(4, 1, &mut events),
|
||||||
|
|||||||
Reference in new issue
Block a user