pinch-points 0.8.0

A fast, kid-friendly crab-routing game: route streams of crabs into your sandcastle before time runs out
Documentation
//! The peers on a socket, as the shell knows them: one row per peer
//! index, kept in step with the transport's own list.
//!
//! As four parallel `Vec`s (seat, name, watch wish, silence), each was
//! grown to reach a new index by its own loop and shifted by hand when a
//! peer was forgotten: four lists that have to move together or not at
//! all. One row per peer cannot come apart.

/// Where a peer stands in the round, as the launch plan dealt it.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Place {
    /// At the table since the launch, holding this chair.
    Seated(u8),
    /// Watching since the launch: in step from frame zero like every
    /// player, but holding no chair.
    Watching,
    /// Not in the launch plan, but watching this round all the same: came
    /// to watch after the launch and was sent the round as it stood
    /// (`net::catch_up`), so it follows from that frame on.
    ///
    /// Its own place rather than `Watching`, because the plan is the
    /// leading run of placed rows ([`PeerBook::planned`]), and a latecomer
    /// counted into it would be answered as a launch watcher: sent a
    /// `Start` for frame zero, which is the one thing lockstep cannot
    /// give anybody halfway through a round.
    LateWatching,
    /// Not in the launch plan: turned up mid-round and is in line for the
    /// next one. Also every peer a joiner or the direct `PINCH_HOST` pair
    /// knows, which keep no plan at all.
    #[default]
    Queued,
}

/// One peer, by its index on the socket.
#[derive(Clone, PartialEq, Debug, Default)]
pub struct Peer {
    pub place: Place,
    /// What it calls itself, from its greeting; empty until it says.
    pub name: String,
    /// Whether it asked to watch rather than play. Kept apart from the
    /// place because a wish greeted mid-round has no place to live in
    /// until the next round deals one.
    pub watch: bool,
    /// Seconds since it was last heard from: anything at all, an input, a
    /// hash, a stray greeting.
    pub silence: f32,
    /// The seat it called to win this round, as a spectator. On the row
    /// so it moves with the peer when one before it is forgotten.
    pub pick: Option<u8>,
    /// Its greeting said a bot drives its seat. The game behind the
    /// greeting sets this, never the name, so the seat wears the robot.
    pub bot: bool,
}

impl Peer {
    /// The chair it holds, if it holds one.
    pub fn seat(&self) -> Option<u8> {
        match self.place {
            Place::Seated(seat) => Some(seat),
            Place::Watching | Place::LateWatching | Place::Queued => None,
        }
    }

    /// Whether it asked to watch rather than play, which is what the next
    /// deal reads.
    ///
    /// Not whether it holds no chair: a player dealt none because the
    /// table was full did not ask to watch, and is seated when one frees.
    /// Read off the place, that player was a watcher for good, every round
    /// of a series and every launch after, while the AI took the chairs.
    /// (A late watcher holds its place because it asked, so the wish says
    /// that too.)
    pub fn watches(&self) -> bool {
        self.watch
    }

    /// Whether it is owed a chair at the next deal without holding one:
    /// in line for the next round, or sitting this one out because the
    /// table was full when it was dealt. Either way, not by wish.
    pub fn wants_a_chair(&self) -> bool {
        !self.watch && self.seat().is_none()
    }
}

/// The peers, indexed as the transport indexes them.
#[derive(Clone, PartialEq, Debug, Default)]
pub struct PeerBook(Vec<Peer>);

impl PeerBook {
    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn get(&self, peer: usize) -> Option<&Peer> {
        self.0.get(peer)
    }

    pub fn iter(&self) -> impl Iterator<Item = &Peer> {
        self.0.iter()
    }

    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Peer> {
        self.0.iter_mut()
    }

    /// Grow to cover `count` peers; new rows are blank and queued.
    pub fn reach(&mut self, count: usize) {
        if self.0.len() < count {
            self.0.resize_with(count, Peer::default);
        }
    }

    /// The row for `peer`, grown to reach it: a peer the socket has only
    /// just registered has no row until something is written about it.
    pub fn row(&mut self, peer: usize) -> &mut Peer {
        self.reach(peer + 1);
        &mut self.0[peer]
    }

