use super::listener::{BotId, GameMsg};
use super::protocol::{Act, Outcome, Reply};
use crate::sim::{Board, PlayerAction, PlayerId, Refusal};
use std::collections::VecDeque;
use std::io::Write;
use std::time::{Duration, Instant};
const GIVE_UP: Duration = Duration::from_millis(250);
const RECENT: usize = 64;
#[derive(Clone, Debug, Default)]
pub struct SeatResult {
pub score: u32,
pub placing: u32,
pub banked: u32,
pub raided: u32,
pub rejected: u32,
pub sent: u32,
pub late: u32,
pub missed: u32,
pub reply_ms: Vec<f64>,
pub forfeit: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Turn {
Send,
Busy,
Skip,
}
pub struct Journal<'a> {
pub log: &'a mut dyn Write,
pub console: &'a mut dyn FnMut(String),
pub echo_notes: bool,
pub game: u32,
}
pub struct BotDriver {
pub id: BotId,
pub seat: PlayerId,
pub name: String,
pub connected: bool,
pub ready: bool,
pub forfeit: bool,
pub waiting_since: Instant,
pub rehello: bool,
deadline: Duration,
in_hand: Option<(Reply, Instant)>,
acted: Option<u64>,
awake_at: u64,
sent: VecDeque<(u64, Instant)>,
answered: VecDeque<u64>,
answered_count: u32,
outstanding: Option<u64>,
pub result: SeatResult,
}
impl BotDriver {
pub fn new(
id: BotId,
seat: PlayerId,
name: String,
connected: bool,
deadline: Duration,
) -> BotDriver {
BotDriver {
id,
seat,
name,
connected,
ready: false,
forfeit: false,
waiting_since: Instant::now(),
rehello: false,
deadline,
in_hand: None,
acted: None,
awake_at: 0,
sent: VecDeque::new(),
answered: VecDeque::new(),
answered_count: 0,
outstanding: None,
result: SeatResult::default(),
}
}
pub fn playing(&self) -> bool {
self.connected && self.ready && !self.forfeit
}
pub fn turn(&mut self, t: u64, now: Instant) -> Turn {
if !self.playing() {
return Turn::Skip;
}
if let Some(waiting) = self.outstanding
&& self
.sent
.iter()
.find(|(tick, _)| *tick == waiting)
.is_none_or(|(_, at)| now.duration_since(*at) > (self.deadline * 8).max(GIVE_UP))
{
self.outstanding = None;
}
if self.outstanding.is_some() {
Turn::Busy
} else if t < self.awake_at {
Turn::Skip
} else {
Turn::Send
}
}
pub fn sent(&mut self, t: u64, at: Instant) {
self.outstanding = Some(t);
self.sent.push_back((t, at));
if self.sent.len() > RECENT {
self.sent.pop_front();
}
self.result.sent += 1;
}
pub fn take(&mut self, msg: GameMsg, t: u64, journal: &mut Journal) -> bool {
let (game, seat, name) = (journal.game, usize::from(self.seat) + 1, &self.name);
match msg {
GameMsg::Reply { reply, at, .. } => self.reply(reply, at, t, journal),
GameMsg::Ready { .. } => {
self.ready = !self.forfeit;
false
}
GameMsg::Garbled { why, .. } => {
let _ = writeln!(
journal.log,
"g{game} t{t} P{seat} {name}: garbled, read as none: {why}"
);
self.outstanding.take().is_some()
}
GameMsg::Dropped { .. } => {
self.connected = false;
(journal.console)(format!(" {name} dropped at tick {t}; its seat idles"));
let _ = writeln!(
journal.log,
"g{game} t{t} P{seat} {name}: connection dropped"
);
self.outstanding.take().is_some()
}
GameMsg::Back { .. } => {
self.connected = true;
self.rehello = true;
self.ready = !self.forfeit;
self.awake_at = 0;
self.outstanding = None;
(journal.console)(format!(" {name} is back at tick {t}"));
let _ = writeln!(journal.log, "g{game} t{t} P{seat} {name}: reconnected");
false
}
}
}
fn reply(&mut self, reply: Reply, at: Instant, t: u64, journal: &mut Journal) -> bool {
if !self.playing() {
return false;
}
if reply.tick > t {
let _ = writeln!(
journal.log,
"g{} t{t} P{} {}: a reply for tick {}, not sent yet, dropped",
journal.game,
usize::from(self.seat) + 1,
self.name,
reply.tick
);
return false;
}
let unblocked = self
.outstanding
.is_some_and(|waiting| reply.tick >= waiting);
if unblocked {
self.outstanding = None;
}
if let Some(&(_, sent)) = self.sent.iter().find(|(tick, _)| *tick == reply.tick)
&& !self.answered.contains(&reply.tick)
{
self.answered.push_back(reply.tick);
self.answered_count += 1;
if self.answered.len() > RECENT {
self.answered.pop_front();
}
let took = at.duration_since(sent);
self.result.reply_ms.push(took.as_secs_f64() * 1000.0);
if took > self.deadline {
self.result.late += 1;
}
}
let newer_than_held = self
.in_hand
.as_ref()
.is_none_or(|(held, _)| held.tick <= reply.tick);
let newer_than_acted = self.acted.is_none_or(|acted| acted < reply.tick);
if newer_than_held && newer_than_acted {
self.awake_at = reply.tick + 1 + u64::from(reply.wait);
self.in_hand = Some((reply, at));
}
unblocked
}
pub fn order(&mut self, journal: &mut Journal) -> Act {
let Some((reply, at)) = self.in_hand.take() else {
return Act::None;
};
self.acted = Some(reply.tick);
self.log(&reply, at, journal);
reply.act
}
fn log(&self, reply: &Reply, at: Instant, journal: &mut Journal) {
if journal.echo_notes
&& let Some(note) = &reply.note
{
(journal.console)(format!(
" t{} {}: {} - {note}",
reply.tick,
self.name,
reply.act.describe()
));
}
if reply.act == Act::None
&& reply.note.is_none()
&& reply.garbled.is_none()
&& reply.wait == 0
{
return;
}
let late = self
.sent
.iter()
.find(|(tick, _)| *tick == reply.tick)
.is_some_and(|(_, sent)| at.duration_since(*sent) > self.deadline);
let mut line = format!(
"g{} t{} P{} {}: {}",
journal.game,
reply.tick,
usize::from(self.seat) + 1,
self.name,
reply.act.describe()
);
if reply.wait > 0 {
line.push_str(&format!(" wait {}", reply.wait));
}
if late {
line.push_str(" [late]");
}
if let Some(why) = &reply.garbled {
line.push_str(&format!(" [garbled: {why}]"));
}
if let Some(note) = &reply.note {
line.push_str(&format!(" note: {}", note.replace('\n', " / ")));
}
let _ = writeln!(journal.log, "{line}");
}
pub fn close(&mut self) {
self.result.missed = self.result.sent.saturating_sub(self.answered_count);
self.result.forfeit = self.forfeit;
}
}
pub fn refusal(board: &Board, seat: PlayerId, action: PlayerAction) -> Option<Refusal> {
match action {
PlayerAction::Place { x, y, .. } => board.placement_refusal(seat, x, y),
PlayerAction::Remove { x, y } => board.removal_refusal(seat, x, y),
PlayerAction::None | PlayerAction::CallEvent(_) => None,
}
}
pub fn outcome(
act: Act,
action: PlayerAction,
refused: Option<Refusal>,
after: &Board,
seat: PlayerId,
tick: u64,
) -> Option<Outcome> {
if matches!(act, Act::None | Act::Move { .. }) {
return None;
}
if let PlayerAction::Place { x, y, .. } = action
&& after
.signpost_at(x, y)
.is_some_and(|post| post.owner == seat && post.placed == tick)
{
return Some(Outcome::Accepted);
}
Some(match (refused, action) {
(Some(why), _) => Outcome::Refused(why),
(None, PlayerAction::None) => Outcome::Refused(Refusal::NoPost),
(None, PlayerAction::Place { x, y, .. }) => match after.signpost_at(x, y) {
Some(post) if post.owner != seat && post.placed == tick => {
Outcome::Refused(Refusal::RivalPost)
}
_ => Outcome::Accepted,
},
(None, PlayerAction::Remove { .. } | PlayerAction::CallEvent(_)) => Outcome::Accepted,
})
}
pub fn act_of(action: PlayerAction) -> Option<Act> {
match action {
PlayerAction::None | PlayerAction::CallEvent(_) => None,
PlayerAction::Place { x, y, dir } => Some(Act::Place { x, y, dir }),
PlayerAction::Remove { x, y } => Some(Act::Remove { x, y }),
}
}
pub fn placings(scores: &[u32], forfeits: &[bool]) -> Vec<u32> {
let playing: Vec<u32> = scores
.iter()
.zip(forfeits)
.filter(|(_, f)| !**f)
.map(|(s, _)| *s)
.collect();
scores
.iter()
.zip(forfeits)
.map(|(&score, &forfeit)| {
if forfeit {
playing.len() as u32 + 1
} else {
1 + playing.iter().filter(|&&other| other > score).count() as u32
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ties_share_a_place_and_a_forfeit_comes_last() {
assert_eq!(placings(&[12, 3, 20, 7], &[false; 4]), vec![2, 4, 1, 3]);
assert_eq!(placings(&[5, 5, 1], &[false; 3]), vec![1, 1, 3]);
assert_eq!(placings(&[50, 3, 20], &[true, false, false]), vec![3, 2, 1]);
}
fn reply(tick: u64, act: Act) -> GameMsg {
GameMsg::Reply {
seat: 0,
reply: Reply {
game: 1,
tick,
act,
wait: 0,
note: None,
garbled: None,
},
at: Instant::now(),
}
}
#[test]
fn the_newest_reply_wins_and_a_busy_bot_is_not_sent_more() {
let mut log = Vec::new();
let mut console = |_: String| {};
let mut journal = Journal {
log: &mut log,
console: &mut console,
echo_notes: false,
game: 1,
};
let mut seat = BotDriver::new(0, 0, "b".into(), true, Duration::from_millis(33));
seat.ready = true;
let now = Instant::now();
assert_eq!(seat.turn(0, now), Turn::Send);
seat.sent(0, now);
assert_eq!(seat.turn(1, now), Turn::Busy, "still answering tick 0");
let place = Act::Place {
x: 1,
y: 1,
dir: crate::sim::Direction::Up,
};
assert!(seat.take(reply(0, place), 1, &mut journal));
assert_eq!(seat.turn(1, now), Turn::Send);
seat.sent(1, now);
assert!(seat.take(reply(1, Act::None), 1, &mut journal));
assert!(!seat.take(reply(1, Act::Clear), 1, &mut journal));
assert!(!seat.take(reply(0, Act::None), 1, &mut journal));
assert_eq!(
seat.order(&mut journal),
Act::Clear,
"the bot thought again"
);
assert_eq!(seat.order(&mut journal), Act::None, "taken once");
assert!(!seat.take(reply(1, Act::Clear), 2, &mut journal));
assert!(!seat.take(reply(0, place), 2, &mut journal));
assert_eq!(seat.order(&mut journal), Act::None, "nothing acts twice");
assert!(!seat.take(reply(9, place), 2, &mut journal));
assert_eq!(seat.order(&mut journal), Act::None);
}
fn quiet() -> (Vec<u8>, impl FnMut(String)) {
(Vec::new(), |_: String| {})
}
#[test]
fn a_wait_read_between_ticks_skips_the_next_one() {
let (mut log, mut console) = quiet();
let mut journal = Journal {
log: &mut log,
console: &mut console,
echo_notes: false,
game: 1,
};
let mut seat = BotDriver::new(0, 0, "b".into(), true, Duration::from_millis(33));
seat.ready = true;
let now = Instant::now();
seat.sent(0, now);
let GameMsg::Reply {
mut reply, seat: s, ..
} = reply(0, Act::None)
else {
unreachable!()
};
reply.wait = 5;
seat.take(
GameMsg::Reply {
seat: s,
reply,
at: now,
},
1,
&mut journal,
);
assert_eq!(seat.turn(1, now), Turn::Skip, "it asked to sleep");
assert_eq!(seat.turn(6, now), Turn::Send, "and wakes when it said");
}
#[test]
fn a_forfeited_seat_does_nothing_its_bot_says() {
let (mut log, mut console) = quiet();
let mut journal = Journal {
log: &mut log,
console: &mut console,
echo_notes: false,
game: 1,
};
let mut seat = BotDriver::new(0, 0, "b".into(), true, Duration::from_millis(33));
seat.forfeit = true;
let place = Act::Place {
x: 1,
y: 1,
dir: crate::sim::Direction::Up,
};
seat.take(reply(0, place), 3, &mut journal);
assert_eq!(seat.order(&mut journal), Act::None);
}
#[test]
fn a_tile_freed_earlier_in_the_tick_is_an_accepted_placement() {
use crate::sim::{Direction, TileKind};
let mut board = Board::new(6, 3, 1);
board.set_tile(0, 1, TileKind::Castle(0));
board.set_tile(5, 1, TileKind::Castle(1));
board.set_signpost_rule(3, crate::sim::CapPolicy::Evict);
assert!(board.place_signpost(0, 2, 1, Direction::Up));
let mut actions = [PlayerAction::None; crate::sim::MAX_PLAYERS];
actions[0] = PlayerAction::Remove { x: 2, y: 1 };
actions[1] = PlayerAction::Place {
x: 2,
y: 1,
dir: Direction::Down,
};
let tick = board.ticks();
let refused = refusal(&board, 1, actions[1]);
assert_eq!(
refused,
Some(Refusal::RivalPost),
"before the tick, it is taken"
);
board.tick(&actions);
let post = board.signpost_at(2, 1).expect("seat 1's post");
assert_eq!(post.owner, 1, "seat 0 went first and let it go");
let act = Act::Place {
x: 2,
y: 1,
dir: Direction::Down,
};
assert_eq!(
outcome(act, actions[1], refused, &board, 1, tick),
Some(Outcome::Accepted)
);
}
}