Compare commits

...

22 commits

Author SHA1 Message Date
Aode (lion) 3827930653 Add a whitespace 2021-12-31 18:34:03 -06:00
Aode (lion) 1456bc530c Remove en_passant validate argument 2021-12-31 18:23:27 -06:00
Aode (lion) 8e6b7ac3c5 Remove unneeded borrow 2021-12-31 18:19:08 -06:00
Aode (lion) 2584113e24 Derive Copy for small types 2021-12-31 18:18:05 -06:00
Aode (lion) 3291f9691b Clarify boolean operation order 2021-12-31 18:14:47 -06:00
Aode (lion) 421381a7ec Simplify en_passant logic, simplify SideEffects 2021-12-31 18:07:20 -06:00
Aode (lion) df4da80905 Rename color_state to castle_state 2021-12-31 17:44:50 -06:00
Aode (lion) b7f5d55059 Rename ColorState to CastleState 2021-12-31 17:43:57 -06:00
Aode (lion) 82504a9c47 Fix en_passant cell choice 2021-12-31 17:27:49 -06:00
Aode (lion) 5064bf6491 Return 'Option<SideEffects>' rather than tuple 2021-12-31 17:23:57 -06:00
Aode (lion) 9edefa920d Split movement side effects from validity checking 2021-12-31 17:11:04 -06:00
Aode (lion) 4a2a4c81c5 Add 'in check' logic 2021-12-31 16:51:12 -06:00
Aode (lion) 99f826ef32 Clean en passant after non-useful turn 2021-12-31 16:12:45 -06:00
Aode (lion) 0d85ce74c8 Fix en passant for black 2021-12-31 15:48:44 -06:00
Aode (lion) bf5580149d Check en passant, validate black pawns 2021-12-31 15:42:39 -06:00
Aode (lion) e24c2ea39f Validate white pawns 2021-12-31 15:11:05 -06:00
Aode (lion) 76f530b91e Group common imports 2021-12-31 14:43:35 -06:00
Aode (lion) 80fcc059f3 simplify game start 2021-12-31 14:37:56 -06:00
Aode (lion) cef5898c44 Move BoardState into own file 2021-12-31 14:21:26 -06:00
Aode (lion) 674f677846 Move JSON types to own module 2021-12-31 14:15:52 -06:00
Aode (lion) 2c3b3510c3 Make illegal moves 2021-12-30 23:54:47 -06:00
Aode (lion) 0175f2d933 Chess server uwu 2021-12-30 23:21:43 -06:00
6 changed files with 587 additions and 283 deletions

17
Cargo.lock generated
View file

@ -19,6 +19,22 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "actix-cors"
version = "0.6.0-beta.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4f1bd0e31c745df129f0e94efd374d21f2a455bcc386c15d78ed9a9e7d4dd50"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"derive_more",
"futures-util",
"log",
"once_cell",
"smallvec",
]
[[package]]
name = "actix-http"
version = "3.0.0-beta.17"
@ -258,6 +274,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "chess"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-web",
"serde",
"serde_repr",

View file

