use std::collections::VecDeque;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Heard {
OnRoute,
Diverged,
Unchecked,
}
#[derive(Clone, Debug)]
pub struct RoutePredictor<P: Copy + PartialEq> {
point: fn(&P) -> [f32; 2],
pub predicted: P,
pub confirmed: P,
plan: VecDeque<P>,
expect: VecDeque<P>,
checking: bool,
from: [f32; 2],
step_ms: u64,
stepped_ms: u64,
next_step_ms: u64,
seeded: bool,
pub confirmations: u64,
pub diverged: u64,
}
impl<P: Copy + PartialEq> RoutePredictor<P> {
pub fn new(initial: P, point: fn(&P) -> [f32; 2], step_ms: u64) -> Self {
Self {
point,
predicted: initial,
confirmed: initial,
plan: VecDeque::new(),
expect: VecDeque::new(),
checking: false,
from: point(&initial),
step_ms: step_ms.max(1),
stepped_ms: 0,
next_step_ms: 0,
seeded: false,
confirmations: 0,
diverged: 0,
}
}
pub fn set_step_ms(&mut self, step_ms: u64) {
self.step_ms = step_ms.max(1);
}
pub fn jump_to(&mut self, at: P, now_ms: u64) {
self.predicted = at;
self.confirmed = at;
self.from = (self.point)(&at);
self.plan.clear();
self.expect.clear();
self.checking = false;
self.stepped_ms = now_ms;
self.next_step_ms = now_ms + self.step_ms;
self.seeded = true;
}
pub fn is_seeded(&self) -> bool {
self.seeded
}
pub fn set_out(&mut self, route: impl IntoIterator<Item = P>, checkable: bool, now_ms: u64) {
let was_walking = !self.plan.is_empty() || self.crossing(now_ms);
self.checking = checkable
&& self.plan.is_empty()
&& self.expect.is_empty()
&& self.confirmed == self.predicted;
self.plan.clear();
self.expect.clear();
for square in route {
self.plan.push_back(square);
self.expect.push_back(square);
}
if !was_walking {
self.next_step_ms = now_ms;
}
}
pub fn advance(&mut self, now_ms: u64, steps_per_tick: u32) {
if !self.seeded || self.plan.is_empty() {
return;
}
while now_ms >= self.next_step_ms && !self.plan.is_empty() {
self.from = self.drawn(now_ms);
for _ in 0..steps_per_tick.max(1) {
if let Some(next) = self.plan.pop_front() {
self.predicted = next;
}
}
self.stepped_ms = now_ms;
self.next_step_ms = now_ms + self.step_ms;
}
}
pub fn drawn(&self, now_ms: u64) -> [f32; 2] {
let t = (now_ms.saturating_sub(self.stepped_ms) as f32 / self.step_ms as f32).clamp(0.0, 1.0);
let to = (self.point)(&self.predicted);
[
self.from[0] + (to[0] - self.from[0]) * t,
self.from[1] + (to[1] - self.from[1]) * t,
]
}
pub fn walking(&self, now_ms: u64) -> bool {
!self.plan.is_empty() || self.crossing(now_ms)
}
pub fn crossing(&self, now_ms: u64) -> bool {
let to = (self.point)(&self.predicted);
let (dx, dy) = (to[0] - self.from[0], to[1] - self.from[1]);
dx * dx + dy * dy > 1e-4 && now_ms.saturating_sub(self.stepped_ms) < self.step_ms
}
pub fn heading(&self, now_ms: u64) -> Option<[f32; 2]> {
if !self.crossing(now_ms) {
return None;
}
let to = (self.point)(&self.predicted);
Some([to[0] - self.from[0], to[1] - self.from[1]])
}
pub fn plan(&self) -> impl Iterator<Item = &P> + '_ {
self.plan.iter()
}
pub fn plan_is_empty(&self) -> bool {
self.plan.is_empty()
}
pub fn confirm(&mut self, at: P, slack: u32) -> Heard {
if at == self.confirmed {
return Heard::Unchecked;
}
self.confirmed = at;
if !self.checking {
return Heard::Unchecked;
}
self.confirmations += 1;
for _ in 0..slack.max(1) {
match self.expect.pop_front() {
Some(next) if next == at => return Heard::OnRoute,
Some(_) => continue,
None => {
self.checking = false;
return Heard::Unchecked;
}
}
}
self.diverged += 1;
self.checking = false;
Heard::Diverged
}
pub fn abandon(&mut self, at: P, now_ms: u64) {
self.from = self.drawn(now_ms);
self.predicted = at;
self.plan.clear();
self.expect.clear();
self.checking = false;
self.stepped_ms = now_ms;
self.next_step_ms = now_ms + self.step_ms;
}
pub fn settle(&mut self, now_ms: u64) -> bool {
if !self.seeded || self.walking(now_ms) || self.confirmed == self.predicted {
return false;
}
self.from = self.drawn(now_ms);
self.predicted = self.confirmed;
self.plan.clear();
self.expect.clear();
self.stepped_ms = now_ms;
self.next_step_ms = now_ms + self.step_ms;
true
}
}
#[cfg(test)]
mod tests {
use super::*;
type Sq = (i16, i16);
fn point(sq: &Sq) -> [f32; 2] {
[sq.0 as f32, sq.1 as f32]
}
fn line(from: Sq, len: i16) -> Vec<Sq> {
(1..=len).map(|i| (from.0 + i, from.1)).collect()
}
fn predictor() -> RoutePredictor<Sq> {
let mut p = RoutePredictor::new((0, 0), point, 100);
p.jump_to((0, 0), 0);
p
}
#[test]
fn the_client_walks_the_route_the_server_walks() {
let mut p = predictor();
let route = line((0, 0), 10);
p.set_out(route.clone(), true, 0);
let mut now = 0;
for square in route {
now += 100;
p.advance(now, 1);
assert_eq!(p.confirm(square, 1), Heard::OnRoute);
}
assert_eq!(p.diverged, 0);
assert_eq!(p.confirmations, 10);
assert!(!p.walking(now + 200));
}
#[test]
fn a_journey_the_ends_started_apart_is_not_checked() {
let mut p = predictor();
p.set_out(line((0, 0), 6), true, 0);
p.advance(150, 1);
p.set_out(line(p.predicted, 4), true, 150);
assert_eq!(p.confirm((2, 0), 1), Heard::Unchecked);
assert_eq!(p.diverged, 0, "a phase difference is not a divergence");
}
#[test]
fn a_click_changes_where_the_body_is_going_and_never_where_it_is() {
let mut p = predictor();
p.set_out(line((0, 0), 6), true, 0);
p.advance(150, 1);
let before = p.drawn(150);
p.set_out(vec![(1, 1), (2, 2)], true, 150);
let after = p.drawn(150);
assert_eq!(before, after, "the drawn body moved on a click");
}
#[test]
fn the_drawn_body_never_jumps_however_fast_the_clicks_come() {
let mut p = predictor();
let mut worst = 0.0f32;
let mut last = p.drawn(0);
for frame in 1..200u64 {
let now = frame * 50;
if frame % 3 == 0 {
let dir: i16 = if (frame / 3) % 2 == 0 { 1 } else { -1 };
let route: Vec<Sq> = (1..=4).map(|i| (p.predicted.0 + i * dir, p.predicted.1)).collect();
p.set_out(route, true, now);
}
p.advance(now, 1);
let drawn = p.drawn(now);
let (dx, dy) = (drawn[0] - last[0], drawn[1] - last[1]);
worst = worst.max((dx * dx + dy * dy).sqrt());
last = drawn;
}
assert!(worst < 0.75, "the body jumped {worst} squares in one frame");
}
#[test]
fn a_run_advances_two_squares_and_the_unreported_one_is_not_a_divergence() {
let mut p = predictor();
p.set_out(line((0, 0), 8), true, 0);
let mut now = 0;
for reported in [2i16, 4, 6, 8] {
now += 100;
p.advance(now, 2);
assert_eq!(p.confirm((reported, 0), 2), Heard::OnRoute, "at {reported}");
}
assert_eq!(p.diverged, 0);
}
#[test]
fn a_different_route_is_a_divergence_said_once() {
let mut p = predictor();
p.set_out(line((0, 0), 4), true, 0);
p.advance(100, 1);
assert_eq!(p.confirm((0, 1), 1), Heard::Diverged, "the server went another way");
assert_eq!(p.diverged, 1);
assert_eq!(p.confirm((0, 2), 1), Heard::Unchecked, "and the check is over");
assert_eq!(p.diverged, 1);
}
#[test]
fn settling_happens_only_at_rest() {
let mut p = predictor();
p.set_out(line((0, 0), 3), true, 0);
p.advance(100, 1);
p.confirm((5, 5), 1);
assert!(!p.settle(150), "a walking body is not settled");
let mut now = 150;
while p.walking(now) {
now += 100;
p.advance(now, 1);
}
assert!(p.settle(now), "at rest the server's square is taken");
assert_eq!(p.predicted, (5, 5));
assert!(!p.settle(now + 100), "and settling is idempotent");
}
#[test]
fn an_arrived_body_stops_walking() {
let mut p = predictor();
p.set_out(vec![(1, 0)], true, 0);
p.advance(100, 1);
assert!(p.walking(150), "mid-crossing is walking");
assert!(!p.walking(300), "arrived is not");
}
#[test]
fn abandoning_a_route_keeps_the_picture_continuous() {
let mut p = predictor();
p.set_out(line((0, 0), 6), true, 0);
p.advance(150, 1);
let before = p.drawn(150);
p.abandon((0, 0), 150);
let after = p.drawn(150);
let (dx, dy) = (after[0] - before[0], after[1] - before[1]);
assert!((dx * dx + dy * dy).sqrt() < 1e-4, "a refusal must not teleport the body");
assert!(p.plan_is_empty());
}
}