Multiplayer step B: every player order is a command (dig, build, cast, hand, orders) and the simulation runs in fixed 50 ms steps, so single player and a match take the same path
This commit is contained in:
1 parent
d6bcce3a76
commit
1ec7492189
8 files changed
+635
-101
No files matched your search
+275
@@ -0,0 +1,275 @@
|
||||
// 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};
|
||||
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),
|
||||
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,
|
||||
}
|
||||
|
||||
#[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) {
|
||||
if self.state != 0 || self.paused || self.demo.is_some() || !self.is_master(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 });
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
self.switch_to(c.side);
|
||||
self.run_act(c.act);
|
||||
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 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::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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::game::{EK, Job, STEP};
|
||||
use crate::map::idx;
|
||||
|
||||
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);
|
||||
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");
|
||||
g.update(STEP);
|
||||
assert!(g.map.t[idx(x, y)].marked, "applied at the step boundary");
|
||||
assert!(g.pending.is_empty());
|
||||
}
|
||||
|
||||
#[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(0.05);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user