@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-cors = "0.6.0-beta.8"
actix-web = { version = "4.0.0-beta.18", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_repr = "0.1"

270
src/api_types.rs Normal file
View file

@ -0,0 +1,270 @@
//! Everything in this file is part of the public JSON api
//!
//! Adding methods on types is fine, but modifying structure breaks compatibility
use uuid::Uuid;
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
#[serde(transparent)]
pub(crate) struct GameId {
inner: Uuid,
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
pub(crate) struct Piece {
pub(crate) kind: PieceKind,
pub(crate) color: Color,
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
pub(crate) struct Coordinates {
pub(crate) file: File,
pub(crate) rank: Rank,
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Move {
pub(crate) piece: Piece,
pub(crate) from: Coordinates,
pub(crate) to: Coordinates,
pub(crate) game_id: GameId,
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Start {
pub(crate) player_color: Color,
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GameStart {
pub(crate) board: Vec<(File, Rank, PieceKind, Color)>,
pub(crate) game_id: GameId,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "camelCase")]
pub(crate) enum Color {
Black,
White,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "camelCase")]
pub(crate) enum PieceKind {
Pawn,
Rook,
Knight,
Bishop,
Queen,
King,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde_repr::Deserialize_repr,
serde_repr::Serialize_repr,
)]
#[repr(u8)]
pub(crate) enum Rank {
One = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
#[serde(rename_all = "camelCase")]
pub(crate) enum File {
A,
B,
C,
D,
E,
F,
G,
H,
}
impl GameId {
pub(crate) fn new() -> Self {
GameId {
inner: Uuid::new_v4(),
}
}
}
impl Color {
pub(crate) fn is_white(self) -> bool {
matches!(self, Self::White)
}
pub(crate) fn is_black(self) -> bool {
matches!(self, Self::Black)
}
}
impl Rank {
pub(crate) fn next(self) -> Option<Self> {
match self {
Self::One => Some(Self::Two),
Self::Two => Some(Self::Three),
Self::Three => Some(Self::Four),
Self::Four => Some(Self::Five),
Self::Five => Some(Self::Six),
Self::Six => Some(Self::Seven),
Self::Seven => Some(Self::Eight),
Self::Eight => None,
}
}
pub(crate) fn prev(self) -> Option<Self> {
match self {
Self::One => None,
Self::Two => Some(Self::One),
Self::Three => Some(Self::Two),
Self::Four => Some(Self::Three),
Self::Five => Some(Self::Four),
Self::Six => Some(Self::Five),
Self::Seven => Some(Self::Six),
Self::Eight => Some(Self::Seven),
}
}
pub(crate) fn diff(self, other: Self) -> Option<u8> {
if self > other {
Some(self.to_number() - other.to_number())
} else {
None
}
}
pub(crate) fn absolute_diff(self, other: Self) -> u8 {
self.diff(other).or_else(|| other.diff(self)).unwrap_or(0)
}
pub(crate) 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,
}
}
pub(crate) 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,
}
}
}
impl File {
pub(crate) fn next(self) -> Option<Self> {
match self {
Self::A => Some(Self::B),
Self::B => Some(Self::C),
Self::C => Some(Self::D),
Self::D => Some(Self::E),
Self::E => Some(Self::F),
Self::F => Some(Self::G),
Self::G => Some(Self::H),
Self::H => None,
}
}
pub(crate) fn prev(self) -> Option<Self> {
match self {
Self::A => None,
Self::B => Some(Self::A),
Self::C => Some(Self::B),
Self::D => Some(Self::C),
Self::E => Some(Self::D),
Self::F => Some(Self::E),
Self::G => Some(Self::F),
Self::H => Some(Self::G),
}
}
pub(crate) fn diff(self, other: Self) -> Option<u8> {
if self > other {
Some(self.to_number() - other.to_number())
} else {
None
}
}
pub(crate) fn absolute_diff(self, other: Self) -> u8 {
self.diff(other).or_else(|| other.diff(self)).unwrap_or(0)
}
pub(crate) 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,
}
}
pub(crate) 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,
}
}
}
impl std::fmt::Display for GameId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.inner, f)
}
}

236
src/board_state.rs Normal file
View file