    /// Exactly `count` rows: grown to reach a peer nothing has been
    /// written about yet, cut back past one the socket no longer has.
    pub fn fit(&mut self, count: usize) {
        self.reach(count);
        self.0.truncate(count);
    }

    /// Drop a peer's row, closing the gap so every index behind it moves
    /// with the transport's own list.
    pub fn forget(&mut self, peer: usize) {
        if peer < self.0.len() {
            self.0.remove(peer);
        }
    }

    /// A peer said something, which is all this needs to know.
    pub fn heard(&mut self, peer: usize) {
        self.row(peer).silence = 0.0;
    }

    /// Age every silence by a frame.
    pub fn age(&mut self, delta: f32) {
        for peer in &mut self.0 {
            peer.silence += delta;
        }
    }

    /// Nobody is late for a round that has not begun.
    pub fn hush(&mut self) {
        for peer in &mut self.0 {
            peer.silence = 0.0;
        }
    }

    /// Deal a launch plan, one place per peer: a chair, or a watching place for a
    /// `None`. The plan covers everyone on the socket, so a row past it is
    /// a peer that has gone, and is dropped with the old plan.
    pub fn deal(&mut self, plan: &[Option<u8>]) {
        self.fit(plan.len());
        for (peer, slot) in self.0.iter_mut().zip(plan) {
            peer.place = match slot {
                Some(seat) => Place::Seated(*seat),
                None => Place::Watching,
            };
        }
    }

    /// How many peers the launch plan covers. Peers are registered in
    /// arrival order and the plan is dealt to everyone at the table when
    /// the round begins, so the planned peers are the leading run and
    /// everyone after them is in line.
    pub fn planned(&self) -> usize {
        self.0
            .iter()
            .take_while(|peer| matches!(peer.place, Place::Seated(_) | Place::Watching))
            .count()
    }

    /// Whether `peer` is following the round and so needs its inputs: at
    /// the table, or watching in step from frame zero. A peer in line for
    /// the next round is not, and sent the whole lockstep anyway costs about
    /// 180 datagrams a second for frames it cannot use: four people waiting
    /// turned a table's 930 a second into 1657.
    ///
    /// Everyone hears when the book holds no plan, which is why this is not
    /// simply `place != Queued`: `Queued` is the default place, so a joiner
    /// (which knows one peer, the host) and the direct `PINCH_HOST` pair
    /// (which keeps no plan) would stop sending inputs to the only peer
    /// they have.
    pub fn follows_the_round(&self, peer: usize) -> bool {
        let planned = self.planned();
        planned == 0
            || peer < planned
            || self
                .get(peer)
                .is_some_and(|row| row.place == Place::LateWatching)
    }

    /// The peer holding `seat`, if any does.
    pub fn holder_of(&self, seat: u8) -> Option<usize> {
        self.0.iter().position(|peer| peer.seat() == Some(seat))
    }

    /// The chair `peer` holds, or `None` for a watcher and for a peer the
    /// plan has never heard of.
    pub fn seat_of(&self, peer: usize) -> Option<u8> {
        self.0.get(peer).and_then(Peer::seat)
    }

