use crate::sim::board::{MAX_PLAYERS, PlayerAction, PlayerId, TICKS_PER_SECOND, TideEvent};
use crate::sim::direction::Direction;
use std::collections::BTreeMap;
pub const DEFAULT_DELAY: u32 = 3;
pub const MAX_DELAY: u32 = 6;
pub fn delay_for_round_trip(round_trip: f32) -> u32 {
let travel = (round_trip.max(0.0) * TICKS_PER_SECOND as f32).ceil() as u32;
(travel + 2).clamp(DEFAULT_DELAY, MAX_DELAY)
}
pub const PAUSE_LEAD: u32 = 10;
pub const MAX_COMMIT_LEAD: u32 = 8;
pub const RESEND_LEAD: u32 = 30;
const _: () = assert!(RESEND_LEAD >= MAX_COMMIT_LEAD);
pub const RECORD_SPAN: u32 = 20 * TICKS_PER_SECOND;
pub const HASH_INTERVAL: u32 = 30;
pub fn encode_action(action: PlayerAction) -> [u8; 3] {
match action {
PlayerAction::None => [0, 0, 0],
PlayerAction::Place { x, y, dir } => [x, y, 1 | (dir.id() << 2)],
PlayerAction::Remove { x, y } => [x, y, 2],
PlayerAction::CallEvent(event) => [event.index() as u8, 0, 3],
}
}
pub fn decode_action(bytes: [u8; 3]) -> PlayerAction {
let (x, y) = (bytes[0], bytes[1]);
match bytes[2] & 0b11 {
1 => PlayerAction::Place {
x,
y,
dir: Direction::from_id((bytes[2] >> 2) & 0b11),
},
2 => PlayerAction::Remove { x, y },
3 => TideEvent::ALL
.get(usize::from(x))
.copied()
.map_or(PlayerAction::None, PlayerAction::CallEvent),
_ => PlayerAction::None,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct InputMsg {
pub player: PlayerId,
pub frame: u32,
pub action: PlayerAction,
}
pub struct Lockstep {
local: Option<PlayerId>,
players: Vec<PlayerId>,
delay: u32,
frame: u32,
next_commit: u32,
pending: BTreeMap<u32, [Option<PlayerAction>; MAX_PLAYERS]>,
record: BTreeMap<u32, [Option<PlayerAction>; MAX_PLAYERS]>,
newest: [Option<u32>; MAX_PLAYERS],
pause: PauseState,
also: Vec<PlayerId>,
nudge: i32,
leaving: Vec<(PlayerId, u32)>,
}
#[derive(Clone, Copy, Debug, Default)]
struct PauseState {
at: Option<u32>,
lifted: Option<u32>,
}
impl PauseState {
fn request(&mut self, next: u32, now: u32, horizon: u32) -> u32 {
let mut frame = next + PAUSE_LEAD;
if let Some(lifted) = self.lifted {
frame = frame.max(lifted + 1);
}
self.propose(frame, now, horizon);
self.at.unwrap_or(frame)
}
fn propose(&mut self, frame: u32, now: u32, horizon: u32) {
if frame < now || frame > horizon || self.is_lifted(frame) {
return;
}
self.at = Some(match self.at {
Some(existing) => existing.min(frame),
None => frame,
});
}
fn resume(&mut self) -> u32 {
self.lift(self.at.unwrap_or(0))
}
fn receive_resume(&mut self, frame: u32, horizon: u32) -> Option<u32> {
let frame = frame.min(horizon.saturating_sub(1));
if self.is_lifted(frame) {
return None;
}
Some(self.lift(frame))
}
fn lift(&mut self, frame: u32) -> u32 {
let lifted = [self.lifted, self.at, Some(frame)]
.into_iter()
.flatten()
.max()
.unwrap_or(0);
self.lifted = Some(lifted);
self.at = None;
lifted
}
fn is_lifted(&self, frame: u32) -> bool {
self.lifted.is_some_and(|lifted| frame <= lifted)
}
fn reached(&self, frame: u32) -> bool {
self.at.is_some_and(|at| frame >= at)
}
}
fn resend_span(delay: u32) -> u32 {
delay + RESEND_LEAD
}
impl Lockstep {
pub fn new(local: PlayerId, players: Vec<PlayerId>, delay: u32) -> Lockstep {
assert!(players.contains(&local));
Lockstep::seated(Some(local), players, delay)
}
pub fn observer(players: Vec<PlayerId>, delay: u32) -> Lockstep {
Lockstep::seated(None, players, delay)
}
pub fn observer_from(players: Vec<PlayerId>, delay: u32, frame: u32) -> Lockstep {
let mut session = Lockstep::seated(None, players, delay);
if frame > 0 {
session.pending.clear();
session.frame = frame;
session.next_commit = frame + delay;
}
session
}
fn seated(local: Option<PlayerId>, players: Vec<PlayerId>, delay: u32) -> Lockstep {
assert!(
players
.iter()
.all(|&p| crate::sim::board::seat(p).is_some()),
"player id out of range"
);
let mut session = Lockstep {
local,
players,
delay,
frame: 0,
next_commit: delay,
pending: BTreeMap::new(),
record: BTreeMap::new(),
newest: [None; MAX_PLAYERS],
also: Vec::new(),
pause: PauseState::default(),
nudge: 0,
leaving: Vec::new(),
};
for frame in 0..delay {
let slot = session.slot(frame);
for entry in slot.iter_mut() {
*entry = Some(PlayerAction::None);
}
}
session
}
fn slot(&mut self, frame: u32) -> &mut [Option<PlayerAction>; MAX_PLAYERS] {
let players = &self.players;
let leaving = &self.leaving;
self.pending.entry(frame).or_insert_with(|| {
let mut slot = [None; MAX_PLAYERS];
for p in 0..MAX_PLAYERS as u8 {
let still_owed = leaving
.iter()
.any(|&(gone, from)| gone == p && frame < from);
if !players.contains(&p) && !still_owed {
slot[p as usize] = Some(PlayerAction::None); }
}
slot
})
}
fn takes(&self, player: PlayerId, frame: u32) -> bool {
self.players.contains(&player)
|| self
.leaving
.iter()
.any(|&(gone, from)| gone == player && frame < from)
}
pub fn request_pause(&mut self) -> Option<u32> {
if self.watching() {
return None;
}
let (now, horizon) = (self.frame, self.horizon());
Some(self.pause.request(self.next_commit, now, horizon))
}
pub fn receive_pause(&mut self, frame: u32) {
let (now, horizon) = (self.frame, self.horizon());
self.pause.propose(frame, now, horizon);
}
pub fn resume(&mut self) -> u32 {
self.nudge = 0;
self.pause.resume()
}
pub fn receive_resume(&mut self, frame: u32) -> Option<u32> {
if !self.within_horizon(frame) && !self.paused() {
return None;
}
let horizon = self.horizon();
let lifted = self.pause.receive_resume(frame, horizon);
if lifted.is_some() {
self.nudge = 0;
}
lifted
}
pub fn is_lifted(&self, frame: u32) -> bool {
self.pause.is_lifted(frame)
}
pub fn pause_frame(&self) -> Option<u32> {
self.pause.at
}
pub fn lifted_pause(&self) -> Option<u32> {
self.pause.lifted
}
pub fn paused(&self) -> bool {
self.pause.at.is_some()
}
pub fn frozen(&self) -> bool {
self.pause.reached(self.frame)
}
pub fn speak_for(&mut self, seats: &[PlayerId]) {
self.also = seats
.iter()
.copied()
.filter(|seat| self.players.contains(seat) && Some(*seat) != self.local)
.collect();
}
pub fn speaks_for(&self) -> &[PlayerId] {
&self.also
}
pub fn nudge(&mut self, frames: i32) {
self.nudge = frames;
}
pub fn nudging(&self) -> bool {
self.nudge != 0
}
pub fn commit_local_with(
&mut self,
action: PlayerAction,
extra: &[(PlayerId, PlayerAction)],
) -> Option<InputMsg> {
if self.paused() {
self.nudge = 0;
}
if self.nudge > 0 && !self.watching() {
self.nudge -= 1;
return None;
}
let Some(msg) = self.commit_frame(action, extra) else {
self.nudge = 0;
return None;
};
if self.nudge < 0 && self.commit_frame(PlayerAction::None, &[]).is_some() {
self.nudge += 1;
}
Some(msg)
}
fn commit_frame(
&mut self,
action: PlayerAction,
extra: &[(PlayerId, PlayerAction)],
) -> Option<InputMsg> {
let msg = self.commit_local(action)?;
for seat in self.also.clone() {
let given = extra
.iter()
.find(|(s, _)| *s == seat)
.map_or(PlayerAction::None, |(_, a)| *a);
let given = decode_action(encode_action(given));
self.slot(msg.frame)[usize::from(seat)] = Some(given);
self.keep(InputMsg {
player: seat,
frame: msg.frame,
action: given,
});
}
Some(msg)
}
pub fn commit_local(&mut self, action: PlayerAction) -> Option<InputMsg> {
if self.watching() {
return None; }
if self.pause.reached(self.next_commit) {
return None;
}
if self.next_commit >= self.frame + self.delay + MAX_COMMIT_LEAD {
return None;
}
let frame = self.next_commit;
self.next_commit += 1;
let local = self.local?;
let action = decode_action(encode_action(action));
debug_assert!(
usize::from(local) < MAX_PLAYERS,
"committing for seat {local}, which is not at the table"
);
self.slot(frame)[local as usize] = Some(action);
let msg = InputMsg {
player: local,
frame,
action,
};
self.keep(msg);
Some(msg)
}
fn keep(&mut self, msg: InputMsg) {
let seat = usize::from(msg.player);
let slot = self.record.entry(msg.frame).or_insert([None; MAX_PLAYERS]);
slot[seat] = Some(msg.action);
self.newest[seat] = self.newest[seat].max(Some(msg.frame));
}
pub fn tail_start(&self) -> u32 {
self.frame.saturating_sub(resend_span(self.delay))
}
pub fn recent_commits(&self) -> Vec<InputMsg> {
let start = Some(self.tail_start());
let mut from = [None; MAX_PLAYERS];
for seat in self.local.iter().chain(&self.also) {
from[usize::from(*seat)] = start;
}
self.held_between(&from, u32::MAX)
}
pub fn held_between(&self, from: &[Option<u32>; MAX_PLAYERS], until: u32) -> Vec<InputMsg> {
let Some(earliest) = from.iter().flatten().min().copied() else {
return Vec::new();
};
let until = until.min(earliest.saturating_add(2 * resend_span(self.delay)));
if until <= earliest {
return Vec::new();
}
let mut held = Vec::new();
for (&frame, slot) in self.record.range(earliest..until) {
for (seat, start) in from.iter().enumerate() {
if let (Some(start), Some(action)) = (start, slot[seat])
&& frame >= *start
{
held.push(InputMsg {
player: seat as PlayerId,
frame,
action,
});
}
}
}
held
}
pub fn wants(&self) -> [u32; MAX_PLAYERS] {
let mut wants = [self.frame; MAX_PLAYERS];
let mut open = [true; MAX_PLAYERS];
for (&frame, slot) in self.record.range(self.frame..) {
for seat in 0..MAX_PLAYERS {
if open[seat] && wants[seat] == frame && slot[seat].is_some() {
wants[seat] = frame + 1;
} else {
open[seat] = false;
}
}
if !open.contains(&true) {
break;
}
}
wants
}
pub fn resend_from(&self, seat: PlayerId, wanted: Option<u32>) -> Option<u32> {
let newest = (*self.newest.get(usize::from(seat))?)?;
Some(match wanted {
Some(wanted) if wanted <= newest + 1 => wanted,
_ => self.tail_start(),
})
}
pub fn owed(
&self,
wants: Option<[u32; MAX_PLAYERS]>,
own: Option<PlayerId>,
until: u32,
) -> Vec<InputMsg> {
let mut from = [None; MAX_PLAYERS];
for (seat, start) in (0..).zip(from.iter_mut()) {
if Some(seat) != own {
*start = self.resend_from(seat, wants.map(|wants| wants[usize::from(seat)]));
}
}
self.held_between(&from, until)
}
pub fn awaiting(&self) -> Vec<PlayerId> {
let frame = self.frame;
let owing = self
.leaving
.iter()
.filter(|&&(_, from)| frame < from)
.map(|&(gone, _)| gone);
let waited: Vec<PlayerId> = self.players.iter().copied().chain(owing).collect();
let Some(slot) = self.pending.get(&self.frame) else {
return waited;
};
waited
.into_iter()
.filter(|player| slot[*player as usize].is_none())
.collect()
}
pub fn abandon(&mut self, player: PlayerId, frame: u32) {
debug_assert!(
usize::from(player) < MAX_PLAYERS,
"no such seat to abandon: {player}"
);
self.players.retain(|seated| *seated != player);
if !self.leaving.iter().any(|&(gone, _)| gone == player) {
self.leaving.push((player, frame));
}
for (&at, slot) in self.pending.iter_mut() {
if at >= frame {
slot[player as usize] = Some(PlayerAction::None);
}
}
for (_, slot) in self.record.range_mut(frame..) {
slot[player as usize] = None;
}
}
pub fn played_tail(&self) -> Vec<InputMsg> {
self.held_between(&[Some(self.tail_start()); MAX_PLAYERS], self.frame)
}
fn horizon(&self) -> u32 {
self.frame + 2 * resend_span(self.delay)
}
pub fn within_horizon(&self, frame: u32) -> bool {
frame <= self.horizon()
}
pub fn receive(&mut self, msg: InputMsg) -> bool {
if !self.within_horizon(msg.frame) || !self.takes(msg.player, msg.frame) {
return false;
}
if msg.frame < self.frame {
return true;
}
debug_assert!(usize::from(msg.player) < MAX_PLAYERS);
let slot = self.slot(msg.frame);
if slot[msg.player as usize].is_none() {
slot[msg.player as usize] = Some(msg.action);
self.keep(msg);
}
true
}
pub fn plays(&self, player: PlayerId) -> bool {
self.players.contains(&player)
}
pub fn advance(&mut self) -> Option<[PlayerAction; MAX_PLAYERS]> {
debug_assert!(
self.pending
.keys()
.next()
.is_none_or(|&at| at >= self.frame),
"inputs kept for a frame already played"
);
let slot = self.slot(self.frame);
if slot.iter().any(|a| a.is_none()) {
return None;
}
let actions = std::array::from_fn(|i| slot[i].unwrap_or(PlayerAction::None));
self.pending.remove(&self.frame);
self.frame += 1;
let frame = self.frame;
self.leaving.retain(|&(_, from)| from > frame);
let oldest_kept = frame.saturating_sub(RECORD_SPAN);
while self
.record
.first_key_value()
.is_some_and(|(&at, _)| at < oldest_kept)
{
self.record.pop_first();
}
Some(actions)
}
pub fn due(&self) -> bool {
self.watching()
|| self.pause.reached(self.next_commit)
|| self.next_commit > self.frame + self.delay
}
pub fn playout(&self, most: usize) -> usize {
if !self.watching() || self.held_whole() > self.delay {
return most;
}
most.min(1)
}
fn held_whole(&self) -> u32 {
let mut frame = self.frame;
while self
.pending
.get(&frame)
.is_some_and(|slot| slot.iter().all(Option::is_some))
{
frame += 1;
}
frame - self.frame
}
pub fn delay(&self) -> u32 {
self.delay
}
pub fn next_commit(&self) -> u32 {
self.next_commit
}
pub fn frame(&self) -> u32 {
self.frame
}
pub fn watching(&self) -> bool {
self.local.is_none()
}
pub fn seat(&self) -> Option<PlayerId> {
self.local
}
pub fn player_count(&self) -> usize {
self.players.len()
}
}
#[cfg(test)]
mod tests;