Masters for a match: the four sides red, green, blue and yellow (side 4 gets its own owner), and start_masters gives every master his core and his starting area

This commit is contained in:
Richard Zink committed 2026-11-04 10:55:17 +01:00
1 parent 35ddfa134c
commit 87890adc53
10 files changed
+132 -9

No files matched your search

+16 -1
View File
@@ -169,7 +169,7 @@ impl Game {
mod tests {
use super::*;
use crate::game::{EK, Job, STEP};
use crate::map::idx;
use crate::map::{idx, Kind, Owner, mh, mw};
fn game(seed: u32) -> Game {
let mut g = Game::new(seed, 64, 64);
@@ -272,4 +272,19 @@ mod tests {
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");
}
}
+88 -3
View File
@@ -1287,9 +1287,9 @@ impl Game {
self.put_active(target);
}
// a new master on `side` with its core at (cx, cy) (top-left of the 3x3 core). Used by the
// multiplayer lobby (and its tests); single player games have no stored masters.
#[allow(dead_code)]
// a new master on `side` with his core at (cx, cy) (top-left of the 3x3 core); single player
// games have no stored masters
#[allow(dead_code)] // used by the tests; the *Multiplayer* menu (step D) will call it
pub fn add_master(&mut self, side: u8, core: (i32, i32)) {
let mut m = self.take_active();
let me = std::mem::replace(&mut m, MasterState::default());
@@ -1311,6 +1311,62 @@ impl Game {
self.masters.push(fresh);
}
// A match: `sides` (out of 0, 2, 3, 4 - side 1 is the heroes') play against each other and
// `local` is the master who plays at this computer. Side 0 keeps the core of the generated map
// (that is where the hero gate path leads); the other masters get their cores at fixed places
// worked out from the map size alone, so every client of a match puts them in the same spots.
#[allow(dead_code)] // used by the tests; the *Multiplayer* menu (step D) will call it
pub fn start_masters(&mut self, sides: &[u8], local: u8) -> bool {
let sides: Vec<u8> = sides.iter().copied().filter(|s| Owner::MASTER_SIDES.contains(s)).collect();
if sides.len() < 2 || !sides.contains(&0) || !sides.contains(&local) {
return false;
}
let (w, h) = (mw() as i32, mh() as i32);
if w < 72 || h < 72 {
return false; // the master areas need room
}
let cores: Vec<(i32, i32)> = sides.iter().map(|&s| match s { 0 => self.map.core, 2 => (w - 30, 12), 3 => (w - 30, h - 16), _ => (14, h - 16) }).collect();
for (k, side) in sides.iter().enumerate() {
if *side == self.me_side {
continue;
}
self.carve_master_area(*side, cores[k]);
self.add_master(*side, cores[k]);
}
self.local_side = local;
self.switch_to(local);
true
}
// the starting area of a master: a 9x9 room of his own floor and his 3x3 soul core
#[allow(dead_code)] // called by start_masters only
fn carve_master_area(&mut self, side: u8, core: (i32, i32)) {
let owner = Owner::of_side(side);
for y in core.1 - 3..core.1 + 6 {
for x in core.0 - 3..core.0 + 6 {
if x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
continue;
}
let t = &mut self.map.t[idx(x, y)];
if matches!(t.kind, Kind::Portal | Kind::Gate | Kind::Core) {
continue;
}
t.kind = Kind::Floor;
t.owner = owner;
t.gold = 0;
t.marked = false;
t.reinforced = false;
}
}
for y in 0..3 {
for x in 0..3 {
let t = &mut self.map.t[idx(core.0 + x, core.1 + y)];
t.kind = Kind::Core;
t.owner = owner;
}
}
}
pub(crate) fn update_units(&mut self, dt: f32) {
for i in 0..self.ents.len() {
if !self.ents[i].alive || self.ents[i].held {
@@ -5341,6 +5397,35 @@ mod tests {
assert!(e.alive && g.map.walk(e.x as i32, e.y as i32));
}
#[test]
fn a_match_gives_every_master_his_core_and_his_ground() {
let mut g = Game::new(7, 96, 128);
g.init_fog();
assert!(g.start_masters(&[0, 2, 3, 4], 2), "four masters, and I am the green one");
assert_eq!(g.local_side, 2);
assert_eq!(g.masters.len(), 3);
for side in Owner::MASTER_SIDES {
let owner = Owner::of_side(side);
assert!(g.is_master(side));
let cores = g.map.t.iter().filter(|t| t.kind == Kind::Core && t.owner == owner).count();
assert_eq!(cores, 9, "master {side} has his soul core");
let ground = g.map.t.iter().filter(|t| t.kind == Kind::Floor && t.owner == owner).count();
assert!(ground >= 40, "master {side} has a starting area of his own");
g.switch_to(side);
assert_eq!(g.core_hp, CORE_HP, "every core starts intact");
assert_eq!(g.gold, 1500.0, "and every master with the same gold");
}
g.switch_to(2);
let green = g.map.core;
g.switch_to(4);
assert_ne!(g.map.core, green, "the masters live apart");
for _ in 0..50 {
g.update(STEP);
}
assert_eq!(g.sim_steps, 50, "every master is simulated");
assert_eq!(g.me_side, 2, "the saddle goes back to the master who plays here");
}
#[test]
fn lava_burns_and_needs_stone_bridges() {
let (mut g, rx) = river_game(true);
+4 -1
View File
@@ -79,6 +79,7 @@ pub const T_GUARDPOST: u32 = 174;
pub const T_ARENA: u32 = 178;
pub const T_ALTAR: u32 = 182;
pub const I_ORDERS: u32 = 186; // attack, guard
pub const T_RIVAL3: u32 = 189; // 4 variants: floor of the yellow master
pub struct Gfx {
pub atlas: Texture2D,
@@ -1035,7 +1036,7 @@ fn gen_atlas() -> Image {
}
// floors of the rival masters: like the player's claimed floor, in their colours
for v in 0..4u32 {
for (id, base, grout, mark) in [(T_RIVAL1, (44, 74, 50), (20, 36, 24), (90, 170, 100)), (T_RIVAL2, (44, 54, 86), (20, 24, 44), (90, 120, 200))] {
for (id, base, grout, mark) in [(T_RIVAL1, (44, 74, 50), (20, 36, 24), (90, 170, 100)), (T_RIVAL2, (44, 54, 86), (20, 24, 44), (90, 120, 200)), (T_RIVAL3, (74, 62, 26), (36, 30, 12), (210, 185, 70))] {
cv.cell(id + v, |x, y| {
let col = stones(x, y, v + 250, base, grout, 10);
if (x % 8 == 4 && y % 8 == 4) && hash(x / 8, y / 8, v + 251) % 2 == 0 {
@@ -1815,6 +1816,8 @@ impl Gfx {
T_RIVAL1 + v
} else if owner == Owner::Rival2 {
T_RIVAL2 + v
} else if owner == Owner::Rival3 {
T_RIVAL3 + v
} else if hero {
T_HEROFLOOR + v
} else {
+7 -1
View File
@@ -145,9 +145,10 @@ pub enum Owner {
Hero,
Rival1, // AI dungeon masters (free play, campaign)
Rival2,
Rival3, // the fourth master side: a third rival, or a human master in a match
}
const OWNERS: [Owner; 5] = [Owner::None, Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2];
const OWNERS: [Owner; 6] = [Owner::None, Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3];
impl Owner {
pub fn of_side(side: u8) -> Owner {
@@ -156,9 +157,14 @@ impl Owner {
1 => Owner::Hero,
2 => Owner::Rival1,
3 => Owner::Rival2,
4 => Owner::Rival3,
_ => Owner::None,
}
}
// the masters of a match: the four sides a dungeon master can play (1 is the heroes' side)
#[allow(dead_code)] // the sides of a match; the lobby (step C/D) uses them
pub const MASTER_SIDES: [u8; 4] = [0, 2, 3, 4];
}
// Short serde names (only used by version 1 savegames, newer ones use the packed codec)
+2
View File
@@ -2555,6 +2555,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Owner::Hero => [170, 175, 190, 255],
Owner::Rival1 => [70, 150, 80, 255],
Owner::Rival2 => [70, 100, 180, 255],
Owner::Rival3 => [150, 130, 50, 255],
Owner::None => [70, 64, 60, 255],
},
Kind::Treasury => [200, 160, 60, 255],
@@ -2565,6 +2566,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Core => match tile.owner {
Owner::Rival1 => [90, 240, 110, 255],
Owner::Rival2 => [100, 150, 255, 255],
Owner::Rival3 => [250, 220, 90, 255],
_ => [255, 60, 60, 255],
},
Kind::Portal => [190, 90, 240, 255],
+1
View File
@@ -1300,6 +1300,7 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
Owner::Hero => "owner.hero",
Owner::Rival1 => "owner.rival1",
Owner::Rival2 => "owner.rival2",
Owner::Rival3 => "owner.rival3",
Owner::None => "owner.none",
};
match t.kind {