fn fabs(v: f32) -> f32 {
if v < 0.0 {
-v
} else {
v
}
}
fn floor_i16(v: f32) -> i16 {
let t = v as i16;
if (t as f32) > v {
t - 1
} else {
t
}
}
#[derive(Debug, Clone, Copy)]
pub struct Body {
x: f32,
y: f32,
rx: i16,
ry: i16,
}
impl Body {
pub fn new(x: f32, y: f32) -> Self {
Self {
x,
y,
rx: floor_i16(x),
ry: floor_i16(y),
}
}
pub fn move_by(&mut self, dx: f32, dy: f32) {
self.x += dx;
self.y += dy;
let fx = floor_i16(self.x);
let fy = floor_i16(self.y);
let (ax, ay) = (fabs(dx), fabs(dy));
if ax >= 1.0 || ay >= 1.0 {
self.rx = fx;
self.ry = fy;
return;
}
if ax >= ay {
let major_stepped = fx != self.rx;
self.rx = fx;
let lag = fy - self.ry;
if lag != 0 && (major_stepped || lag.abs() >= 2) {
self.ry += lag.signum();
}
} else {
let major_stepped = fy != self.ry;
self.ry = fy;
let lag = fx - self.rx;
if lag != 0 && (major_stepped || lag.abs() >= 2) {
self.rx += lag.signum();
}
}
}
pub fn x(&self) -> f32 {
self.x
}
pub fn y(&self) -> f32 {
self.y
}
pub fn pos(&self) -> (f32, f32) {
(self.x, self.y)
}
pub fn draw_x(&self) -> i16 {
self.rx
}
pub fn draw_y(&self) -> i16 {
self.ry
}
pub fn draw_pos(&self) -> (i16, i16) {
(self.rx, self.ry)
}
pub fn set_pos(&mut self, x: f32, y: f32) {
self.x = x;
self.y = y;
self.rx = floor_i16(x);
self.ry = floor_i16(y);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn classify(path: &[(i16, i16)]) -> String {
path.windows(2)
.map(|w| {
let (dx, dy) = (w[1].0 - w[0].0, w[1].1 - w[0].1);
match (dx != 0, dy != 0) {
(true, true) => 'D',
(true, false) => 'H',
(false, true) => 'V',
(false, false) => '.',
}
})
.collect()
}
fn body_path(x0: f32, y0: f32, vx: f32, vy: f32, n: usize) -> Vec<(i16, i16)> {
let mut b = Body::new(x0, y0);
let mut path = Vec::with_capacity(n);
for _ in 0..n {
path.push((b.draw_x(), b.draw_y()));
b.move_by(vx, vy);
}
path
}
fn naive_path(x0: f32, y0: f32, vx: f32, vy: f32, n: usize) -> Vec<(i16, i16)> {
let (mut x, mut y) = (x0, y0);
let mut path = Vec::with_capacity(n);
for _ in 0..n {
path.push((floor_i16(x), floor_i16(y)));
x += vx;
y += vy;
}
path
}
fn minor_never_steps_alone(path: &[(i16, i16)], vx: f32, vy: f32) -> bool {
let x_major = fabs(vx) >= fabs(vy);
path.windows(2).all(|w| {
let (dx, dy) = (w[1].0 - w[0].0, w[1].1 - w[0].1);
let (major, minor) = if x_major { (dx, dy) } else { (dy, dx) };
minor == 0 || major != 0
})
}
#[test]
fn button_diagonal_is_a_clean_staircase_at_any_phase() {
for &(speed, y0) in &[(0.5_f32, 10.5_f32), (0.7, 10.3), (0.6, 10.91)] {
let naive = classify(&naive_path(10.0, y0, speed, speed, 40));
let body = classify(&body_path(10.0, y0, speed, speed, 40));
assert!(
naive.contains('V') && naive.contains('H'),
"test setup: naive path should zigzag, got {naive}"
);
assert!(
!body.contains('V') && !body.contains('H'),
"Body diagonal should be pure D/.: got {body}"
);
}
}
#[test]
fn matched_phase_diagonal_is_unchanged() {
let body = classify(&body_path(10.0, 10.0, 0.5, 0.5, 32));
assert_eq!(body, ".D.D.D.D.D.D.D.D.D.D.D.D.D.D.D.");
}
#[test]
fn no_lone_minor_step_at_any_heading() {
for k in 1..18 {
let deg = k as f32 * 5.0; let rad = deg * core::f32::consts::PI / 180.0;
let (vx, vy) = (0.7 * rad.cos(), 0.7 * rad.sin());
let path = body_path(0.3, 0.6, vx, vy, 200);
assert!(
minor_never_steps_alone(&path, vx, vy),
"lone minor step at {deg}°: {}",
classify(&path)
);
}
}
#[test]
fn render_tracks_true_position_within_one_pixel() {
for k in 1..18 {
let deg = k as f32 * 5.0;
let rad = deg * core::f32::consts::PI / 180.0;
let (vx, vy) = (0.7 * rad.cos(), 0.7 * rad.sin());
let mut b = Body::new(3.3, 7.6);
for _ in 0..50_000 {
b.move_by(vx, vy);
let ex = (b.draw_x() - floor_i16(b.x())).abs();
let ey = (b.draw_y() - floor_i16(b.y())).abs();
assert!(ex <= 1 && ey <= 1, "drift {ex},{ey} at {deg}°");
}
}
}
#[test]
fn axis_aligned_and_fast_motion_render_exactly() {
for &(vx, vy) in &[
(0.5_f32, 0.0_f32),
(0.0, -0.7),
(1.0, 1.0),
(1.7, 1.7),
(2.3, -0.4),
] {
let mut b = Body::new(10.0, 10.5);
for _ in 0..300 {
b.move_by(vx, vy);
assert_eq!(b.draw_x(), floor_i16(b.x()));
assert_eq!(b.draw_y(), floor_i16(b.y()));
}
}
}
#[test]
fn direction_changes_stay_clean() {
let mut b = Body::new(20.0, 20.3);
let mut path = vec![(b.draw_x(), b.draw_y())];
for (vx, vy, frames) in [(0.7, 0.0, 10), (0.7, -0.7, 16), (0.0, -0.7, 10)] {
for _ in 0..frames {
b.move_by(vx, vy);
path.push((b.draw_x(), b.draw_y()));
}
}
let s = classify(&path);
assert!(!zigzags(&s), "direction changes introduced a zigzag: {s}");
assert!(s.contains('D'), "expected a diagonal segment: {s}");
}
fn zigzags(s: &str) -> bool {
let b = s.as_bytes();
b.windows(2)
.any(|w| (w[0] == b'H' && w[1] == b'V') || (w[0] == b'V' && w[1] == b'H'))
}
#[test]
fn set_pos_teleports_and_resyncs_render() {
let mut b = Body::new(0.0, 0.0);
b.move_by(0.5, 0.5);
b.set_pos(40.9, 12.2);
assert_eq!(b.pos(), (40.9, 12.2));
assert_eq!(b.draw_pos(), (40, 12));
}
#[test]
fn negative_positions_floor_correctly() {
let b = Body::new(-0.5, -2.0);
assert_eq!(b.draw_pos(), (-1, -2));
}
}