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:
1 parent
2721638910
commit
dc22e27184
11 files changed
+436
-70
No files matched your search
+104
-20
@@ -14,9 +14,9 @@
|
||||
// {"t":"cmds","cmds":[Command]} -> commands of the sender's side
|
||||
// {"t":"sum","turn":1,"sum":1234} -> checksum for a peer
|
||||
// {"t":"chat","text":"hello"}
|
||||
// {"t":"resume"} -> side 0 resumes after a side went missing
|
||||
// {"t":"resume"} -> side 0 resumes a paused match
|
||||
// {"t":"abort"} -> side 0 closes the match
|
||||
// {"t":"leave"}
|
||||
// {"t":"leave"} -> a player leaves; empty seats are AI-controlled while anyone is connected
|
||||
//
|
||||
// Server messages include: hello, rooms, room, start, turn, sum, chat, closed, error.
|
||||
|
||||
@@ -81,6 +81,20 @@ struct Seat {
|
||||
ready: bool,
|
||||
}
|
||||
|
||||
impl Room {
|
||||
fn has_connection(&self) -> bool {
|
||||
self.seats.values().any(|s| s.conn.is_some())
|
||||
}
|
||||
|
||||
fn ai_sides(&self) -> Vec<u8> {
|
||||
self.sides
|
||||
.iter()
|
||||
.filter(|side| self.seats.get(side).map(|seat| seat.conn.is_none()).unwrap_or(true))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WsQuery {
|
||||
name: Option<String>,
|
||||
@@ -238,15 +252,15 @@ impl Netplay {
|
||||
}
|
||||
}
|
||||
r.turn += 1;
|
||||
Some((r.turn, cmds))
|
||||
Some((r.turn, cmds, r.ai_sides()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => return,
|
||||
};
|
||||
if let Some((turn, cmds)) = action {
|
||||
self.broadcast_room(code, json!({"t":"turn","turn":turn,"cmds":cmds}));
|
||||
if let Some((turn, cmds, ai)) = action {
|
||||
self.broadcast_room(code, json!({"t":"turn","turn":turn,"cmds":cmds,"ai":ai}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +483,17 @@ impl Netplay {
|
||||
None => return,
|
||||
};
|
||||
let name = self.clients.get(&conn).map(|c| c.name.clone()).unwrap_or_else(|| "Guest".into());
|
||||
self.broadcast_room(&code, json!({"t":"chat","name":name,"text":text}));
|
||||
let side = self.side_of(&code, conn);
|
||||
let channel = msg.get("channel").and_then(|v| v.as_str()).unwrap_or("all");
|
||||
let chat = json!({"t":"chat","name":name,"text":text,"side":side,"channel":channel});
|
||||
if channel == "team" {
|
||||
match side {
|
||||
Some(side) => self.broadcast_room_side(&code, side, chat),
|
||||
None => self.broadcast_room(&code, chat),
|
||||
}
|
||||
} else {
|
||||
self.broadcast_room(&code, chat);
|
||||
}
|
||||
}
|
||||
|
||||
fn resume(&mut self, conn: u64) {
|
||||
@@ -507,10 +531,9 @@ impl Netplay {
|
||||
self.rooms_msg(conn);
|
||||
}
|
||||
|
||||
fn leave_room(&mut self, conn: u64, code: &str, disconnect: bool) {
|
||||
fn leave_room(&mut self, conn: u64, code: &str, _disconnect: bool) {
|
||||
let (remove_room, messages) = match self.rooms.get_mut(code) {
|
||||
Some(r) => {
|
||||
let side0_left = r.seats.iter().any(|(&side, s)| s.conn == Some(conn) && side == 0);
|
||||
for s in r.seats.values_mut() {
|
||||
if s.conn == Some(conn) {
|
||||
s.conn = None;
|
||||
@@ -519,19 +542,17 @@ impl Netplay {
|
||||
}
|
||||
}
|
||||
if r.started {
|
||||
if side0_left {
|
||||
(true, vec![json!({"t":"closed","why":"host left"})])
|
||||
if r.has_connection() {
|
||||
r.paused = false;
|
||||
r.missing = false;
|
||||
(false, Vec::new())
|
||||
} else {
|
||||
r.paused = true;
|
||||
r.missing = true;
|
||||
(false, vec![json!({"t":"room","why":"missing","code":code,"turn":r.turn,"started":r.started,"paused":r.paused,"missing":r.missing})])
|
||||
(true, vec![json!({"t":"closed","why":"last player left"})])
|
||||
}
|
||||
} else {
|
||||
let empty = r.seats.values().all(|s| s.conn.is_none());
|
||||
if empty {
|
||||
(true, Vec::new())
|
||||
} else if disconnect {
|
||||
(false, vec![json!({"t":"room","why":"left","code":code,"turn":r.turn,"started":r.started,"paused":r.paused,"missing":r.missing})])
|
||||
} else {
|
||||
(false, Vec::new())
|
||||
}
|
||||
@@ -542,6 +563,9 @@ impl Netplay {
|
||||
for msg in messages {
|
||||
self.broadcast_room(code, msg);
|
||||
}
|
||||
if !remove_room && self.rooms.contains_key(code) {
|
||||
self.room_msg_all(code);
|
||||
}
|
||||
if remove_room {
|
||||
if let Some(c) = self.clients.get_mut(&conn) {
|
||||
c.room = None;
|
||||
@@ -580,6 +604,17 @@ impl Netplay {
|
||||
}
|
||||
}
|
||||
|
||||
fn broadcast_room_side(&mut self, code: &str, side: u8, v: Value) {
|
||||
let targets: Vec<u64> = self
|
||||
.rooms
|
||||
.get(code)
|
||||
.map(|r| r.seats.iter().filter(|(s, seat)| **s == side && seat.conn.is_some()).filter_map(|(_, seat)| seat.conn).collect())
|
||||
.unwrap_or_default();
|
||||
for conn in targets {
|
||||
self.send(conn, v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn room_msg(&mut self, conn: u64, code: &str) {
|
||||
if let Some(v) = self.room_json(code) {
|
||||
self.send(conn, v);
|
||||
@@ -599,11 +634,13 @@ impl Netplay {
|
||||
.iter()
|
||||
.filter_map(|side| r.seats.get(side).map(|s| json!({"side":side,"name":s.name,"ready":s.ready,"connected":s.conn.is_some()})))
|
||||
.collect();
|
||||
let ai: Vec<u8> = if r.started { r.ai_sides() } else { Vec::new() };
|
||||
Some(json!({
|
||||
"t":"room",
|
||||
"code":r.code,
|
||||
"sides":r.sides,
|
||||
"seats":seats,
|
||||
"ai":ai,
|
||||
"turn":r.turn,
|
||||
"started":r.started,
|
||||
"paused":r.paused,
|
||||
@@ -701,7 +738,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_started_match_pauses_when_a_side_goes_missing() {
|
||||
fn a_started_match_continues_with_an_ai_side_when_a_player_leaves() {
|
||||
let mut net = Netplay::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let red = net.connect(tx, Some("red".into()), None);
|
||||
@@ -717,11 +754,58 @@ mod tests {
|
||||
until(&mut rx2, "start");
|
||||
|
||||
net.disconnect(green);
|
||||
let missing = until(&mut rx, "room");
|
||||
assert_eq!(missing["missing"].as_bool(), Some(true));
|
||||
net.handle(red, json!({"t":"resume"}));
|
||||
let room = until(&mut rx, "room");
|
||||
assert_eq!(room["ai"].as_array().unwrap()[0].as_u64(), Some(2));
|
||||
net.tick(&code);
|
||||
let turn = until(&mut rx, "turn");
|
||||
assert_eq!(turn["turn"].as_u64(), Some(1));
|
||||
assert_eq!(turn["ai"].as_array().unwrap()[0].as_u64(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_relay_closes_a_started_match_after_the_last_player_leaves() {
|
||||
let mut net = Netplay::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let red = net.connect(tx, Some("red".into()), None);
|
||||
let rooms = net.handle(red, json!({"t":"create","sides":[0,2]}));
|
||||
let code = rooms[0].clone();
|
||||
let (tx2, mut rx2) = mpsc::unbounded_channel();
|
||||
let green = net.connect(tx2, Some("green".into()), None);
|
||||
net.handle(green, json!({"t":"join","code":&code}));
|
||||
net.handle(red, json!({"t":"ready"}));
|
||||
net.handle(green, json!({"t":"ready"}));
|
||||
net.handle(red, json!({"t":"start"}));
|
||||
until(&mut rx, "start");
|
||||
until(&mut rx2, "start");
|
||||
|
||||
net.disconnect(green);
|
||||
until(&mut rx, "room");
|
||||
net.disconnect(red);
|
||||
assert!(net.rooms.get(&code).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_chat_only_reaches_the_same_side() {
|
||||
let mut net = Netplay::new();
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let red = net.connect(tx, Some("red".into()), None);
|
||||
let rooms = net.handle(red, json!({"t":"create","sides":[0,2]}));
|
||||
let code = rooms[0].clone();
|
||||
let (tx2, mut rx2) = mpsc::unbounded_channel();
|
||||
let green = net.connect(tx2, Some("green".into()), None);
|
||||
net.handle(green, json!({"t":"join","code":&code}));
|
||||
|
||||
net.handle(red, json!({"t":"chat","text":"hello","channel":"all"}));
|
||||
let all_red = until(&mut rx, "chat");
|
||||
assert_eq!(all_red["text"].as_str(), Some("hello"));
|
||||
assert_eq!(all_red["side"].as_u64(), Some(0));
|
||||
let all_green = until(&mut rx2, "chat");
|
||||
assert_eq!(all_green["text"].as_str(), Some("hello"));
|
||||
assert_eq!(all_green["side"].as_u64(), Some(0));
|
||||
|
||||
net.handle(red, json!({"t":"chat","text":"secret","channel":"team"}));
|
||||
let team_red = until(&mut rx, "chat");
|
||||
assert_eq!(team_red["text"].as_str(), Some("secret"));
|
||||
assert_eq!(team_red["side"].as_u64(), Some(0));
|
||||
assert!(rx2.try_recv().is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user