155d90f3a5
Neue Tests treiben Host und Gast Nachricht für Nachricht und erzeugen, was das Netz im Speicher nie herstellt: einen Gast, der hinterherhängt, einen verlorenen Takt und Beitritte, die an den Grenzen warten. Sie halten fest, dass ein abgewichener Gast nichts rechnet, bis die Welt des Hosts kommt, dass er dann ältere Takte verwirft, dass die Zahl der Aktionen eines Gastes in jedem Takt neu beginnt und dass wartende Beitritte bei beiden Spielergrenzen zählen. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
297 lines
9.5 KiB
Rust
297 lines
9.5 KiB
Rust
//! 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())
|
|
);
|
|
}
|