pinch-points 0.9.0

A fast, kid-friendly crab-routing game: route streams of crabs into your sandcastle before time runs out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
//! Where the player stands in the lobby: choosing a beach, greeting one,
//! or hosting one, and everything that goes with standing there.

use super::*;

/// What a joiner says to a host, every second until it is answered: a
/// bot's seat (route 1) greets under its seat name with the flag that says
/// a bot drives it, else a `Watch` if it came to look, else a `Hello`
/// carrying its name. The same greeting from the lobby, from a results
/// card and from a round whose host has gone quiet, so the host reads the
/// wish to watch the same way wherever it was greeted from.
pub(crate) fn greeting(bot: Option<&str>, watching: bool, name: &str) -> NetMsg {
    match (bot, watching) {
        (Some(seat), _) => NetMsg::hello_bot(seat),
        (None, true) => NetMsg::watch(name),
        (None, false) => NetMsg::hello(name),
    }
}

/// The name a peer goes by at the table, `me` being the player's own: the
/// seat name a bot (route 1) greeted with, when it joined as one, since
/// that is the name the host wrote down and vouches its chat under. The
/// lobby and the round both read it here, so a line is echoed on the
/// sender's own screen under the name everyone else reads it under.
pub(crate) fn speaking_as<'a>(bot: Option<&'a str>, me: &'a str) -> &'a str {
    bot.unwrap_or(me)
}

/// A lobby on the air on a fresh socket, with nobody at its table yet:
/// where most tests of hosting start.
#[cfg(test)]
pub(super) fn hosting_lobby() -> Standing {
    Standing::hosting(
        Announcer::new(0xB0A7).expect("announcer"),
        UdpTransport::host(0).expect("game socket"),
    )
}

/// Where the player stands in the lobby, and everything that goes with
/// standing there.
///
/// One value rather than the four fields that used to say it (`hosting`,
/// `joining`, `watching`, and whether anything had been heard): a field
/// that only means anything in one standing (the host's peers, the
/// joiner's patience with a silent host) is a field that can be read in
/// the wrong one.
pub enum Standing {
    /// Choosing a beach, with W armed or not: armed, the next pick
    /// watches rather than plays.
    Choosing { watching: bool },
    /// Greeting a host, waiting to be given a seat.
    Joining(Joined),
    /// On the air, gathering a table.
    Hosting(Hosted),
}

impl Default for Standing {
    fn default() -> Self {
        Standing::Choosing { watching: false }
    }
}

impl Standing {
    /// Whether there is anybody at the other end: somebody to talk to, a
    /// table to show, and no list worth reading.
    pub fn at_a_beach(&self) -> bool {
        matches!(self, Standing::Joining(_) | Standing::Hosting(_))
    }

    /// A beach going on the air this frame.
    pub fn hosting(announcer: Announcer, transport: UdpTransport) -> Standing {
        Standing::Hosting(Hosted {
            announcer,
            transport,
            peers: PeerBook::default(),
            announce_in: 0.0,
            kicking: None,
            bot_string: None,
            tail: RoundTail::default(),
            rounds_dealt: 0,
            picking: None,
            partnering: false,
            arranged: None,
        })
    }
}

