//! 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, DIG_COOLDOWN_TICKS}; 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> { 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 { 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 = 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()) ); } /// Hands a pickaxe hit of `guest` at `pos` to the host as from `peer`. fn dig(host: &mut Host, peer: Peer, guest: &mut Guest, pos: Pos) { guest.act(Action::Dig { pos }); for bytes in guest.take_outbox() { host.receive(peer, &bytes); } } /// The tiles that pickaxe hits landed on, without breaking them. fn hits(events: &[Event]) -> Vec { events .iter() .filter_map(|event| match event { Event::TileHit { pos, .. } => Some(*pos), _ => None, }) .collect() } #[test] fn a_guests_hit_that_comes_during_the_cooldown_waits_for_it() { let (mut host, mut ada) = host_with_ada(); let rock = outcrop_origin(host.world().map()); let next = Pos::new(rock.x + 1, rock.y); dig(&mut host, 2, &mut ada, rock); let first = host.world().tick(); assert_eq!(hits(&host.step()), [rock], "the first hit lands at once"); // Two more hits come during the cooldown, as if the round trip had // become shorter. The newer one waits for the end of the cooldown. dig(&mut host, 2, &mut ada, rock); dig(&mut host, 2, &mut ada, next); let mut landed = Vec::new(); for _ in 0..DIG_COOLDOWN_TICKS { let tick = host.world().tick(); for pos in hits(&host.step()) { landed.push((tick, pos)); } } assert_eq!(landed, [(first + DIG_COOLDOWN_TICKS, next)]); } #[test] fn a_waiting_hit_is_dropped_when_its_guest_leaves() { let (mut host, mut ada) = host_with_ada(); let rock = outcrop_origin(host.world().map()); dig(&mut host, 2, &mut ada, rock); host.step(); dig(&mut host, 2, &mut ada, rock); host.disconnect(2); for _ in 0..DIG_COOLDOWN_TICKS { assert_eq!(hits(&host.step()), [], "nobody hits"); } }