Compare commits
21 commits
main
...
cbdc46aca6
Author | SHA1 | Date | |
---|---|---|---|
Kumu | cbdc46aca6 | ||
Kumu | d5168182dd | ||
Kumu | 509ffd524c | ||
Kumu | 2e8f0f6005 | ||
Kumu | 8c6a230138 | ||
Kumu | 3a50c7a708 | ||
Kumu | e1d1779875 | ||
Aode (lion) | 82504a9c47 | ||
Aode (lion) | 5064bf6491 | ||
Aode (lion) | 9edefa920d | ||
Aode (lion) | 4a2a4c81c5 | ||
Aode (lion) | 99f826ef32 | ||
Aode (lion) | 0d85ce74c8 | ||
Aode (lion) | bf5580149d | ||
Aode (lion) | e24c2ea39f | ||
Aode (lion) | 76f530b91e | ||
Aode (lion) | 80fcc059f3 | ||
Aode (lion) | cef5898c44 | ||
Aode (lion) | 674f677846 | ||
Aode (lion) | 2c3b3510c3 | ||
Aode (lion) | 0175f2d933 |
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
276
src/api_types.rs
Normal file
276
src/api_types.rs
Normal file
|
@ -0,0 +1,276 @@
|
|||
//! 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, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum Color {
|
||||
Black,
|
||||
White,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, 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,
|
||||
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, 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 opposite(&self) -> Self {
|
||||
match self {
|
||||
Self::White => Self::Black,
|
||||
Self::Black => Self::White,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
420
src/board_state.rs
Normal file
420
src/board_state.rs
Normal file
|
@ -0,0 +1,420 @@
|
|||
use std::cell::RefCell;
|
||||
use crate::api_types;
|
||||
use crate::api_types::{PieceKind, Color, Move, Coordinates};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops;
|
||||
use tracing::span;
|
||||
use crate::api_types::Color::White;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub(crate) struct Position {
|
||||
file: u8,
|
||||
rank: u8,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
fn from_coordinates(coord: Coordinates) -> Self {
|
||||
let file = coord.file.to_number();
|
||||
let rank = coord.rank.to_number();
|
||||
Position { file, rank }
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<(i8, i8)> for &Position {
|
||||
type Output = Option<Position>;
|
||||
|
||||
fn add(self, rhs: (i8, i8)) -> Self::Output {
|
||||
let file = self.file as i8 + rhs.0;
|
||||
let rank = self.rank as i8 + rhs.1;
|
||||
if file < 0 || file > 7 || rank < 0 || rank > 7 {
|
||||
None
|
||||
} else {
|
||||
Some(Position { file: file as u8, rank: rank as u8 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Piece {
|
||||
kind: PieceKind,
|
||||
color: Color,
|
||||
visible: RefCell<HashSet<Position>>,
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
fn get_visible_in_direction(direction: (i8, i8), pos: &Position, state: &GameState) -> HashSet<Position> {
|
||||
let mut visible = HashSet::new();
|
||||
let mut current_square = pos.clone();
|
||||
|
||||
loop {
|
||||
// keep moving in the specified direction, adding empty squares, until we reach edge of board
|
||||
// if we encounter a piece, that is the last visible square
|
||||
current_square = match ¤t_square + direction {
|
||||
None => { break }
|
||||
Some(pos) => { pos }
|
||||
};
|
||||
|
||||
// add current square to visible
|
||||
visible.insert(current_square.clone());
|
||||
|
||||
// path is blocked by piece, so cannot see/move past it
|
||||
if state.board.get(¤t_square).is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
visible
|
||||
}
|
||||
|
||||
fn generate_visible(&self, pos: &Position, state: &GameState) {
|
||||
let mut visible = HashSet::new();
|
||||
let mut move_directions: Vec<(i8, i8)> = Vec::new();
|
||||
|
||||
match self.kind {
|
||||
PieceKind::Pawn => {
|
||||
match self.color {
|
||||
Color::Black => {
|
||||
// can capture diagonally one rank ahead
|
||||
visible.extend(pos + (-1, -1));
|
||||
visible.extend(pos + (1, -1));
|
||||
}
|
||||
Color::White => {
|
||||
visible.extend(pos + (-1, 1));
|
||||
visible.extend(pos + (1, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
PieceKind::Rook => {
|
||||
move_directions.extend([
|
||||
(0, 1), (1, 0), (-1, 0), (1, 0)
|
||||
]);
|
||||
}
|
||||
PieceKind::Knight => {
|
||||
let knight_moves: Vec<(i8, i8)> = vec![
|
||||
(-1, -2), (-1, 2), (1, -2), (1, 2),
|
||||
(-2, -1), (-2, 1), (2, -1), (2, 1),
|
||||
];
|
||||
visible.extend(
|
||||
knight_moves.iter().filter_map(|&mv| {
|
||||
pos + mv
|
||||
})
|
||||
);
|
||||
}
|
||||
PieceKind::Bishop => {
|
||||
move_directions.extend([
|
||||
(1, 1), (-1, -1), (-1, 1), (1, -1)
|
||||
]);
|
||||
}
|
||||
PieceKind::Queen => {
|
||||
move_directions.extend([
|
||||
(0, 1), (1, 0), (-1, 0), (1, 0),
|
||||
(1, 1), (-1, -1), (-1, 1), (1, -1),
|
||||
]);
|
||||
}
|
||||
PieceKind::King => {
|
||||
let king_moves: Vec<(i8, i8)> = vec![
|
||||
(1, 0), (-1, 0), (1, 1), (-1, 1),
|
||||
(1, -1), (-1, -1), (0, -1), (0, 1)
|
||||
];
|
||||
visible.extend(
|
||||
king_moves.iter().filter_map(|&mv| {
|
||||
pos + mv
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for direction in move_directions.drain(..) {
|
||||
visible.extend(
|
||||
Piece::get_visible_in_direction(direction, pos, state)
|
||||
)
|
||||
}
|
||||
|
||||
*self.visible.borrow_mut() = visible;
|
||||
}
|
||||
|
||||
fn get_possible_moves(&self, piece_pos: &Position, state: &GameState) -> HashSet<Position> {
|
||||
let mut possible_moves = HashSet::new();
|
||||
|
||||
// for all pieces except pawn, visible squares are squares you can move to
|
||||
// for pawns, visible squares are diagonally ahead, possible moves are either 1 or 2 squares
|
||||
// directly ahead
|
||||
if self.kind == PieceKind::Pawn {
|
||||
let (opposite_color, starting_rank, one_square, two_square) =
|
||||
match self.color {
|
||||
Color::Black => {
|
||||
(Color::White, 6, (0, -1), (0, -2))
|
||||
}
|
||||
Color::White => {
|
||||
(Color::Black, 1, (0, 1), (0, 2))
|
||||
}
|
||||
};
|
||||
|
||||
// can move to visible squares diagonally ahead of a piece of opposite color is there
|
||||
// OR if it is the en passant target square
|
||||
for pos in self.visible.borrow().iter() {
|
||||
if let Some(piece) = state.board.get(&pos) {
|
||||
if piece.color == opposite_color {
|
||||
possible_moves.insert(pos.clone());
|
||||
}
|
||||
}
|
||||
if state.en_passant_target.as_ref() == Some(pos) {
|
||||
possible_moves.insert(pos.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// can move to square directly ahead if no piece is there
|
||||
let one_square = (piece_pos + one_square).unwrap();
|
||||
if state.board.get(&one_square).is_none() {
|
||||
possible_moves.insert(one_square.clone());
|
||||
// can move two squares ahead if we are on starting rank
|
||||
if piece_pos.rank == starting_rank {
|
||||
let two_square = (piece_pos + two_square).unwrap();
|
||||
if state.board.get(&two_square).is_none() {
|
||||
possible_moves.insert(two_square);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// for all other pieces, can move to any visible square unless it contains
|
||||
// a piece of the same color (can capture opponent piece, but not own)
|
||||
possible_moves = self.visible.borrow().iter().filter(|&pos| {
|
||||
if let Some(piece) = state.board.get(&pos) {
|
||||
// space is occupied, check if it is same color or not
|
||||
piece.color != self.color
|
||||
} else {
|
||||
// space is empty, so able to move there
|
||||
true
|
||||
}
|
||||
}).cloned().collect();
|
||||
|
||||
// check if king can castle
|
||||
if self.kind == PieceKind::King {
|
||||
// king side castle - must still be allowed and have two empty squares between
|
||||
// king and rook, not visible to opponent
|
||||
if state.castle.get(&self.color).unwrap().can_castle_king_side &&
|
||||
state.board.get(&(piece_pos + (1, 0)).unwrap()).is_none() &&
|
||||
state.board.get(&(piece_pos + (2, 0)).unwrap()).is_none() &&
|
||||
!state.visible.get(&self.color.opposite()).unwrap().contains(&(piece_pos + (1, 0)).unwrap()) &&
|
||||
!state.player_in_check(&self.color) {
|
||||
// move is represented as king moving two squares
|
||||
possible_moves.insert((piece_pos + (2, 0)).unwrap());
|
||||
}
|
||||
// queen side castle - must still be allowed and have three empty squares between
|
||||
// king and rook
|
||||
if state.castle.get(&self.color).unwrap().can_castle_queen_side &&
|
||||
state.board.get(&(piece_pos + (-1, 0)).unwrap()).is_none() &&
|
||||
state.board.get(&(piece_pos + (-2, 0)).unwrap()).is_none() &&
|
||||
state.board.get(&(piece_pos + (-3, 0)).unwrap()).is_none() &&
|
||||
!state.visible.get(&self.color.opposite()).unwrap().contains(&(piece_pos + (-1, 0)).unwrap()) &&
|
||||
!state.player_in_check(&self.color) {
|
||||
// move is represented as king moving two squares
|
||||
possible_moves.insert((piece_pos + (-2, 0)).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// final validation - possible moves must not lead to check
|
||||
let possible_moves= possible_moves.iter().filter(|&to_pos| {
|
||||
let mut new_state = state.clone();
|
||||
// apply move
|
||||
new_state.apply_move(piece_pos.clone(), to_pos.clone());
|
||||
//new_state.generate_visible();
|
||||
!new_state.player_in_check(&self.color)
|
||||
}).cloned().collect();
|
||||
possible_moves
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CastleState {
|
||||
can_castle_king_side: bool,
|
||||
can_castle_queen_side: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct GameState {
|
||||
board: HashMap<Position, Piece>,
|
||||
visible: HashMap<Color, HashSet<Position>>,
|
||||
en_passant_target: Option<Position>,
|
||||
castle: HashMap<Color, CastleState>,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
pub(crate) fn starting_positions() -> Self {
|
||||
let mut this = Self::default();
|
||||
|
||||
for (file, rank, kind, color) in INITIAL_PIECES {
|
||||
this.board.insert(
|
||||
Position { file, rank },
|
||||
Piece { kind, color, visible: RefCell::new(HashSet::new()) },
|
||||
);
|
||||
}
|
||||
|
||||
this.generate_visible();
|
||||
|
||||
this.castle.insert(Color::Black, CastleState { can_castle_king_side: true, can_castle_queen_side: true });
|
||||
this.castle.insert(Color::White, CastleState { can_castle_king_side: true, can_castle_queen_side: true });
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub(crate) fn to_serializable(&self) -> Vec<(api_types::File, api_types::Rank, PieceKind, Color)> {
|
||||
self.board
|
||||
.iter()
|
||||
.map(|(pos, piece)| {
|
||||
(
|
||||
api_types::File::from_number(pos.file).unwrap(),
|
||||
api_types::Rank::from_number(pos.rank).unwrap(),
|
||||
piece.kind.clone(),
|
||||
piece.color.clone(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_visible(&mut self) {
|
||||
let mut black_visible = HashSet::new();
|
||||
let mut white_visible = HashSet::new();
|
||||
|
||||
for (pos, piece) in self.board.iter() {
|
||||
piece.generate_visible(&pos, self);
|
||||
match piece.color {
|
||||
Color::Black => {
|
||||
black_visible.extend(piece.visible.borrow().clone());
|
||||
}
|
||||
White => {
|
||||
white_visible.extend(piece.visible.borrow().clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut visible = HashMap::new();
|
||||
visible.insert(Color::Black, black_visible);
|
||||
visible.insert(Color::White, white_visible);
|
||||
|
||||
self.visible = visible;
|
||||
}
|
||||
|
||||
// determines if given color king is in check
|
||||
fn player_in_check(&self, color: &Color) -> bool {
|
||||
let opponent_visible = self.visible.get(&color.opposite()).unwrap();
|
||||
let king_color_collection: Vec<(&Position, &Piece)> = self.board.iter().filter(|(pos, piece)| {
|
||||
piece.kind == PieceKind::King && &piece.color == color
|
||||
}).collect();
|
||||
let (king_pos, _king) = &king_color_collection[0];
|
||||
opponent_visible.contains(king_pos)
|
||||
}
|
||||
|
||||
fn apply_move(&mut self, from: Position, to: Position) {
|
||||
let piece = self.board.remove(&from).unwrap();
|
||||
|
||||
// if pawn is moving to en passant square, remove captured pawn
|
||||
if piece.kind == PieceKind::Pawn && Some(&to) == self.en_passant_target.as_ref() {
|
||||
let pawn_pos = &to + match piece.color {
|
||||
Color::Black => {(0, 1)}
|
||||
White => {(0, -1)}
|
||||
};
|
||||
self.board.remove(&pawn_pos.unwrap());
|
||||
}
|
||||
// update en passant square if a pawn moves 2 squares ahead
|
||||
if piece.kind == PieceKind::Pawn &&
|
||||
(
|
||||
(piece.color == Color::White && from.rank == 1 && to.rank == 3) ||
|
||||
(piece.color == Color::Black && from.rank == 6 && to.rank == 4)
|
||||
) {
|
||||
// en passant square is square behind where pawn moves to
|
||||
self.en_passant_target = &to + match piece.color {
|
||||
Color::Black => {(0, 1)}
|
||||
White => {(0, -1)}
|
||||
};
|
||||
} else {
|
||||
self.en_passant_target = None;
|
||||
}
|
||||
|
||||
// check if we are castling
|
||||
if piece.kind == PieceKind::King {
|
||||
if self.castle.get(&piece.color).unwrap().can_castle_king_side && Some(&to) == (&from + (2, 0)).as_ref() {
|
||||
// perform king side castle by moving rook (king gets moved to to_pos)
|
||||
let rook_pos = Position{ file: 7, rank: to.rank};
|
||||
if let Some(rook) = self.board.remove(&rook_pos) {
|
||||
self.board.insert((&rook_pos + (-2, 0)).unwrap(), rook);
|
||||
}
|
||||
} else if self.castle.get(&piece.color).unwrap().can_castle_queen_side && Some(&to) == (&from + (-2, 0)).as_ref() {
|
||||
// perform queen side castle
|
||||
let rook_pos = Position{ file: 0, rank: to.rank};
|
||||
if let Some(rook) = self.board.remove(&rook_pos) {
|
||||
self.board.insert((&rook_pos + (3, 0)).unwrap(), rook);
|
||||
}
|
||||
}
|
||||
// even if not castling, if you move king you can no longer castle
|
||||
self.castle.get_mut(&piece.color).unwrap().can_castle_queen_side = false;
|
||||
self.castle.get_mut(&piece.color).unwrap().can_castle_king_side = false;
|
||||
}
|
||||
|
||||
// if moving rook from starting position, can no longer castle on that side
|
||||
if piece.kind == PieceKind::Rook {
|
||||
if from.file == 7 {
|
||||
self.castle.get_mut(&piece.color).unwrap().can_castle_king_side = false;
|
||||
} else if from.file == 0 {
|
||||
self.castle.get_mut(&piece.color).unwrap().can_castle_queen_side = false;
|
||||
}
|
||||
}
|
||||
|
||||
// move the piece
|
||||
self.board.insert(to, piece);
|
||||
|
||||
self.generate_visible()
|
||||
}
|
||||
|
||||
pub(crate) fn handle(&mut self, action: Move) {
|
||||
let from_pos = Position::from_coordinates(action.from);
|
||||
let to_pos = Position::from_coordinates(action.to);
|
||||
|
||||
let piece = match self.board.get(&from_pos) {
|
||||
None => {
|
||||
panic!("No piece found at position specified in move");
|
||||
}
|
||||
Some(piece) => {piece}
|
||||
}.clone();
|
||||
|
||||
if piece.get_possible_moves(&from_pos, &self).contains(&to_pos) {
|
||||
self.apply_move(from_pos, to_pos)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const INITIAL_PIECES: [(u8, u8, PieceKind, Color); 32] = [
|
||||
(0, 0, PieceKind::Rook, Color::White),
|
||||
(1, 0, PieceKind::Knight, Color::White),
|
||||
(2, 0, PieceKind::Bishop, Color::White),
|
||||
(3, 0, PieceKind::Queen, Color::White),
|
||||
(4, 0, PieceKind::King, Color::White),
|
||||
(5, 0, PieceKind::Bishop, Color::White),
|
||||
(6, 0, PieceKind::Knight, Color::White),
|
||||
(7, 0, PieceKind::Rook, Color::White),
|
||||
(0, 1, PieceKind::Pawn, Color::White),
|
||||
(1, 1, PieceKind::Pawn, Color::White),
|
||||
(2, 1, PieceKind::Pawn, Color::White),
|
||||
(3, 1, PieceKind::Pawn, Color::White),
|
||||
(4, 1, PieceKind::Pawn, Color::White),
|
||||
(5, 1, PieceKind::Pawn, Color::White),
|
||||
(6, 1, PieceKind::Pawn, Color::White),
|
||||
(7, 1, PieceKind::Pawn, Color::White),
|
||||
(0, 6, PieceKind::Pawn, Color::Black),
|
||||
(1, 6, PieceKind::Pawn, Color::Black),
|
||||
(2, 6, PieceKind::Pawn, Color::Black),
|
||||
(3, 6, PieceKind::Pawn, Color::Black),
|
||||
(4, 6, PieceKind::Pawn, Color::Black),
|
||||
(5, 6, PieceKind::Pawn, Color::Black),
|
||||
(6, 6, PieceKind::Pawn, Color::Black),
|
||||
(7, 6, PieceKind::Pawn, Color::Black),
|
||||
(0, 7, PieceKind::Rook, Color::Black),
|
||||
(1, 7, PieceKind::Knight, Color::Black),
|
||||
(2, 7, PieceKind::Bishop, Color::Black),
|
||||
(3, 7, PieceKind::Queen, Color::Black),
|
||||
(4, 7, PieceKind::King, Color::Black),
|
||||
(5, 7, PieceKind::Bishop, Color::Black),
|
||||
(6, 7, PieceKind::Knight, Color::Black),
|
||||
(7, 7, PieceKind::Rook, Color::Black),
|
||||
];
|
|
@ -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()?;
|
||||
|
|
342
src/main.rs
342
src/main.rs
|
@ -1,245 +1,25 @@
|
|||
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};
|
||||
|
||||
struct Reply {
|
||||
board_state: BoardState,
|
||||
board_state: board_state::GameState,
|
||||
}
|
||||
|
||||
struct MoveSession {
|
||||
|
@ -247,13 +27,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 +37,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 +55,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 = board_state::GameState::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 +120,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))
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue