use super::connstr::Key;
use super::game::{self, GameSpec, Seat, Sinks};
use super::listener::{Admission, Config, Event, Invite, Listener};
use super::protocol::Clock;
use crate::sim::{Board, BotLevel, CapPolicy, CrabKind, Direction, Handedness, TileKind};
use serde_json::{Value, json};
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpStream};
use std::sync::mpsc::Receiver;
use std::time::Duration;
struct Client {
stream: TcpStream,
reader: BufReader<TcpStream>,
}
impl Client {
fn connect(addr: SocketAddr) -> Client {
let stream = TcpStream::connect(addr).expect("connect");
stream.set_nodelay(true).expect("nodelay");
stream
.set_read_timeout(Some(Duration::from_secs(20)))
.expect("timeout");
let reader = BufReader::new(stream.try_clone().expect("clone"));
Client { stream, reader }
}
fn send(&mut self, msg: &Value) {
let _ = writeln!(self.stream, "{msg}");
}
fn raw(&mut self, line: &str) {
writeln!(self.stream, "{line}").expect("write");
}
fn next(&mut self) -> Option<Value> {
loop {
let mut line = String::new();
if self.reader.read_line(&mut line).ok()? == 0 {
return None;
}
let msg: Value = serde_json::from_str(&line).expect("the listener writes JSON");
if msg["type"] == "ping" {
let id = msg["id"].clone();
self.send(&json!({"type": "pong", "id": id}));
continue;
}
return Some(msg);
}
}
fn register(addr: SocketAddr, name: &str, key: Option<&Key>) -> (Client, Value) {
let mut client = Client::connect(addr);
let mut msg = json!({"type": "register", "protocol": 1, "name": name, "version": "t"});
if let Some(key) = key {
msg["key"] = json!(key.to_string());
}
client.send(&msg);
let answer = client.next().expect("an answer");
(client, answer)
}
}
fn listen(admission: Admission) -> (Listener, Receiver<Event>) {
Listener::bind(
SocketAddr::from(([127, 0, 0, 1], 0)),
Config::new(admission),
)
.expect("bind")
}
fn invite(key: &Key, uses: Option<u32>) -> Invite {
Invite {
key: key.clone(),
uses,
owner: None,
slot: None,
}
}
fn little_beach(ticks: u32) -> Board {
let mut board = Board::new(12, 3, 9);
board.set_tile(11, 1, TileKind::Castle(0));
board.set_tile(0, 1, TileKind::Castle(1));
board.spawn_crab(9, 1, Direction::Right, Handedness::Left, CrabKind::Giant);
board.rules_mut().round_length = Some(ticks);
board.set_signpost_rule(3, CapPolicy::Evict);
board
}
fn spec(board: Board, seats: Vec<Seat>, deadline_ms: u32) -> GameSpec {
spec_on(board, seats, deadline_ms, false)
}
fn spec_on(board: Board, seats: Vec<Seat>, deadline_ms: u32, live: bool) -> GameSpec {
GameSpec {
game: 7,
replay_id: "t-g7".into(),
board,
names: seats
.iter()
.enumerate()
.map(|(i, _)| format!("seat{i}"))
.collect(),
seats,
clock: Clock {
live,
deadline_ms,
input_delay: 0,
},
fair_cursor: false,
forfeit_after: Duration::from_millis(300),
ready_within: Duration::from_secs(5),
}
}
fn play(listener: &Listener, spec: GameSpec) -> (game::GameResult, String) {
let mut log = Vec::new();
let mut console = |_: String| {};
let result = {
let mut sinks = Sinks {
log: &mut log,
trace: None,
feed: None,
console: &mut console,
echo_notes: false,
};
game::play(listener, spec, &mut sinks)
};
(result, String::from_utf8(log).expect("utf8"))
}
#[test]
fn a_bot_registers_plays_and_is_told_how_it_went() {
let key = Key::draw();
let (listener, events) = listen(Admission::Keys(vec![invite(&key, Some(1))]));
let addr = listener.local_addr();
let (mut bot, answer) = Client::register(addr, "Plucky", Some(&key));
assert_eq!(answer["type"], "registered");
assert!(answer["token"].as_str().is_some_and(|t| t.len() >= 20));
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
let mut ticks = 0;
let mut refused = None;
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => {
assert_eq!(msg["seat"], 0);
assert_eq!(msg["kinds"], json!(["bot", "ai"]));
bot.send(&json!({"type": "ready", "game": msg["game"]}));
}
Some("tick") => {
ticks += 1;
let t = msg["tick"].clone();
if refused.is_none() && msg["you"]["last"]["accepted"] == false {
refused = Some(msg["you"]["last"]["reason"].clone());
}
let act = if t == 0 {
json!({"game": 7, "tick": t, "act": "place", "x": 0, "y": 1, "dir": "up", "note": "hi"})
} else {
json!({"game": 7, "tick": t, "act": "none"})
};
bot.send(&act);
}
Some("end") => return (ticks, refused, msg),
_ => {}
}
}
panic!("the connection closed before the end");
});
let (result, log) = play(
&listener,
spec(
little_beach(120),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
),
);
let (ticks, refused, end) = player.join().expect("player");
assert_eq!(ticks, 120, "one tick message per tick of the round");
assert_eq!(refused, Some(json!("castle")));
assert_eq!(result.seats[0].rejected, 1);
assert_eq!(result.seats[0].banked, 1, "the giant walked home");
assert_eq!(end["scores"][0], 10);
assert_eq!(end["placing"], 1);
assert_eq!(end["replay"], "t-g7");
assert!(log.contains("note: hi"), "{log}");
assert!(log.contains("refused: castle"), "{log}");
assert_eq!(result.replay.inputs.len(), 120);
}
#[test]
fn a_second_bot_on_a_single_use_key_is_turned_away() {
let key = Key::draw();
let (listener, _events) = listen(Admission::Keys(vec![invite(&key, Some(1))]));
let addr = listener.local_addr();
let (_first, answer) = Client::register(addr, "One", Some(&key));
assert_eq!(answer["type"], "registered");
let (_second, answer) = Client::register(addr, "Two", Some(&key));
assert_eq!(answer["type"], "error");
assert_eq!(answer["fatal"], true);
}
#[test]
fn a_taken_name_does_not_spend_the_key() {
let key = Key::draw();
let (listener, _events) = listen(Admission::Keys(vec![invite(&key, Some(2))]));
let addr = listener.local_addr();
let (_a, answer) = Client::register(addr, "Same", Some(&key));
assert_eq!(answer["type"], "registered");
let (_b, answer) = Client::register(addr, "same", Some(&key));
assert_eq!(
answer["type"], "error",
"names are unique, whatever the case"
);
let (_c, answer) = Client::register(addr, "Other", Some(&key));
assert_eq!(
answer["type"], "registered",
"the refused one did not use the key up"
);
}
#[test]
fn three_wrong_keys_turn_the_address_away_and_not_the_key() {
let key = Key::draw();
let (listener, _events) = listen(Admission::Keys(vec![invite(&key, None)]));
let addr = listener.local_addr();
for _ in 0..3 {
let (_c, answer) = Client::register(addr, "Guess", Some(&Key::draw()));
assert_eq!(answer["type"], "error");
}
let (_c, answer) = Client::register(addr, "Honest", Some(&key));
assert_eq!(answer["type"], "error");
assert!(
answer["message"]
.as_str()
.is_some_and(|m| m.contains("minute"))
);
}
#[test]
fn a_protocol_this_build_does_not_speak_is_refused_by_number() {
let (listener, _events) = listen(Admission::Open);
let mut client = Client::connect(listener.local_addr());
client.send(&json!({"type": "register", "protocol": 3, "name": "Future"}));
let answer = client.next().expect("answer");
assert_eq!(answer["type"], "error");
assert!(
answer["message"]
.as_str()
.is_some_and(|m| m.contains("protocol 1 to 2"))
);
}
#[test]
fn a_line_too_long_closes_the_connection() {
let (listener, _events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Wordy", None);
let long = format!("{{\"note\": \"{}\"}}", "x".repeat(70 * 1024));
let _ = writeln!(bot.stream, "{long}");
let mut saw_error = false;
while let Some(msg) = bot.next() {
saw_error |= msg["type"] == "error" && msg["fatal"] == true;
}
assert!(saw_error);
}
#[test]
fn a_bot_that_comes_back_with_its_token_plays_on() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (mut bot, answer) = Client::register(addr, "Phoenix", None);
let token = answer["token"].as_str().expect("token").to_string();
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
let mut seen = 0;
while let Some(msg) = bot.next() {
if msg["type"] == "hello" {
bot.send(&json!({"type": "ready", "game": msg["game"]}));
}
if msg["type"] == "tick" {
seen += 1;
bot.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"}));
if seen == 5 {
break;
}
}
}
drop(bot);
std::thread::sleep(Duration::from_millis(100));
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
let answer = back.next().expect("registered again");
assert_eq!(answer["type"], "registered");
assert_eq!(answer["name"], "Phoenix");
let hello = back.next().expect("hello again");
assert_eq!(hello["type"], "hello");
assert_eq!(hello["resumed"], true);
let mut after = 0;
while let Some(msg) = back.next() {
if msg["type"] == "tick" {
after += 1;
back.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"}));
}
if msg["type"] == "end" {
return after;
}
}
panic!("no end");
});
let (result, log) = play(
&listener,
spec_on(
little_beach(150),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
33,
true,
),
);
let after = player.join().expect("player");
assert!(
after > 100,
"it played on after coming back ({after} ticks)"
);
assert!(!result.seats[0].forfeit);
assert!(
log.contains("connection dropped") && log.contains("reconnected"),
"{log}"
);
}
#[test]
fn a_bot_that_never_comes_forfeits_and_places_last() {
let (listener, events) = listen(Admission::Open);
let (bot, _) = Client::register(listener.local_addr(), "Ghost", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
drop(bot);
while listener.connected(id) {
std::thread::sleep(Duration::from_millis(5));
}
let mut board = little_beach(60);
board.set_score(1, 0);
let (result, _) = play(
&listener,
spec(board, vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)], 50),
);
assert!(result.seats[0].forfeit);
assert_eq!(result.seats[0].placing, 2);
assert_eq!(result.seats[1].placing, 1);
}
#[test]
fn a_lookahead_is_answered_off_the_game_and_a_replay_fetched_after_it() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (mut bot, _) = Client::register(addr, "Seer", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
let mut lookahead = None;
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => bot.send(&json!({"type": "ready", "game": 7})),
Some("tick") if msg["tick"] == 0 => {
bot.send(&json!({"type": "simulate", "game": 7, "ticks": 100, "plan": []}));
let answer = bot.next().expect("simulated");
lookahead = Some(answer);
bot.send(&json!({"game": 7, "tick": 0, "act": "none"}));
}
Some("tick") => bot.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"})),
Some("end") => {
bot.send(&json!({"type": "replay", "id": "t-g7"}));
let replay = bot.next().expect("replay");
return (lookahead, replay);
}
_ => {}
}
}
panic!("no end");
});
play(
&listener,
spec(
little_beach(80),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
),
);
let (lookahead, replay) = player.join().expect("player");
let lookahead = lookahead.expect("a lookahead");
assert_eq!(lookahead["type"], "simulated");
assert_eq!(lookahead["ticks"], 80, "it stops where the round does");
assert!(
lookahead["events"]
.as_array()
.is_some_and(|e| e.iter().any(|e| e["what"] == "banked" && e["owner"] == 0)),
"{lookahead}"
);
assert_eq!(replay["type"], "replay");
let text = replay["text"].as_str().expect("text");
let parsed = crate::sim::Replay::parse(text).expect("a replay this build reads");
assert_eq!(parsed.inputs.len(), 80);
assert_eq!(parsed.names[0], "seat0");
assert_eq!(parsed.kinds[0], crate::sim::SeatKind::Bot);
}
#[test]
fn a_slow_bot_is_late_but_never_buried_under_ticks() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Sloth", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
let mut ticks = 0;
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => bot.send(&json!({"type": "ready", "game": 7})),
Some("tick") => {
ticks += 1;
std::thread::sleep(Duration::from_millis(12));
bot.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"}));
}
Some("end") => return ticks,
_ => {}
}
}
panic!("no end");
});
let (result, _) = play(
&listener,
spec(
little_beach(90),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
5,
),
);
let ticks = player.join().expect("player");
let seat = &result.seats[0];
assert!(seat.late > 0, "a 12 ms bot on a 5 ms deadline is late");
assert!(
ticks < 60,
"and is not sent ticks it has not caught up with ({ticks})"
);
assert_eq!(seat.sent as usize, ticks);
}
#[test]
fn garbage_counts_as_none_and_is_logged() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Babbler", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => bot.send(&json!({"type": "ready", "game": 7})),
Some("tick") if msg["tick"] == 3 => bot.raw("{this is not json"),
Some("tick") if msg["tick"] == 4 => {
bot.send(&json!({"game": 7, "tick": 4, "act": "teleport", "x": 1, "y": 1}));
}
Some("tick") => bot.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"})),
Some("end") => return,
_ => {}
}
}
});
let (result, log) = play(
&listener,
spec(
little_beach(10),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
),
);
player.join().expect("player");
assert!(log.contains("garbled"), "{log}");
assert!(log.contains("teleport"), "{log}");
assert_eq!(result.seats[0].rejected, 0);
}
#[test]
fn a_per_author_key_names_the_owner_and_one_owner_enters_once() {
let shared = Key::draw();
let ana = Key::draw();
let mut config = Config::new(Admission::Keys(vec![invite(&shared, None)]));
config.per_owner = Some(1);
let (listener, events) =
Listener::bind(SocketAddr::from(([127, 0, 0, 1], 0)), config).expect("bind");
listener.invite(Invite {
key: ana.clone(),
uses: None,
owner: Some("Ana".into()),
slot: None,
});
let addr = listener.local_addr();
let mut client = Client::connect(addr);
client.send(&json!({
"type": "register", "protocol": 1, "name": "Sly", "owner": "Somebody Else",
"key": ana.to_string(),
}));
assert_eq!(client.next().expect("answer")["type"], "registered");
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let info = listener.bot(id).expect("info");
assert_eq!(info.owner.as_deref(), Some("Ana"));
assert!(!info.owner_declared);
let mut second = Client::connect(addr);
second.send(&json!({
"type": "register", "protocol": 1, "name": "Sly2", "owner": "ana",
"key": shared.to_string(),
}));
let answer = second.next().expect("answer");
assert_eq!(answer["type"], "error", "{answer}");
}
#[test]
fn a_closed_listener_lets_its_port_and_its_bots_go() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (mut bot, _) = Client::register(addr, "Leaving", None);
let Ok(Event::Registered(_)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
listener.close();
while bot.next().is_some() {}
let mut again = None;
for _ in 0..250 {
if let Ok(socket) = std::net::TcpListener::bind(addr) {
again = Some(socket);
break;
}
std::thread::sleep(Duration::from_millis(20));
}
assert!(again.is_some(), "the port was never let go");
}
fn registered(events: &Receiver<Event>) -> usize {
loop {
match events.recv_timeout(Duration::from_secs(5)) {
Ok(Event::Registered(id)) => return id,
Ok(_) => {}
Err(_) => panic!("no registration event"),
}
}
}
#[test]
fn an_answer_to_a_bad_request_earns_nothing() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Drum", None);
let id = registered(&events);
for _ in 0..300 {
bot.send(&json!({"type": "register", "protocol": 1, "name": "Drum"}));
assert_eq!(bot.next().expect("an answer")["type"], "error");
}
assert!(
listener.allowance(id) <= Listener::BURST,
"300 errors earned the bot {} lines",
listener.allowance(id)
);
}
#[test]
fn a_flood_of_requests_answered_with_errors_is_let_go() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Drum", None);
let id = registered(&events);
let again = json!({"type": "register", "protocol": 1, "name": "Drum"}).to_string();
let mut flood = bot.stream.try_clone().expect("a second handle");
let writer = std::thread::spawn(move || {
for _ in 0..20_000 {
if writeln!(flood, "{again}").is_err() {
break;
}
}
});
while let Some(msg) = bot.next() {
if msg["type"] == "error" && msg["fatal"] == true {
assert_eq!(msg["message"], "flooding", "{msg}");
break;
}
}
drop(bot);
let _ = writer.join();
let gone = (0..250).any(|_| {
std::thread::sleep(Duration::from_millis(20));
!listener.connected(id)
});
assert!(gone, "a flood and still welcome");
}
#[test]
fn a_locked_out_address_still_takes_a_bot_back_by_its_token() {
let key = Key::draw();
let (listener, events) = listen(Admission::Keys(vec![invite(&key, None)]));
let addr = listener.local_addr();
let (keeper, answer) = Client::register(addr, "Keeper", Some(&key));
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
for _ in 0..3 {
let mut stale = Client::connect(addr);
stale.send(&json!({"type": "register", "protocol": 1, "token": "0".repeat(26)}));
assert_eq!(stale.next().expect("answer")["type"], "error");
}
let (_other, answer) = Client::register(addr, "Other", Some(&key));
assert_eq!(answer["type"], "registered", "{answer}");
for _ in 0..3 {
let (_c, answer) = Client::register(addr, "Guess", Some(&Key::draw()));
assert_eq!(answer["type"], "error");
}
let (_c, answer) = Client::register(addr, "Late", Some(&key));
assert_eq!(answer["type"], "error", "the keys are locked out");
drop(keeper);
while listener.connected(id) {
std::thread::sleep(Duration::from_millis(5));
}
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
let answer = back.next().expect("answer");
assert_eq!(answer["type"], "registered", "{answer}");
}
#[test]
fn silent_connections_are_capped_and_told_so_at_once() {
let (listener, _events) = listen(Admission::Open);
let addr = listener.local_addr();
let mut silent: Vec<Client> = Vec::new();
for _ in 0..8 {
silent.push(Client::connect(addr));
}
std::thread::sleep(Duration::from_millis(200));
let started = std::time::Instant::now();
let mut over = Client::connect(addr);
let answer = over.next().expect("an answer");
assert_eq!(answer["type"], "error");
assert!(
started.elapsed() < Duration::from_secs(3),
"told after {:?}",
started.elapsed()
);
drop(silent);
std::thread::sleep(Duration::from_millis(200));
let (_bot, answer) = Client::register(addr, "Patient", None);
assert_eq!(answer["type"], "registered", "{answer}");
}
#[test]
fn a_lookahead_does_not_hold_the_game_up() {
use super::listener::{View, lock};
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Thinker", None);
let id = registered(&events);
let link = listener.open_game(7, vec![(id, 0)]);
let board = crate::sim::classic_arena(false, 2);
let ticks = 3000;
let alone = std::time::Instant::now();
super::lookahead::run(&board, 0, ticks, &[]);
let alone = alone.elapsed();
*lock(&link.view) = Some(View {
board,
cursors: vec![None, None],
budget: vec![ticks, ticks],
});
let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let thinker = {
let done = std::sync::Arc::clone(&done);
std::thread::spawn(move || {
bot.send(&json!({"type": "simulate", "game": 7, "ticks": ticks, "plan": []}));
let answer = bot.next().expect("simulated");
done.store(true, std::sync::atomic::Ordering::SeqCst);
answer
})
};
let mut longest = Duration::ZERO;
while !done.load(std::sync::atomic::Ordering::SeqCst) {
let asked = std::time::Instant::now();
drop(lock(&link.view));
longest = longest.max(asked.elapsed());
std::thread::sleep(Duration::from_millis(1));
}
let answer = thinker.join().expect("thinker");
assert_eq!(answer["type"], "simulated", "{answer}");
assert!(
longest < alone / 2,
"the game waited {longest:?} for a lookahead that takes {alone:?}"
);
}
#[test]
fn a_bot_that_asks_for_replays_and_never_reads_is_dropped() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Hoarder", None);
let id = registered(&events);
listener.keep_replay("big".into(), vec![id], "x".repeat(200 * 1024));
let ask = json!({"type": "replay", "id": "big"}).to_string();
for _ in 0..150 {
if writeln!(bot.stream, "{ask}").is_err() {
break;
}
}
let deadline = std::time::Instant::now() + Duration::from_secs(60);
let mut dropped = false;
while let Some(left) = deadline.checked_duration_since(std::time::Instant::now())
&& let Ok(event) = events.recv_timeout(left)
{
if matches!(event, Event::Dropped(gone) if gone == id) {
dropped = true;
break;
}
}
assert!(
dropped,
"30 MB asked for and never read, and still connected"
);
}
#[test]
fn a_garbled_lookahead_is_an_error_and_not_the_move() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Fumble", None);
let id = registered(&events);
let player = std::thread::spawn(move || {
let mut after = None;
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => bot.send(&json!({"type": "ready", "game": 7})),
Some("tick") if msg["tick"] == 0 => {
bot.send(&json!({
"type": "simulate", "game": 7, "ticks": 10,
"plan": [{"at": 0, "act": "place", "x": 9, "y": 1}],
}));
after = bot.next();
bot.send(&json!({"game": 7, "tick": 0, "act": "place", "x": 9, "y": 0, "dir": "down"}));
}
Some("tick") => bot.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"})),
Some("end") => return after,
_ => {}
}
}
panic!("no end");
});
let (result, _) = play(
&listener,
spec(
little_beach(10),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
),
);
let after = player.join().expect("player").expect("an answer");
assert_eq!(after["type"], "error", "{after}");
assert_eq!(after["game"], 7, "{after}");
let placed = result.replay.inputs.first().map(|actions| actions[0]);
assert!(
matches!(
placed,
Some(crate::sim::PlayerAction::Place { x: 9, y: 0, .. })
),
"{placed:?}"
);
}
fn dropped(events: &Receiver<Event>, id: usize) {
loop {
match events.recv_timeout(Duration::from_secs(5)) {
Ok(Event::Dropped(gone)) if gone == id => return,
Ok(_) => {}
Err(_) => panic!("bot {id} was never reported gone"),
}
}
}
#[test]
fn a_dropped_bot_restarted_without_its_token_takes_its_chair_back() {
let slot_key = Key::draw();
let shared_key = Key::draw();
let (listener, events) = listen(Admission::Keys(vec![
Invite {
key: slot_key.clone(),
uses: Some(1),
owner: None,
slot: Some(0),
},
Invite {
key: shared_key.clone(),
uses: Some(1),
owner: None,
slot: Some(1),
},
]));
let addr = listener.local_addr();
for (name, key) in [("Greedy", &slot_key), ("Second", &shared_key)] {
let (bot, answer) = Client::register(addr, name, Some(key));
assert_eq!(answer["type"], "registered", "{answer}");
let mut token = answer["token"].clone();
let id = registered(&events);
let mut bot = Some(bot);
for _ in 0..4 {
drop(bot.take());
dropped(&events, id);
let (back, answer) = Client::register(addr, name, Some(key));
assert_eq!(answer["type"], "registered", "{name}: {answer}");
assert_eq!(answer["resumed"], true, "{answer}");
assert_ne!(answer["token"], token, "a new token: {answer}");
token = answer["token"].clone();
bot = Some(back);
}
let (_twin, answer) = Client::register(addr, name, Some(key));
assert_eq!(answer["type"], "error", "{answer}");
drop(bot.take());
dropped(&events, id);
let (_other, answer) = Client::register(addr, "Other", Some(key));
assert_eq!(answer["type"], "error", "{answer}");
}
}
#[test]
fn a_shared_key_does_not_take_another_bots_chair() {
let shared = Key::draw();
for uses in [None, Some(2)] {
let (listener, events) = listen(Admission::Keys(vec![invite(&shared, uses)]));
let addr = listener.local_addr();
let (bot, answer) = Client::register(addr, "Victim", Some(&shared));
assert_eq!(answer["type"], "registered", "{answer}");
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
drop(bot);
dropped(&events, id);
let (_rival, answer) = Client::register(addr, "Victim", Some(&shared));
assert_eq!(answer["type"], "error", "not the victim's chair: {answer}");
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
let answer = back.next().expect("an answer");
assert_eq!(answer["resumed"], true, "its token still works: {answer}");
}
}
#[test]
fn a_bot_let_go_by_the_host_frees_its_name() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
for dismissed in [false, true] {
let (mut bot, answer) = Client::register(addr, "Shown Out", None);
assert_eq!(answer["type"], "registered", "{answer}");
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
if dismissed {
listener.dismiss(id, "no seat for you");
} else {
listener.disconnect(id);
}
dropped(&events, id);
let mut told = None;
while let Some(msg) = bot.next() {
if msg["type"] == "error" {
told = Some(msg);
}
}
if dismissed {
let told = told.expect("told why");
assert_eq!(told["message"], "no seat for you", "{told}");
assert_eq!(told["fatal"], true, "{told}");
}
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
let answer = back.next().expect("an answer");
assert_eq!(answer["type"], "error", "the token lands nowhere: {answer}");
assert_eq!(answer["fatal"], true, "{answer}");
let (anew, answer) = Client::register(addr, "Shown Out", None);
assert_eq!(answer["type"], "registered", "the name is free: {answer}");
let again = registered(&events);
drop(anew);
listener.disconnect(again);
dropped(&events, again);
}
}
#[test]
fn a_steady_flood_is_let_go_too() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Drip", None);
let id = registered(&events);
let again = json!({"type": "register", "protocol": 1, "name": "Drip"}).to_string();
let mut flood = bot.stream.try_clone().expect("a second handle");
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let writer = {
let stop = std::sync::Arc::clone(&stop);
std::thread::spawn(move || {
let until = std::time::Instant::now() + Duration::from_secs(15);
while !stop.load(std::sync::atomic::Ordering::SeqCst)
&& std::time::Instant::now() < until
{
if writeln!(flood, "{again}").is_err() {
return;
}
std::thread::sleep(Duration::from_millis(1));
}
})
};
let mut flooding = false;
while let Some(msg) = bot.next() {
if msg["type"] == "error" && msg["fatal"] == true {
assert_eq!(msg["message"], "flooding", "{msg}");
flooding = true;
break;
}
}
stop.store(true, std::sync::atomic::Ordering::SeqCst);
drop(bot);
let _ = writer.join();
assert!(flooding, "a steady flood and never told");
let gone = (0..250).any(|_| {
std::thread::sleep(Duration::from_millis(20));
!listener.connected(id)
});
assert!(gone, "a steady flood and still welcome");
}
#[test]
fn an_undeclared_owner_counts_as_its_address() {
let mut config = Config::new(Admission::Open);
config.per_owner = Some(1);
let (listener, _events) =
Listener::bind(SocketAddr::from(([127, 0, 0, 1], 0)), config).expect("bind");
let addr = listener.local_addr();
let (_first, answer) = Client::register(addr, "Anon", None);
assert_eq!(answer["type"], "registered", "{answer}");
let (_second, answer) = Client::register(addr, "Anon Again", None);
assert_eq!(answer["type"], "error", "{answer}");
assert!(
answer["message"]
.as_str()
.is_some_and(|m| m.contains("owner")),
"it says what to do: {answer}"
);
let mut declared = Client::connect(addr);
declared.send(&json!({"type": "register", "protocol": 1, "name": "Bea's", "owner": "Bea"}));
let answer = declared.next().expect("an answer");
assert_eq!(answer["type"], "registered", "{answer}");
}
#[test]
fn a_bot_that_never_came_is_not_waited_for_twice() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (bot, answer) = Client::register(addr, "Truant", None);
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
drop(bot);
while listener.connected(id) {
std::thread::sleep(Duration::from_millis(5));
}
let wait = Duration::from_secs(1);
let game = |listener: &Listener| {
let mut spec = spec(
little_beach(30),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
50,
);
spec.forfeit_after = wait;
let started = std::time::Instant::now();
let (result, _) = play(listener, spec);
assert!(result.seats[0].forfeit);
started.elapsed()
};
let first = game(&listener);
assert!(first >= wait, "the first game waits for it ({first:?})");
assert!(listener.absent(id));
let second = game(&listener);
assert!(second < wait / 2, "the second does not ({second:?})");
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
assert_eq!(back.next().expect("an answer")["type"], "registered");
assert!(!listener.absent(id), "back, and waited for again");
}
#[test]
fn a_bot_back_on_a_new_connection_before_the_first_tick_is_greeted_there() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (mut first, answer) = Client::register(addr, "Twice", None);
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
let player = std::thread::spawn(move || {
let hello = first.next().expect("hello");
assert_eq!(hello["type"], "hello");
let mut second = Client::connect(addr);
second.send(&json!({"type": "register", "protocol": 1, "token": token}));
assert_eq!(second.next().expect("an answer")["type"], "registered");
let hello = second.next().expect("hello on the new connection");
assert_eq!(hello["type"], "hello", "{hello}");
second.send(&json!({"type": "ready", "game": hello["game"]}));
let mut ticks = 0;
while let Some(msg) = second.next() {
if msg["type"] == "tick" {
ticks += 1;
second.send(&json!({"game": 7, "tick": msg["tick"], "act": "none"}));
}
if msg["type"] == "end" {
break;
}
}
drop(first);
ticks
});
let mut spec = spec(
little_beach(30),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
50,
);
spec.ready_within = Duration::from_secs(3);
let (result, log) = play(&listener, spec);
let ticks = player.join().expect("player");
assert!(!result.seats[0].forfeit, "{log}");
assert!(ticks > 0, "it played on the new connection");
}
#[test]
fn a_forfeit_elsewhere_does_not_cut_short_a_wait_already_begun() {
let (listener, events) = listen(Admission::Open);
let (bot, _) = Client::register(listener.local_addr(), "Away", None);
let id = registered(&events);
drop(bot);
while listener.connected(id) {
std::thread::sleep(Duration::from_millis(5));
}
let wait = Duration::from_secs(1);
let mut spec = spec(
little_beach(30),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
50,
);
spec.forfeit_after = wait;
let took = std::thread::scope(|scope| {
let game = scope.spawn(|| {
let started = std::time::Instant::now();
let (result, _) = play(&listener, spec);
assert!(result.seats[0].forfeit);
started.elapsed()
});
std::thread::sleep(Duration::from_millis(150));
listener.mark_absent(id);
game.join().expect("game")
});
assert!(took >= wait, "the wait was cut short ({took:?})");
}
#[test]
fn a_bot_that_came_and_went_before_the_first_tick_is_not_marked_absent() {
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let (bot, answer) = Client::register(addr, "Flicker", None);
let token = answer["token"].as_str().expect("token").to_string();
let id = registered(&events);
drop(bot);
while listener.connected(id) {
std::thread::sleep(Duration::from_millis(5));
}
let player = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
let mut back = Client::connect(addr);
back.send(&json!({"type": "register", "protocol": 1, "token": token}));
assert_eq!(back.next().expect("an answer")["type"], "registered");
assert_eq!(back.next().expect("hello")["type"], "hello");
});
let mut spec = spec(
little_beach(30),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
50,
);
spec.forfeit_after = Duration::from_millis(500);
let (result, log) = play(&listener, spec);
player.join().expect("player");
assert!(result.seats[0].forfeit);
assert!(!log.contains("forfeit, never came"), "{log}");
assert!(log.contains("left before the first tick"), "{log}");
assert!(!listener.absent(id), "it came, so it is waited for again");
}
#[test]
fn a_dropped_bots_walk_does_not_land_under_the_fair_cursor() {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Walker", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
while let Some(msg) = bot.next() {
if msg["type"] == "hello" {
bot.send(&json!({"type": "ready", "game": msg["game"]}));
}
if msg["type"] == "tick" {
bot.send(&json!({
"game": 7, "tick": msg["tick"], "act": "place", "x": 1, "y": 0, "dir": "down",
}));
return;
}
}
});
let mut spec = spec(
little_beach(90),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
);
spec.fair_cursor = true;
let (result, log) = play(&listener, spec);
player.join().expect("player");
assert!(log.contains("connection dropped"), "{log}");
assert!(
result
.replay
.inputs
.iter()
.all(|tick| tick[0] == crate::sim::PlayerAction::None),
"the walk landed after the bot was gone"
);
}
#[test]
fn a_port_listened_on_again_takes_its_first_bot() {
let (listener, _events) = listen(Admission::Open);
let lan = listener
.also_listen(SocketAddr::from(([127, 0, 0, 1], 0)))
.expect("listen");
listener.stop_listening(lan);
let mut again = None;
for _ in 0..200 {
if let Ok(addr) = listener.also_listen(lan) {
again = Some(addr);
break;
}
std::thread::sleep(Duration::from_millis(10));
}
let again = again.expect("the port free again");
assert_eq!(again, lan);
let (_bot, answer) = Client::register(again, "First", None);
assert_eq!(answer["type"], "registered", "{answer}");
}
#[test]
fn a_declared_owner_takes_none_of_an_invited_owners_places() {
let shared = Key::draw();
let alice = Key::draw();
let mut config = Config::new(Admission::Keys(vec![invite(&shared, None)]));
config.per_owner = Some(1);
let (listener, _events) =
Listener::bind(SocketAddr::from(([127, 0, 0, 1], 0)), config).expect("bind");
let addr = listener.local_addr();
let declare = |name: &str| {
let mut client = Client::connect(addr);
client.send(&json!({
"type": "register", "protocol": 1, "name": name, "owner": "alice",
"key": shared.to_string(),
}));
let answer = client.next().expect("answer");
(client, answer)
};
let (_rival, answer) = declare("Rival");
assert_eq!(answer["type"], "registered", "{answer}");
listener.invite(Invite {
key: alice.clone(),
uses: None,
owner: Some("Alice".into()),
slot: None,
});
let (_late, answer) = declare("Late Rival");
assert_eq!(answer["type"], "error", "{answer}");
let (_own, answer) = Client::register(addr, "Alice's", Some(&alice));
assert_eq!(answer["type"], "registered", "{answer}");
let owners: Vec<Option<String>> = listener
.bots()
.iter()
.map(|b| Some(b.owner_key()))
.collect();
assert_eq!(owners.len(), 2);
assert_ne!(owners[0], owners[1]);
assert!(super::draw::draw(&owners, 2, 1, 1, 7).is_ok());
}
#[test]
fn a_reply_held_across_a_drop_and_a_return_is_not_acted_on() {
use super::listener::GameMsg;
use super::protocol::{Act, Reply};
use super::seat::{BotDriver, Journal};
use std::time::Instant;
let mut bot = BotDriver::new(
super::listener::BotId::default(),
0,
"Hopper".into(),
true,
Duration::from_millis(100),
);
let mut log = Vec::new();
let mut console = |_: String| {};
let mut journal = Journal {
log: &mut log,
console: &mut console,
echo_notes: false,
game: 7,
};
bot.take(GameMsg::Ready { seat: 0 }, 0, &mut journal);
let now = Instant::now();
bot.sent(3, now);
let place = Act::Place {
x: 1,
y: 0,
dir: Direction::Down,
};
let reply = Reply {
game: 7,
tick: 3,
act: place,
wait: 0,
note: None,
garbled: None,
};
bot.take(
GameMsg::Reply {
seat: 0,
reply,
at: now,
},
3,
&mut journal,
);
bot.take(GameMsg::Dropped { seat: 0 }, 4, &mut journal);
bot.take(GameMsg::Back { seat: 0, serial: 2 }, 4, &mut journal);
assert!(bot.playing(), "it is back and ready");
assert_eq!(bot.order(&mut journal), Act::None);
}
#[test]
fn a_placement_far_off_the_beach_is_refused_and_not_a_crash() {
for fair in [false, true] {
let (listener, events) = listen(Admission::Open);
let (mut bot, _) = Client::register(listener.local_addr(), "Wanderer", None);
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
let player = std::thread::spawn(move || {
let mut refused = None;
while let Some(msg) = bot.next() {
match msg["type"].as_str() {
Some("hello") => bot.send(&json!({"type": "ready", "game": 7})),
Some("tick") => {
let t = msg["tick"].clone();
if refused.is_none() && msg["you"]["last"]["accepted"] == false {
refused = Some(msg["you"]["last"]["reason"].clone());
}
bot.send(&json!({
"game": 7, "tick": t, "act": "place", "x": 200, "y": 200, "dir": "up",
}));
}
Some("end") => return refused,
_ => {}
}
}
panic!("the connection closed before the end");
});
let mut spec = spec(
little_beach(40),
vec![Seat::Bot(id), Seat::Ai(BotLevel::Easy)],
1000,
);
spec.fair_cursor = fair;
let (result, _) = play(&listener, spec);
let refused = player.join().expect("player");
assert_eq!(refused, Some(json!("off_board")), "fair cursor: {fair}");
assert_eq!(result.replay.inputs.len(), 40, "the round played out");
}
}
#[test]
fn a_protocol_2_bot_holds_by_diffs_what_a_protocol_1_bot_is_sent_whole() {
use super::protocol::{FULL_EVERY, apply_delta};
const TICKS: u32 = 400;
let (listener, events) = listen(Admission::Open);
let addr = listener.local_addr();
let registered = |events: &Receiver<Event>| {
let Ok(Event::Registered(id)) = events.recv_timeout(Duration::from_secs(5)) else {
panic!("no registration event");
};
id
};
let (mut whole, _) = Client::register(addr, "Whole", None);
let whole_id = registered(&events);
let mut diffing = Client::connect(addr);
diffing.send(&json!({"type": "register", "protocol": 2, "name": "Diffing"}));
assert_eq!(diffing.next().expect("answer")["type"], "registered");
let diffing_id = registered(&events);
let whole_player = std::thread::spawn(move || {
let mut seen = Vec::new();
let mut sand = Vec::new();
while let Some(msg) = whole.next() {
match msg["type"].as_str() {
Some("hello") => {
for (y, row) in msg["board"]["tiles"]
.as_array()
.expect("tiles")
.iter()
.enumerate()
{
for (x, c) in row.as_str().expect("row").chars().enumerate() {
if c == '.' {
sand.push((x, y));
}
}
}
whole.send(&json!({"type": "ready", "game": 7}));
}
Some("tick") => {
let t = msg["tick"].as_u64().expect("tick");
assert!(msg.get("base").is_none(), "protocol 1 is sent no diff");
let mut reply = json!({"game": 7, "tick": t, "act": "none"});
if t % 9 == 0 {
let (x, y) = sand[(t as usize * 7) % sand.len()];
reply = json!({"game": 7, "tick": t, "act": "place", "x": x, "y": y, "dir": "up"});
}
whole.send(&reply);
seen.push(msg);
}
Some("end") => return seen,
_ => {}
}
}
panic!("the connection closed before the end");
});
let diffing_player = std::thread::spawn(move || {
let mut held: Option<Value> = None;
let mut seen = Vec::new();
let mut whole_at = Vec::new();
let mut refused = Vec::new();
while let Some(msg) = diffing.next() {
match msg["type"].as_str() {
Some("hello") => diffing.send(&json!({"type": "ready", "game": 7})),
Some("tick") => {
let t = msg["tick"].as_u64().expect("tick");
if msg.get("base").is_none() {
whole_at.push(t);
}
let lost = match (t == 100, apply_delta(held.as_ref(), &msg)) {
(true, _) => false,
(false, Some(state)) => {
seen.push(state.clone());
held = Some(state);
false
}
(false, None) => {
refused.push(t);
true
}
};
if lost || t == 40 {
diffing.send(&json!({"type": "resync", "game": 7}));
}
diffing.send(&json!({"game": 7, "tick": t, "act": "none"}));
}
Some("end") => return (seen, whole_at, refused),
_ => {}
}
}
panic!("the connection closed before the end");
});
let mut board = crate::sim::generate_arena(7, 2, 12, 9);
crate::sim::set_versus_rules(&mut board, 240);
board.rules_mut().round_length = Some(TICKS);
let (result, _) = play(
&listener,
spec(
board,
vec![Seat::Bot(whole_id), Seat::Bot(diffing_id)],
1000,
),
);
let sent_whole = whole_player.join().expect("whole");
let (held, whole_at, refused) = diffing_player.join().expect("diffing");
assert_eq!(result.replay.inputs.len(), TICKS as usize);
assert_eq!(refused, vec![101], "the diff after the one it dropped");
assert!(whole_at.starts_with(&[0, 41, 102]), "whole at {whole_at:?}");
assert!(
whole_at.contains(&(102 + FULL_EVERY)),
"whole at {whole_at:?}"
);
let mut compared = 0;
for state in &held {
let t = &state["tick"];
let sent = sent_whole
.iter()
.find(|s| &s["tick"] == t)
.expect("both seats are sent every tick");
for (field, value) in sent.as_object().expect("a tick") {
if field == "you" {
continue;
}
let mut a = value.clone();
let mut b = state[field].clone();
for list in [&mut a, &mut b] {
if let Some(entries) = list.as_array_mut() {
entries.sort_by_key(|e| e.to_string());
}
}
assert_eq!(a, b, "{field} at tick {t}");
}
compared += 1;
}
assert_eq!(compared, TICKS as usize - 2, "all but 100 and 101");
assert!(
sent_whole
.iter()
.any(|s| !s["signposts"].as_array().expect("posts").is_empty()),
"posts stood"
);
}