Multiplayer continues after a player leaves: AI side takeover, background match while menus are open, match chat, five-tile fog start

This commit is contained in:
Richard Zink committed 2026-11-04 18:07:02 +01:00
1 parent 2721638910
commit dc22e27184
11 files changed
+436 -70

No files matched your search

+58 -1
View File
@@ -69,6 +69,35 @@ impl Game {
self.netplay.is_some()
}
// a seat without a human in a started match: that side keeps playing as a rival-style AI
pub fn sync_ai_sides(&mut self, ai: &[u8], difficulty: u8) {
if self.netplay.is_none() {
return;
}
self.ai_difficulty = difficulty;
self.ai_sides = ai
.iter()
.copied()
.filter(|side| *side != self.local_side)
.collect();
for side in self.ai_sides.clone() {
let core = if side == self.me_side {
continue;
} else if let Some(m) = self.masters.iter().find(|m| m.side == side) {
Some(m.core)
} else {
None
};
let Some(core) = core else { continue };
if let Some(k) = self.masters.iter().position(|m| m.side == side) {
self.masters.remove(k);
}
if !self.is_rival_side(side) {
self.add_rival(side, core.0 + 1, core.1 + 1, self.ai_difficulty);
}
}
}
// the relay ended a match, or the player leaves it: the world stops being a turn-based match
pub fn end_match(&mut self) {
self.netplay = None;
@@ -80,7 +109,7 @@ impl Game {
}
// the server confirmed a turn: run the orders of all masters, then the fixed steps of the turn.
// A client that is paused or finished leaves the turn out (a match pauses while somebody is away).
// A local client that is paused or finished leaves the turn out; empty match seats are AI sides.
pub fn receive_turn(&mut self, cmds: &[Command]) -> u32 {
if self.netplay.is_none() || self.state != 0 || self.paused {
return 0;
@@ -225,6 +254,21 @@ impl Game {
mix(&mut h, *springs as u64);
mixp(&mut h, Some(*core));
}
for r in self.rivals.iter() {
mixp(&mut h, Some(r.core));
mix(&mut h, r.side as u64);
mix(&mut h, r.alive as u64);
mix(&mut h, r.radius as u64);
mix(&mut h, r.difficulty as u64);
mixf(&mut h, r.hp);
mixf(&mut h, r.grow_t);
mixf(&mut h, r.spawn_t);
mixf(&mut h, r.attack_t);
mixf(&mut h, r.power);
}
for side in self.ai_sides.iter() {
mix(&mut h, *side as u64);
}
h
}
}
@@ -296,6 +340,19 @@ mod tests {
assert_eq!(g.worker_count(), 5, "the summon counts as the master's own worker");
}
#[test]
fn a_lost_seat_in_a_match_is_converted_to_the_same_ai_side() {
let (mut a, mut b) = (client(31, 0), client(31, 2));
a.sync_ai_sides(&[3], 1);
b.sync_ai_sides(&[3], 1);
assert_eq!(a.checksum(), b.checksum(), "both clients convert side 3 the same way");
assert!(a.is_rival_side(3) && b.is_rival_side(3));
assert!(!a.is_master(3) && !b.is_master(3));
a.receive_turn(&[]);
b.receive_turn(&[]);
assert_eq!(a.checksum(), b.checksum(), "and keep playing it identically");
}
#[test]
fn a_client_without_the_next_turn_stands_still_and_its_orders_wait() {
let mut g = client(22, 0);