/// A beach on the air: its beacon, the game socket gathering the table,
/// and everyone who has turned up on it.
pub struct Hosted {
    pub announcer: Announcer,
    pub transport: UdpTransport,
    /// Everyone on the socket, by its index there: the name its greeting
    /// carried, whether it asked to watch, and how long since it was last
    /// heard from. A joiner re-greets every second for as long as it sits
    /// in the lobby, so silence is the only evidence of leaving there is:
    /// UDP will never say so. Nobody holds a chair here; the plan is dealt
    /// at the launch.
    pub peers: PeerBook,
    pub(super) announce_in: f32,
    /// The host pressed K and is picking whom to ask to leave: the next
    /// number is a seat at the table or, past the card's rows, one of the
    /// peers it has none for, the page of them shown (Tab turns it), and
    /// Esc is never mind. `None` while not picking.
    pub kicking: Option<usize>,
    /// The string a bot connects straight to this beach with (route 2),
    /// while the host says bots are welcome and there is a chair for one.
    pub bot_string: Option<String>,
    /// The last frames of the match the table walked back from, repeated
    /// for a while to anyone still a frame short of its end.
    pub(super) tail: RoundTail,
    /// How many rounds this socket has dealt, over every match played on
    /// it: the next match numbers its first `Start` on from the last
    /// round of the one before, so a joiner still on that round's results
    /// card reads it as a round it has not been dealt (`is_next_round`).
    /// Begun again at one, the joiner sits the new match out.
    pub(super) rounds_dealt: u32,
    /// The host started the match and is picking the map for the table
    /// frozen then ([`super::pick`]). `None` while gathering.
    pub picking: Option<Picking>,
    /// The host pressed P and is picking partners: the next number is the
    /// seat whose partners change ([`super::partners`]), and Esc or P puts
    /// it away.
    pub partnering: bool,
    /// The seats the host's partner picks put its bots and the AI in, once
    /// it has picked any ([`crate::app::net::order`]); the peers' are on
    /// their rows. `None` for a table dealt in seat order.
    pub arranged: Option<crate::app::net::order::Arranged>,
}

/// How long a host back in the lobby keeps repeating the last frames of
/// the round it played, in seconds: a player short of them is at most a
/// commit lead behind and finishes on the first copy that arrives, and a
/// spectator a few seconds behind has as long again to play the rest out.
const TAIL_KEPT: f32 = 10.0;

/// The end of a round the host has finished and walked away from, and the
/// lockstep it is cut from.
///
/// The host relays an input once, as it arrives, and stops the moment its
/// round is over: the relay of the input that completed the last frame,
/// lost, left a joiner a frame short of the end for good, on "waiting
/// for" a player who had long stopped sending, while the lobby's roster
/// kept it from ever calling the host gone. So the lobby goes on sending
/// it: the last frames once a second to everybody, and to a peer still in
/// the round, whose batches say what it holds, everything it is missing
/// up to the last frame (`Lockstep::owed`), as the results card does. A
/// spectator seconds behind is a long way short of the last second.
#[derive(Default)]
pub struct RoundTail {
    played: Option<Box<crate::sim::Lockstep>>,
    round: u8,
    left: f32,
}

impl RoundTail {
    /// The round just walked away from, the `rounds_dealt`-th this socket
    /// dealt, kept for `TAIL_KEPT`.
    pub(super) fn kept(played: Box<crate::sim::Lockstep>, rounds_dealt: u32) -> RoundTail {
        RoundTail {
            played: Some(played),
            round: crate::transport::round_tag(rounds_dealt),
            left: TAIL_KEPT,
        }
    }

    /// One tick of the lobby: the last frames to everybody when
    /// `announced`, and to each peer in `asked` (its batch this tick) what
    /// it says it is still missing, whatever the clock.
    pub(super) fn repeat(
        &mut self,
        transport: &UdpTransport,
        delta: f32,
        announced: bool,
        asked: &[(usize, crate::transport::Batch)],
    ) {
        self.left -= delta;
        if self.left <= 0.0 {
            self.played = None;
        }
        let Some(played) = &self.played else {
            return;
        };
        let round = self.round;
        if announced {
            let tail = crate::transport::Batch {
                round,
                inputs: played.played_tail(),
                ..Default::default()
            };
            transport.send_batch(&tail, |_| true);
        }
        for (peer, batch) in asked {
            if batch.round != round {
                continue;
            }
            let wants = batch
                .wants
                .filter(|wants| wants.iter().all(|&frame| played.within_horizon(frame)));
            let owed = crate::transport::Batch {
                round,
                inputs: played.owed(wants, None, played.frame()),
                ..Default::default()
            };
            transport.send_batch(&owed, |to| to == *peer);
        }
    }
}

