// Multiplayer step B: everything a master orders becomes a Command. The simulation only changes // through Game::apply_turn (commands, then a fixed number of steps), so a single player game and a // network match run exactly the same path. Commands are applied at step boundaries on every client, // in the order the server confirmed them. use crate::game::{Game, TowerKind, MARK_ATTACK, MARK_BRIDGE, MARK_CANCEL, MARK_DIG, MARK_DROP, MARK_FLAG, MARK_GUARD, MARK_HEAL, MARK_LIGHTNING, MARK_PICKUP, MARK_ROOM, MARK_SELL, MARK_SLAP, MARK_SPELL, MARK_STEEL, MARK_SUMMON, MARK_TOWER, MARK_TRAP}; use crate::spells::Spell; use crate::traps::TrapKind; use serde::{Deserialize, Serialize}; // a rectangle of tiles, as dragged with the dig, room, sell, bridge and steel tools #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Rect { pub x0: i32, pub y0: i32, pub x1: i32, pub y1: i32, } impl Rect { pub fn new(x0: i32, y0: i32, x1: i32, y1: i32) -> Rect { Rect { x0, y0, x1, y1 } } } // one ordered action of a master; entity targets are ids, so they mean the same on every client #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum Act { Dig(Rect), Room { room: u8, r: Rect, }, Sell(Rect), Bridge(Rect), Steel(Rect), Siege { which: u8, // 0 ram, 1 catapult x: i32, y: i32, }, Wall(Rect), // plan walls on own floor (or cancel planned ones) Drawbridge(Rect), // build drawbridges, or (one tile on an own drawbridge) switch it: down, up, automatic Tower { kind: TowerKind, x: i32, y: i32, }, Trap { kind: TrapKind, x: i32, y: i32, }, Cast { spell: Spell, x: f32, y: f32, }, Lightning { x: f32, y: f32, }, Heal { x: f32, y: f32, }, Summon { x: f32, y: f32, }, Flag { x: i32, y: i32, }, Pickup { ent: u32, }, Drop { x: f32, y: f32, }, Slap { ent: u32, }, Attack { ent: Option, x: f32, y: f32, }, Guard { x: i32, y: i32, }, CancelOrders, Unmark { x: f32, y: f32, }, // right click with an order tool: removes the marker there (or cancels the guard orders) // gold for an allied master of a match (`crate::netplay::GOLD_ALL`: everything) SendGold { to: u8, amount: u32, }, } #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Command { pub side: u8, // the master who gave the order pub seq: u32, // order within that master's commands (the server numbers them) pub act: Act, } impl Game { // the local master orders something: queued until the next step boundary pub fn queue(&mut self, act: Act) { // a master whose heart fell only watches if self.state != 0 || self.paused || self.demo.is_some() || !self.is_master(self.local_side) || self.is_out(self.local_side) { return; } let seq = self.cmd_seq; self.cmd_seq = self.cmd_seq.wrapping_add(1); self.pending.push(Command { side: self.local_side, seq, act }); self.mark_command(act, self.local_side); } // one command of one master: that master's state is switched in, the action runs, the local // master's state is switched back. Messages, hints and sounds only come from the local master. pub fn apply(&mut self, c: &Command) { if !self.is_master(c.side) || self.is_out(c.side) { return; } self.switch_to(c.side); self.run_act(c.act); self.mark_command(c.act, c.side); let local = self.local_side; self.switch_to(local); } // a turn: the commands of all masters, then a fixed number of simulation steps pub fn apply_turn(&mut self, cmds: &[Command], steps: usize) { for c in cmds { self.apply(c); } for _ in 0..steps { self.step_fixed(); } } fn mark_command(&mut self, act: Act, side: u8) { let ((x, y), kind, dur) = match act { Act::SendGold { .. } => return, // nothing to point at on the map Act::Dig(r) => (rect_center(r), MARK_DIG, 4.0), Act::Room { r, .. } => (rect_center(r), MARK_ROOM, 5.0), Act::Sell(r) => (rect_center(r), MARK_SELL, 4.0), Act::Bridge(r) | Act::Drawbridge(r) | Act::Wall(r) => (rect_center(r), MARK_BRIDGE, 4.0), Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0), Act::Tower { x, y, .. } | Act::Siege { x, y, .. } => (point(x as f32, y as f32), MARK_TOWER, 6.0), Act::Trap { x, y, .. } => (point(x as f32, y as f32), MARK_TRAP, 6.0), Act::Cast { x, y, .. } => (point(x, y), MARK_SPELL, 5.0), Act::Lightning { x, y } => (point(x, y), MARK_LIGHTNING, 5.0), Act::Heal { x, y } => (point(x, y), MARK_HEAL, 5.0), Act::Summon { x, y } => (point(x, y), MARK_SUMMON, 6.0), Act::Flag { x, y } => (point(x as f32, y as f32), MARK_FLAG, 4.0), Act::Pickup { ent } => match self.find(ent) { Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_PICKUP, 3.5), None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_PICKUP, 3.5), }, Act::Drop { x, y } => (point(x, y), MARK_DROP, 3.5), Act::Slap { ent } => match self.find(ent) { Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_SLAP, 4.0), None => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_SLAP, 4.0), }, Act::Attack { ent, x, y } => match ent.and_then(|id| self.find(id)) { Some(j) => (point(self.ents[j].x as f32, self.ents[j].y as f32), MARK_ATTACK, 10.0), None => (point(x, y), MARK_ATTACK, 10.0), }, Act::Guard { x, y } => (point(x as f32, y as f32), MARK_GUARD, 10.0), Act::CancelOrders => (point(self.map.core.0 as f32, self.map.core.1 as f32), MARK_CANCEL, 4.0), Act::Unmark { x, y } => (point(x, y), MARK_CANCEL, 3.0), }; let fixed = matches!(act, Act::Guard { .. } | Act::Attack { .. } | Act::Flag { .. }); let dur = match act { Act::Guard { .. } => 120.0, Act::Attack { .. } => 20.0, Act::Flag { .. } => 120.0, _ => dur, }; self.add_mark(x, y, kind, side, dur, fixed); } fn run_act(&mut self, act: Act) { match act { Act::Dig(r) => { let on = !self.map.get(r.x0, r.y0).map_or(true, |t| t.marked); self.mark_rect(r.x0, r.y0, r.x1, r.y1, on); } Act::Room { room, r } => self.build_rect(room as usize, r.x0, r.y0, r.x1, r.y1), Act::Sell(r) => self.sell_rect(r.x0, r.y0, r.x1, r.y1), Act::Bridge(r) => self.build_bridges(r.x0, r.y0, r.x1, r.y1), Act::Drawbridge(r) => self.drawbridge_order(r.x0, r.y0, r.x1, r.y1), Act::Wall(r) => self.plan_walls(r.x0, r.y0, r.x1, r.y1), Act::Siege { which, x, y } => self.build_siege(which, x, y), Act::Steel(r) => self.build_steel(r.x0, r.y0, r.x1, r.y1), Act::Tower { kind, x, y } => self.build_tower(kind, x, y), Act::Trap { kind, x, y } => self.place_trap(kind, x, y), Act::Cast { spell, x, y } => self.cast(spell, x, y), Act::Lightning { x, y } => self.lightning(x, y), Act::Heal { x, y } => self.heal(x, y), Act::Summon { x, y } => self.summon(x, y), Act::Flag { x, y } => self.set_flag(x, y), Act::Pickup { ent } => { let j = self.find(ent); self.pickup(j); } Act::Drop { x, y } => { self.drop(x, y); } Act::Slap { ent } => { let j = self.find(ent); self.slap(j); } Act::Attack { ent, x, y } => { let j = ent.and_then(|id| self.find(id)); self.order_attack(j, x, y); } Act::Guard { x, y } => self.order_guard(x, y), Act::CancelOrders => self.cancel_orders(), Act::Unmark { x, y } => self.unmark(x, y), Act::SendGold { to, amount } => self.send_gold(to, amount), } } } fn rect_center(r: Rect) -> (f32, f32) { ( (r.x0 as f32 + r.x1 as f32 + 1.0) * 0.5, (r.y0 as f32 + r.y1 as f32 + 1.0) * 0.5, ) } fn point(x: f32, y: f32) -> (f32, f32) { (x.floor() + 0.5, y.floor() + 0.5) } #[cfg(test)] mod tests { use super::*; use crate::game::{EK, Job, STEP}; use crate::map::{idx, Kind, Owner, mh, mw}; fn game(seed: u32) -> Game { let mut g = Game::new(seed, 64, 64); g.init_fog(); g } #[test] fn a_queued_command_is_applied_at_the_next_step() { let mut g = game(7); g.debug_markers = true; let (cx, cy) = g.map.core; let (x, y) = (cx + 6, cy); // outside the treasury ring: rock that can be marked g.queue(Act::Dig(Rect::new(x, y, x + 1, y + 1))); assert!(!g.map.t[idx(x, y)].marked, "queued, not applied yet"); assert!(g.marks.iter().any(|m| m.kind == MARK_DIG), "queued command has a marker"); g.update(STEP); assert!(g.map.t[idx(x, y)].marked, "applied at the step boundary"); assert!(g.pending.is_empty()); } #[test] fn commands_leave_visible_markers() { let mut g = game(12); g.debug_markers = true; let (cx, cy) = g.map.core; g.queue(Act::Dig(Rect::new(cx + 6, cy, cx + 7, cy + 1))); g.queue(Act::Guard { x: cx, y: cy }); g.queue(Act::Attack { ent: None, x: (cx + 20) as f32, y: (cy + 20) as f32 }); g.update(STEP); assert!(g.marks.iter().any(|m| m.kind == MARK_DIG && m.side == 0 && !m.fixed), "dig debug marker"); assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.side == 0 && m.fixed), "fixed guard flag"); assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.side == 0 && m.fixed), "fixed attack flag"); } #[test] fn fixed_order_flags_do_not_need_the_debug_marker_setting() { let mut g = game(13); g.debug_markers = false; let (cx, cy) = g.map.core; g.queue(Act::Dig(Rect::new(cx + 6, cy, cx + 7, cy + 1))); g.queue(Act::Guard { x: cx, y: cy }); g.queue(Act::Attack { ent: None, x: (cx + 20) as f32, y: (cy + 20) as f32 }); g.queue(Act::Flag { x: cx, y: cy }); assert!(!g.marks.iter().any(|m| m.kind == MARK_DIG), "dig stays hidden without debug markers"); assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.fixed), "guard is fixed"); assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.fixed), "attack is fixed"); assert!(g.marks.iter().any(|m| m.kind == MARK_FLAG && m.fixed), "gather flag is fixed"); } #[test] fn a_command_of_another_master_leaves_the_local_one_alone() { let mut g = game(8); let (cx, cy) = g.map.core; g.add_master(2, (cx + 20, cy + 20)); let (gold, hand) = (g.gold, g.hand.len()); let (msgs, events) = (g.msgs.len(), g.events.len()); // an order far away, given by another master, reaches his state only g.apply(&Command { side: 2, seq: 0, act: Act::Attack { ent: None, x: (cx + 21) as f32, y: (cy + 21) as f32 } }); assert_eq!(g.gold, gold, "his gold, not mine"); assert_eq!(g.hand.len(), hand); assert_eq!(g.msgs.len(), msgs, "he hears his own order, not I"); assert_eq!(g.events.len(), events, "and it makes no sound here"); assert_eq!(g.me_side, g.local_side, "I am back in the saddle"); assert_eq!(g.map.core, (cx, cy), "my heart is still mine"); // while it was in the saddle, that master had his own heart and his own ground g.switch_to(2); assert_eq!(g.map.core, (cx + 20, cy + 20)); assert_eq!(g.me, crate::map::Owner::of_side(2)); g.switch_to(0); // the same order from the local master is heard here g.apply(&Command { side: 0, seq: 1, act: Act::Attack { ent: None, x: (cx + 21) as f32, y: (cy + 21) as f32 } }); assert!(g.msgs.len() > msgs, "my own orders speak to me"); assert!(g.master_sides().contains(&2)); } #[test] fn unknown_sides_cannot_order_anything() { let mut g = game(9); let (cx, cy) = g.map.core; let (x, y) = (cx + 6, cy); g.apply(&Command { side: 3, seq: 0, act: Act::Dig(Rect::new(x, y, x, y)) }); g.update(STEP); assert!(!g.map.t[idx(x, y)].marked); } #[test] fn the_same_actions_on_both_paths_build_the_same_world() { let dig = Rect::new(20, 20, 22, 22); let room = Act::Room { room: 0, r: Rect::new(30, 30, 31, 31) }; let drop_at = (21.4f32, 22.6f32); // one game driven like a single player session (queue + update) let mut a = game(11); let ogre = a.spawn(EK::Ogre, false, 21.5, 21.5, 3); a.queue(Act::Dig(dig)); a.queue(room); a.queue(Act::Pickup { ent: ogre }); a.queue(Act::Drop { x: drop_at.0, y: drop_at.1 }); for _ in 0..20 { a.update(STEP); } // the same game driven like a match (one turn with all commands, then the same steps) let mut b = game(11); assert_eq!(b.spawn(EK::Ogre, false, 21.5, 21.5, 3), ogre); let turn = vec![ Command { side: 0, seq: 0, act: Act::Dig(dig) }, Command { side: 0, seq: 1, act: room }, Command { side: 0, seq: 2, act: Act::Pickup { ent: ogre } }, Command { side: 0, seq: 3, act: Act::Drop { x: drop_at.0, y: drop_at.1 } }, ]; b.apply_turn(&turn, 20); assert_eq!(a.time, b.time, "the same number of fixed steps"); assert_eq!(a.sim_steps, b.sim_steps); assert_eq!(a.gold, b.gold); assert_eq!(a.hand, b.hand); assert!(a.map.t.iter().zip(b.map.t.iter()).all(|(x, y)| x.kind == y.kind && x.owner == y.owner && x.marked == y.marked && x.hp == y.hp && x.gold == y.gold && x.reserved == y.reserved), "the maps diverged"); assert_eq!(fingerprint(&a), fingerprint(&b), "the creatures diverged"); } // what the simulation has to agree on for two clients of one match fn fingerprint(g: &Game) -> Vec<(u32, EK, bool, u8, f32, f32, f32, Job)> { g.ents.iter().map(|e| (e.id, e.kind, e.alive, e.side, e.x, e.y, e.hp, e.job)).collect() } #[test] fn commands_survive_a_savegame_round_trip() { let c = Command { side: 1, seq: 4, act: Act::Attack { ent: Some(9), x: 3.5, y: 4.5 } }; let json = serde_json::to_string(&c).unwrap(); assert_eq!(serde_json::from_str::(&json).unwrap(), c); } #[test] fn a_match_command_builds_on_the_ground_of_its_own_master() { let mut g = Game::new(13, 96, 128); g.init_fog(); assert!(g.start_masters(&[0, 2], 0), "a match of two masters"); let home = g.map.t.iter().position(|t| t.kind == Kind::Core && t.owner == Owner::Rival1).expect("the green master got his core"); let (cx, cy) = ((home % mw() as usize) as i32, (home / mh() as usize) as i32); let gold = g.gold; g.apply_turn(&[Command { side: 2, seq: 0, act: Act::Room { room: 0, r: Rect::new(cx + 4, cy + 4, cx + 4, cy + 4) } }], 1); let t = &g.map.t[idx(cx + 4, cy + 4)]; assert_eq!(t.kind, Kind::Treasury, "he builds on his own ground"); assert_eq!(t.owner, Owner::Rival1); assert_eq!(g.gold, gold, "my gold is untouched"); } }