326 lines
8.3 KiB
Rust
326 lines
8.3 KiB
Rust
|
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<Rank, Piece>,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Default)]
|
||
|
struct BoardState {
|
||
|
inner: HashMap<File, FileState>,
|
||
|
}
|
||
|
|
||
|
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<Reply>,
|
||
|
}
|
||
|
|
||
|
#[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<MoveSession>,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Default)]
|
||
|
struct GameState {
|
||
|
inner: Arc<Mutex<HashMap<GameId, GameData>>>,
|
||
|
}
|
||
|
|
||
|
impl GameState {
|
||
|
fn data_for_id(&self, game_id: &GameId) -> Option<GameData> {
|
||
|
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<Start>, game_state: Data<GameState>) -> 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<Move>, game_state: Data<GameState>) -> 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<dyn std::error::Error>> {
|
||
|
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<dyn std::error::Error>> {
|
||
|
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(())
|
||
|
}
|