399 lines
16 KiB
Rust
399 lines
16 KiB
Rust
// 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<u32>,
|
|
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::<Command>(&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");
|
|
}
|
|
}
|