fix(physics): restore binary record scan order
Interleave static ranges, type-three captures, type-four triggers, magnetic records, and dynamic ball records in exact ID order while tracking predicted position separately from mutable motion. Preserve type-three/dynamic broadphase and the raw stack quirk where fixed candidate slot one wins even when a later candidate is nearer; seal that behavior in both C and Rust harnesses. Test Plan: - bash original/tools/test_reconstructed_c.sh - python3 original/tools/audit_reconstruction.py --require-complete - cargo test --all-targets - cargo clippy --all-targets --all-features -- -D warnings - rumdl check original/C_RECONSTRUCTION_FINAL_AUDIT.md original/MECHANICS_PROGRESS.md tdkpin-rs/CHANGELOG.md tdkpin-rs/RECONSTRUCTION.md tdkpin-rs/README.md - git diff --check
This commit is contained in:
+381
-92
@@ -5,8 +5,8 @@ use crate::{
|
||||
original_physics::{
|
||||
CollisionMaterial, CollisionResponse, GRAVITY_MILLI_PER_STEP,
|
||||
MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS, StaticCollisionCandidate,
|
||||
ball_collision_response, capture_collision_candidate, circle_collision_candidate,
|
||||
line_collision_candidate, milli_distance, path_intersects_circle,
|
||||
ball_collision_response, capture_collision_candidate, circle_collision_candidate_at,
|
||||
line_collision_candidate_at, milli_distance, path_intersects_circle,
|
||||
},
|
||||
real48::Real48,
|
||||
table::{
|
||||
@@ -225,7 +225,15 @@ enum BallAction {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct SensorScanResult {
|
||||
action: BallAction,
|
||||
capture_candidate: Option<(u8, StaticCollisionCandidate)>,
|
||||
}
|
||||
|
||||
fn retain_first_collision(
|
||||
best: &mut Option<(u8, bool, StaticCollisionCandidate)>,
|
||||
candidate: (u8, bool, StaticCollisionCandidate),
|
||||
) {
|
||||
if best.is_none() {
|
||||
*best = Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -679,41 +687,32 @@ impl Game {
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
self.apply_magnetic_fields(old_position, &mut velocity);
|
||||
let movement_velocity = velocity;
|
||||
let mut best_static = self.find_static_collision_candidate(old_position, velocity);
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
let scan = self.check_sensor_objects_for_ball(
|
||||
let (best_static, scan, predicted) = self.scan_ordered_records_for_ball(
|
||||
&mut ball,
|
||||
1,
|
||||
ball_count,
|
||||
old_position,
|
||||
movement_velocity,
|
||||
&mut velocity,
|
||||
events,
|
||||
);
|
||||
if let Some((id, candidate)) = scan.capture_candidate
|
||||
&& best_static.is_none_or(|(_, _, closest)| {
|
||||
candidate.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
best_static = Some((id, false, candidate));
|
||||
}
|
||||
velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
let mut best_collision = best_static
|
||||
.map(|(id, is_wall, candidate)| (id, is_wall, candidate.resolve(velocity, ball.spin)));
|
||||
let mut transferred_secondary_velocity = None;
|
||||
if let Some(secondary) = initial_secondary {
|
||||
let other_position = MilliVec::from_position(secondary.position);
|
||||
let response = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
ball.spin,
|
||||
MilliVec::from_position(secondary.position),
|
||||
other_position,
|
||||
MilliVec::from_velocity_per_second(secondary.velocity),
|
||||
);
|
||||
if let Some(response) = response
|
||||
&& best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
if predicted.x >= other_position.x.wrapping_sub(21_000)
|
||||
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
||||
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
||||
&& predicted.y <= other_position.y.wrapping_add(21_000)
|
||||
&& let Some(response) = response
|
||||
&& best_collision.is_none()
|
||||
{
|
||||
transferred_secondary_velocity = Some(response.other_velocity);
|
||||
best_collision = Some((
|
||||
@@ -815,15 +814,166 @@ impl Game {
|
||||
collided
|
||||
}
|
||||
|
||||
fn find_static_collision_candidate(
|
||||
fn retain_static_range(
|
||||
&self,
|
||||
old_position: MilliVec,
|
||||
predicted: MilliVec,
|
||||
velocity: MilliVec,
|
||||
record_ids: std::ops::RangeInclusive<u8>,
|
||||
best: &mut Option<(u8, bool, StaticCollisionCandidate)>,
|
||||
) {
|
||||
if let Some(candidate) = self.find_static_collision_candidate_in_range(
|
||||
old_position,
|
||||
predicted,
|
||||
velocity,
|
||||
record_ids,
|
||||
) {
|
||||
retain_first_collision(best, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
fn scan_ordered_records_for_ball(
|
||||
&mut self,
|
||||
ball: &mut Ball,
|
||||
ball_number: u16,
|
||||
ball_count: u16,
|
||||
old_position: MilliVec,
|
||||
velocity: &mut MilliVec,
|
||||
events: &mut Vec<Event>,
|
||||
) -> (
|
||||
Option<(u8, bool, StaticCollisionCandidate)>,
|
||||
SensorScanResult,
|
||||
MilliVec,
|
||||
) {
|
||||
let mut predicted = old_position.add(*velocity);
|
||||
let mut best = None;
|
||||
let mut capture_candidate = None;
|
||||
let mut action = BallAction::Keep;
|
||||
|
||||
self.retain_static_range(old_position, predicted, *velocity, 1..=5, &mut best);
|
||||
if self.apply_magnetic_record(6, old_position, velocity) {
|
||||
predicted = old_position.add(*velocity);
|
||||
}
|
||||
self.retain_static_range(old_position, predicted, *velocity, 7..=88, &mut best);
|
||||
|
||||
if old_position.y < 250_000 && !self.claw.active {
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
match self.capture_record_step(
|
||||
ball,
|
||||
ball_number,
|
||||
ball_count,
|
||||
89,
|
||||
CLAW_TRIGGER_CENTER,
|
||||
CLAW_TRIGGER_RADIUS,
|
||||
old_position,
|
||||
predicted,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
) {
|
||||
CaptureStep::Complete if ball_count == 1 => {
|
||||
let terminal_frame = self.next_claw_terminal_frame();
|
||||
self.begin_claw_capture_after_hold(ball, terminal_frame, events);
|
||||
action = BallAction::Suspend;
|
||||
}
|
||||
CaptureStep::Complete => action = BallAction::Remove,
|
||||
CaptureStep::Outside | CaptureStep::Holding => {}
|
||||
}
|
||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
}
|
||||
|
||||
self.retain_static_range(old_position, predicted, *velocity, 90..=128, &mut best);
|
||||
if old_position.y < 250_000 {
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
if let Some(completed_action) = self.check_lock_holes(
|
||||
ball,
|
||||
ball_number,
|
||||
ball_count,
|
||||
old_position,
|
||||
predicted,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
) {
|
||||
action = completed_action;
|
||||
}
|
||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
}
|
||||
|
||||
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
||||
if old_position.y < 250_000 {
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
let predicted_velocity = MilliVec {
|
||||
x: predicted.x.wrapping_sub(old_position.x),
|
||||
y: predicted.y.wrapping_sub(old_position.y),
|
||||
};
|
||||
self.check_target_sensors(
|
||||
ball,
|
||||
old_position,
|
||||
predicted_velocity,
|
||||
140..=147,
|
||||
events,
|
||||
);
|
||||
if let Some(completed_action) = self.check_wheel_reset(
|
||||
ball,
|
||||
ball_number,
|
||||
ball_count,
|
||||
old_position,
|
||||
predicted,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
) {
|
||||
action = completed_action;
|
||||
}
|
||||
self.check_effect_sensor(
|
||||
ball,
|
||||
ball_number,
|
||||
old_position,
|
||||
predicted_velocity,
|
||||
events,
|
||||
);
|
||||
self.check_target_sensors(
|
||||
ball,
|
||||
old_position,
|
||||
predicted_velocity,
|
||||
150..=152,
|
||||
events,
|
||||
);
|
||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
}
|
||||
|
||||
if self.apply_magnetic_record(153, old_position, velocity) {
|
||||
predicted = old_position.add(*velocity);
|
||||
}
|
||||
if self.apply_magnetic_record(154, old_position, velocity) {
|
||||
predicted = old_position.add(*velocity);
|
||||
}
|
||||
self.retain_static_range(old_position, predicted, *velocity, 155..=173, &mut best);
|
||||
|
||||
if let Some((id, candidate)) = capture_candidate
|
||||
&& best.is_none_or(|(static_id, _, _)| id < static_id)
|
||||
{
|
||||
best = Some((id, false, candidate));
|
||||
}
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
(
|
||||
best,
|
||||
SensorScanResult {
|
||||
action,
|
||||
},
|
||||
predicted,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_static_collision_candidate_in_range(
|
||||
&self,
|
||||
old_position: MilliVec,
|
||||
predicted: MilliVec,
|
||||
velocity: MilliVec,
|
||||
record_ids: std::ops::RangeInclusive<u8>,
|
||||
) -> Option<(u8, bool, StaticCollisionCandidate)> {
|
||||
let mut best = None;
|
||||
let layer = if old_position.y < 250_000 { 0x01 } else { 0x02 };
|
||||
let predicted = old_position.add(velocity);
|
||||
for object_id in 1..=175 {
|
||||
for object_id in record_ids {
|
||||
if !self.object_active[usize::from(object_id)]
|
||||
|| self.claw.active && (12..=20).contains(&object_id)
|
||||
{
|
||||
@@ -856,16 +1006,15 @@ impl Game {
|
||||
f64::from(wall.response_kick),
|
||||
)
|
||||
};
|
||||
if let Some(candidate) = line_collision_candidate(
|
||||
if let Some(candidate) = line_collision_candidate_at(
|
||||
old_position,
|
||||
predicted,
|
||||
velocity,
|
||||
segment.start,
|
||||
segment.end,
|
||||
material,
|
||||
) && best.is_none_or(|(_, _, closest): (u8, bool, StaticCollisionCandidate)| {
|
||||
candidate.surface_distance <= closest.surface_distance
|
||||
}) {
|
||||
best = Some((wall.id, true, candidate));
|
||||
) {
|
||||
retain_first_collision(&mut best, (wall.id, true, candidate));
|
||||
}
|
||||
}
|
||||
let circle = PASSIVE_CIRCLES
|
||||
@@ -885,8 +1034,9 @@ impl Game {
|
||||
&& predicted.y >= center.y.wrapping_sub(margin)
|
||||
&& predicted.y <= center.y.wrapping_add(margin)
|
||||
}
|
||||
&& let Some(candidate) = circle_collision_candidate(
|
||||
&& let Some(candidate) = circle_collision_candidate_at(
|
||||
old_position,
|
||||
predicted,
|
||||
velocity,
|
||||
self.live_circle_center(circle.id, circle.center),
|
||||
circle.contact_radius,
|
||||
@@ -900,16 +1050,28 @@ impl Game {
|
||||
},
|
||||
),
|
||||
)
|
||||
&& best.is_none_or(|(_, _, closest)| {
|
||||
candidate.surface_distance <= closest.surface_distance
|
||||
})
|
||||
&& best.is_none()
|
||||
{
|
||||
best = Some((circle.id, false, candidate));
|
||||
retain_first_collision(&mut best, (circle.id, false, candidate));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn find_static_collision_candidate(
|
||||
&self,
|
||||
old_position: MilliVec,
|
||||
velocity: MilliVec,
|
||||
) -> Option<(u8, bool, StaticCollisionCandidate)> {
|
||||
self.find_static_collision_candidate_in_range(
|
||||
old_position,
|
||||
old_position.add(velocity),
|
||||
velocity,
|
||||
1..=175,
|
||||
)
|
||||
}
|
||||
|
||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) -> bool {
|
||||
self.advance_secondary_ball_slot(events, true)
|
||||
}
|
||||
@@ -929,41 +1091,32 @@ impl Game {
|
||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
self.apply_magnetic_fields(old_position, &mut velocity);
|
||||
let movement_velocity = velocity;
|
||||
let mut best_static = self.find_static_collision_candidate(old_position, velocity);
|
||||
ball.velocity = velocity.to_velocity_per_second();
|
||||
let scan = self.check_sensor_objects_for_ball(
|
||||
let (best_static, scan, predicted) = self.scan_ordered_records_for_ball(
|
||||
&mut ball,
|
||||
2,
|
||||
2,
|
||||
old_position,
|
||||
movement_velocity,
|
||||
&mut velocity,
|
||||
events,
|
||||
);
|
||||
if let Some((id, candidate)) = scan.capture_candidate
|
||||
&& best_static.is_none_or(|(_, _, closest)| {
|
||||
candidate.surface_distance <= closest.surface_distance
|
||||
})
|
||||
{
|
||||
best_static = Some((id, false, candidate));
|
||||
}
|
||||
velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||
let mut best_collision = best_static
|
||||
.map(|(id, is_wall, candidate)| (id, is_wall, candidate.resolve(velocity, ball.spin)));
|
||||
|
||||
let mut transferred_primary_velocity = None;
|
||||
let other_position = MilliVec::from_position(primary_position);
|
||||
if primary_slot_active
|
||||
&& predicted.x >= other_position.x.wrapping_sub(21_000)
|
||||
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
||||
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
||||
&& predicted.y <= other_position.y.wrapping_add(21_000)
|
||||
&& let Some(response) = ball_collision_response(
|
||||
old_position,
|
||||
velocity,
|
||||
ball.spin,
|
||||
MilliVec::from_position(primary_position),
|
||||
other_position,
|
||||
MilliVec::from_velocity_per_second(primary_velocity),
|
||||
)
|
||||
&& best_collision.is_none_or(|(_, _, closest)| {
|
||||
response.surface_distance <= closest.surface_distance
|
||||
})
|
||||
&& best_collision.is_none()
|
||||
{
|
||||
transferred_primary_velocity = Some(response.other_velocity);
|
||||
best_collision = Some((
|
||||
@@ -1153,6 +1306,7 @@ impl Game {
|
||||
scan.action
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn check_sensor_objects_for_ball(
|
||||
&mut self,
|
||||
ball: &mut Ball,
|
||||
@@ -1164,10 +1318,10 @@ impl Game {
|
||||
) -> SensorScanResult {
|
||||
let mut capture_candidate = None;
|
||||
let mut action = BallAction::Keep;
|
||||
let predicted_position = old_position.add(movement_velocity);
|
||||
if old_position.y >= 250_000 {
|
||||
return SensorScanResult {
|
||||
action: BallAction::Keep,
|
||||
capture_candidate,
|
||||
};
|
||||
}
|
||||
if !self.claw.active {
|
||||
@@ -1179,6 +1333,7 @@ impl Game {
|
||||
CLAW_TRIGGER_CENTER,
|
||||
CLAW_TRIGGER_RADIUS,
|
||||
old_position,
|
||||
predicted_position,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
) {
|
||||
@@ -1201,6 +1356,7 @@ impl Game {
|
||||
ball_number,
|
||||
ball_count,
|
||||
old_position,
|
||||
predicted_position,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
)
|
||||
@@ -1220,6 +1376,7 @@ impl Game {
|
||||
ball_number,
|
||||
ball_count,
|
||||
old_position,
|
||||
predicted_position,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
)
|
||||
@@ -1242,7 +1399,6 @@ impl Game {
|
||||
);
|
||||
SensorScanResult {
|
||||
action,
|
||||
capture_candidate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,6 +1412,7 @@ impl Game {
|
||||
center: Vec2,
|
||||
radius: f32,
|
||||
previous_position: MilliVec,
|
||||
predicted_position: MilliVec,
|
||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||
events: &mut Vec<Event>,
|
||||
) -> CaptureStep {
|
||||
@@ -1264,6 +1421,14 @@ impl Game {
|
||||
let center = MilliVec::from_position(center_view);
|
||||
let radius_view = radius;
|
||||
let radius = (radius_view * 1_000.0).round() as i32;
|
||||
let broadphase_margin = MilliVec::from_position(vec2(radius_view + 5.0, 0.0)).x;
|
||||
if predicted_position.x < center.x.wrapping_sub(broadphase_margin)
|
||||
|| predicted_position.x > center.x.wrapping_add(broadphase_margin)
|
||||
|| predicted_position.y < center.y.wrapping_sub(broadphase_margin)
|
||||
|| predicted_position.y > center.y.wrapping_add(broadphase_margin)
|
||||
{
|
||||
return CaptureStep::Outside;
|
||||
}
|
||||
let dx = center.x.wrapping_sub(current_position.x);
|
||||
let dy = center
|
||||
.y
|
||||
@@ -1340,21 +1505,21 @@ impl Game {
|
||||
radius_view,
|
||||
CollisionMaterial::line(0.6, 0.0),
|
||||
)
|
||||
&& best_capture.is_none_or(|(_, closest)| {
|
||||
candidate.surface_distance <= closest.surface_distance
|
||||
})
|
||||
&& best_capture.is_none()
|
||||
{
|
||||
*best_capture = Some((record_id, candidate));
|
||||
}
|
||||
CaptureStep::Outside
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn check_lock_holes(
|
||||
&mut self,
|
||||
ball: &mut Ball,
|
||||
ball_number: u16,
|
||||
ball_count: u16,
|
||||
old_position: MilliVec,
|
||||
predicted_position: MilliVec,
|
||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||
events: &mut Vec<Event>,
|
||||
) -> Option<BallAction> {
|
||||
@@ -1367,6 +1532,7 @@ impl Game {
|
||||
sensor.center,
|
||||
sensor.radius,
|
||||
old_position,
|
||||
predicted_position,
|
||||
best_capture,
|
||||
events,
|
||||
) {
|
||||
@@ -1397,12 +1563,14 @@ impl Game {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn check_wheel_reset(
|
||||
&mut self,
|
||||
ball: &mut Ball,
|
||||
ball_number: u16,
|
||||
ball_count: u16,
|
||||
old_position: MilliVec,
|
||||
predicted_position: MilliVec,
|
||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||
events: &mut Vec<Event>,
|
||||
) -> Option<BallAction> {
|
||||
@@ -1414,6 +1582,7 @@ impl Game {
|
||||
WHEEL_RESET_SENSOR.center,
|
||||
WHEEL_RESET_SENSOR.radius,
|
||||
old_position,
|
||||
predicted_position,
|
||||
best_capture,
|
||||
events,
|
||||
) {
|
||||
@@ -1546,44 +1715,57 @@ impl Game {
|
||||
self.launcher_velocity_milli = 0;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_magnetic_fields(&mut self, old_position: MilliVec, velocity: &mut MilliVec) {
|
||||
for (object_id, min_x, min_y, max_x, max_y) in [
|
||||
(6, 143_000, 421_000, 169_000, 452_000),
|
||||
(153, 1_000, 315_000, 32_000, 389_000),
|
||||
(154, 278_000, 315_000, 312_000, 387_000),
|
||||
] {
|
||||
if !self.object_active[object_id] {
|
||||
continue;
|
||||
}
|
||||
if old_position.x < min_x
|
||||
|| old_position.x > max_x
|
||||
|| old_position.y < min_y
|
||||
|| old_position.y > max_y
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(damping).round_i32();
|
||||
let vertical_factor = Real48::from_i32(1).subtract(
|
||||
self.random
|
||||
.real48()
|
||||
.multiply(Real48::from_bytes([0x7f, 0x9a, 0x99, 0x99, 0x99, 0x19])),
|
||||
);
|
||||
velocity.y = Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
.multiply(vertical_factor)
|
||||
.round_i32()
|
||||
.wrapping_neg();
|
||||
let predicted = old_position.add(*velocity);
|
||||
if predicted.x < min_x
|
||||
|| predicted.x > max_x
|
||||
|| predicted.y < min_y
|
||||
|| predicted.y > max_y
|
||||
{
|
||||
self.object_active[object_id] = false;
|
||||
}
|
||||
for object_id in [6, 153, 154] {
|
||||
self.apply_magnetic_record(object_id, old_position, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_magnetic_record(
|
||||
&mut self,
|
||||
object_id: usize,
|
||||
old_position: MilliVec,
|
||||
velocity: &mut MilliVec,
|
||||
) -> bool {
|
||||
if self.tilted || !self.object_active[object_id] {
|
||||
return false;
|
||||
}
|
||||
let (min_x, min_y, max_x, max_y) = match object_id {
|
||||
6 => (143_000, 421_000, 169_000, 452_000),
|
||||
153 => (1_000, 315_000, 32_000, 389_000),
|
||||
154 => (278_000, 315_000, 312_000, 387_000),
|
||||
_ => unreachable!("only the three rectangular type-four records are magnetic"),
|
||||
};
|
||||
if old_position.x < min_x
|
||||
|| old_position.x > max_x
|
||||
|| old_position.y < min_y
|
||||
|| old_position.y > max_y
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||
velocity.x = Real48::from_i32(velocity.x).multiply(damping).round_i32();
|
||||
let vertical_factor = Real48::from_i32(1).subtract(
|
||||
self.random
|
||||
.real48()
|
||||
.multiply(Real48::from_bytes([0x7f, 0x9a, 0x99, 0x99, 0x99, 0x19])),
|
||||
);
|
||||
velocity.y = Real48::from_i32(MAXIMUM_SPEED_MILLI_PER_STEP)
|
||||
.multiply(vertical_factor)
|
||||
.round_i32()
|
||||
.wrapping_neg();
|
||||
let predicted = old_position.add(*velocity);
|
||||
if predicted.x < min_x
|
||||
|| predicted.x > max_x
|
||||
|| predicted.y < min_y
|
||||
|| predicted.y > max_y
|
||||
{
|
||||
self.object_active[object_id] = false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn magnetic_field_active(&self, object_id: usize) -> bool {
|
||||
debug_assert!([6, 153, 154].contains(&object_id));
|
||||
self.object_active[object_id]
|
||||
@@ -1882,6 +2064,7 @@ mod tests {
|
||||
) -> CaptureStep {
|
||||
let mut ball = game.ball;
|
||||
let mut capture_candidate = None;
|
||||
let predicted_position = previous_position.add(MilliVec::from_velocity_per_second(ball.velocity));
|
||||
let result = game.capture_record_step(
|
||||
&mut ball,
|
||||
1,
|
||||
@@ -1890,6 +2073,7 @@ mod tests {
|
||||
center,
|
||||
radius,
|
||||
previous_position,
|
||||
predicted_position,
|
||||
&mut capture_candidate,
|
||||
events,
|
||||
);
|
||||
@@ -2544,6 +2728,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_detected_record_wins_even_when_a_later_candidate_is_nearer() {
|
||||
let old = MilliVec::default();
|
||||
let velocity = MilliVec { x: 10_000, y: 0 };
|
||||
let predicted = old.add(velocity);
|
||||
let material = CollisionMaterial::line(0.6, 0.1);
|
||||
let first = line_collision_candidate_at(
|
||||
old,
|
||||
predicted,
|
||||
velocity,
|
||||
vec2(8.0, 1.0),
|
||||
vec2(8.0, -1.0),
|
||||
material,
|
||||
)
|
||||
.expect("the first record must detect the farther rail");
|
||||
let later = line_collision_candidate_at(
|
||||
old,
|
||||
predicted,
|
||||
velocity,
|
||||
vec2(2.0, 1.0),
|
||||
vec2(2.0, -1.0),
|
||||
material,
|
||||
)
|
||||
.expect("the later record must detect the nearer rail");
|
||||
assert!(later.surface_distance < first.surface_distance);
|
||||
let mut retained = None;
|
||||
|
||||
retain_first_collision(&mut retained, (10, true, first));
|
||||
retain_first_collision(&mut retained, (20, true, later));
|
||||
|
||||
let retained = retained.expect("one candidate must remain");
|
||||
assert_eq!(retained.0, 10);
|
||||
assert_eq!(retained.2.surface_distance, 8_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bumper_base_score_and_kick_require_the_recovered_speed_threshold() {
|
||||
let mut weak = Game::new(1);
|
||||
@@ -3112,6 +3331,60 @@ mod tests {
|
||||
|
||||
assert_eq!(exiting_sideways.x, -1_800);
|
||||
assert!(!game.object_active[153]);
|
||||
|
||||
game.object_active[153] = true;
|
||||
game.tilted = true;
|
||||
let tilted_seed = game.random.seed();
|
||||
let mut tilted_velocity = MilliVec { x: 200, y: 300 };
|
||||
game.apply_magnetic_fields(
|
||||
MilliVec {
|
||||
x: 15_000,
|
||||
y: 350_000,
|
||||
},
|
||||
&mut tilted_velocity,
|
||||
);
|
||||
assert_eq!(tilted_velocity, MilliVec { x: 200, y: 300 });
|
||||
assert_eq!(game.random.seed(), tilted_seed);
|
||||
assert!(game.object_active[153]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_six_mutates_motion_after_earlier_candidate_detection() {
|
||||
let mut game = Game::new_with_seed(1, 7);
|
||||
game.object_active.fill(false);
|
||||
game.object_active[5] = true;
|
||||
game.object_active[6] = true;
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = vec2(149.0, 430.0);
|
||||
game.ball.velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
||||
let old_position = MilliVec::from_position(game.ball.position);
|
||||
|
||||
let mut expected_game = game.clone();
|
||||
let mut expected_motion = MilliVec { x: -2_000, y: 15 };
|
||||
let candidate = expected_game
|
||||
.find_static_collision_candidate_in_range(
|
||||
old_position,
|
||||
old_position.add(expected_motion),
|
||||
expected_motion,
|
||||
1..=5,
|
||||
)
|
||||
.expect("record five must be detected before record six");
|
||||
assert_eq!(candidate.0, 5);
|
||||
assert!(expected_game.apply_magnetic_record(6, old_position, &mut expected_motion));
|
||||
let expected_response = candidate
|
||||
.2
|
||||
.resolve(expected_motion, expected_game.ball.spin);
|
||||
let mut expected_velocity = expected_response.velocity;
|
||||
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||
|
||||
assert!(game.fixed_update(STEP_SECONDS, &mut Vec::new()));
|
||||
|
||||
assert_eq!(game.last_collision_id, Some(5));
|
||||
assert_eq!(
|
||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
||||
expected_velocity
|
||||
);
|
||||
assert_eq!(game.random.seed(), expected_game.random.seed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3269,6 +3542,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_three_broadphase_skip_preserves_contact_state() {
|
||||
let sensor = LOCK_HOLES[0];
|
||||
let mut game = Game::new(1);
|
||||
game.object_active.fill(false);
|
||||
game.object_active[usize::from(sensor.id)] = true;
|
||||
game.record_contacts[usize::from(sensor.id)] = 1;
|
||||
game.ball.in_launcher = false;
|
||||
game.ball.position = sensor.center + vec2(23.0, 0.0);
|
||||
game.ball.velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
|
||||
|
||||
game.fixed_update(STEP_SECONDS, &mut Vec::new());
|
||||
|
||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_type_four_randomizes_motion_before_position_publication() {
|
||||
let sensor = TARGET_SENSORS
|
||||
|
||||
Reference in New Issue
Block a user