// Multiplayer, client side of deterministic lockstep: the local orders go to the server, the server // confirms a turn with the orders of all masters, and the world advances one turn at a time (five // steps of 20 ms). A client that receives nothing stands still. Every client computes the same // world from the same turns, so comparing a checksum of that world shows a divergence (desync). #![allow(dead_code)] // the server relay and the Multiplayer menu (steps C and D) call all of this use crate::commands::Command; use crate::game::Game; use crate::map::{Kind, Owner}; // one server turn of 100 ms: five fixed simulation steps pub const STEPS_PER_TURN: usize = 5; #[derive(Clone, Debug, Default)] pub struct Netplay { pub turn: u32, // turns applied so far pub desync: bool, // a peer reported a different world pub last_sum: u32, // last turn whose checksum was sent } const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; const FNV_PRIME: u64 = 0x100_0000_01b3; fn mix(h: &mut u64, v: u64) { *h ^= v; *h = h.wrapping_mul(FNV_PRIME); } fn mixf(h: &mut u64, v: f32) { mix(h, v.to_bits() as u64); } fn mixp(h: &mut u64, p: Option<(i32, i32)>) { match p { Some((x, y)) => { mix(h, x as u32 as u64); mix(h, y as u32 as u64); } None => mix(h, u64::MAX), } } // a value whose type is not a plain number (Job carries which tile it works on) fn mixdbg(h: &mut u64, v: impl std::fmt::Debug) { for b in format!("{v:?}").bytes() { mix(h, b as u64); } } // the side whose ground a tile of `owner` is (unowned ground counts as the first player's) pub fn side_of_owner(owner: Owner) -> u8 { match owner { Owner::Hero => 1, Owner::Rival1 => 2, Owner::Rival2 => 3, Owner::Rival3 => 4, Owner::Player | Owner::None => 0, } } // gold amounts a master can send to an ally; `GOLD_ALL` sends everything he has pub const GOLD_ALL: u32 = u32::MAX; pub const GOLD_AMOUNTS: [u32; 4] = [100, 500, 1000, GOLD_ALL]; impl Game { // ------------------------------------------------------------ teams and allies // the team of a master seat in a versus match (sides without an entry are a team of their own) pub fn team_of(&self, side: u8) -> u8 { self.teams.iter().find(|t| t.0 == side).map_or(10 + side, |t| t.1) } // a master seat of the match: a human master or an AI seat pub fn is_seat_side(&self, side: u8) -> bool { Owner::MASTER_SIDES.contains(&side) && (self.is_master(side) || self.ai_sides.contains(&side)) } // sides that never fight each other: the same side, co-op partners and team mates. Only shared // state goes in, so every client of a match decides the same. pub fn allied(&self, a: u8, b: u8) -> bool { if a == b { return true; } if !self.is_seat_side(a) || !self.is_seat_side(b) { return false; } self.coop || self.team_of(a) == self.team_of(b) } // the teams of a versus match: (side, team) for the master seats pub fn set_teams(&mut self, teams: &[(u8, u8)]) { self.teams = teams.iter().copied().filter(|(s, _)| Owner::MASTER_SIDES.contains(s)).collect(); } // ------------------------------------------------------------ fallen hearts pub fn is_out(&self, side: u8) -> bool { self.out_sides.contains(&side) } // the master at this computer lost his heart: he watches the rest of the match pub fn spectating(&self) -> bool { self.netplay.is_some() && self.is_out(self.local_side) } fn master_state(&self, side: u8) -> Option<&crate::game::MasterState> { self.masters.iter().find(|m| m.side == side) } pub fn core_hp_of(&self, side: u8) -> f32 { if side == self.me_side { self.core_hp } else { self.master_state(side).map_or(0.0, |m| m.core_hp) } } pub fn core_of_side(&self, side: u8) -> (i32, i32) { if side == self.me_side { self.map.core } else { self.master_state(side).map_or(self.map.core, |m| m.core) } } pub fn gold_of(&self, side: u8) -> f32 { if side == self.me_side { self.gold } else { self.master_state(side).map_or(0.0, |m| m.gold) } } // damage to the soul core of a master (whichever master is being processed) pub fn damage_master_core(&mut self, side: u8, dmg: f32) { if side == self.me_side { self.core_hp -= dmg; } else if let Some(m) = self.masters.iter_mut().find(|m| m.side == side) { m.core_hp -= dmg; } } // the heart of a master of the match that is no ally of `side`, near (x, y) pub(crate) fn enemy_master_near(&self, side: u8, x: f32, y: f32) -> Option { if self.masters.is_empty() { return None; } self.master_sides().into_iter().filter(|&s| s != side && !self.is_out(s) && !self.allied(side, s)).find(|&s| { let (cx, cy) = self.core_of_side(s); (x - cx as f32 - 1.5).powi(2) + (y - cy as f32 - 1.5).powi(2) <= 8.0 * 8.0 }) } // a message and a sound for the player at this computer, whichever master is being processed pub(crate) fn tell_local(&mut self, key: &'static str, args: Vec, sfx: Option) { let me = self.me_side; let local = self.local_side; self.switch_to(local); self.msg(key, args); if let Some(sfx) = sfx { self.sfx_ui(sfx); } self.switch_to(me); } // a master of a match whose core has no strength left is out. His creatures fight on without a // heart until they fall; he keeps watching. Runs in the shared part of the step. pub(crate) fn update_fallen_masters(&mut self) { if self.netplay.is_none() { return; } for side in self.master_sides() { if self.is_out(side) || self.core_hp_of(side) > 0.0 { continue; } self.out_sides.push(side); self.master_fell(side); } } fn master_fell(&mut self, side: u8) { let me = self.me_side; self.switch_to(side); self.core_hp = 0.0; self.flag = None; // what he held in his hand is put down beside the broken heart let (cx, cy) = self.map.core; let hand = std::mem::take(&mut self.hand); for (k, id) in hand.iter().enumerate() { if let Some(e) = self.ents.iter_mut().find(|e| e.id == *id && e.held) { e.held = false; e.x = cx as f32 + 1.5 + (k % 3) as f32 * 0.6 - 0.6; e.y = cy as f32 + 3.5; e.disp_x = e.x; e.disp_y = e.y; e.job = crate::game::Job::None; e.path.clear(); } } // the heart is gone: its tiles become plain ground of that master let owner = Owner::of_side(side); for t in self.map.t.iter_mut() { if t.kind == Kind::Core && t.owner == owner { t.kind = Kind::Floor; } } self.switch_to(me); if side == self.local_side { self.tell_local("msg.master_out_you", vec![], Some(crate::game::Sfx::Defeat)); } else { self.tell_local("msg.master_out", vec![format!("@net.side{side}")], Some(crate::game::Sfx::CoreHit)); } } // goals of a mission played in a match: what one master holds (mana springs) counts for any of // the masters still in the game, everything else is shared pub fn match_goal_done(&self, goal: crate::game::Goal) -> bool { match goal { crate::game::Goal::Springs(n) => self.master_sides().into_iter().filter(|s| !self.is_out(*s)).any(|s| { let springs = if s == self.me_side { self.springs } else { self.master_state(s).map_or(0, |m| m.springs) }; springs >= n as usize }), g => self.goal_done(g), } } // The end of a match, decided from shared state only so that all clients stop in the same step: // a mission (co-op) is lost when every master's heart fell; a versus match is over when only one // team is left - the masters of that team win, the others lose. pub(crate) fn check_match_end(&mut self) { if self.state != 0 { return; } let alive: Vec = self.master_sides().into_iter().filter(|s| !self.is_out(*s)).collect(); let lost = |g: &mut Game, key: &'static str| { g.state = 2; g.msg(key, vec![]); g.sfx_ui(crate::game::Sfx::Defeat); }; if let Some(m) = &self.mission { let (limit, goals) = (m.time_limit, m.goals.clone()); if alive.is_empty() { return lost(self, "msg.defeat"); } if limit > 0.0 && self.time > limit { return lost(self, "msg.time_up"); } if !goals.is_empty() && goals.iter().all(|&g| self.match_goal_done(g)) { self.state = 1; self.msg("msg.mission_won", vec![]); self.sfx_ui(crate::game::Sfx::Victory); } return; } // the teams that started (masters and AI seats) and the ones still standing let mut sides: Vec = self.master_sides(); sides.extend(self.rivals.iter().map(|r| r.side)); let mut teams: Vec = sides.iter().map(|&s| self.team_of(s)).collect(); teams.sort_unstable(); teams.dedup(); let mut standing: Vec = alive.iter().copied().chain(self.rivals.iter().filter(|r| r.alive).map(|r| r.side)).map(|s| self.team_of(s)).collect(); standing.sort_unstable(); standing.dedup(); if alive.is_empty() { return lost(self, "msg.defeat"); } let over = if teams.len() >= 2 { standing.len() <= 1 } else { // everybody on one team: the humans' gate is the enemy self.gate_on && self.gate_hp <= 0.0 }; if !over { return; } if standing.contains(&self.team_of(self.local_side)) { self.state = 1; self.msg("msg.match_won", vec![]); self.sfx_ui(crate::game::Sfx::Victory); } else { lost(self, "msg.match_lost"); } } // ------------------------------------------------------------ gold for allies // the masters the local master can send gold to: human allies whose heart still beats pub fn gold_allies(&self) -> Vec { if self.netplay.is_none() || self.is_out(self.local_side) { return Vec::new(); } self.master_sides().into_iter().filter(|&s| s != self.local_side && !self.is_out(s) && self.allied(self.local_side, s)).collect() } // the master being processed sends gold to an ally (never more than he has, never more than // the ally's treasury takes) pub(crate) fn send_gold(&mut self, to: u8, amount: u32) { let from = self.me_side; if to == from || !self.is_master(to) || !self.allied(from, to) || self.is_out(to) || self.is_out(from) { return; } let want = (amount as f32).min(self.gold.floor()); if want < 1.0 { self.msg("msg.gold_none", vec![]); self.sfx_ui(crate::game::Sfx::Error); return; } self.switch_to(to); let got = self.add_gold(want).floor(); if got >= 1.0 { self.msg("msg.gold_received", vec![(got as i32).to_string(), format!("@net.side{from}")]); self.sfx_ui(crate::game::Sfx::Payday); } self.switch_to(from); if got < 1.0 { self.msg("msg.gold_full", vec![format!("@net.side{to}")]); self.sfx_ui(crate::game::Sfx::Error); return; } self.gold -= got; self.msg("msg.gold_sent", vec![(got as i32).to_string(), format!("@net.side{to}")]); self.sfx_ui(crate::game::Sfx::Deposit); } // a match with masters on `sides` (of 0, 2, 3, 4), `local` is the master who plays at this computer pub fn start_match(&mut self, sides: &[u8], local: u8) -> bool { if !self.start_masters(sides, local) { return false; } // every master has to be able to build his dungeon at once if let Some(v) = self.known.get_mut(crate::game::SUMMON_TOOL) { *v = true; } for m in self.masters.iter_mut() { if let Some(v) = m.known.get_mut(crate::game::SUMMON_TOOL) { *v = true; } } self.netplay = Some(Netplay::default()); true } pub fn in_match(&self) -> bool { self.netplay.is_some() } // a seat without a human in a started match: that side keeps playing as a rival-style AI pub fn sync_ai_sides(&mut self, ai: &[u8], difficulty: u8) { if self.netplay.is_none() { return; } self.ai_difficulty = difficulty; self.ai_sides = ai .iter() .copied() .filter(|side| *side != self.local_side) .collect(); for side in self.ai_sides.clone() { if self.is_out(side) { continue; // a fallen master who left has nothing left for an AI to play } let core = if side == self.me_side { continue; } else if let Some(m) = self.masters.iter().find(|m| m.side == side) { Some(m.core) } else { None }; let Some(core) = core else { continue }; if let Some(k) = self.masters.iter().position(|m| m.side == side) { self.masters.remove(k); } if !self.is_rival_side(side) { self.add_rival(side, core.0 + 1, core.1 + 1, self.ai_difficulty); } } } // the relay ended a match, or the player leaves it: the world stops being a turn-based match pub fn end_match(&mut self) { self.netplay = None; } // what the local master ordered since the last turn: these go to the server pub fn take_orders(&mut self) -> Vec { std::mem::take(&mut self.pending) } // the server confirmed a turn: run the orders of all masters, then the fixed steps of the turn. // A local client that is paused or finished leaves the turn out; empty match seats are AI sides. pub fn receive_turn(&mut self, cmds: &[Command]) -> u32 { if self.netplay.is_none() || self.state != 0 || self.paused { return 0; } self.apply_turn(cmds, STEPS_PER_TURN); match self.netplay.as_mut() { Some(np) => { np.turn += 1; np.turn } None => 0, } } // a peer's checksum of turn `turn`: when we are standing at that turn as well, the numbers have // to match. A mismatch is reported once and makes the match unusable (the message says it). pub fn check_peer(&mut self, turn: u32, sum: u64) -> bool { let same = match &self.netplay { Some(np) => np.turn != turn || sum == self.checksum(), None => true, }; if same { return true; } if let Some(np) = self.netplay.as_mut() { np.desync = true; } self.msg("net.desync", vec![]); self.sfx_ui(crate::game::Sfx::Error); false } pub fn checksum_due(&mut self) -> Option { let turn = match &self.netplay { Some(np) => np.turn, None => return None, }; if turn < 10 { return None; } let due = match &self.netplay { Some(np) => turn.saturating_sub(np.last_sum) >= 10, None => false, }; if !due { return None; } if let Some(np) = self.netplay.as_mut() { np.last_sum = turn; } Some(turn) } // the part of the world every client of a match has to agree on: the map, the creatures, the // economy of every master, the wave and the random state. Fog, particles and the camera are // local and stay out of it. pub fn checksum(&self) -> u64 { let mut h = FNV_OFFSET; mix(&mut h, self.sim_steps); // whether the world is still running: the end itself is told per team (won or lost) mix(&mut h, (self.state != 0) as u64); mix(&mut h, self.next_id as u64); mix(&mut h, self.rng.0 as u64); mix(&mut h, self.wave as u64); mixf(&mut h, self.time); mixf(&mut h, self.gate_hp); mixf(&mut h, self.last_wave_value); mix(&mut h, self.towers.len() as u64); mix(&mut h, self.traps.len() as u64); mix(&mut h, self.rollers.len() as u64); mix(&mut h, self.graves.len() as u64); for t in self.map.t.iter() { mix(&mut h, t.kind as u8 as u64); mix(&mut h, t.owner as u8 as u64); mix(&mut h, t.marked as u64); mix(&mut h, t.reinforced as u64); mix(&mut h, t.res as u64); mix(&mut h, t.var as u64); mixf(&mut h, t.hp); mix(&mut h, t.gold as u32 as u64); mix(&mut h, t.reserved as u64); mix(&mut h, t.bed as u64); mixf(&mut h, t.food); } for e in self.ents.iter() { mix(&mut h, e.id as u64); mix(&mut h, e.kind as u8 as u64); mix(&mut h, e.side as u64); mix(&mut h, e.hero as u64); mix(&mut h, e.alive as u64); mix(&mut h, e.held as u64); mix(&mut h, e.level as u64); mixf(&mut h, e.x); mixf(&mut h, e.y); mixf(&mut h, e.hp); mixf(&mut h, e.max_hp); mixf(&mut h, e.xp); mixdbg(&mut h, &e.job); mixf(&mut h, e.timer); mixf(&mut h, e.cd); mixf(&mut h, e.think); mixf(&mut h, e.repath); mix(&mut h, e.carry as u32 as u64); mix(&mut h, e.carry_res as u64); mixf(&mut h, e.hunger); mixf(&mut h, e.energy); mixf(&mut h, e.happy); mixp(&mut h, e.bed.map(|b| (b as i32, 0))); mix(&mut h, e.moving as u64); mix(&mut h, e.working as u64); mixf(&mut h, e.slow); mix(&mut h, e.captive as u64); mix(&mut h, e.carrier as u64); mixf(&mut h, e.torture_t); mixf(&mut h, e.skill_cd); mixp(&mut h, e.waypoint); mixf(&mut h, e.boost); mixf(&mut h, e.stun); mixf(&mut h, e.haste); mixf(&mut h, e.shield); mixf(&mut h, e.invis); mixf(&mut h, e.plague); mixf(&mut h, e.ab_cd); mixf(&mut h, e.mood_t); mix(&mut h, e.guard as u64); mix(&mut h, e.path.len() as u64); } let mut sums: Vec<(u8, f32, f32, f32, f32, usize, usize, f32, usize, (i32, i32))> = Vec::new(); sums.push((self.me_side, self.gold, self.mana, self.core_hp, self.payday, self.hand.len(), self.counts.iter().sum(), self.reserve, self.springs, self.map.core)); for m in self.masters.iter() { sums.push((m.side, m.gold, m.mana, m.core_hp, m.payday, m.hand.len(), m.counts.iter().sum(), m.reserve, m.springs, m.core)); } sums.sort_by(|a, b| a.0.cmp(&b.0)); for (side, gold, mana, core_hp, payday, hand, count, reserve, springs, core) in sums.iter() { mix(&mut h, *side as u64); mixf(&mut h, *gold); mixf(&mut h, *mana); mixf(&mut h, *core_hp); mixf(&mut h, *payday); mix(&mut h, *hand as u64); mix(&mut h, *count as u64); mixf(&mut h, *reserve); mix(&mut h, *springs as u64); mixp(&mut h, Some(*core)); } for r in self.rivals.iter() { mixp(&mut h, Some(r.core)); mix(&mut h, r.side as u64); mix(&mut h, r.alive as u64); mix(&mut h, r.radius as u64); mix(&mut h, r.difficulty as u64); mixf(&mut h, r.hp); mixf(&mut h, r.grow_t); mixf(&mut h, r.spawn_t); mixf(&mut h, r.attack_t); mixf(&mut h, r.power); } for side in self.ai_sides.iter() { mix(&mut h, *side as u64); } for side in self.out_sides.iter() { mix(&mut h, 100 + *side as u64); } h } } #[cfg(test)] mod tests { use super::*; use crate::commands::{Act, Rect}; use crate::map::{Kind, Owner}; fn client(seed: u32, local: u8) -> Game { let mut g = Game::new(seed, 96, 128); g.init_fog(); assert!(g.start_match(&[0, 2, 3], local), "a match of three masters"); g } // the top-left corner of the core of `owner` fn core_of(g: &Game, owner: Owner) -> (i32, i32) { for y in 0..128i32 { for x in 0..96i32 { let t = &g.map.t[(y as usize) * 96 + x as usize]; if t.kind == Kind::Core && t.owner == owner { return (x, y); } } } panic!("no core of {owner:?}"); } #[test] fn two_clients_of_one_match_see_the_same_world() { let (mut a, mut b) = (client(21, 0), client(21, 2)); assert_eq!(a.checksum(), b.checksum(), "the same start"); let (red, green) = (core_of(&a, Owner::Player), core_of(&a, Owner::Rival1)); for t in 0..40u32 { let (d, o) = (t as i32 * 2, t as i32 * 2); let turn = vec![ Command { side: 0, seq: t * 2, act: Act::Dig(Rect::new(red.0 + 7 + o, red.1 - 4, red.0 + 8 + o, red.1 - 3)) }, Command { side: 2, seq: t * 2 + 1, act: Act::Dig(Rect::new(green.0 + 4, green.1 + 5 + d, green.0 + 5, green.1 + 6 + d)) }, ]; a.receive_turn(&turn); b.receive_turn(&turn); } assert_eq!(a.sim_steps, b.sim_steps); assert_eq!(a.sim_steps, 40 * STEPS_PER_TURN as u64, "five steps in every turn"); assert_eq!(a.gold, b.gold); assert_eq!(a.checksum(), b.checksum(), "the worlds stayed identical"); // real time alone never moves a match forward a.update(1.0); b.update(1.0); assert_eq!(a.sim_steps, 40 * STEPS_PER_TURN as u64); assert_eq!(a.checksum(), b.checksum()); } #[test] fn a_new_match_master_can_summon_at_once() { let mut g = client(24, 2); assert!(g.tool_allowed(crate::game::SUMMON_TOOL), "summoning is known in a match"); assert_eq!(g.worker_count(), 4, "a new master starts with his own four imps"); let (cx, cy) = core_of(&g, Owner::Rival1); let mana = g.mana; let cost = g.summon_cost(); g.queue(Act::Summon { x: cx as f32 - 0.5, y: cy as f32 + 0.5 }); let turn = g.take_orders(); g.receive_turn(&turn); assert!(cost > 0.0 && g.mana < mana, "summoning costs mana"); assert!(g.ents.iter().any(|e| e.kind == crate::game::EK::Worker && e.side == 2 && e.alive), "the summoned imp belongs to the master who summoned it"); assert_eq!(g.worker_count(), 5, "the summon counts as the master's own worker"); } #[test] fn a_lost_seat_in_a_match_is_converted_to_the_same_ai_side() { let (mut a, mut b) = (client(31, 0), client(31, 2)); a.sync_ai_sides(&[3], 1); b.sync_ai_sides(&[3], 1); assert_eq!(a.checksum(), b.checksum(), "both clients convert side 3 the same way"); assert!(a.is_rival_side(3) && b.is_rival_side(3)); assert!(!a.is_master(3) && !b.is_master(3)); a.receive_turn(&[]); b.receive_turn(&[]); assert_eq!(a.checksum(), b.checksum(), "and keep playing it identically"); } #[test] fn a_client_without_the_next_turn_stands_still_and_its_orders_wait() { let mut g = client(22, 0); let (cx, cy) = core_of(&g, Owner::Player); g.queue(Act::Dig(Rect::new(cx + 7, cy, cx + 8, cy + 1))); for _ in 0..10 { g.update(0.2); } assert_eq!(g.sim_steps, 0, "no turn, no step"); assert_eq!(g.netplay.as_ref().unwrap().turn, 0); let orders = g.take_orders(); assert_eq!(orders.len(), 1, "the order waits for the server"); assert!(g.pending.is_empty()); g.receive_turn(&orders); assert_eq!(g.sim_steps, STEPS_PER_TURN as u64); assert_eq!(g.netplay.as_ref().unwrap().turn, 1); } // every client of one match: the same world, seen from another seat fn clients(seed: u32, locals: &[u8], teams: &[(u8, u8)]) -> Vec { locals .iter() .map(|&local| { let mut g = client(seed, local); g.set_teams(teams); g }) .collect() } fn turn_all(games: &mut [Game], cmds: &[Command]) { for g in games.iter_mut() { g.receive_turn(cmds); } } fn same_world(games: &[Game], why: &str) { let sum = games[0].checksum(); for g in games.iter().skip(1) { assert_eq!(g.checksum(), sum, "{why}"); } } #[test] fn a_fallen_master_watches_while_the_match_goes_on() { let mut games = clients(41, &[0, 2, 3], &[]); for g in games.iter_mut() { g.damage_master_core(2, crate::game::CORE_HP * 2.0); } turn_all(&mut games, &[]); same_world(&games, "the heart fell in the same step everywhere"); for g in games.iter() { assert!(g.is_out(2) && !g.is_out(0) && !g.is_out(3)); assert_eq!(g.state, 0, "the match goes on for the others"); } let green = &games[1]; assert!(green.spectating() && !games[0].spectating()); assert!(green.msgs.iter().any(|m| m.key == "msg.master_out_you")); assert!(games[0].msgs.iter().any(|m| m.key == "msg.master_out")); assert!(!green.map.t.iter().any(|t| t.kind == Kind::Core && t.owner == Owner::Rival1), "the heart is broken"); let workers = green.ents.iter().filter(|e| e.alive && e.side == 2).count(); assert!(workers > 0, "his creatures go on without a heart"); // the spectator sees everything and cannot order anything let mut games = games; for _ in 0..30 { turn_all(&mut games, &[]); } assert!(games[1].map.visible.iter().all(|v| *v), "no fog for the spectator"); assert!(!games[0].map.visible.iter().all(|v| *v), "the others still have fog"); let (gx, gy) = games[1].core_of_side(2); games[1].queue(Act::Dig(Rect::new(gx + 7, gy, gx + 8, gy + 1))); assert!(games[1].take_orders().is_empty(), "a spectator gives no orders"); // an order that still comes in for the fallen side changes nothing let stray = vec![Command { side: 2, seq: 0, act: Act::Dig(Rect::new(gx + 7, gy, gx + 8, gy + 1)) }]; turn_all(&mut games, &stray); assert!(!games[0].map.t[crate::map::idx(gx + 7, gy)].marked); for _ in 0..50 { turn_all(&mut games, &[]); } same_world(&games, "the spectator stays in step"); assert!(games.iter().all(|g| g.state == 0)); // the last enemy falls: red wins, blue (and the spectator) lose, in the same step for g in games.iter_mut() { g.damage_master_core(3, crate::game::CORE_HP * 2.0); } turn_all(&mut games, &[]); same_world(&games, "the end came in the same step everywhere"); assert_eq!(games[0].state, 1, "red is the last master standing"); assert_eq!(games[1].state, 2); assert_eq!(games[2].state, 2); assert!(games[0].msgs.iter().any(|m| m.key == "msg.match_won")); } #[test] fn team_mates_do_not_fight_and_win_together() { // red and green are team A, blue is team B let teams = [(0u8, 0u8), (2, 0), (3, 1)]; let mut games = clients(43, &[0, 2, 3], &teams); let g = &games[0]; assert!(g.allied(0, 2) && g.allied(2, 0) && !g.allied(0, 3) && !g.allied(2, 3)); assert_eq!(games[1].gold_allies(), vec![0u8], "green can send gold to red"); assert!(games[2].gold_allies().is_empty(), "blue has no team mate"); // a creature of red next to one of green: no target; one of blue is for g in games.iter_mut() { let (cx, cy) = g.core_of_side(2); let (x, y) = (cx as f32 + 4.5, cy as f32 + 4.5); let red = g.spawn(crate::game::EK::Kobold, false, x, y, 3); let green = g.spawn(crate::game::EK::Kobold, false, x + 1.0, y, 3); for (id, side) in [(red, 0u8), (green, 2u8)] { if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) { e.side = side; } } let red_e = g.ents.iter().find(|e| e.id == red).unwrap().clone(); assert_eq!(g.nearest_enemy(&red_e, 3.0), None, "team mates are no targets"); let blue = g.spawn(crate::game::EK::Kobold, false, x, y + 1.0, 3); if let Some(e) = g.ents.iter_mut().find(|e| e.id == blue) { e.side = 3; } assert_eq!(g.nearest_enemy(&red_e, 3.0), Some(blue), "the other team is"); // the blue test unit next to green's heart has done its job (it would see the heart) g.ents.retain(|e| e.id != blue); } for _ in 0..40 { turn_all(&mut games, &[]); } same_world(&games, "teams play the same on every client"); // the fog shows the team mate's ground let (gx, gy) = games[0].core_of_side(2); assert!(games[0].map.visible[crate::map::idx(gx + 1, gy + 1)], "red sees green's heart"); assert!(!games[2].map.visible[crate::map::idx(gx + 1, gy + 1)], "blue does not"); // one of team A falls: the match goes on; team B falls: team A wins together for g in games.iter_mut() { g.damage_master_core(0, crate::game::CORE_HP * 2.0); } turn_all(&mut games, &[]); assert!(games.iter().all(|g| g.state == 0), "green still stands for team A"); for g in games.iter_mut() { g.damage_master_core(3, crate::game::CORE_HP * 2.0); } turn_all(&mut games, &[]); same_world(&games, "the end came in the same step everywhere"); assert_eq!(games[0].state, 1, "red was out, but his team won"); assert_eq!(games[1].state, 1); assert_eq!(games[2].state, 2); } #[test] fn gold_goes_to_an_ally_on_every_client() { let mut games = clients(45, &[0, 2, 3], &[(0, 0), (2, 0), (3, 1)]); let red0 = games[0].gold_of(0); let green0 = games[0].gold_of(2); games[0].queue(Act::SendGold { to: 2, amount: 500 }); let orders = games[0].take_orders(); assert_eq!(orders.len(), 1); turn_all(&mut games, &orders); for g in games.iter() { assert_eq!(g.gold_of(0), red0 - 500.0, "red paid"); assert_eq!(g.gold_of(2), green0 + 500.0, "green got it"); } assert!(games[1].msgs.iter().any(|m| m.key == "msg.gold_received"), "the receiver hears of it"); assert!(games[1].events.iter().any(|e| e.sfx == crate::game::Sfx::Payday)); assert!(games[0].msgs.iter().any(|m| m.key == "msg.gold_sent")); same_world(&games, "both clients have the same gold"); // not more than he has (and not more than the ally's treasury takes); never to an enemy let all = vec![ Command { side: 2, seq: 0, act: Act::SendGold { to: 0, amount: crate::netplay::GOLD_ALL } }, Command { side: 0, seq: 1, act: Act::SendGold { to: 3, amount: 100 } }, Command { side: 3, seq: 0, act: Act::SendGold { to: 0, amount: 100 } }, ]; let blue = games[0].gold_of(3); let (red1, green1) = (games[0].gold_of(0), games[0].gold_of(2)); turn_all(&mut games, &all); for g in games.iter() { let sent = green1 - g.gold_of(2); assert!(sent > 0.0 && sent <= green1, "green sends what he has: {sent}"); assert_eq!(g.gold_of(0), red1 + sent, "red gets all of it"); assert!(g.gold_of(0) <= g.gold_cap() + 0.5 || g.local_side != 0); assert!((g.gold_of(3) - blue).abs() < 1.0, "blue got nothing from red and gave nothing"); } same_world(&games, "still the same gold everywhere"); } #[test] fn a_world_that_develops_differently_is_caught_by_the_checksum() { let (mut a, mut b) = (client(23, 0), client(23, 0)); let (cx, cy) = core_of(&a, Owner::Player); let turn = vec![Command { side: 0, seq: 0, act: Act::Dig(Rect::new(cx + 7, cy, cx + 8, cy + 1)) }]; a.receive_turn(&turn); b.receive_turn(&[]); // b lost that turn assert_ne!(a.checksum(), b.checksum()); assert!(!b.check_peer(a.netplay.as_ref().unwrap().turn, a.checksum()), "b notices the difference"); assert!(b.netplay.as_ref().unwrap().desync, "and it is recorded"); assert!(b.msgs.iter().any(|m| m.key == "net.desync")); } }