chess-server/src/main.rs

362 lines
9 KiB
Rust

use actix_web::web::{Data, Json};
use actix_web::{App, HttpResponse, HttpServer};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::mpsc::Sender;
use tracing_actix_web::TracingLogger;
use uuid::Uuid;
mod init_tracing;
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: Color,
}
#[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,
}
impl Rank {
fn to_number(&self) -> u8 {
match self {
Self::One => 0,
Self::Two => 1,
Self::Three => 2,
Self::Four => 3,
Self::Five => 4,
Self::Six => 5,
Self::Seven => 6,
Self::Eight => 7,
}
}
fn from_number(value: u8) -> Option<Self> {
match value {
0 => Some(Self::One),
1 => Some(Self::Two),
2 => Some(Self::Three),
3 => Some(Self::Four),
4 => Some(Self::Five),
5 => Some(Self::Six),
6 => Some(Self::Seven),
7 => Some(Self::Eight),
_ => None,
}
}
}
#[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,
}
impl File {
fn to_number(&self) -> u8 {
match self {
Self::A => 0,
Self::B => 1,
Self::C => 2,
Self::D => 3,
Self::E => 4,
Self::F => 5,
Self::G => 6,
Self::H => 7,
}
}
fn from_number(value: u8) -> Option<Self> {
match value {
0 => Some(Self::A),
1 => Some(Self::B),
2 => Some(Self::C),
3 => Some(Self::D),
4 => Some(Self::E),
5 => Some(Self::F),
6 => Some(Self::G),
7 => Some(Self::H),
_ => None,
}
}
}
#[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>) -> HttpResponse {
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>) -> HttpResponse {
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()
}
#[actix_web::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
init_tracing::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(())
}