M1c: Gleichschritt im Netz #9

Merged
steven merged 20 commits from m1c-gleichschritt-im-netz into main 2026-10-02 06:41:02 +00:00
Showing only changes of commit 155d90f3a5 - Show all commits
+296
View File
@@ -0,0 +1,296 @@
//! Host and guest driven by hand, message by message. These tests produce
//! what the in-memory network of `session.rs` never does, because it lets
//! every guest run each tick before the next one arrives: a guest that lags
//! behind, a tick that gets lost, and joins that wait at the limits.
use grovewright_core::command::Command;
use grovewright_core::guest::{Arrival, Guest};
use grovewright_core::host::{Host, Peer, MAX_ACTIONS_PER_TICK};
use grovewright_core::map::Pos;
use grovewright_core::mapgen::outcrop_origin;
use grovewright_core::player::{PlayerId, PlayerKey};
use grovewright_core::protocol::{Action, Refusal, ToGuest, ToHost, CHECK_INTERVAL_TICKS};
use grovewright_core::world::{Event, World};
const HOST_KEY: PlayerKey = [100; 16];
/// A host whose own player is in a new world.
fn started_host() -> Host {
let mut host = Host::new(World::new(5));
host.join_local(HOST_KEY, "Host");
host.step();
host
}
/// Connects a guest with a key made of `key` as `peer` and hands its
/// greeting to the host.
fn greet(host: &mut Host, peer: Peer, key: u8) -> Guest {
host.connect(peer);
let mut guest = Guest::new([key; 16], "Guest");
for bytes in guest.take_outbox() {
host.receive(peer, &bytes);
}
guest
}
/// Takes the host's whole outbox and keeps what is for `peer`.
fn messages_for(host: &mut Host, peer: Peer) -> Vec<Vec<u8>> {
host.take_outbox()
.into_iter()
.filter(|(to, _)| *to == peer)
.map(|(_, bytes)| bytes)
.collect()
}
/// The host's answer to a greeting from `peer`: `None` for its world, or
/// the reason it turned the guest away.
fn answer_to(host: &mut Host, peer: Peer) -> Option<Refusal> {
let messages = messages_for(host, peer);
match ToGuest::decode(&messages[0]) {
Ok(ToGuest::World { .. }) => None,
Ok(ToGuest::Refused(refusal)) => Some(refusal),
other => panic!("unexpected answer {other:?}"),
}
}
/// A host and the guest Ada at peer 2, in the same state.
fn host_with_ada() -> (Host, Guest) {
let mut host = started_host();
let mut ada = greet(&mut host, 2, 1);
for bytes in messages_for(&mut host, 2) {
ada.receive(&bytes).unwrap();
}
host.step();
for bytes in messages_for(&mut host, 2) {
ada.receive(&bytes).unwrap();
}
while ada.step().is_some() {}
assert!(ada.local_player().is_some());
assert_eq!(
ada.world().map(World::state_hash),
Some(host.world().state_hash())
);
(host, ada)
}
/// A world that knows players with keys made of 1 to `count`, all offline.
fn world_with_offline_players(count: u8) -> World {
let mut world = World::new(5);
for key in 1..=count {
world.step(&[Command::Join {
key: [key; 16],
name: "Visitor".to_owned(),
}]);
world.step(&[Command::Leave {
player: PlayerId(key - 1),
}]);
}
world
}
#[test]
fn the_hosts_own_queued_join_counts_for_the_online_limit() {
let mut host = Host::new(World::new(5));
host.join_local(HOST_KEY, "Host");
// No tick has run: the host's join is still queued.
let mut answers = Vec::new();
for key in 1..=4 {
let peer = Peer::from(key) + 1;
greet(&mut host, peer, key);
answers.push(answer_to(&mut host, peer));
}
assert_eq!(answers, [None, None, None, Some(Refusal::WorldFull)]);
}
#[test]
fn queued_new_keys_count_for_the_limit_of_known_players_but_known_ones_do_not() {
let mut host = Host::new(world_with_offline_players(15));
greet(&mut host, 2, 3);
assert_eq!(answer_to(&mut host, 2), None, "a returning player");
greet(&mut host, 3, 200);
assert_eq!(
answer_to(&mut host, 3),
None,
"a new player fits as the 16th"
);
greet(&mut host, 4, 201);
assert_eq!(
answer_to(&mut host, 4),
Some(Refusal::TooManyPlayers),
"a second new player does not"
);
let events = host.step();
assert!(!events
.iter()
.any(|event| matches!(event, Event::JoinRefused { .. })));
assert_eq!(host.world().players().len(), 16);
}
#[test]
fn a_queued_join_of_a_player_who_is_online_already_counts_once() {
let mut world = World::new(5);
let joins = [HOST_KEY, [1; 16], [2; 16]].map(|key| Command::Join {
key,
name: "Online".to_owned(),
});
world.step(&joins);
let mut host = Host::new(world);
// The host's player is online already; its queued join changes nothing.
host.join_local(HOST_KEY, "Host");
greet(&mut host, 2, 3);
assert_eq!(answer_to(&mut host, 2), None, "the fourth player fits");
}
#[test]
fn the_count_of_a_guests_actions_starts_again_in_every_tick() {
let (mut host, mut ada) = host_with_ada();
for tick in 0..3 {
for x in 0..10 {
ada.act(Action::Dig {
pos: Pos::new(x, 1),
});
}
for bytes in ada.take_outbox() {
host.receive(2, &bytes);
}
host.step();
let counts: Vec<usize> = messages_for(&mut host, 2)
.iter()
.filter_map(|bytes| match ToGuest::decode(bytes) {
Ok(ToGuest::Tick { commands, .. }) => Some(commands.len()),
_ => None,
})
.collect();
assert_eq!(counts, [MAX_ACTIONS_PER_TICK as usize], "tick {tick}");
}
}
#[test]
fn a_tick_carries_the_number_of_the_tick_before_it_runs() {
let (mut host, mut ada) = host_with_ada();
let before = host.world().tick();
host.step();
let messages = messages_for(&mut host, 2);
let Ok(ToGuest::Tick { tick, .. }) = ToGuest::decode(&messages[0]) else {
panic!("not a tick");
};
assert_eq!(tick, before);
for bytes in messages {
ada.receive(&bytes).unwrap();
}
ada.step().unwrap();
assert_eq!(ada.world().map(World::tick), Some(before + 1));
}
#[test]
fn a_lagging_guest_that_diverges_runs_nothing_until_the_world_comes_and_drops_older_ticks() {
let (mut host, mut ada) = host_with_ada();
// The host's player hits the rock, but Ada's copy of that tick loses the
// command, as if her machine had run it differently.
host.act_local(Action::Dig {
pos: outcrop_origin(host.world().map()),
});
host.step();
for bytes in messages_for(&mut host, 2) {
let bytes = match ToGuest::decode(&bytes) {
Ok(ToGuest::Tick { tick, check, .. }) => ToGuest::Tick {
tick,
commands: vec![],
check,
}
.encode(),
_ => bytes,
};
ada.receive(&bytes).unwrap();
}
while ada.step().is_some() {}
assert!(!ada.has_diverged());
// Thirty more ticks arrive before Ada runs any of them.
for _ in 0..30 {
host.step();
for bytes in messages_for(&mut host, 2) {
ada.receive(&bytes).unwrap();
}
}
assert!(ada.pending_ticks() >= 30);
// She catches up and trips over the next checksum.
while ada.step().is_some() && !ada.has_diverged() {}
assert!(ada.has_diverged());
let waiting = ada.pending_ticks();
assert!(waiting > 0, "newer ticks are still waiting");
assert!(ada.step().is_none(), "no tick runs after a divergence");
assert_eq!(ada.pending_ticks(), waiting, "and none is used up");
// Her report names her tick after the step that showed the divergence.
let reports = ada.take_outbox();
assert_eq!(reports.len(), 1);
let Ok(ToHost::Diverged { tick, hash }) = ToHost::decode(&reports[0]) else {
panic!("not a divergence report");
};
assert_eq!(Some(tick), ada.world().map(World::tick));
assert_eq!(tick % CHECK_INTERVAL_TICKS, 0);
host.receive(2, &reports[0]);
let divergences = host.take_divergences();
assert_eq!(divergences.len(), 1);
assert_eq!((divergences[0].tick, divergences[0].hash), (tick, hash));
// The host has gone on meanwhile. Its world comes first, then later ticks.
let world_tick = host.world().tick();
let answer = messages_for(&mut host, 2);
assert_eq!(answer.len(), 1);
host.step();
host.step();
let later = messages_for(&mut host, 2);
assert_eq!(later.len(), 2);
assert_eq!(
ada.receive(&answer[0]),
Ok(Arrival::World { replaced: true })
);
assert_eq!(ada.pending_ticks(), 0, "the older ticks are dropped");
assert_eq!(ada.world().map(World::tick), Some(world_tick));
assert!(!ada.has_diverged());
for bytes in later {
ada.receive(&bytes).unwrap();
}
while ada.step().is_some() {}
assert_eq!(
ada.world().map(World::state_hash),
Some(host.world().state_hash())
);
assert!(ada.take_diverged_recording().is_some());
}
#[test]
fn a_missing_tick_makes_the_guest_ask_for_the_world() {
let (mut host, mut ada) = host_with_ada();
host.step();
// This tick never arrives.
messages_for(&mut host, 2);
host.step();
for bytes in messages_for(&mut host, 2) {
ada.receive(&bytes).unwrap();
}
assert!(ada.step().is_none());
assert!(ada.has_diverged());
let reports = ada.take_outbox();
assert_eq!(reports.len(), 1);
host.receive(2, &reports[0]);
for bytes in messages_for(&mut host, 2) {
assert_eq!(ada.receive(&bytes), Ok(Arrival::World { replaced: true }));
}
host.step();
for bytes in messages_for(&mut host, 2) {
ada.receive(&bytes).unwrap();
}
while ada.step().is_some() {}
assert_eq!(
ada.world().map(World::state_hash),
Some(host.world().state_hash())
);
}