@ -0,0 +1,236 @@
use crate::api_types::{Color, Coordinates, File, Move, Piece, PieceKind, Rank};
use std::collections::HashMap;
#[derive(Clone, Debug)]
struct CastleState {
can_king_castle: bool,
can_queen_castle: bool,
}
#[derive(Default)]
struct SideEffects {
en_passant: Option<Coordinates>,
king_castle_invalidated: bool,
queen_castle_invalidated: bool,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct BoardState {
inner: HashMap<Coordinates, Piece>,
white_state: CastleState,
black_state: CastleState,
en_passant: Option<Coordinates>,
}
impl BoardState {
pub(crate) fn starting_positions() -> Self {
let mut this = Self::default();
for (file, rank, kind, color) in INITIAL_PIECES {
this.inner
.insert(Coordinates { file, rank }, Piece { kind, color });
}
this
}
pub(crate) fn to_serializable(&self) -> Vec<(File, Rank, PieceKind, Color)> {
self.inner
.iter()
.map(|(coordinates, piece)| {
(coordinates.file, coordinates.rank, piece.kind, piece.color)
})
.collect()
}
pub(crate) fn handle(&mut self, action: Move) {
let mut proposed = self.clone();
let moving_color = action.piece.color;
if let Some(piece) = proposed.inner.remove(&action.from) {
// not allowed to move mismatched piece
if piece != action.piece {
return;
}
let opt = piece.can_move_to(&action.from, &action.to, &proposed);
if let Some(side_effects) = opt {
proposed.apply_side_effects(moving_color, side_effects);
proposed.inner.insert(action.to, piece);
} else {
proposed.inner.insert(action.from, piece);
}
}
if proposed.is_in_check(moving_color) {
return;
}
*self = proposed;
}
fn apply_side_effects(&mut self, color: Color, side_effects: SideEffects) {
self.en_passant = side_effects.en_passant;
if color.is_white() {
if side_effects.king_castle_invalidated {
self.white_state.can_king_castle = false;
}
if side_effects.queen_castle_invalidated {
self.white_state.can_queen_castle = false;
}
} else {
if side_effects.king_castle_invalidated {
self.black_state.can_king_castle = false;
}
if side_effects.queen_castle_invalidated {
self.black_state.can_queen_castle = false;
}
}
}
fn is_in_check(&self, color: Color) -> bool {
let king = Piece {
kind: PieceKind::King,
color,
};
let king_opt = self.inner.iter().find(|(_, piece)| **piece == king);
if let Some((king_coords, _)) = king_opt {
self.inner
.iter()
.filter(|(_, piece)| piece.color != color)
.any(|(coords, piece)| piece.can_move_to(coords, king_coords, self).is_some())
} else {
false
}
}
}
impl Piece {
fn can_move_to(
&self,
from: &Coordinates,
to: &Coordinates,
state: &BoardState,
) -> Option<SideEffects> {
match self.kind {
PieceKind::Pawn if self.color.is_white() && to.rank > from.rank => {
self.check_pawn(from, to, state, Rank::Two, Rank::next)
}
PieceKind::Pawn if self.color.is_black() && to.rank < from.rank => {
self.check_pawn(from, to, state, Rank::Seven, Rank::prev)
}
_ => None,
}
}
fn check_pawn(
&self,
from: &Coordinates,
to: &Coordinates,
state: &BoardState,
en_passant_rank: Rank,
forward: impl Fn(Rank) -> Option<Rank>,
) -> Option<SideEffects> {
let rank_diff = to.rank.absolute_diff(from.rank);
let file_diff = to.file.absolute_diff(from.file);
let mut side_effects = SideEffects::default();
// move forward 1 or two steps as long as not blocked
if file_diff == 0 && ((rank_diff == 2 && from.rank == en_passant_rank) || rank_diff == 1) {
let mut path_rank = from.rank;
while let Some(rank) = forward(path_rank) {
let block_check = Coordinates {
rank,
file: to.file,
};
if state.inner.contains_key(&block_check) {
return None;
}
if rank == to.rank {
break;
}
// set en_passant to cell directly behind pawn
side_effects.en_passant = Some(block_check);
path_rank = rank;
}
return Some(side_effects);
}
// pawn capture move
if file_diff == 1 && rank_diff == 1 {
// check for real capture
if let Some(piece) = state.inner.get(to) {
if piece.color != self.color {
return Some(side_effects);
} else {
return None;
}
}
// check en_passant
if let Some(coords) = &state.en_passant {
if coords == to {
return Some(side_effects);
}
}
}
None
}
}
impl Default for CastleState {
fn default() -> Self {
Self {
can_king_castle: true,
can_queen_castle: true,
}
}
}
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),
];

