mod card;
mod doorway;
mod round;
#[cfg(test)]
pub(crate) mod test_bots;
pub use card::{door_card, door_input};
pub use doorway::{BotLine, Doorway};
use round::PartyTable;
pub use round::{BotRound, Committed, News, STAND_IN};
use crate::app::i18n::fill;
use crate::app::{Controllers, SeatController, SeatKinds, SeatNames, Sim};
use crate::bots::listener::BotId;
use crate::bots::protocol::Act;
use crate::sim::{Board, MAX_PLAYERS, PlayerAction, Replay};
use bevy::prelude::*;
use std::net::SocketAddr;
#[derive(Resource, Default)]
pub struct BotSeats {
pub door: Option<Doorway>,
round: Option<BotRound>,
pub news: Vec<(u8, News)>,
pub door_for: DoorFor,
pub feedback: String,
games: u32,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DoorFor {
#[default]
Nothing,
Couch,
Join(SocketAddr),
Joined(SocketAddr),
}
impl DoorFor {
pub fn card_up(self) -> bool {
match self {
DoorFor::Couch | DoorFor::Join(_) => true,
DoorFor::Nothing | DoorFor::Joined(_) => false,
}
}
}
impl BotSeats {
pub fn leave_the_beach(&mut self, at_the_beach: bool) {
if let DoorFor::Joined(_) = self.door_for
&& !at_the_beach
{
self.door = None;
self.door_for = DoorFor::Nothing;
}
}
pub fn online_seats(&self) -> Vec<u8> {
self.round
.as_ref()
.map_or_else(Vec::new, BotRound::online_seats)
}
pub fn commit_online(&mut self, board: &Board) -> Vec<(u8, PlayerAction, Option<Act>)> {
let BotSeats { round, news, .. } = self;
match round {
Some(round) => round.commit_online(board, news),
None => Vec::new(),
}
}
pub fn committed(&mut self, frame: u32, seat: u8, act: Option<Act>, action: PlayerAction) {
if let Some(round) = self.round.as_mut() {
round.committed(frame, seat, act, action);
}
}
pub fn before_frame(&mut self, frame: u32, board: &Board) {
if let Some(round) = self.round.as_mut() {
round.before_frame(frame, board);
}
}
pub fn after_frame(&mut self, frame: u32, board: &Board) {
if let Some(round) = self.round.as_mut() {
round.after_frame(frame, board);
}
}
pub fn send_online(&mut self, board: &Board, replay: Option<&Replay>) {
let BotSeats { door, round, .. } = self;
if let Some(round) = round.as_mut() {
round.send_next(board);
if board.round_over() {
round.finish(board, door.as_ref(), replay);
}
}
}
pub fn commit(
&mut self,
board: &Board,
actions: &mut [PlayerAction; MAX_PLAYERS],
) -> Option<Vec<Committed>> {
let BotSeats { round, news, .. } = self;
Some(round.as_mut()?.commit(board, actions, news))
}
pub fn after(
&mut self,
board: &Board,
committed: Option<Vec<Committed>>,
replay: Option<&Replay>,
) {
let BotSeats { door, round, .. } = self;
let (Some(round), Some(committed)) = (round.as_mut(), committed) else {
return;
};
round.after(board, committed);
if board.round_over() {
round.finish(board, door.as_ref(), replay);
}
}
}
impl BotSeats {
pub fn round_mut(&mut self) -> Option<&mut BotRound> {
self.round.as_mut()
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Drivers<'w> {
pub controllers: Res<'w, Controllers>,
pub bots: ResMut<'w, BotSeats>,
}
impl Drivers<'_> {
pub fn commit(
&mut self,
board: &Board,
actions: &mut [PlayerAction; MAX_PLAYERS],
) -> Option<Vec<Committed>> {
self.bots.commit(board, actions)
}
pub fn after(
&mut self,
board: &Board,
committed: Option<Vec<Committed>>,
replay: Option<&Replay>,
) {
self.bots.after(board, committed, replay);
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct RoundTable<'w> {
controllers: Res<'w, Controllers>,
scoring: crate::app::teams::Scoring<'w>,
names: ResMut<'w, SeatNames>,
kinds: Res<'w, SeatKinds>,
}
enum BotRoute {
Couch(Vec<u8>),
JoinedAsBot(u8),
Hosted(Vec<(u8, BotId)>),
}
impl BotRoute {
fn seats(&self) -> Vec<u8> {
match self {
BotRoute::Couch(seats) => seats.clone(),
BotRoute::JoinedAsBot(seat) => vec![*seat],
BotRoute::Hosted(hosted) => hosted.iter().map(|(seat, _)| *seat).collect(),
}
}
fn bot_for(&self, door: &Doorway, seat: u8) -> Option<BotId> {
match self {
BotRoute::Couch(_) => door
.chairs
.iter()
.find(|c| c.seat == seat)
.and_then(|c| c.bot),
BotRoute::JoinedAsBot(_) => door.chairs.first().and_then(|c| c.bot),
BotRoute::Hosted(hosted) => hosted.iter().find(|(s, _)| *s == seat).map(|&(_, id)| id),
}
}
fn online(&self) -> bool {
match self {
BotRoute::Couch(_) => false,
BotRoute::JoinedAsBot(_) | BotRoute::Hosted(_) => true,
}
}
}
pub fn begin_round(
table: RoundTable,
sim: Res<Sim>,
mut bots: ResMut<BotSeats>,
mut recorder: ResMut<crate::app::Recorder>,
) {
let RoundTable {
controllers,
scoring,
mut names,
kinds,
} = table;
let online = &scoring.online;
bots.round = None;
bots.news.clear();
let route = match online.0.as_ref() {
Some(session) => match session.session.seat().filter(|_| session.bot.is_some()) {
Some(seat) => BotRoute::JoinedAsBot(seat),
None => BotRoute::Hosted(
session
.bots_here
.iter()
.map(|bot| (bot.seat, bot.id))
.collect(),
),
},
None => BotRoute::Couch(
(0..MAX_PLAYERS as u8)
.filter(|&seat| controllers.0[usize::from(seat)] == SeatController::Bot)
.collect(),
),
};
if let BotRoute::Hosted(hosted) = &route
&& online.0.as_ref().is_some_and(|session| session.is_host())
&& let Some(door) = &mut bots.door
{
let seated: Vec<BotId> = hosted.iter().map(|(_, id)| *id).collect();
door.seat_the_table(&seated);
}
let seats = route.seats();
if seats.is_empty() {
return;
}
bots.games += 1;
let game = bots.games;
let Some(door) = &bots.door else {
return;
};
let tr = scoring.local.settings.tr();
if let BotRoute::Couch(_) = route {
for &seat in &seats {
if let Some(name) = door.seat_name(tr, seat) {
names.0[usize::from(seat)] = name;
}
}
if let Some(replay) = &mut recorder.0 {
replay.names = names.0.clone();
}
}
let board = &sim.0;
let n = usize::from(board.seats_in_play()).max(2);
let held: Vec<(u8, BotId)> = seats
.iter()
.filter_map(|&seat| Some((seat, route.bot_for(door, seat)?)))
.collect();
let round = BotRound::open(
door,
board,
&held,
PartyTable::new(
(0..n).map(|seat| names.label(tr, seat as u8)).collect(),
kinds.0[..n].to_vec(),
scoring.mode(n as u8),
),
route.online().then(|| {
online
.0
.as_ref()
.map_or(crate::sim::DEFAULT_DELAY, |session| session.session.delay())
}),
game,
);
bots.round = Some(round);
}
pub fn end_round(mut bots: ResMut<BotSeats>) {
bots.round = None;
}
pub fn close_door(mut bots: ResMut<BotSeats>) {
*bots = BotSeats::default();
}
pub fn announce(
mut bots: ResMut<BotSeats>,
names: Res<SeatNames>,
settings: Res<crate::app::settings::GameSettings>,
mut log: ResMut<crate::app::side_panels::EventLog>,
) {
if bots.news.is_empty() {
return;
}
let tr = settings.tr();
for (seat, news) in std::mem::take(&mut bots.news) {
let who = names.label(tr, seat);
let line = match news {
News::StandIn => fill(tr.bot_stand_in, &[("p", &who)]),
News::Back => fill(tr.bot_back, &[("p", &who)]),
};
log.push(line, crate::app::palette::seat_text(seat));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn a_failed_join_as_a_bot_closes_its_doorway() {
let beach = SocketAddr::from(([127, 0, 0, 1], 1));
let mut bots = BotSeats {
door: Some(Doorway::open().expect("a port on this machine")),
door_for: DoorFor::Joined(beach),
..BotSeats::default()
};
let here = bots.door.as_ref().expect("open").here();
bots.leave_the_beach(true);
assert!(bots.door.is_some());
bots.door_for = DoorFor::Join(beach);
bots.leave_the_beach(false);
assert!(bots.door.is_some(), "the card is still up");
bots.door_for = DoorFor::Joined(beach);
bots.leave_the_beach(false);
assert!(bots.door.is_none(), "the doorway closed");
assert_eq!(bots.door_for, DoorFor::Nothing);
let mut refused = false;
for _ in 0..100 {
if std::net::TcpStream::connect_timeout(&here, Duration::from_millis(50)).is_err() {
refused = true;
break;
}
std::thread::sleep(Duration::from_millis(5));
}
assert!(refused, "nobody listens on {here} any more");
let mut host = BotSeats {
door: Some(Doorway::open_host_here().expect("a port on this machine")),
..BotSeats::default()
};
host.leave_the_beach(false);
assert!(host.door.is_some());
}
}