    /// Back in the lobby nobody holds a chair: the next launch deals from
    /// the wish alone.
    pub fn unseat(&mut self) {
        for peer in &mut self.0 {
            peer.place = Place::Queued;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Forgetting a peer moves every row behind it down one, and the row
    /// keeps its name, its wish and its silence together: the failure this
    /// type exists to rule out is somebody wearing another player's name.
    #[test]
    fn forgetting_a_peer_moves_the_rows_behind_it_together() {
        let mut peers = PeerBook::default();
        peers.row(0).name = "Bo".into();
        peers.row(1).name = "Cy".into();
        peers.row(2).name = "Dee".into();
        peers.row(2).watch = true;
        peers.row(2).silence = 3.0;
        peers.deal(&[Some(1), Some(2), None]);
        peers.forget(0);
        assert_eq!(peers.len(), 2);
        assert_eq!(peers.get(0).map(|p| p.name.as_str()), Some("Cy"));
        let dee = peers.get(1).expect("Dee moved down");
        assert_eq!(dee.name, "Dee");
        assert!(dee.watches(), "the wish followed Dee down");
        assert_eq!(dee.silence, 3.0, "and so did the silence");
        assert_eq!(dee.place, Place::Watching);
        assert_eq!(peers.seat_of(0), Some(2), "Cy keeps the chair it held");
    }

    /// The plan is the leading run of placed peers: everyone the socket
    /// picked up after the launch is in line behind it, and a row grown
    /// for a stray greeting is queued rather than seated.
    #[test]
    fn the_plan_is_the_leading_run_and_the_rest_are_in_line() {
        let mut peers = PeerBook::default();
        peers.deal(&[Some(1), None]);
        assert_eq!(peers.planned(), 2);
        peers.heard(3);
        assert_eq!(peers.len(), 4, "grown to reach the stranger");
        assert_eq!(peers.planned(), 2, "and the stranger is in line");
        assert_eq!(peers.holder_of(1), Some(0));
        assert_eq!(peers.holder_of(0), None, "the host's chair is nobody's");
        // Dealt again for a smaller table, the rows past the plan go with
        // the peers that left rather than keeping stale chairs.
        peers.deal(&[Some(1)]);
        assert_eq!(peers.planned(), 1);
        assert_eq!(peers.len(), 1);
    }

    /// Walking back to the lobby keeps who watches and forgets who sat
    /// where: the next launch deals the chairs again from the wishes.
    #[test]
    fn unseating_keeps_the_watchers_and_frees_the_chairs() {
        let mut peers = PeerBook::default();
        peers.deal(&[Some(1), None, Some(2), None]);
        // Peer one asked to watch at the launch, peer two since; peer
        // three was dealt no chair because there was none, and asked for
        // nothing.
        peers.row(1).watch = true;
        peers.row(2).watch = true;
        peers.unseat();
        let watching: Vec<bool> = peers.iter().map(Peer::watches).collect();
        assert_eq!(watching, [false, true, true, false]);
        assert!(peers.iter().all(|p| p.seat().is_none()));
        assert_eq!(peers.planned(), 0);
    }

    /// Who the round's inputs are for. A peer in line is not simulating
    /// anything and used to be sent the lot anyway.
    #[test]
    fn only_the_table_and_the_watchers_follow_the_round() {
        let mut peers = PeerBook::default();
        peers.reach(4);
        peers.deal(&[Some(1), None]);
        peers.reach(4);
        assert_eq!(peers.planned(), 2, "a seat and a watching place");
        assert!(peers.follows_the_round(0), "the seated peer");
        assert!(peers.follows_the_round(1), "the watcher, in step from zero");
        assert!(!peers.follows_the_round(2), "the one in line");
        assert!(!peers.follows_the_round(3), "and the one behind it");
    }

    /// A watcher that arrived late follows the round without joining the
    /// plan: the peer in line behind it is still in line, and so is the
    /// one in front of it, whatever order they greeted in.
    #[test]
    fn a_late_watcher_follows_the_round_outside_the_plan() {
        let mut peers = PeerBook::default();
        peers.deal(&[Some(1), None]);
        peers.reach(4);
        // As the host takes one in: its greeting asked to watch.
        peers.row(2).watch = true;
        peers.row(2).place = Place::LateWatching;
        assert_eq!(peers.planned(), 2, "the plan is still the launch's");
        assert!(
            peers.follows_the_round(2),
            "the late watcher is sent the round"
        );
        assert!(!peers.follows_the_round(3), "the one in line is not");
        assert!(peers.get(2).is_some_and(Peer::watches));
        assert_eq!(peers.seat_of(2), None);
        // First in line, straight after the plan, it still does not extend it.
        let mut peers = PeerBook::default();
        peers.deal(&[Some(1)]);
        peers.row(1).place = Place::LateWatching;
        peers.row(2).place = Place::Queued;
        assert_eq!(peers.planned(), 1);
    }

    /// The exception that keeps online play working at all: `Queued` is
    /// the default place, and a joiner knows exactly one peer (the host)
    /// which it has no plan for. Filtering on the place alone would stop
    /// it sending its own inputs anywhere.
    #[test]
    fn a_book_with_no_plan_sends_to_everyone() {
        let mut peers = PeerBook::default();
        peers.reach(2);
        assert_eq!(peers.planned(), 0, "nothing was ever dealt");
        assert!(peers.follows_the_round(0));
        assert!(peers.follows_the_round(1));
    }
}