impl Hosted {
    /// Drop a peer from the socket and from the table, so whoever came
    /// after it moves up one in both.
    pub(super) fn forget_peer(&mut self, peer: usize) {
        self.transport.forget(peer);
        self.peers.forget(peer);
    }

    /// Ask a peer to leave, and keep it out of this beach.
    pub(super) fn turn_away(&mut self, peer: usize) {
        self.transport.turn_away(peer);
        self.peers.forget(peer);
    }

    /// Peers here to play, the ones who fill the seats. Capped at the five
    /// chairs beside the host's: seats run out long before the socket
    /// does, which takes [`MAX_PEERS`](crate::transport::MAX_PEERS), and
    /// `PeerBook::seat_plan` turns the surplus into onlookers, so a table of eight
    /// would-be rivals reads as "5 aboard".
    pub fn players_aboard(&self) -> usize {
        self.peers.at_the_table().count()
    }

    /// Peers here to look rather than play.
    pub(super) fn watchers_aboard(&self) -> usize {
        self.peers.iter().filter(|peer| peer.watch).count()
    }
}

/// A beach being greeted: the socket greeting it, and what has come back.
pub struct Joined {
    pub transport: UdpTransport,
    /// We asked to watch, so we greet with `Watch` and expect no seat back.
    pub watching: bool,
    /// How many are in front of us in line for the next round, once the
    /// host has said we are in one. `None` while there is still a lobby
    /// to be in: a round has to be under way before anybody queues.
    pub queued: Option<u8>,
    /// The host's word on its last lobby roster: this peer came to play,
    /// and the table has no chair left for it (`NetMsg::Roster::chair`),
    /// so the launch seats it watching.
    pub no_chair: bool,
    /// The host's word on its last lobby roster: it has started the match
    /// and is picking the map (`NetMsg::Roster::picking`).
    pub host_picking: bool,
    /// The host answered a greeting with `Full`: its socket has no room for
    /// this peer. Not taken as the end of the call, since a place frees
    /// as soon as the host forgets a peer gone quiet, and the greeting
    /// goes on until [`NO_ANSWER_AFTER`] of no other word,
    /// which then says the beach is full rather than unanswered.
    pub(super) told_full: bool,
    pub(super) hello_in: f32,
    /// Seconds since the host last said anything, and whether it has ever
    /// said anything at all.
    ///
    /// Dialling opens a socket, which on UDP proves nothing: there may be
    /// no machine at that address, or no game on it, or a firewall in the
    /// way, and all three look just like a host who has not started the
    /// round yet. These two are how the difference gets onto the screen.
    pub(super) host_silence: f32,
    pub(super) host_answered: bool,
    /// Whether the host said it was going, and how long ago: believed once
    /// it has stood for [`LOBBY_GOODBYE_GRACE`], longer than
    /// the host's once-a-second roster, which takes it back.
    pub(super) host_goodbye: crate::app::net::Goodbye,
    /// The seed of the round just played, set when a finished match walks
    /// back into this lobby still connected. The host may still be on its
    /// results card, where it re-answers every greeting with that round's
    /// `Start`; without this the lobby would read the repeat as an
    /// invitation and walk straight back into the finished round.
    played_seed: Option<u64>,
    /// The host's dials and its beach's name, from its [`NetMsg::Roster`];
    /// `None` until the first roster lands.
    pub host_terms: Option<HostTerms>,
    /// The round as it stands, arriving in parts, for a watcher who
    /// greeted a beach mid-round (`net::catch_up`).
    pub(super) catching_up: crate::app::net::catch_up::Assembly,
    /// Joining as a bot (route 1): the seat's name, "Greedy (Ana)",
    /// greeted with the flag that says a bot will drive it.
    pub bot: Option<String>,
    /// The beacon id of the beach this is, once known: taken from its row
    /// when it was picked off the list, or learnt from the row that
    /// matches its address. Rows are found by it rather than by address,
    /// because one beach arrives from two addresses (the broadcast and the
    /// loopback copy) and the list keeps whichever it heard first.
    pub beach: Option<u64>,
}

