use crate::movement::{braking_distance, tick_movement};
const DT: f64 = 1.0 / 60.0;
const MAX_SPEED: f64 = 2.0;
const ACCELERATION: f64 = 1.5;
const DECELERATION: f64 = 2.0;
#[test]
fn already_at_target() {
let result = tick_movement(5.0, 0.0, 5.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(result.arrived);
assert!((result.position - 5.0).abs() < 1e-9);
assert!((result.velocity).abs() < 1e-9);
}
#[test]
fn accelerate_from_rest() {
let result = tick_movement(0.0, 0.0, 100.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(!result.arrived);
assert!(result.velocity > 0.0, "velocity should be positive");
assert!(result.position > 0.0, "position should have advanced");
}
#[test]
fn decelerate_to_stop() {
let mut pos = 9.5;
let mut vel = 1.5;
let target = 10.0;
for _ in 0..600 {
let r = tick_movement(pos, vel, target, MAX_SPEED, ACCELERATION, DECELERATION, DT);
pos = r.position;
vel = r.velocity;
if r.arrived {
assert!((pos - target).abs() < 1e-9);
assert!(vel.abs() < 1e-9);
return;
}
}
panic!("did not arrive within 600 ticks");
}
#[test]
fn short_distance_triangle_profile() {
let mut pos = 0.0;
let mut vel = 0.0;
let target = 0.5;
let mut peak_speed: f64 = 0.0;
for _ in 0..600 {
let r = tick_movement(pos, vel, target, MAX_SPEED, ACCELERATION, DECELERATION, DT);
pos = r.position;
vel = r.velocity;
peak_speed = peak_speed.max(vel.abs());
if r.arrived {
assert!((pos - target).abs() < 1e-9);
assert!(
peak_speed < MAX_SPEED,
"should not reach max_speed on short trip, peak was {peak_speed}"
);
return;
}
}
panic!("did not arrive within 600 ticks");
}
#[test]
fn full_trip() {
let mut pos = 0.0;
let mut vel = 0.0;
let target = 15.0;
let mut ticks = 0u32;
loop {
let r = tick_movement(pos, vel, target, MAX_SPEED, ACCELERATION, DECELERATION, DT);
pos = r.position;
vel = r.velocity;
ticks += 1;
if r.arrived {
assert!((pos - target).abs() < 1e-9);
assert!(vel.abs() < 1e-9);
assert!(
ticks > 100 && ticks < 1200,
"trip took {ticks} ticks, seems unreasonable"
);
return;
}
assert!(ticks < 2000, "did not arrive within 2000 ticks");
}
}
#[test]
fn overshoot_prevention() {
let result = tick_movement(9.99, 2.0, 10.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(result.arrived, "should snap to target on overshoot");
assert!((result.position - 10.0).abs() < 1e-9);
assert!(result.velocity.abs() < 1e-9);
}
#[test]
fn braking_distance_matches_kinematic_formula() {
assert!((braking_distance(2.0, 2.0) - 1.0).abs() < 1e-9);
assert!((braking_distance(10.0, 2.0) - 25.0).abs() < 1e-9);
let d1 = braking_distance(3.0, 2.0);
let d2 = braking_distance(6.0, 2.0);
assert!(
(d2 / d1 - 4.0).abs() < 1e-9,
"doubling velocity should 4× the braking distance, got {d2}/{d1}"
);
assert_eq!(braking_distance(0.0, 2.0), 0.0);
assert_eq!(braking_distance(10.0, 0.0), 0.0);
assert_eq!(braking_distance(10.0, -2.0), 0.0);
assert!((braking_distance(-5.0, 2.0) - braking_distance(5.0, 2.0)).abs() < 1e-9);
}
#[test]
fn tick_movement_exact_single_step_from_rest() {
let r = tick_movement(0.0, 0.0, 100.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
let expected_v = ACCELERATION * DT; let expected_p = expected_v * DT; assert!(
(r.velocity - expected_v).abs() < 1e-12,
"velocity after one tick: expected {expected_v}, got {}",
r.velocity
);
assert!(
(r.position - expected_p).abs() < 1e-12,
"position after one tick: expected {expected_p}, got {}",
r.position
);
assert!(!r.arrived);
}
#[test]
fn tick_movement_caps_velocity_at_max_speed() {
let start_v = MAX_SPEED - 0.001;
let r = tick_movement(
0.0,
start_v,
1000.0,
MAX_SPEED,
ACCELERATION,
DECELERATION,
DT,
);
assert!(
(r.velocity - MAX_SPEED).abs() < 1e-9,
"velocity should clamp to max_speed = {MAX_SPEED}, got {}",
r.velocity
);
}
#[test]
fn tick_movement_cruise_phase_holds_max_speed() {
let r = tick_movement(
0.0,
MAX_SPEED,
1000.0,
MAX_SPEED,
ACCELERATION,
DECELERATION,
DT,
);
assert!(
(r.velocity - MAX_SPEED).abs() < 1e-9,
"cruise velocity should equal max_speed"
);
let expected_p = MAX_SPEED * DT;
assert!(
(r.position - expected_p).abs() < 1e-12,
"position advances by v·dt during cruise"
);
}
#[test]
fn tick_movement_snaps_to_target_on_overshoot() {
let r = tick_movement(9.999, 2.0, 10.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(r.arrived);
assert!(
(r.position - 10.0).abs() < 1e-9,
"snap to exact target, got {}",
r.position
);
assert!(
r.velocity.abs() < 1e-9,
"velocity zeroed on snap, got {}",
r.velocity
);
}
#[test]
fn tick_movement_decelerates_near_target() {
let r = tick_movement(9.0, 2.0, 10.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
let decel_step = DECELERATION * DT;
let expected_v = 2.0 - decel_step;
assert!(
(r.velocity - expected_v).abs() < 1e-9,
"should decelerate by decel·dt: expected {expected_v}, got {}",
r.velocity
);
assert!(!r.arrived);
}
#[test]
fn tick_movement_zero_sign_when_already_at_target() {
let r = tick_movement(5.0, 0.0, 5.0, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(r.arrived);
assert!(
(r.position - 5.0).abs() < 1e-9,
"position should be target, got {}",
r.position
);
assert!(
r.velocity.abs() < 1e-9,
"velocity should be zero, got {}",
r.velocity
);
}
#[test]
fn moving_downward() {
let mut pos = 10.0;
let mut vel = 0.0;
let target = 3.0;
let r = tick_movement(pos, vel, target, MAX_SPEED, ACCELERATION, DECELERATION, DT);
assert!(
r.velocity < 0.0,
"velocity should be negative when moving down"
);
pos = r.position;
vel = r.velocity;
for _ in 0..2000 {
let r = tick_movement(pos, vel, target, MAX_SPEED, ACCELERATION, DECELERATION, DT);
pos = r.position;
vel = r.velocity;
if r.arrived {
assert!((pos - target).abs() < 1e-9);
return;
}
}
panic!("did not arrive within 2000 ticks");
}
#[test]
fn dt_zero_does_not_change_state() {
let r = tick_movement(5.0, 1.0, 10.0, MAX_SPEED, ACCELERATION, DECELERATION, 0.0);
assert!(
!r.arrived,
"should not arrive with dt=0 when far from target"
);
assert!(
(r.position - 5.0).abs() < 1e-9,
"position should not change with dt=0, got {}",
r.position
);
assert!(
r.velocity.abs() <= MAX_SPEED + 1e-9,
"velocity must not exceed max_speed with dt=0"
);
assert!(!r.position.is_nan(), "position must not be NaN");
assert!(!r.velocity.is_nan(), "velocity must not be NaN");
}
#[test]
fn very_large_dt_arrives_without_overshoot() {
let r = tick_movement(0.0, 0.0, 10.0, MAX_SPEED, ACCELERATION, DECELERATION, 1e6);
assert!(r.arrived, "should arrive with very large dt");
assert!(
(r.position - 10.0).abs() < 1e-9,
"position should be exactly at target after large dt, got {}",
r.position
);
assert!(
r.velocity.abs() < 1e-9,
"velocity should be zero on arrival, got {}",
r.velocity
);
}
#[test]
fn very_large_dt_downward() {
let r = tick_movement(100.0, 0.0, 3.0, MAX_SPEED, ACCELERATION, DECELERATION, 1e6);
assert!(r.arrived, "should arrive with very large dt (downward)");
assert!(
(r.position - 3.0).abs() < 1e-9,
"position should be at target, got {}",
r.position
);
}
#[test]
fn very_small_dt_makes_minimal_progress() {
let r = tick_movement(
0.0,
0.0,
100.0,
MAX_SPEED,
ACCELERATION,
DECELERATION,
1e-15,
);
assert!(!r.arrived, "should not arrive with tiny dt");
assert!(!r.position.is_nan(), "position must not be NaN");
assert!(!r.velocity.is_nan(), "velocity must not be NaN");
assert!(
r.position >= 0.0,
"position should not go backward, got {}",
r.position
);
assert!(
r.velocity >= 0.0,
"velocity should not be negative when heading up, got {}",
r.velocity
);
}