The clone's moving-flipper gate used the cross-product operands in the opposite order, mirroring and narrowing the hit wedge. Right-flipper release also used record 81's first endpoint even though the original reads its second endpoint. Correct both geometry paths, retain the recovered swept tip bounds, and add live-binary boundary vectors plus a dense transition regression. Test Plan: - `cargo test --workspace --all-targets --all-features` -- passed (134 tests) - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `cargo build --profile production` -- passed - `LSAN_OPTIONS=detect_leaks=0 ASAN_OPTIONS=detect_leaks=0 bash original/tools/test_reconstructed_c.sh` -- passed - `python3 original/tools/audit_reconstruction.py --require-complete` -- passed - `git diff --cached --check` -- passed
447 lines
15 KiB
Rust
447 lines
15 KiB
Rust
//! Moving-flipper collision response reconstructed from `1000:7ed9`.
|
|
|
|
use crate::{original_physics::MilliVec, real48::Real48};
|
|
|
|
const SEARCH_RADIUS: i32 = 54_000;
|
|
const ONE: Real48 = Real48::from_bytes([0x81, 0, 0, 0, 0, 0]);
|
|
const TWO: Real48 = Real48::from_bytes([0x82, 0, 0, 0, 0, 0]);
|
|
const TWO_FIFTHS: Real48 =
|
|
Real48::from_bytes([0x7f, 0xcd, 0xcc, 0xcc, 0xcc, 0x4c]);
|
|
const THOUSAND: Real48 = Real48::from_bytes([0x8a, 0, 0, 0, 0, 0x7a]);
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum FlipperSide {
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct FlipperResponse {
|
|
pub velocity: MilliVec,
|
|
pub movement: MilliVec,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Geometry {
|
|
pivot: MilliVec,
|
|
negative_edge: MilliVec,
|
|
positive_edge: MilliVec,
|
|
}
|
|
|
|
impl Geometry {
|
|
fn for_side(side: FlipperSide, delta: i32) -> Self {
|
|
match side {
|
|
FlipperSide::Left => Self {
|
|
pivot: MilliVec {
|
|
x: 93_000,
|
|
y: 397_000_i32.wrapping_add(delta.wrapping_mul(15_000)),
|
|
},
|
|
negative_edge: MilliVec {
|
|
x: 161_000,
|
|
y: 413_000,
|
|
},
|
|
positive_edge: MilliVec {
|
|
x: if delta == 1 { 157_000 } else { 150_000 },
|
|
y: if delta == 1 { 384_000 } else { 427_000 },
|
|
},
|
|
},
|
|
FlipperSide::Right => Self {
|
|
pivot: MilliVec {
|
|
x: 220_000,
|
|
y: 397_000_i32.wrapping_add(delta.wrapping_mul(15_000)),
|
|
},
|
|
negative_edge: MilliVec {
|
|
x: 152_000,
|
|
y: 413_000,
|
|
},
|
|
// The binary reads record 81 point 1 but uses point 2 for this
|
|
// edge. On release that point is already in its raised state.
|
|
positive_edge: MilliVec {
|
|
x: if delta == 1 { 151_000 } else { 157_000 },
|
|
y: if delta == 1 { 378_000 } else { 427_000 },
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn isolated_c_fixture(side: FlipperSide, delta: i32) -> Self {
|
|
let mut geometry = Self::for_side(side, delta);
|
|
geometry.positive_edge = match side {
|
|
FlipperSide::Left => MilliVec {
|
|
x: 150_000,
|
|
y: 427_000,
|
|
},
|
|
FlipperSide::Right => MilliVec {
|
|
x: 157_000,
|
|
y: 427_000,
|
|
},
|
|
};
|
|
geometry
|
|
}
|
|
}
|
|
|
|
fn cross_for_edge(edge: MilliVec, pivot: MilliVec, ball: MilliVec) -> i32 {
|
|
let edge_from_ball_x = edge.x.wrapping_sub(ball.x) / 1_000;
|
|
let edge_from_pivot_y = edge.y.wrapping_sub(pivot.y) / 1_000;
|
|
let edge_from_ball_y = edge.y.wrapping_sub(ball.y) / 1_000;
|
|
let edge_from_pivot_x = edge.x.wrapping_sub(pivot.x) / 1_000;
|
|
// 1000:8208 computes C*D first, then computes A*B and subtracts C*D.
|
|
// Reversing these operands mirrors the complete moving-hit wedge.
|
|
edge_from_ball_x
|
|
.wrapping_mul(edge_from_pivot_y)
|
|
.wrapping_sub(edge_from_ball_y.wrapping_mul(edge_from_pivot_x))
|
|
}
|
|
|
|
fn collision_distance(ball: MilliVec, pivot: MilliVec) -> i32 {
|
|
let dx = Real48::from_i32(ball.x.wrapping_sub(pivot.x)).divide(THOUSAND);
|
|
let dy = Real48::from_i32(ball.y.wrapping_sub(pivot.y)).divide(THOUSAND);
|
|
dx.square()
|
|
.add(dy.square())
|
|
.sqrt()
|
|
.multiply(THOUSAND)
|
|
.round_i32()
|
|
}
|
|
|
|
fn contains(geometry: Geometry, side: FlipperSide, delta: i32, ball: MilliVec) -> Option<i32> {
|
|
let (first_edge, second_edge) = if delta == -1 {
|
|
(
|
|
MilliVec {
|
|
x: geometry.negative_edge.x,
|
|
y: geometry.negative_edge.y.wrapping_sub(43_000),
|
|
},
|
|
MilliVec {
|
|
x: geometry.negative_edge.x,
|
|
y: geometry.negative_edge.y.wrapping_add(12_000),
|
|
},
|
|
)
|
|
} else {
|
|
(
|
|
MilliVec {
|
|
x: geometry.positive_edge.x,
|
|
y: geometry.positive_edge.y.wrapping_sub(12_000),
|
|
},
|
|
MilliVec {
|
|
x: geometry.positive_edge.x,
|
|
y: geometry.positive_edge.y.wrapping_add(43_000),
|
|
},
|
|
)
|
|
};
|
|
if ball.x.wrapping_sub(geometry.pivot.x).wrapping_abs() >= SEARCH_RADIUS
|
|
|| ball.y.wrapping_sub(geometry.pivot.y).wrapping_abs() >= SEARCH_RADIUS
|
|
{
|
|
return None;
|
|
}
|
|
let distance = collision_distance(ball, geometry.pivot);
|
|
if distance > SEARCH_RADIUS {
|
|
return None;
|
|
}
|
|
let first = cross_for_edge(first_edge, geometry.pivot, ball);
|
|
let second = cross_for_edge(second_edge, geometry.pivot, ball);
|
|
let inside = match side {
|
|
FlipperSide::Left => first > -1_000 && second < 1_000 && ball.x > geometry.pivot.x,
|
|
FlipperSide::Right => first < 1_000 && second > -1_000 && ball.x < geometry.pivot.x,
|
|
};
|
|
inside.then_some(distance)
|
|
}
|
|
|
|
fn penetration(geometry: Geometry, delta: i32, ball: MilliVec) -> i32 {
|
|
let collision_y = if delta == -1 {
|
|
let edge_dx = geometry
|
|
.negative_edge
|
|
.x
|
|
.wrapping_sub(geometry.pivot.x)
|
|
.wrapping_abs();
|
|
if edge_dx > 0 {
|
|
geometry
|
|
.pivot
|
|
.y
|
|
.wrapping_sub(ball.x.wrapping_sub(geometry.pivot.x).wrapping_abs())
|
|
.wrapping_add(5_000)
|
|
} else {
|
|
geometry.negative_edge.y.wrapping_sub(43_000)
|
|
}
|
|
} else {
|
|
let edge_dx = geometry.positive_edge.x.wrapping_sub(geometry.pivot.x);
|
|
if edge_dx.wrapping_abs() > 0 {
|
|
let numerator = ball.x.wrapping_sub(geometry.pivot.x).wrapping_mul(
|
|
geometry
|
|
.positive_edge
|
|
.y
|
|
.wrapping_add(43_000)
|
|
.wrapping_sub(geometry.pivot.y),
|
|
);
|
|
geometry
|
|
.pivot
|
|
.y
|
|
.wrapping_sub(
|
|
Real48::from_i32(numerator)
|
|
.divide(Real48::from_i32(edge_dx))
|
|
.round_i32(),
|
|
)
|
|
} else {
|
|
geometry.positive_edge.y.wrapping_add(43_000)
|
|
}
|
|
};
|
|
let value = collision_y.wrapping_sub(ball.y);
|
|
if value < 0 {
|
|
value.wrapping_mul(delta).wrapping_neg()
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub fn moving_flipper_response(
|
|
ball: MilliVec,
|
|
velocity: MilliVec,
|
|
delta: i32,
|
|
side: FlipperSide,
|
|
response_normal: Real48,
|
|
maximum_speed: i32,
|
|
) -> Option<FlipperResponse> {
|
|
let geometry = Geometry::for_side(side, delta);
|
|
response_with_geometry(
|
|
ball,
|
|
velocity,
|
|
delta,
|
|
side,
|
|
response_normal,
|
|
maximum_speed,
|
|
geometry,
|
|
)
|
|
}
|
|
|
|
fn response_with_geometry(
|
|
ball: MilliVec,
|
|
velocity: MilliVec,
|
|
delta: i32,
|
|
side: FlipperSide,
|
|
response_normal: Real48,
|
|
maximum_speed: i32,
|
|
geometry: Geometry,
|
|
) -> Option<FlipperResponse> {
|
|
let distance = contains(geometry, side, delta, ball)?;
|
|
let penetration = penetration(geometry, delta, ball);
|
|
let mut normal_x = ball.x.wrapping_sub(geometry.pivot.x).wrapping_mul(delta);
|
|
let mut normal_y = ball
|
|
.y
|
|
.wrapping_sub(geometry.pivot.y)
|
|
.wrapping_add(4_000)
|
|
.wrapping_mul(delta);
|
|
if side == FlipperSide::Right {
|
|
normal_x = normal_x.wrapping_neg();
|
|
normal_y = normal_y.wrapping_neg();
|
|
}
|
|
|
|
let normal_x = Real48::from_i32(normal_x);
|
|
let normal_y = Real48::from_i32(normal_y);
|
|
let response_radius = Real48::from_i32(44_000);
|
|
let tangent_projection = Real48::from_i32(velocity.x)
|
|
.multiply(normal_y)
|
|
.subtract(Real48::from_i32(velocity.y).multiply(normal_x))
|
|
.divide(response_radius)
|
|
.round_i32();
|
|
let gain = Real48::from_i32(distance)
|
|
.divide(response_radius)
|
|
.sqrt()
|
|
.multiply(TWO)
|
|
.add(TWO_FIFTHS);
|
|
let tangent_projection = Real48::from_i32(tangent_projection)
|
|
.multiply(ONE.add(response_normal))
|
|
.round_i32()
|
|
.wrapping_add(Real48::from_i32(maximum_speed).multiply(gain).round_i32());
|
|
let delta_velocity_x = Real48::from_i32(tangent_projection)
|
|
.multiply(normal_y)
|
|
.subtract(Real48::ZERO)
|
|
.divide(response_radius)
|
|
.round_i32()
|
|
.wrapping_neg();
|
|
let delta_velocity_y = Real48::from_i32(tangent_projection)
|
|
.multiply(normal_x)
|
|
.divide(response_radius)
|
|
.round_i32();
|
|
let velocity = MilliVec {
|
|
x: velocity.x.wrapping_add(delta_velocity_x),
|
|
y: velocity.y.wrapping_add(delta_velocity_y),
|
|
};
|
|
if velocity.y == 0 {
|
|
return None;
|
|
}
|
|
let movement_x = Real48::from_i32(penetration.wrapping_mul(velocity.x))
|
|
.divide(Real48::from_i32(velocity.y))
|
|
.round_i32();
|
|
Some(FlipperResponse {
|
|
velocity,
|
|
movement: MilliVec {
|
|
x: movement_x,
|
|
y: penetration,
|
|
},
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn hash_u32(mut hash: u64, value: u32) -> u64 {
|
|
for byte in value.to_le_bytes() {
|
|
hash ^= u64::from(byte);
|
|
hash = hash.wrapping_mul(1_099_511_628_211);
|
|
}
|
|
hash
|
|
}
|
|
|
|
const fn i32_bits(value: i32) -> u32 {
|
|
u32::from_ne_bytes(value.to_ne_bytes())
|
|
}
|
|
|
|
fn dense_transition_digest(side: FlipperSide, delta: i32) -> (u32, u64) {
|
|
let velocities = [
|
|
MilliVec { x: 0, y: 2_000 },
|
|
MilliVec { x: 1_000, y: 2_000 },
|
|
MilliVec { x: -1_000, y: 2_000 },
|
|
MilliVec { x: 2_700, y: -2_700 },
|
|
];
|
|
let mut hash = 14_695_981_039_346_656_037_u64;
|
|
let mut hits = 0_u32;
|
|
for x in (40_000..=280_000).step_by(1_000) {
|
|
for y in (320_000..=470_000).step_by(1_000) {
|
|
for (velocity_index, input_velocity) in velocities.into_iter().enumerate() {
|
|
let response = moving_flipper_response(
|
|
MilliVec { x, y },
|
|
input_velocity,
|
|
delta,
|
|
side,
|
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
|
3_800,
|
|
);
|
|
let (hit, velocity, movement) = response.map_or(
|
|
(0_u32, input_velocity, MilliVec::default()),
|
|
|response| (1, response.velocity, response.movement),
|
|
);
|
|
hits += hit;
|
|
for value in [
|
|
i32_bits(x),
|
|
i32_bits(y),
|
|
u32::try_from(velocity_index).expect("four velocities fit u32"),
|
|
hit,
|
|
i32_bits(velocity.x),
|
|
i32_bits(velocity.y),
|
|
i32_bits(movement.x),
|
|
i32_bits(movement.y),
|
|
] {
|
|
hash = hash_u32(hash, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
(hits, hash)
|
|
}
|
|
|
|
#[test]
|
|
fn four_direction_vectors_match_the_reconstructed_c_harness() {
|
|
let cases = [
|
|
(
|
|
MilliVec { x: 104_000, y: 384_000 },
|
|
MilliVec { x: 1_000, y: 2_000 },
|
|
-1,
|
|
FlipperSide::Left,
|
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
|
MilliVec { x: 1_266, y: 1_511 },
|
|
MilliVec { x: -6_703, y: -8_000 },
|
|
),
|
|
(
|
|
MilliVec { x: 209_000, y: 419_000 },
|
|
MilliVec { x: -1_000, y: 2_000 },
|
|
1,
|
|
FlipperSide::Right,
|
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0x40]),
|
|
MilliVec { x: -737, y: 2_263 },
|
|
MilliVec { x: -5_578, y: 17_127 },
|
|
),
|
|
(
|
|
MilliVec { x: 104_000, y: 421_000 },
|
|
MilliVec { x: 1_000, y: 2_000 },
|
|
1,
|
|
FlipperSide::Left,
|
|
Real48::from_bytes([0x7f, 0, 0, 0, 0, 0]),
|
|
MilliVec { x: 622, y: 2_320 },
|
|
MilliVec { x: 5_414, y: 20_193 },
|
|
),
|
|
(
|
|
MilliVec { x: 209_000, y: 385_000 },
|
|
MilliVec { x: -1_000, y: 2_000 },
|
|
-1,
|
|
FlipperSide::Right,
|
|
Real48::ZERO,
|
|
MilliVec { x: -1_280, y: 1_560 },
|
|
MilliVec { x: 7_385, y: -9_000 },
|
|
),
|
|
];
|
|
for (ball, velocity, delta, side, response, expected_velocity, expected_movement) in cases {
|
|
let result = response_with_geometry(
|
|
ball,
|
|
velocity,
|
|
delta,
|
|
side,
|
|
response,
|
|
1_000,
|
|
Geometry::isolated_c_fixture(side, delta),
|
|
)
|
|
.expect("C fixture must contact the moving flipper");
|
|
assert_eq!(result.velocity, expected_velocity);
|
|
assert_eq!(result.movement, expected_movement);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn boundary_vectors_match_the_live_original_binary() {
|
|
for (ball, velocity, delta, side, expected_velocity, expected_movement) in [
|
|
(
|
|
MilliVec { x: 100_000, y: 370_000 },
|
|
MilliVec { x: 1_000, y: 2_000 },
|
|
-1,
|
|
FlipperSide::Left,
|
|
MilliVec { x: -189, y: 960 },
|
|
MilliVec::default(),
|
|
),
|
|
(
|
|
MilliVec { x: 209_000, y: 419_000 },
|
|
MilliVec { x: 1_000, y: 2_000 },
|
|
1,
|
|
FlipperSide::Right,
|
|
MilliVec { x: 2_133, y: 3_133 },
|
|
MilliVec { x: 5_743, y: 8_435 },
|
|
),
|
|
] {
|
|
let response = moving_flipper_response(
|
|
ball,
|
|
velocity,
|
|
delta,
|
|
side,
|
|
Real48::from_bytes([0x80, 0, 0, 0, 0, 0]),
|
|
3_800,
|
|
)
|
|
.expect("the live original hits this flipper boundary");
|
|
assert_eq!(response.velocity, expected_velocity);
|
|
assert_eq!(response.movement, expected_movement);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dense_transition_matrix_matches_the_reconstructed_c_oracle() {
|
|
for (side, delta, expected_hits, expected_hash) in [
|
|
(FlipperSide::Left, -1, 9_704, 0x280f_c478_572e_ccc5),
|
|
(FlipperSide::Left, 1, 10_256, 0x65f5_a7da_3013_c0f5),
|
|
(FlipperSide::Right, -1, 9_704, 0x875b_7dd6_1574_7fac),
|
|
(FlipperSide::Right, 1, 9_512, 0x4713_d619_16d8_0fe1),
|
|
] {
|
|
assert_eq!(
|
|
dense_transition_digest(side, delta),
|
|
(expected_hits, expected_hash),
|
|
"moving-flipper matrix mismatch for {side:?} delta {delta}"
|
|
);
|
|
}
|
|
}
|
|
}
|