/// What a joiner knows of the match it is joining, from the host's
/// roster.
pub struct HostTerms {
    /// The host's dials. The terms card paints these rather than this
    /// machine's own [`MatchConfig`], which is nothing to do with the match
    /// being joined.
    pub terms: MatchTerms,
}

impl Joined {
    /// A beach dialled this frame: calling, not aboard, since nothing has
    /// answered yet and on UDP an open socket is no evidence that anything
    /// will.
    pub fn dialled(transport: UdpTransport, watching: bool) -> Joined {
        Joined {
            transport,
            watching,
            queued: None,
            no_chair: false,
            host_picking: false,
            told_full: false,
            hello_in: ANNOUNCE_EVERY,
            host_silence: 0.0,
            host_answered: false,
            host_goodbye: Default::default(),
            played_seed: None,
            host_terms: None,
            catching_up: Default::default(),
            bot: None,
            beach: None,
        }
    }

    /// A beach walked back into from a finished round: the host was
    /// talking moments ago, so this is a beach, not an unanswered address,
    /// until the silence rule says otherwise, and it greets again at once.
    pub fn returned(
        transport: UdpTransport,
        watching: bool,
        played_seed: u64,
        bot: Option<String>,
    ) -> Joined {
        Joined {
            transport,
            watching,
            queued: None,
            no_chair: false,
            host_picking: false,
            told_full: false,
            hello_in: 0.0,
            host_silence: 0.0,
            host_answered: true,
            host_goodbye: Default::default(),
            played_seed: Some(played_seed),
            host_terms: None,
            catching_up: Default::default(),
            bot,
            beach: None,
        }
    }

    /// What this peer greets the host with ([`greeting`]).
    pub fn greeting(&self, name: &str) -> NetMsg {
        greeting(self.bot.as_deref(), self.watching, name)
    }

    /// The seed of the round this table walked out of, if it did.
    pub fn played_seed(&self) -> Option<u64> {
        self.played_seed
    }
}

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

    /// Both greetings say who is greeting. The watcher's did not, and the
    /// host has two uses for a name it never got: the feed, which
    /// announced every player and no spectator at all, and the check on
    /// who a chat line claims to be from, which a nameless peer walks
    /// straight through.
    #[test]
    fn a_spectator_greets_by_name_like_everybody_else() {
        assert_eq!(
            greeting(None, true, "Dee"),
            NetMsg::Watch {
                name: crate::transport::wire_name("Dee")
            },
        );
        assert_eq!(
            greeting(None, false, "Bo"),
            NetMsg::Hello {
                name: crate::transport::wire_name("Bo"),
                bot: false,
            },
        );
    }

    /// A peer that joined as a bot speaks under the seat name it greeted
    /// with, which is the name the host vouches its lines under: its own
    /// echo named the player at the keyboard, and the rest of the table
    /// read the bot's.
    #[test]
    fn a_bot_chats_under_the_name_it_greeted_with() {
        let socket = UdpTransport::host(0).expect("socket");
        let joined = Joined::returned(socket, false, 0, Some("Greedy (Ana)".into()));
        let NetMsg::Hello { name, bot: true } = joined.greeting("Bo") else {
            panic!("a bot greets as one");
        };
        // The host writes the greeting's name down, and vouches by it.
        let mut peers = PeerBook::default();
        peers.row(0).name = crate::transport::name_from_wire(&name);
        let vouched = peers.vouch(0, crate::transport::wire_name("Bo"));
        assert_eq!(
            crate::transport::name_from_wire(&vouched),
            crate::transport::name_as_sent(speaking_as(joined.bot.as_deref(), "Bo")),
            "the echo and the table read one name"
        );
    }
}