use actix_web::web::{Data, Json}; use actix_web::{App, HttpResponse, HttpServer, Responder}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::mpsc::Sender; use tracing::subscriber::set_global_default; use tracing_actix_web::TracingLogger; use tracing_log::LogTracer; use tracing_subscriber::filter::Targets; use tracing_subscriber::fmt::format::FmtSpan; use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; use tracing_subscriber::Registry; use uuid::Uuid; const INITIAL_PIECES: [(File, Rank, PieceKind, Color); 32] = [ (File::A, Rank::One, PieceKind::Rook, Color::White), (File::B, Rank::One, PieceKind::Knight, Color::White), (File::C, Rank::One, PieceKind::Bishop, Color::White), (File::D, Rank::One, PieceKind::Queen, Color::White), (File::E, Rank::One, PieceKind::King, Color::White), (File::F, Rank::One, PieceKind::Bishop, Color::White), (File::G, Rank::One, PieceKind::Knight, Color::White), (File::H, Rank::One, PieceKind::Rook, Color::White), (File::A, Rank::Two, PieceKind::Pawn, Color::White), (File::B, Rank::Two, PieceKind::Pawn, Color::White), (File::C, Rank::Two, PieceKind::Pawn, Color::White), (File::D, Rank::Two, PieceKind::Pawn, Color::White), (File::E, Rank::Two, PieceKind::Pawn, Color::White), (File::F, Rank::Two, PieceKind::Pawn, Color::White), (File::G, Rank::Two, PieceKind::Pawn, Color::White), (File::H, Rank::Two, PieceKind::Pawn, Color::White), (File::A, Rank::Seven, PieceKind::Pawn, Color::Black), (File::B, Rank::Seven, PieceKind::Pawn, Color::Black), (File::C, Rank::Seven, PieceKind::Pawn, Color::Black), (File::D, Rank::Seven, PieceKind::Pawn, Color::Black), (File::E, Rank::Seven, PieceKind::Pawn, Color::Black), (File::F, Rank::Seven, PieceKind::Pawn, Color::Black), (File::G, Rank::Seven, PieceKind::Pawn, Color::Black), (File::H, Rank::Seven, PieceKind::Pawn, Color::Black), (File::A, Rank::Eight, PieceKind::Rook, Color::Black), (File::B, Rank::Eight, PieceKind::Knight, Color::Black), (File::C, Rank::Eight, PieceKind::Bishop, Color::Black), (File::D, Rank::Eight, PieceKind::Queen, Color::Black), (File::E, Rank::Eight, PieceKind::King, Color::Black), (File::F, Rank::Eight, PieceKind::Bishop, Color::Black), (File::G, Rank::Eight, PieceKind::Knight, Color::Black), (File::H, Rank::Eight, PieceKind::Rook, Color::Black), ]; #[derive( Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, )] #[serde(transparent)] struct GameId { inner: Uuid, } impl GameId { fn new() -> Self { GameId { inner: Uuid::new_v4(), } } } #[derive(Clone, serde::Deserialize, serde::Serialize)] struct Piece { kind: PieceKind, color: Color, } #[derive(serde::Deserialize, serde::Serialize)] struct Coordinates { file: File, rank: Rank, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] struct Move { piece: Piece, from: Coordinates, to: Coordinates, game_id: GameId, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] struct Start { player_color: String, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] enum Color { Black, White, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] enum PieceKind { Pawn, Rook, Knight, Bishop, Queen, King, } #[derive( Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde_repr::Deserialize_repr, serde_repr::Serialize_repr, )] #[repr(u8)] enum Rank { One = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6, Seven = 7, Eight = 8, } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] enum File { A, B, C, D, E, F, G, H, } #[derive(Clone, Default)] struct FileState { inner: HashMap, } #[derive(Clone, Default)] struct BoardState { inner: HashMap, } impl BoardState { fn starting_positions() -> Self { let mut this = Self::default(); for (file, rank, kind, color) in INITIAL_PIECES { let rank_entry = this.inner.entry(file).or_default(); rank_entry.inner.insert(rank, Piece { kind, color }); } this } fn to_serializable(&self) -> Vec<(File, Rank, PieceKind, Color)> { self.inner .iter() .flat_map(|(file, ranks)| { ranks.inner.iter().map(|(rank, piece)| { ( file.clone(), rank.clone(), piece.kind.clone(), piece.color.clone(), ) }) }) .collect() } } struct Reply { board_state: BoardState, } struct MoveSession { action: Move, reply: tokio::sync::oneshot::Sender, } #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct GameStart { board: Vec<(File, Rank, PieceKind, Color)>, game_id: GameId, } #[derive(Clone, Debug)] struct GameData { sender: Sender, } #[derive(Clone, Default)] struct GameState { inner: Arc>>, } impl GameState { fn data_for_id(&self, game_id: &GameId) -> Option { self.inner.lock().unwrap().get(&game_id).cloned() } } struct GameDropper(GameState, GameId); impl Drop for GameDropper { fn drop(&mut self) { self.0.inner.lock().unwrap().remove(&self.1); } } async fn start(start: Json, game_state: Data) -> impl Responder { let game_id = GameId::new(); let (tx, mut rx) = tokio::sync::mpsc::channel(1); let initial_board_state = BoardState::starting_positions(); let board_state = initial_board_state.clone(); let game_dropper = GameDropper((**game_state).clone(), game_id.clone()); let game_data = GameData { sender: tx }; game_state .inner .lock() .unwrap() .insert(game_id.clone(), game_data); actix_web::rt::spawn(async move { while let Ok(Some(msg)) = actix_web::rt::time::timeout(Duration::from_secs(60 * 5), rx.recv()).await { /* do stuff */ let _ = msg.reply.send(Reply { board_state: board_state.clone(), }); } drop(game_dropper); }); HttpResponse::Ok().json(GameStart { board: initial_board_state.to_serializable(), game_id, }) } async fn make_move(action: Json, game_state: Data) -> impl Responder { if let Some(data) = game_state.data_for_id(&action.0.game_id) { let (tx, rx) = tokio::sync::oneshot::channel(); let res = data .sender .send(MoveSession { action: action.into_inner(), reply: tx, }) .await; if res.is_err() { return HttpResponse::InternalServerError().finish(); } if let Ok(reply) = rx.await { return HttpResponse::Ok().json(reply.board_state.to_serializable()); } return HttpResponse::InternalServerError().finish(); } HttpResponse::BadRequest().finish() } fn init_tracing() -> Result<(), Box> { LogTracer::init()?; let targets: Targets = std::env::var("RUST_LOG") .unwrap_or_else(|_| "info".into()) .parse()?; let fmt_layer = tracing_subscriber::fmt::layer() .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) .pretty(); let subscriber = Registry::default().with(targets).with(fmt_layer); set_global_default(subscriber)?; Ok(()) } #[actix_web::main] async fn main() -> Result<(), Box> { init_tracing()?; HttpServer::new(|| { App::new() .app_data(Data::new(GameState::default())) .wrap(TracingLogger::default()) .route("/start", actix_web::web::post().to(start)) .route("/move", actix_web::web::post().to(make_move)) }) .bind("0.0.0.0:8000")? .run() .await?; Ok(()) }