go-fish-game-server 0.3.0

A WebSocket game server for the Go Fish card game, supporting human players and bots.
Documentation
pub mod connection;
pub mod lobby;

use serde::Deserialize;
use std::net::SocketAddr;
use tokio::sync::mpsc;
use tracing::info;

pub use connection::{
    ClientEvent, ClientHandle, ConnectionManager, DisconnectReason,
    LobbyEvent, ManagerCommand, ServerMessage,
};
pub use lobby::{
    ClientPhase, Lobby, LobbyCommand, LobbyManager, PlayerRecord,
};

#[derive(Debug, Deserialize)]
pub struct SimpleBotConfig {
    pub memory_limit: u8,
    pub error_margin: f32,
}

#[derive(Debug, Deserialize)]
pub struct BotConfig {
    pub thinking_time_min_ms: u64,
    pub thinking_time_max_ms: u64,
    pub simple_bot: SimpleBotConfig,
}

#[derive(Debug, Deserialize)]
pub struct Config {
    pub address: SocketAddr,
    pub lobby_max_players: usize,
    pub max_client_connections: usize,
    pub bots: Option<BotConfig>,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            address: "127.0.0.1:9001".parse().unwrap(),
            lobby_max_players: 4,
            max_client_connections: 10,
            bots: None,
        }
    }
}

pub async fn run(config: Config) -> Result<(), anyhow::Error> {
    let (lobby_event_tx, lobby_event_rx) = mpsc::channel::<LobbyEvent>(64);
    let (lobby_cmd_tx, lobby_cmd_rx) = mpsc::channel::<LobbyCommand>(8);

    let manager = ConnectionManager::new(lobby_event_tx.clone(), config.max_client_connections);
    let event_tx = manager.event_tx();
    let command_tx = manager.command_tx();

    let bot_config = config.bots.unwrap_or(BotConfig {
        thinking_time_min_ms: 2000,
        thinking_time_max_ms: 4500,
        simple_bot: SimpleBotConfig { memory_limit: 3, error_margin: 0.2 },
    });

    let lobby_manager = LobbyManager::new(
        lobby_event_rx,
        lobby_cmd_rx,
        config.lobby_max_players,
        lobby_event_tx.clone(),
        bot_config,
    );

    let (listener_cmd_tx, listener_cmd_rx) = mpsc::channel::<ManagerCommand>(1);
    tokio::spawn(connection::run_tcp_listener(config.address, event_tx, listener_cmd_rx));
    tokio::spawn(lobby_manager.run());

    let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;

    async fn shutdown(
        listener_cmd_tx: mpsc::Sender<ManagerCommand>,
        command_tx: mpsc::Sender<ManagerCommand>,
        lobby_cmd_tx: mpsc::Sender<LobbyCommand>,
    ) {
        let _ = listener_cmd_tx.send(ManagerCommand::Shutdown).await;
        let _ = command_tx.send(ManagerCommand::Shutdown).await;
        let _ = lobby_cmd_tx.send(LobbyCommand::Shutdown).await;
    }

    tokio::select! {
        _ = manager.run() => {}
        _ = tokio::signal::ctrl_c() => {
            info!(event = "shutdown", signal = "SIGINT");
            shutdown(listener_cmd_tx, command_tx, lobby_cmd_tx).await;
        }
        _ = sigterm.recv() => {
            info!(event = "shutdown", signal = "SIGTERM");
            shutdown(listener_cmd_tx, command_tx, lobby_cmd_tx).await;
        }
    }

    Ok(())
}