Versus teams, gold for allies, fallen hearts as spectators, multi-mission editor co-op
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
811c955bad
commit
fc9c49d4ca
16 files changed
+1896
-49
No files matched your search
+446
-1
@@ -6,6 +6,7 @@
|
||||
|
||||
use crate::commands::Command;
|
||||
use crate::game::Game;
|
||||
use crate::map::{Kind, Owner};
|
||||
|
||||
// one server turn of 100 ms: five fixed simulation steps
|
||||
pub const STEPS_PER_TURN: usize = 5;
|
||||
@@ -46,7 +47,281 @@ fn mixdbg(h: &mut u64, v: impl std::fmt::Debug) {
|
||||
}
|
||||
}
|
||||
|
||||
// the side whose ground a tile of `owner` is (unowned ground counts as the first player's)
|
||||
pub fn side_of_owner(owner: Owner) -> u8 {
|
||||
match owner {
|
||||
Owner::Hero => 1,
|
||||
Owner::Rival1 => 2,
|
||||
Owner::Rival2 => 3,
|
||||
Owner::Rival3 => 4,
|
||||
Owner::Player | Owner::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// gold amounts a master can send to an ally; `GOLD_ALL` sends everything he has
|
||||
pub const GOLD_ALL: u32 = u32::MAX;
|
||||
pub const GOLD_AMOUNTS: [u32; 4] = [100, 500, 1000, GOLD_ALL];
|
||||
|
||||
impl Game {
|
||||
// ------------------------------------------------------------ teams and allies
|
||||
|
||||
// the team of a master seat in a versus match (sides without an entry are a team of their own)
|
||||
pub fn team_of(&self, side: u8) -> u8 {
|
||||
self.teams.iter().find(|t| t.0 == side).map_or(10 + side, |t| t.1)
|
||||
}
|
||||
|
||||
// a master seat of the match: a human master or an AI seat
|
||||
pub fn is_seat_side(&self, side: u8) -> bool {
|
||||
Owner::MASTER_SIDES.contains(&side) && (self.is_master(side) || self.ai_sides.contains(&side))
|
||||
}
|
||||
|
||||
// sides that never fight each other: the same side, co-op partners and team mates. Only shared
|
||||
// state goes in, so every client of a match decides the same.
|
||||
pub fn allied(&self, a: u8, b: u8) -> bool {
|
||||
if a == b {
|
||||
return true;
|
||||
}
|
||||
if !self.is_seat_side(a) || !self.is_seat_side(b) {
|
||||
return false;
|
||||
}
|
||||
self.coop || self.team_of(a) == self.team_of(b)
|
||||
}
|
||||
|
||||
// the teams of a versus match: (side, team) for the master seats
|
||||
pub fn set_teams(&mut self, teams: &[(u8, u8)]) {
|
||||
self.teams = teams.iter().copied().filter(|(s, _)| Owner::MASTER_SIDES.contains(s)).collect();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ fallen hearts
|
||||
|
||||
pub fn is_out(&self, side: u8) -> bool {
|
||||
self.out_sides.contains(&side)
|
||||
}
|
||||
|
||||
// the master at this computer lost his heart: he watches the rest of the match
|
||||
pub fn spectating(&self) -> bool {
|
||||
self.netplay.is_some() && self.is_out(self.local_side)
|
||||
}
|
||||
|
||||
fn master_state(&self, side: u8) -> Option<&crate::game::MasterState> {
|
||||
self.masters.iter().find(|m| m.side == side)
|
||||
}
|
||||
|
||||
pub fn core_hp_of(&self, side: u8) -> f32 {
|
||||
if side == self.me_side {
|
||||
self.core_hp
|
||||
} else {
|
||||
self.master_state(side).map_or(0.0, |m| m.core_hp)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn core_of_side(&self, side: u8) -> (i32, i32) {
|
||||
if side == self.me_side {
|
||||
self.map.core
|
||||
} else {
|
||||
self.master_state(side).map_or(self.map.core, |m| m.core)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gold_of(&self, side: u8) -> f32 {
|
||||
if side == self.me_side {
|
||||
self.gold
|
||||
} else {
|
||||
self.master_state(side).map_or(0.0, |m| m.gold)
|
||||
}
|
||||
}
|
||||
|
||||
// damage to the soul core of a master (whichever master is being processed)
|
||||
pub fn damage_master_core(&mut self, side: u8, dmg: f32) {
|
||||
if side == self.me_side {
|
||||
self.core_hp -= dmg;
|
||||
} else if let Some(m) = self.masters.iter_mut().find(|m| m.side == side) {
|
||||
m.core_hp -= dmg;
|
||||
}
|
||||
}
|
||||
|
||||
// the heart of a master of the match that is no ally of `side`, near (x, y)
|
||||
pub(crate) fn enemy_master_near(&self, side: u8, x: f32, y: f32) -> Option<u8> {
|
||||
if self.masters.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.master_sides().into_iter().filter(|&s| s != side && !self.is_out(s) && !self.allied(side, s)).find(|&s| {
|
||||
let (cx, cy) = self.core_of_side(s);
|
||||
(x - cx as f32 - 1.5).powi(2) + (y - cy as f32 - 1.5).powi(2) <= 8.0 * 8.0
|
||||
})
|
||||
}
|
||||
|
||||
// a message and a sound for the player at this computer, whichever master is being processed
|
||||
fn tell_local(&mut self, key: &'static str, args: Vec<String>, sfx: Option<crate::game::Sfx>) {
|
||||
let me = self.me_side;
|
||||
let local = self.local_side;
|
||||
self.switch_to(local);
|
||||
self.msg(key, args);
|
||||
if let Some(sfx) = sfx {
|
||||
self.sfx_ui(sfx);
|
||||
}
|
||||
self.switch_to(me);
|
||||
}
|
||||
|
||||
// a master of a match whose core has no strength left is out. His creatures fight on without a
|
||||
// heart until they fall; he keeps watching. Runs in the shared part of the step.
|
||||
pub(crate) fn update_fallen_masters(&mut self) {
|
||||
if self.netplay.is_none() {
|
||||
return;
|
||||
}
|
||||
for side in self.master_sides() {
|
||||
if self.is_out(side) || self.core_hp_of(side) > 0.0 {
|
||||
continue;
|
||||
}
|
||||
self.out_sides.push(side);
|
||||
self.master_fell(side);
|
||||
}
|
||||
}
|
||||
|
||||
fn master_fell(&mut self, side: u8) {
|
||||
let me = self.me_side;
|
||||
self.switch_to(side);
|
||||
self.core_hp = 0.0;
|
||||
self.flag = None;
|
||||
// what he held in his hand is put down beside the broken heart
|
||||
let (cx, cy) = self.map.core;
|
||||
let hand = std::mem::take(&mut self.hand);
|
||||
for (k, id) in hand.iter().enumerate() {
|
||||
if let Some(e) = self.ents.iter_mut().find(|e| e.id == *id && e.held) {
|
||||
e.held = false;
|
||||
e.x = cx as f32 + 1.5 + (k % 3) as f32 * 0.6 - 0.6;
|
||||
e.y = cy as f32 + 3.5;
|
||||
e.disp_x = e.x;
|
||||
e.disp_y = e.y;
|
||||
e.job = crate::game::Job::None;
|
||||
e.path.clear();
|
||||
}
|
||||
}
|
||||
// the heart is gone: its tiles become plain ground of that master
|
||||
let owner = Owner::of_side(side);
|
||||
for t in self.map.t.iter_mut() {
|
||||
if t.kind == Kind::Core && t.owner == owner {
|
||||
t.kind = Kind::Floor;
|
||||
}
|
||||
}
|
||||
self.switch_to(me);
|
||||
if side == self.local_side {
|
||||
self.tell_local("msg.master_out_you", vec![], Some(crate::game::Sfx::Defeat));
|
||||
} else {
|
||||
self.tell_local("msg.master_out", vec![format!("@net.side{side}")], Some(crate::game::Sfx::CoreHit));
|
||||
}
|
||||
}
|
||||
|
||||
// goals of a mission played in a match: what one master holds (mana springs) counts for any of
|
||||
// the masters still in the game, everything else is shared
|
||||
pub fn match_goal_done(&self, goal: crate::game::Goal) -> bool {
|
||||
match goal {
|
||||
crate::game::Goal::Springs(n) => self.master_sides().into_iter().filter(|s| !self.is_out(*s)).any(|s| {
|
||||
let springs = if s == self.me_side { self.springs } else { self.master_state(s).map_or(0, |m| m.springs) };
|
||||
springs >= n as usize
|
||||
}),
|
||||
g => self.goal_done(g),
|
||||
}
|
||||
}
|
||||
|
||||
// The end of a match, decided from shared state only so that all clients stop in the same step:
|
||||
// a mission (co-op) is lost when every master's heart fell; a versus match is over when only one
|
||||
// team is left - the masters of that team win, the others lose.
|
||||
pub(crate) fn check_match_end(&mut self) {
|
||||
if self.state != 0 {
|
||||
return;
|
||||
}
|
||||
let alive: Vec<u8> = self.master_sides().into_iter().filter(|s| !self.is_out(*s)).collect();
|
||||
let lost = |g: &mut Game, key: &'static str| {
|
||||
g.state = 2;
|
||||
g.msg(key, vec![]);
|
||||
g.sfx_ui(crate::game::Sfx::Defeat);
|
||||
};
|
||||
if let Some(m) = &self.mission {
|
||||
let (limit, goals) = (m.time_limit, m.goals.clone());
|
||||
if alive.is_empty() {
|
||||
return lost(self, "msg.defeat");
|
||||
}
|
||||
if limit > 0.0 && self.time > limit {
|
||||
return lost(self, "msg.time_up");
|
||||
}
|
||||
if !goals.is_empty() && goals.iter().all(|&g| self.match_goal_done(g)) {
|
||||
self.state = 1;
|
||||
self.msg("msg.mission_won", vec![]);
|
||||
self.sfx_ui(crate::game::Sfx::Victory);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// the teams that started (masters and AI seats) and the ones still standing
|
||||
let mut sides: Vec<u8> = self.master_sides();
|
||||
sides.extend(self.rivals.iter().map(|r| r.side));
|
||||
let mut teams: Vec<u8> = sides.iter().map(|&s| self.team_of(s)).collect();
|
||||
teams.sort_unstable();
|
||||
teams.dedup();
|
||||
let mut standing: Vec<u8> = alive.iter().copied().chain(self.rivals.iter().filter(|r| r.alive).map(|r| r.side)).map(|s| self.team_of(s)).collect();
|
||||
standing.sort_unstable();
|
||||
standing.dedup();
|
||||
if alive.is_empty() {
|
||||
return lost(self, "msg.defeat");
|
||||
}
|
||||
let over = if teams.len() >= 2 {
|
||||
standing.len() <= 1
|
||||
} else {
|
||||
// everybody on one team: the humans' gate is the enemy
|
||||
self.gate_on && self.gate_hp <= 0.0
|
||||
};
|
||||
if !over {
|
||||
return;
|
||||
}
|
||||
if standing.contains(&self.team_of(self.local_side)) {
|
||||
self.state = 1;
|
||||
self.msg("msg.match_won", vec![]);
|
||||
self.sfx_ui(crate::game::Sfx::Victory);
|
||||
} else {
|
||||
lost(self, "msg.match_lost");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ gold for allies
|
||||
|
||||
// the masters the local master can send gold to: human allies whose heart still beats
|
||||
pub fn gold_allies(&self) -> Vec<u8> {
|
||||
if self.netplay.is_none() || self.is_out(self.local_side) {
|
||||
return Vec::new();
|
||||
}
|
||||
self.master_sides().into_iter().filter(|&s| s != self.local_side && !self.is_out(s) && self.allied(self.local_side, s)).collect()
|
||||
}
|
||||
|
||||
// the master being processed sends gold to an ally (never more than he has, never more than
|
||||
// the ally's treasury takes)
|
||||
pub(crate) fn send_gold(&mut self, to: u8, amount: u32) {
|
||||
let from = self.me_side;
|
||||
if to == from || !self.is_master(to) || !self.allied(from, to) || self.is_out(to) || self.is_out(from) {
|
||||
return;
|
||||
}
|
||||
let want = (amount as f32).min(self.gold.floor());
|
||||
if want < 1.0 {
|
||||
self.msg("msg.gold_none", vec![]);
|
||||
self.sfx_ui(crate::game::Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.switch_to(to);
|
||||
let got = self.add_gold(want).floor();
|
||||
if got >= 1.0 {
|
||||
self.msg("msg.gold_received", vec![(got as i32).to_string(), format!("@net.side{from}")]);
|
||||
self.sfx_ui(crate::game::Sfx::Payday);
|
||||
}
|
||||
self.switch_to(from);
|
||||
if got < 1.0 {
|
||||
self.msg("msg.gold_full", vec![format!("@net.side{to}")]);
|
||||
self.sfx_ui(crate::game::Sfx::Error);
|
||||
return;
|
||||
}
|
||||
self.gold -= got;
|
||||
self.msg("msg.gold_sent", vec![(got as i32).to_string(), format!("@net.side{to}")]);
|
||||
self.sfx_ui(crate::game::Sfx::Deposit);
|
||||
}
|
||||
|
||||
// a match with masters on `sides` (of 0, 2, 3, 4), `local` is the master who plays at this computer
|
||||
pub fn start_match(&mut self, sides: &[u8], local: u8) -> bool {
|
||||
if !self.start_masters(sides, local) {
|
||||
@@ -81,6 +356,9 @@ impl Game {
|
||||
.filter(|side| *side != self.local_side)
|
||||
.collect();
|
||||
for side in self.ai_sides.clone() {
|
||||
if self.is_out(side) {
|
||||
continue; // a fallen master who left has nothing left for an AI to play
|
||||
}
|
||||
let core = if side == self.me_side {
|
||||
continue;
|
||||
} else if let Some(m) = self.masters.iter().find(|m| m.side == side) {
|
||||
@@ -169,7 +447,8 @@ impl Game {
|
||||
pub fn checksum(&self) -> u64 {
|
||||
let mut h = FNV_OFFSET;
|
||||
mix(&mut h, self.sim_steps);
|
||||
mix(&mut h, self.state as u64);
|
||||
// whether the world is still running: the end itself is told per team (won or lost)
|
||||
mix(&mut h, (self.state != 0) as u64);
|
||||
mix(&mut h, self.next_id as u64);
|
||||
mix(&mut h, self.rng.0 as u64);
|
||||
mix(&mut h, self.wave as u64);
|
||||
@@ -269,6 +548,9 @@ impl Game {
|
||||
for side in self.ai_sides.iter() {
|
||||
mix(&mut h, *side as u64);
|
||||
}
|
||||
for side in self.out_sides.iter() {
|
||||
mix(&mut h, 100 + *side as u64);
|
||||
}
|
||||
h
|
||||
}
|
||||
}
|
||||
@@ -371,6 +653,169 @@ mod tests {
|
||||
assert_eq!(g.netplay.as_ref().unwrap().turn, 1);
|
||||
}
|
||||
|
||||
// every client of one match: the same world, seen from another seat
|
||||
fn clients(seed: u32, locals: &[u8], teams: &[(u8, u8)]) -> Vec<Game> {
|
||||
locals
|
||||
.iter()
|
||||
.map(|&local| {
|
||||
let mut g = client(seed, local);
|
||||
g.set_teams(teams);
|
||||
g
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn turn_all(games: &mut [Game], cmds: &[Command]) {
|
||||
for g in games.iter_mut() {
|
||||
g.receive_turn(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
fn same_world(games: &[Game], why: &str) {
|
||||
let sum = games[0].checksum();
|
||||
for g in games.iter().skip(1) {
|
||||
assert_eq!(g.checksum(), sum, "{why}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fallen_master_watches_while_the_match_goes_on() {
|
||||
let mut games = clients(41, &[0, 2, 3], &[]);
|
||||
for g in games.iter_mut() {
|
||||
g.damage_master_core(2, crate::game::CORE_HP * 2.0);
|
||||
}
|
||||
turn_all(&mut games, &[]);
|
||||
same_world(&games, "the heart fell in the same step everywhere");
|
||||
for g in games.iter() {
|
||||
assert!(g.is_out(2) && !g.is_out(0) && !g.is_out(3));
|
||||
assert_eq!(g.state, 0, "the match goes on for the others");
|
||||
}
|
||||
let green = &games[1];
|
||||
assert!(green.spectating() && !games[0].spectating());
|
||||
assert!(green.msgs.iter().any(|m| m.key == "msg.master_out_you"));
|
||||
assert!(games[0].msgs.iter().any(|m| m.key == "msg.master_out"));
|
||||
assert!(!green.map.t.iter().any(|t| t.kind == Kind::Core && t.owner == Owner::Rival1), "the heart is broken");
|
||||
let workers = green.ents.iter().filter(|e| e.alive && e.side == 2).count();
|
||||
assert!(workers > 0, "his creatures go on without a heart");
|
||||
// the spectator sees everything and cannot order anything
|
||||
let mut games = games;
|
||||
for _ in 0..30 {
|
||||
turn_all(&mut games, &[]);
|
||||
}
|
||||
assert!(games[1].map.visible.iter().all(|v| *v), "no fog for the spectator");
|
||||
assert!(!games[0].map.visible.iter().all(|v| *v), "the others still have fog");
|
||||
let (gx, gy) = games[1].core_of_side(2);
|
||||
games[1].queue(Act::Dig(Rect::new(gx + 7, gy, gx + 8, gy + 1)));
|
||||
assert!(games[1].take_orders().is_empty(), "a spectator gives no orders");
|
||||
// an order that still comes in for the fallen side changes nothing
|
||||
let stray = vec![Command { side: 2, seq: 0, act: Act::Dig(Rect::new(gx + 7, gy, gx + 8, gy + 1)) }];
|
||||
turn_all(&mut games, &stray);
|
||||
assert!(!games[0].map.t[crate::map::idx(gx + 7, gy)].marked);
|
||||
for _ in 0..50 {
|
||||
turn_all(&mut games, &[]);
|
||||
}
|
||||
same_world(&games, "the spectator stays in step");
|
||||
assert!(games.iter().all(|g| g.state == 0));
|
||||
// the last enemy falls: red wins, blue (and the spectator) lose, in the same step
|
||||
for g in games.iter_mut() {
|
||||
g.damage_master_core(3, crate::game::CORE_HP * 2.0);
|
||||
}
|
||||
turn_all(&mut games, &[]);
|
||||
same_world(&games, "the end came in the same step everywhere");
|
||||
assert_eq!(games[0].state, 1, "red is the last master standing");
|
||||
assert_eq!(games[1].state, 2);
|
||||
assert_eq!(games[2].state, 2);
|
||||
assert!(games[0].msgs.iter().any(|m| m.key == "msg.match_won"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_mates_do_not_fight_and_win_together() {
|
||||
// red and green are team A, blue is team B
|
||||
let teams = [(0u8, 0u8), (2, 0), (3, 1)];
|
||||
let mut games = clients(43, &[0, 2, 3], &teams);
|
||||
let g = &games[0];
|
||||
assert!(g.allied(0, 2) && g.allied(2, 0) && !g.allied(0, 3) && !g.allied(2, 3));
|
||||
assert_eq!(games[1].gold_allies(), vec![0u8], "green can send gold to red");
|
||||
assert!(games[2].gold_allies().is_empty(), "blue has no team mate");
|
||||
// a creature of red next to one of green: no target; one of blue is
|
||||
for g in games.iter_mut() {
|
||||
let (cx, cy) = g.core_of_side(2);
|
||||
let (x, y) = (cx as f32 + 4.5, cy as f32 + 4.5);
|
||||
let red = g.spawn(crate::game::EK::Kobold, false, x, y, 3);
|
||||
let green = g.spawn(crate::game::EK::Kobold, false, x + 1.0, y, 3);
|
||||
for (id, side) in [(red, 0u8), (green, 2u8)] {
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
|
||||
e.side = side;
|
||||
}
|
||||
}
|
||||
let red_e = g.ents.iter().find(|e| e.id == red).unwrap().clone();
|
||||
assert_eq!(g.nearest_enemy(&red_e, 3.0), None, "team mates are no targets");
|
||||
let blue = g.spawn(crate::game::EK::Kobold, false, x, y + 1.0, 3);
|
||||
if let Some(e) = g.ents.iter_mut().find(|e| e.id == blue) {
|
||||
e.side = 3;
|
||||
}
|
||||
assert_eq!(g.nearest_enemy(&red_e, 3.0), Some(blue), "the other team is");
|
||||
}
|
||||
for _ in 0..40 {
|
||||
turn_all(&mut games, &[]);
|
||||
}
|
||||
same_world(&games, "teams play the same on every client");
|
||||
// the fog shows the team mate's ground
|
||||
let (gx, gy) = games[0].core_of_side(2);
|
||||
assert!(games[0].map.visible[crate::map::idx(gx + 1, gy + 1)], "red sees green's heart");
|
||||
assert!(!games[2].map.visible[crate::map::idx(gx + 1, gy + 1)], "blue does not");
|
||||
// one of team A falls: the match goes on; team B falls: team A wins together
|
||||
for g in games.iter_mut() {
|
||||
g.damage_master_core(0, crate::game::CORE_HP * 2.0);
|
||||
}
|
||||
turn_all(&mut games, &[]);
|
||||
assert!(games.iter().all(|g| g.state == 0), "green still stands for team A");
|
||||
for g in games.iter_mut() {
|
||||
g.damage_master_core(3, crate::game::CORE_HP * 2.0);
|
||||
}
|
||||
turn_all(&mut games, &[]);
|
||||
same_world(&games, "the end came in the same step everywhere");
|
||||
assert_eq!(games[0].state, 1, "red was out, but his team won");
|
||||
assert_eq!(games[1].state, 1);
|
||||
assert_eq!(games[2].state, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gold_goes_to_an_ally_on_every_client() {
|
||||
let mut games = clients(45, &[0, 2, 3], &[(0, 0), (2, 0), (3, 1)]);
|
||||
let red0 = games[0].gold_of(0);
|
||||
let green0 = games[0].gold_of(2);
|
||||
games[0].queue(Act::SendGold { to: 2, amount: 500 });
|
||||
let orders = games[0].take_orders();
|
||||
assert_eq!(orders.len(), 1);
|
||||
turn_all(&mut games, &orders);
|
||||
for g in games.iter() {
|
||||
assert_eq!(g.gold_of(0), red0 - 500.0, "red paid");
|
||||
assert_eq!(g.gold_of(2), green0 + 500.0, "green got it");
|
||||
}
|
||||
assert!(games[1].msgs.iter().any(|m| m.key == "msg.gold_received"), "the receiver hears of it");
|
||||
assert!(games[1].events.iter().any(|e| e.sfx == crate::game::Sfx::Payday));
|
||||
assert!(games[0].msgs.iter().any(|m| m.key == "msg.gold_sent"));
|
||||
same_world(&games, "both clients have the same gold");
|
||||
// not more than he has (and not more than the ally's treasury takes); never to an enemy
|
||||
let all = vec![
|
||||
Command { side: 2, seq: 0, act: Act::SendGold { to: 0, amount: crate::netplay::GOLD_ALL } },
|
||||
Command { side: 0, seq: 1, act: Act::SendGold { to: 3, amount: 100 } },
|
||||
Command { side: 3, seq: 0, act: Act::SendGold { to: 0, amount: 100 } },
|
||||
];
|
||||
let blue = games[0].gold_of(3);
|
||||
let (red1, green1) = (games[0].gold_of(0), games[0].gold_of(2));
|
||||
turn_all(&mut games, &all);
|
||||
for g in games.iter() {
|
||||
let sent = green1 - g.gold_of(2);
|
||||
assert!(sent > 0.0 && sent <= green1, "green sends what he has: {sent}");
|
||||
assert_eq!(g.gold_of(0), red1 + sent, "red gets all of it");
|
||||
assert!(g.gold_of(0) <= g.gold_cap() + 0.5 || g.local_side != 0);
|
||||
assert!((g.gold_of(3) - blue).abs() < 1.0, "blue got nothing from red and gave nothing");
|
||||
}
|
||||
same_world(&games, "still the same gold everywhere");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_world_that_develops_differently_is_caught_by_the_checksum() {
|
||||
let (mut a, mut b) = (client(23, 0), client(23, 0));
|
||||
|
||||
Reference in new issue
Block a user