Files
dungeoncraft/src/netplay.rs
T

834 lines
33 KiB
Rust

// 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<u8> {
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<String>, sfx: Option<crate::game::Sfx>) {
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<u8> = 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<u8> = self.master_sides();
sides.extend(self.rivals.iter().map(|r| r.side));
let mut teams: Vec<u8> = sides.iter().map(|&s| self.team_of(s)).collect();
teams.sort_unstable();
teams.dedup();
let mut standing: Vec<u8> = 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<u8> {
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<Command> {
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<u32> {
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<Game> {
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"));
}
}