Files
dungeoncraft/src/commands.rs
T

351 lines
13 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),
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 });
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) {
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::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) => (rect_center(r), MARK_BRIDGE, 4.0),
Act::Steel(r) => (rect_center(r), MARK_STEEL, 4.0),
Act::Tower { 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),
};
self.add_mark(x, y, kind, side, dur);
}
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(),
}
}
}
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);
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);
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), "dig marker");
assert!(g.marks.iter().any(|m| m.kind == MARK_GUARD && m.side == 0), "guard marker");
assert!(g.marks.iter().any(|m| m.kind == MARK_ATTACK && m.side == 0), "attack marker");
}
#[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");
}
}