659 lines
22 KiB
Rust
659 lines
22 KiB
Rust
// Multiplayer step C (client): the browser-side lobby and turn bridge.
|
|
//
|
|
// The WebSocket itself is opened by the browser plugin (`dc_ws_*` in www/dc_plugin.js). This module
|
|
// only speaks the relay protocol from server/src/netplay.rs, turns `Command`s into JSON and feeds the
|
|
// game with confirmed turns. It can also run without a socket in tests: requests go into `outbox`,
|
|
// server answers are put back with `feed`.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use crate::commands::Command;
|
|
use serde_json::{json, Value};
|
|
use std::collections::VecDeque;
|
|
|
|
const MAX_CHAT: usize = 80;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
|
pub enum Phase {
|
|
#[default]
|
|
Offline,
|
|
Lobby,
|
|
InLobby,
|
|
Playing,
|
|
Closed,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct SeatUi {
|
|
pub side: u8,
|
|
pub name: String,
|
|
pub ready: bool,
|
|
pub connected: bool,
|
|
pub ai: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct RoomUi {
|
|
pub code: String,
|
|
pub players: usize,
|
|
pub sides: Vec<u8>,
|
|
pub started: bool,
|
|
pub paused: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct MatchUi {
|
|
pub phase: Phase,
|
|
pub connected: bool,
|
|
pub name: String,
|
|
pub code: Option<String>,
|
|
pub local_side: Option<u8>,
|
|
pub ready: bool,
|
|
pub is_host: bool,
|
|
pub sides: Vec<u8>,
|
|
pub seats: Vec<SeatUi>,
|
|
pub ai: Vec<u8>,
|
|
pub rooms: Vec<RoomUi>,
|
|
pub started: bool,
|
|
pub paused: bool,
|
|
pub missing: bool,
|
|
pub turn: u32,
|
|
pub error: Option<String>,
|
|
pub status: Option<String>,
|
|
pub chat: Vec<(Option<u8>, String, String)>,
|
|
}
|
|
|
|
impl MatchUi {
|
|
pub fn room_open(&self) -> bool {
|
|
self.code.is_some() || !self.seats.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct StartInfo {
|
|
pub seed: u32,
|
|
pub map_w: usize,
|
|
pub map_h: usize,
|
|
pub difficulty: u8,
|
|
pub sides: Vec<u8>,
|
|
pub local_side: Option<u8>,
|
|
pub mission: Option<usize>,
|
|
pub ai: Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct SeatInfo {
|
|
side: u8,
|
|
name: String,
|
|
ready: bool,
|
|
connected: bool,
|
|
ai: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct RoomInfo {
|
|
code: String,
|
|
sides: Vec<u8>,
|
|
seats: Vec<SeatInfo>,
|
|
ai: Vec<u8>,
|
|
turn: u32,
|
|
started: bool,
|
|
paused: bool,
|
|
missing: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MatchClient {
|
|
pub name: String,
|
|
pub connected: bool,
|
|
pub outbox: VecDeque<String>,
|
|
code: Option<String>,
|
|
room: Option<RoomInfo>,
|
|
rooms: Vec<RoomUi>,
|
|
local_side: Option<u8>,
|
|
error: Option<String>,
|
|
status: Option<String>,
|
|
chat: Vec<(Option<u8>, String, String)>,
|
|
chat_seen: usize,
|
|
start: Option<StartInfo>,
|
|
turns: Vec<(u32, Vec<Command>, Vec<u8>)>,
|
|
checks: Vec<(Option<u8>, u32, u64)>,
|
|
pub ping_ms: f32,
|
|
ping_seq: u64,
|
|
ping_next: f64,
|
|
ping_pending: Option<(u64, f64)>,
|
|
}
|
|
|
|
impl MatchClient {
|
|
pub fn new() -> Self {
|
|
MatchClient {
|
|
name: "Guest".into(),
|
|
connected: false,
|
|
outbox: VecDeque::new(),
|
|
code: None,
|
|
room: None,
|
|
rooms: Vec::new(),
|
|
local_side: None,
|
|
error: None,
|
|
status: None,
|
|
chat: Vec::new(),
|
|
chat_seen: 0,
|
|
start: None,
|
|
turns: Vec::new(),
|
|
checks: Vec::new(),
|
|
ping_ms: 0.0,
|
|
ping_seq: 0,
|
|
ping_next: 0.0,
|
|
ping_pending: None,
|
|
}
|
|
}
|
|
|
|
pub fn phase(&self) -> Phase {
|
|
if !self.connected {
|
|
return Phase::Offline;
|
|
}
|
|
match &self.room {
|
|
Some(r) if r.started => Phase::Playing,
|
|
Some(_) => Phase::InLobby,
|
|
None if self.error.is_some() || self.status.as_deref().map(|s| s != "offline").unwrap_or(false) => Phase::Closed,
|
|
None => Phase::Lobby,
|
|
}
|
|
}
|
|
|
|
pub fn on_connect(&mut self, name: &str) {
|
|
self.connected = true;
|
|
self.name = name.to_string();
|
|
self.status = None;
|
|
self.ping_ms = 0.0;
|
|
self.ping_pending = None;
|
|
self.ping_next = 0.0;
|
|
self.rooms_request();
|
|
}
|
|
|
|
pub fn on_disconnect(&mut self) {
|
|
self.connected = false;
|
|
self.room = None;
|
|
self.code = None;
|
|
self.local_side = None;
|
|
self.start = None;
|
|
self.turns.clear();
|
|
self.status = Some("offline".into());
|
|
self.ping_ms = 0.0;
|
|
self.ping_pending = None;
|
|
self.ping_next = 0.0;
|
|
}
|
|
|
|
pub fn tick_ping(&mut self, now: f64) {
|
|
if let Some((_, start)) = self.ping_pending {
|
|
if now - start > 5.0 {
|
|
self.ping_pending = None;
|
|
self.ping_ms = 0.0;
|
|
}
|
|
}
|
|
if !self.connected || self.ping_pending.is_some() || now < self.ping_next {
|
|
return;
|
|
}
|
|
self.ping_seq = self.ping_seq.wrapping_add(1);
|
|
self.outbox.push_back(json!({"t":"ping","c":self.ping_seq}).to_string());
|
|
self.ping_pending = Some((self.ping_seq, now));
|
|
self.ping_next = now + 2.0;
|
|
}
|
|
|
|
pub fn rooms_request(&mut self) {
|
|
self.outbox.push_back(json!({"t":"rooms"}).to_string());
|
|
}
|
|
|
|
pub fn create(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8) {
|
|
self.send_create(sides, map_w, map_h, difficulty, None);
|
|
}
|
|
|
|
pub fn create_coop(&mut self, mission: usize, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8) {
|
|
self.send_create(sides, map_w, map_h, difficulty, Some(mission));
|
|
}
|
|
|
|
fn send_create(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, mission: Option<usize>) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.error = None;
|
|
self.status = None;
|
|
self.chat.clear();
|
|
self.chat_seen = 0;
|
|
self.outbox.push_back(json!({"t":"create","sides":sides,"map_w":map_w,"map_h":map_h,"difficulty":difficulty,"mission":mission}).to_string());
|
|
}
|
|
|
|
pub fn join(&mut self, code: &str) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
let code = code.trim().to_uppercase();
|
|
if code.len() != 6 {
|
|
self.error = Some("match code needs 6 characters".into());
|
|
return;
|
|
}
|
|
self.error = None;
|
|
self.status = None;
|
|
self.chat.clear();
|
|
self.chat_seen = 0;
|
|
self.outbox.push_back(json!({"t":"join","code":code}).to_string());
|
|
}
|
|
|
|
pub fn take_side(&mut self, side: u8) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"take_side","side":side}).to_string());
|
|
}
|
|
|
|
pub fn set_ai(&mut self, side: u8, ai: bool) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"set_ai","side":side,"ai":ai}).to_string());
|
|
}
|
|
|
|
pub fn ready(&mut self, ready: bool) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"ready","ready":ready}).to_string());
|
|
}
|
|
|
|
pub fn start(&mut self) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"start"}).to_string());
|
|
}
|
|
|
|
pub fn resume(&mut self) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"resume"}).to_string());
|
|
}
|
|
|
|
pub fn abort(&mut self) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"abort"}).to_string());
|
|
}
|
|
|
|
pub fn leave(&mut self) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"leave"}).to_string());
|
|
}
|
|
|
|
pub fn chat(&mut self, text: &str, team: bool) {
|
|
let text: String = text.trim().chars().take(MAX_CHAT).collect();
|
|
if !self.connected || text.is_empty() {
|
|
return;
|
|
}
|
|
let msg = if team { json!({"t":"chat","text":text,"channel":"team"}) } else { json!({"t":"chat","text":text,"channel":"all"}) };
|
|
self.outbox.push_back(msg.to_string());
|
|
}
|
|
|
|
pub fn send_commands(&mut self, cmds: &[Command]) {
|
|
if !self.connected || cmds.is_empty() {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"cmds","cmds":cmds}).to_string());
|
|
}
|
|
|
|
pub fn send_checksum(&mut self, turn: u32, sum: u64) {
|
|
if !self.connected {
|
|
return;
|
|
}
|
|
self.outbox.push_back(json!({"t":"sum","turn":turn,"sum":sum}).to_string());
|
|
}
|
|
|
|
pub fn feed(&mut self, text: &str) {
|
|
self.feed_at(text, 0.0);
|
|
}
|
|
|
|
pub fn feed_at(&mut self, text: &str, now: f64) {
|
|
let v: Value = match serde_json::from_str(text) {
|
|
Ok(v) => v,
|
|
Err(_) => return,
|
|
};
|
|
match v.get("t").and_then(|x| x.as_str()).unwrap_or("") {
|
|
"hello" => {
|
|
if let Some(n) = v.get("name").and_then(|x| x.as_str()) {
|
|
self.name = n.to_string();
|
|
}
|
|
}
|
|
"pong" => {
|
|
if let Some(c) = v.get("c").and_then(|x| x.as_u64()) {
|
|
if let Some((seq, start)) = self.ping_pending {
|
|
if seq == c {
|
|
self.ping_ms = ((now - start).max(0.0) * 1000.0) as f32;
|
|
self.ping_pending = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
"rooms" => self.parse_rooms(&v),
|
|
"room" => self.parse_room(&v),
|
|
"start" => self.parse_start(&v),
|
|
"turn" => self.parse_turn(&v),
|
|
"sum" => self.parse_sum(&v),
|
|
"chat" => {
|
|
let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("?").to_string();
|
|
let text = v.get("text").and_then(|x| x.as_str()).unwrap_or("").to_string();
|
|
let side = v.get("side").and_then(|x| x.as_u64()).map(|x| x as u8);
|
|
self.chat.push((side, name, text));
|
|
while self.chat.len() > 40 {
|
|
self.chat.remove(0);
|
|
if self.chat_seen > 0 {
|
|
self.chat_seen -= 1;
|
|
}
|
|
}
|
|
}
|
|
"closed" => {
|
|
self.status = Some(v.get("why").and_then(|x| x.as_str()).unwrap_or("closed").to_string());
|
|
self.room = None;
|
|
self.code = None;
|
|
self.local_side = None;
|
|
self.start = None;
|
|
self.turns.clear();
|
|
self.chat.clear();
|
|
self.chat_seen = 0;
|
|
}
|
|
"error" => {
|
|
self.error = Some(v.get("why").and_then(|x| x.as_str()).unwrap_or("error").to_string());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
pub fn take_start(&mut self) -> Option<StartInfo> {
|
|
self.start.take()
|
|
}
|
|
|
|
pub fn take_turns(&mut self) -> Vec<(u32, Vec<Command>, Vec<u8>)> {
|
|
std::mem::take(&mut self.turns)
|
|
}
|
|
|
|
pub fn take_checks(&mut self) -> Vec<(Option<u8>, u32, u64)> {
|
|
std::mem::take(&mut self.checks)
|
|
}
|
|
|
|
pub fn take_new_chat(&mut self) -> Vec<(Option<u8>, String, String)> {
|
|
if self.chat_seen >= self.chat.len() {
|
|
return Vec::new();
|
|
}
|
|
let out = self.chat[self.chat_seen..].to_vec();
|
|
self.chat_seen = self.chat.len();
|
|
out
|
|
}
|
|
|
|
fn parse_rooms(&mut self, v: &Value) {
|
|
let Some(arr) = v.get("rooms").and_then(|x| x.as_array()) else { return };
|
|
self.rooms = arr
|
|
.iter()
|
|
.map(|r| RoomUi {
|
|
code: r.get("code").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
|
players: r.get("players").and_then(|x| x.as_u64()).unwrap_or(0) as usize,
|
|
sides: array_bytes(r.get("sides")),
|
|
started: r.get("started").and_then(|x| x.as_bool()).unwrap_or(false),
|
|
paused: r.get("paused").and_then(|x| x.as_bool()).unwrap_or(false),
|
|
})
|
|
.collect();
|
|
}
|
|
|
|
fn parse_room(&mut self, v: &Value) {
|
|
let mut room = self.room.take().unwrap_or_default();
|
|
if let Some(code) = v.get("code").and_then(|x| x.as_str()) {
|
|
room.code = code.to_string();
|
|
self.code = Some(code.to_string());
|
|
}
|
|
let sides = array_bytes(v.get("sides"));
|
|
if !sides.is_empty() {
|
|
room.sides = sides;
|
|
}
|
|
let seats = seats_from(v.get("seats"));
|
|
if !seats.is_empty() {
|
|
room.seats = seats;
|
|
}
|
|
if let Some(x) = v.get("turn").and_then(|x| x.as_u64()) {
|
|
room.turn = x as u32;
|
|
}
|
|
room.ai = array_bytes(v.get("ai"));
|
|
if let Some(x) = v.get("started").and_then(|x| x.as_bool()) {
|
|
room.started = x;
|
|
}
|
|
if let Some(x) = v.get("paused").and_then(|x| x.as_bool()) {
|
|
room.paused = x;
|
|
}
|
|
if let Some(x) = v.get("missing").and_then(|x| x.as_bool()) {
|
|
room.missing = x;
|
|
}
|
|
if let Some(why) = v.get("why").and_then(|x| x.as_str()) {
|
|
self.status = Some(why.to_string());
|
|
}
|
|
self.local_side = room.seats.iter().find(|s| s.name == self.name && s.connected).map(|s| s.side);
|
|
if !room.code.is_empty() || !room.seats.is_empty() {
|
|
self.room = Some(room);
|
|
} else {
|
|
self.room = None;
|
|
}
|
|
}
|
|
|
|
fn parse_start(&mut self, v: &Value) {
|
|
let seed = v.get("seed").and_then(|x| x.as_u64()).unwrap_or(0) as u32;
|
|
let map_w = v.get("map_w").and_then(|x| x.as_u64()).unwrap_or(96) as usize;
|
|
let map_h = v.get("map_h").and_then(|x| x.as_u64()).unwrap_or(128) as usize;
|
|
let difficulty = v.get("difficulty").and_then(|x| x.as_u64()).unwrap_or(1) as u8;
|
|
let sides = array_bytes(v.get("sides"));
|
|
let mission = v.get("mission").and_then(|x| x.as_u64()).map(|x| x as usize);
|
|
let mut ai = array_bytes(v.get("ai"));
|
|
if ai.is_empty() {
|
|
if let Some(r) = self.room.as_ref() {
|
|
ai = r.ai.clone();
|
|
}
|
|
}
|
|
let local_side = self.room.as_ref().and_then(|r| r.seats.iter().find(|s| s.name == self.name && s.connected).map(|s| s.side));
|
|
self.local_side = local_side;
|
|
if let Some(r) = self.room.as_mut() {
|
|
r.started = true;
|
|
r.paused = false;
|
|
r.missing = false;
|
|
r.turn = 0;
|
|
if !sides.is_empty() {
|
|
r.sides = sides.clone();
|
|
}
|
|
r.ai = ai.clone();
|
|
}
|
|
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai });
|
|
self.error = None;
|
|
self.status = None;
|
|
}
|
|
|
|
fn parse_turn(&mut self, v: &Value) {
|
|
let turn = v.get("turn").and_then(|x| x.as_u64()).unwrap_or(0) as u32;
|
|
if let Some(r) = self.room.as_mut() {
|
|
r.turn = turn;
|
|
r.ai = array_bytes(v.get("ai"));
|
|
}
|
|
let cmds = match v.get("cmds").cloned() {
|
|
Some(v) => serde_json::from_value::<Vec<Command>>(v).unwrap_or_default(),
|
|
None => Vec::new(),
|
|
};
|
|
let ai = array_bytes(v.get("ai"));
|
|
self.turns.push((turn, cmds, ai));
|
|
}
|
|
|
|
fn parse_sum(&mut self, v: &Value) {
|
|
let turn = v.get("turn").and_then(|x| x.as_u64()).unwrap_or(0) as u32;
|
|
let sum = match v.get("sum").and_then(|x| x.as_u64()) {
|
|
Some(s) => s,
|
|
None => return,
|
|
};
|
|
let from = v.get("from").and_then(|x| x.as_u64()).map(|n| n as u8);
|
|
self.checks.push((from, turn, sum));
|
|
}
|
|
|
|
pub fn ui(&self) -> MatchUi {
|
|
let room = self.room.clone().unwrap_or_default();
|
|
let local = self.local_side;
|
|
let ready = room.seats.iter().find(|s| Some(s.side) == local).map(|s| s.ready).unwrap_or(false);
|
|
let host = room.seats.iter().find(|s| s.side == 0);
|
|
let is_host = local == Some(0) && host.map(|s| s.connected).unwrap_or(false);
|
|
MatchUi {
|
|
phase: self.phase(),
|
|
connected: self.connected,
|
|
name: self.name.clone(),
|
|
code: room.code.is_empty().then(|| self.code.clone()).unwrap_or(Some(room.code.clone())).clone(),
|
|
local_side: local,
|
|
ready,
|
|
is_host,
|
|
sides: room.sides.clone(),
|
|
ai: room.ai.clone(),
|
|
seats: room.seats.iter().map(|s| SeatUi { side: s.side, name: s.name.clone(), ready: s.ready, connected: s.connected, ai: s.ai }).collect(),
|
|
rooms: self.rooms.clone(),
|
|
started: room.started,
|
|
paused: room.paused,
|
|
missing: room.missing,
|
|
turn: room.turn,
|
|
error: self.error.clone(),
|
|
status: self.status.clone(),
|
|
chat: self.chat.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn array_bytes(v: Option<&Value>) -> Vec<u8> {
|
|
v.and_then(|x| x.as_array())
|
|
.map(|a| a.iter().filter_map(|x| x.as_u64().map(|n| n as u8)).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn seats_from(v: Option<&Value>) -> Vec<SeatInfo> {
|
|
v.and_then(|x| x.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.map(|s| SeatInfo {
|
|
side: s.get("side").and_then(|x| x.as_u64()).unwrap_or(0) as u8,
|
|
name: s.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string(),
|
|
ready: s.get("ready").and_then(|x| x.as_bool()).unwrap_or(false),
|
|
connected: s.get("connected").and_then(|x| x.as_bool()).unwrap_or(false),
|
|
ai: s.get("ai").and_then(|x| x.as_bool()).unwrap_or(false),
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::commands::{Act, Rect};
|
|
|
|
fn pop(client: &mut MatchClient) -> Value {
|
|
let s = client.outbox.pop_front().expect("no request");
|
|
serde_json::from_str(&s).expect("bad json")
|
|
}
|
|
|
|
#[test]
|
|
fn the_bridge_turns_orders_into_json_and_turns_back_into_commands() {
|
|
let mut client = MatchClient::new();
|
|
client.on_connect("red");
|
|
let req = pop(&mut client); // rooms
|
|
assert_eq!(req["t"].as_str(), Some("rooms"));
|
|
|
|
client.create(&[0, 2], 96, 128, 1);
|
|
let req = pop(&mut client);
|
|
assert_eq!(req["t"].as_str(), Some("create"));
|
|
|
|
client.feed(r#"{"t":"room","code":"ABC123","sides":[0,2],"seats":[{"side":0,"name":"red","ready":false,"connected":true}],"turn":0,"started":false,"paused":false,"missing":false}"#);
|
|
client.feed(r#"{"t":"start","seed":7,"map_w":96,"map_h":128,"difficulty":1,"sides":[0,2]}"#);
|
|
let start = client.take_start().expect("start info");
|
|
assert_eq!(start.seed, 7);
|
|
assert_eq!(start.local_side, Some(0));
|
|
assert_eq!(start.mission, None);
|
|
|
|
client.create_coop(2, &[0, 2], 80, 72, 1);
|
|
let coop = pop(&mut client);
|
|
assert_eq!(coop["t"].as_str(), Some("create"));
|
|
assert_eq!(coop["mission"].as_u64(), Some(2));
|
|
client.feed(r#"{"t":"start","seed":8,"map_w":80,"map_h":72,"difficulty":1,"sides":[0,2],"mission":2}"#);
|
|
let coop_start = client.take_start().expect("co-op start info");
|
|
assert_eq!(coop_start.mission, Some(2));
|
|
|
|
client.send_commands(&[Command { side: 0, seq: 0, act: Act::Dig(Rect::new(1, 2, 3, 4)) }]);
|
|
let req = pop(&mut client);
|
|
assert_eq!(req["t"].as_str(), Some("cmds"));
|
|
assert_eq!(req["cmds"][0]["side"].as_u64(), Some(0));
|
|
|
|
client.feed(r#"{"t":"turn","turn":1,"cmds":[{"side":0,"seq":0,"act":{"Dig":{"x0":1,"y0":2,"x1":3,"y1":4}}}]}"#);
|
|
let turns = client.take_turns();
|
|
assert_eq!(turns.len(), 1);
|
|
assert_eq!(turns[0].1[0].side, 0);
|
|
assert!(matches!(turns[0].1[0].act, Act::Dig(_)));
|
|
|
|
client.feed(r#"{"t":"sum","from":2,"turn":1,"sum":42}"#);
|
|
let checks = client.take_checks();
|
|
assert_eq!(checks[0], (Some(2), 1, 42));
|
|
|
|
client.chat("hello", true);
|
|
let req = pop(&mut client);
|
|
assert_eq!(req["t"].as_str(), Some("chat"));
|
|
assert_eq!(req["channel"].as_str(), Some("team"));
|
|
client.feed(r#"{"t":"chat","name":"red","text":"hello","side":0,"channel":"team"}"#);
|
|
let chat = client.take_new_chat();
|
|
assert_eq!(chat[0], (Some(0), "red".to_string(), "hello".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn the_bridge_times_the_server_ping() {
|
|
let mut client = MatchClient::new();
|
|
client.on_connect("red");
|
|
pop(&mut client); // rooms
|
|
|
|
client.tick_ping(10.0);
|
|
let req = pop(&mut client);
|
|
assert_eq!(req["t"].as_str(), Some("ping"));
|
|
assert_eq!(req["c"].as_u64(), Some(1));
|
|
|
|
client.feed_at(r#"{"t":"pong","c":1}"#, 10.25);
|
|
assert!(client.ping_ms > 200.0 && client.ping_ms < 300.0, "the pong came 250 ms after the ping");
|
|
|
|
client.tick_ping(11.0);
|
|
assert!(client.outbox.is_empty(), "the next ping waits for its two-second interval");
|
|
|
|
client.tick_ping(12.0);
|
|
let next = pop(&mut client);
|
|
assert_eq!(next["c"].as_u64(), Some(2));
|
|
}
|
|
|
|
#[test]
|
|
fn the_bridge_holds_ai_seats_through_the_lobby() {
|
|
let mut client = MatchClient::new();
|
|
client.on_connect("red");
|
|
pop(&mut client); // rooms
|
|
|
|
client.create(&[0, 2, 3, 4], 96, 128, 1);
|
|
pop(&mut client); // create
|
|
|
|
client.set_ai(3, true);
|
|
let req = pop(&mut client);
|
|
assert_eq!(req["t"].as_str(), Some("set_ai"));
|
|
assert_eq!(req["side"].as_u64(), Some(3));
|
|
assert_eq!(req["ai"].as_bool(), Some(true));
|
|
|
|
client.feed(r#"{"t":"room","code":"ABC123","sides":[0,2,3,4],"seats":[{"side":0,"name":"red","ready":false,"connected":true,"ai":false},{"side":2,"name":"","ready":false,"connected":false,"ai":false},{"side":3,"name":"","ready":false,"connected":false,"ai":true},{"side":4,"name":"","ready":false,"connected":false,"ai":false}],"ai":[3],"turn":0,"started":false,"paused":false,"missing":false}"#);
|
|
let ui = client.ui();
|
|
assert_eq!(ui.ai, vec![3u8]);
|
|
assert!(ui.seats.iter().any(|s| s.side == 3 && s.ai));
|
|
assert!(ui.seats.iter().any(|s| s.side == 2 && !s.connected && !s.ai));
|
|
|
|
client.feed(r#"{"t":"start","seed":7,"map_w":96,"map_h":128,"difficulty":1,"sides":[0,3],"ai":[3]}"#);
|
|
let start = client.take_start().expect("start info with ai seats");
|
|
assert_eq!(start.sides, vec![0u8, 3u8]);
|
|
assert_eq!(start.ai, vec![3u8]);
|
|
}
|
|
}
|