View file

@ -1,9 +1,6 @@
use tracing::subscriber::set_global_default;
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 tracing_subscriber::{filter::Targets, fmt::format::FmtSpan, layer::SubscriberExt, Registry};
pub(crate) fn init_tracing() -> Result<(), Box<dyn std::error::Error>> {
LogTracer::init()?;

View file

@ -1,242 +1,23 @@
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 actix_cors::Cors;
use actix_web::{
web::{Data, Json},
App, HttpResponse, HttpServer,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::Duration,
};
use tokio::sync::mpsc::Sender;
use tracing::Instrument;
use tracing_actix_web::TracingLogger;
use uuid::Uuid;
mod api_types;
mod board_state;
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()
}
}
use api_types::{GameId, GameStart, Move, Start};
use board_state::BoardState;
struct Reply {
board_state: BoardState,
@ -247,13 +28,6 @@ struct MoveSession {
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>,
@ -264,13 +38,17 @@ struct GameState {
inner: Arc<Mutex<HashMap<GameId, GameData>>>,
}
struct GameDropper(GameState, GameId);
impl GameState {
fn data_for_id(&self, game_id: &GameId) -> Option<GameData> {
self.inner.lock().unwrap().get(game_id).cloned()
}
}
struct GameDropper(GameState, GameId);
fn save_data(&self, game_id: GameId, data: GameData) {
self.inner.lock().unwrap().insert(game_id, data);
}
}
impl Drop for GameDropper {
fn drop(&mut self) {
@ -278,55 +56,52 @@ impl Drop for GameDropper {
}
}
async fn start(start: Json<Start>, game_state: Data<GameState>) -> HttpResponse {
async fn start(Json(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 };
let (sender, mut rx) = tokio::sync::mpsc::channel(1);
game_state
.inner
.lock()
.unwrap()
.insert(game_id.clone(), game_data);
game_state.save_data(game_id.clone(), GameData { sender });
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 game_span = tracing::info_span!(
parent: None,
"Game Session",
game.id = %game_id,
);
let _ = msg.reply.send(Reply {
board_state: board_state.clone(),
});
let mut board_state = BoardState::starting_positions();
let serializable = board_state.to_serializable();
actix_web::rt::spawn(
async move {
while let Ok(Some(msg)) =
actix_web::rt::time::timeout(Duration::from_secs(60 * 5), rx.recv()).await
{
board_state.handle(msg.action);
let _ = msg.reply.send(Reply {
board_state: board_state.clone(),
});
}
drop(game_dropper);
}
drop(game_dropper);
});
.instrument(game_span),
);
HttpResponse::Ok().json(GameStart {
board: initial_board_state.to_serializable(),
board: 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();
async fn make_move(Json(action): Json<Move>, game_state: Data<GameState>) -> HttpResponse {
if let Some(data) = game_state.data_for_id(&action.game_id) {
let (reply, rx) = tokio::sync::oneshot::channel();
let res = data
.sender
.send(MoveSession {
action: action.into_inner(),
reply: tx,
})
.await;
let res = data.sender.send(MoveSession { action, reply }).await;
if res.is_err() {
return HttpResponse::InternalServerError().finish();
@ -346,10 +121,18 @@ async fn make_move(action: Json<Move>, game_state: Data<GameState>) -> HttpRespo
async fn main() -> Result<(), Box<dyn std::error::Error>> {
init_tracing::init_tracing()?;
HttpServer::new(|| {
let game_state = GameState::default();
HttpServer::new(move || {
App::new()
.app_data(Data::new(GameState::default()))
.app_data(Data::new(game_state.clone()))
.wrap(TracingLogger::default())
.wrap(
Cors::default()
.allow_any_origin()
.allow_any_header()
.allow_any_method(),
)
.route("/start", actix_web::web::post().to(start))
.route("/move", actix_web::web::post().to(make_move))
})