M1c: Gleichschritt im Netz #9
@@ -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())
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user