feat: initial commit
This commit is contained in:
commit
2fd10446b6
38 changed files with 4653 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/node_modules
|
||||||
|
/dist
|
1374
Cargo.lock
generated
Normal file
1374
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "chs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.5.17", features = ["ws"] }
|
||||||
|
futures = "0.3.25"
|
||||||
|
mime_guess = "2.0.4"
|
||||||
|
rayon = "1.5.3"
|
||||||
|
rust-embed = "6.4.2"
|
||||||
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
|
serde_json = "1.0.87"
|
||||||
|
thiserror = "1.0.37"
|
||||||
|
tokio = { version = "1.21.2", features = ["full"] }
|
||||||
|
tower-http = { version = "0.3.4", features = ["trace", "fs"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
|
uuid = { version = "1.2.1", features = ["v4", "serde"] }
|
||||||
|
xtra = { git = "https://github.com/Restioson/xtra.git", version = "0.6.0", features = ["tokio", "instrumentation", "macros", "sink"] }
|
8
Makefile.toml
Normal file
8
Makefile.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[tasks.build-ui]
|
||||||
|
command = "pnpm"
|
||||||
|
args = ["build"]
|
||||||
|
|
||||||
|
[tasks.build-release]
|
||||||
|
command = "cargo"
|
||||||
|
args = ["build", "--release"]
|
||||||
|
dependencies = ["build-ui"]
|
78
examples/perft_test.rs
Normal file
78
examples/perft_test.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use chs::constants::*;
|
||||||
|
use chs::chess::perft::run_perft;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestRunner {
|
||||||
|
passed: usize,
|
||||||
|
failed: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestRunner {
|
||||||
|
fn run_test<F: Fn() -> bool>(&mut self, f: F) {
|
||||||
|
let result = f();
|
||||||
|
if result {
|
||||||
|
self.passed += 1;
|
||||||
|
} else {
|
||||||
|
self.failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summarise(&self) {
|
||||||
|
println!("Ran {total} tests: {passed} passed, {failed} failed", total = self.passed + self.failed, passed = self.passed, failed = self.failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut runner = TestRunner::default();
|
||||||
|
let mut perft = |fen: &str, depth: u64, expected_positions: u64| runner.run_test(|| run_perft(fen, depth, expected_positions));
|
||||||
|
|
||||||
|
// Start position
|
||||||
|
println!("Start position");
|
||||||
|
perft(START_FEN, 0, 1);
|
||||||
|
perft(START_FEN, 1, 20);
|
||||||
|
perft(START_FEN, 2, 400);
|
||||||
|
perft(START_FEN, 3, 8_902);
|
||||||
|
perft(START_FEN, 4, 197_281);
|
||||||
|
|
||||||
|
// Other test positions (https://www.chessprogramming.org/Perft_Results)
|
||||||
|
|
||||||
|
// Position 2
|
||||||
|
println!("Position 2");
|
||||||
|
perft(POSITION_2, 1, 48);
|
||||||
|
perft(POSITION_2, 2, 2_039);
|
||||||
|
perft(POSITION_2, 3, 97_862);
|
||||||
|
perft(POSITION_2, 4, 4_085_603);
|
||||||
|
|
||||||
|
// Position 3
|
||||||
|
println!("Position 3");
|
||||||
|
perft(POSITION_3, 1, 14);
|
||||||
|
perft(POSITION_3, 2, 191);
|
||||||
|
perft(POSITION_3, 3, 2_812);
|
||||||
|
perft(POSITION_3, 4, 43_238);
|
||||||
|
perft(POSITION_3, 5, 674_624);
|
||||||
|
perft(POSITION_3, 6, 11_030_083);
|
||||||
|
|
||||||
|
// Position 4
|
||||||
|
println!("Position 4");
|
||||||
|
perft(POSITION_4, 1, 6);
|
||||||
|
perft(POSITION_4, 2, 264);
|
||||||
|
perft(POSITION_4, 3, 9_467);
|
||||||
|
perft(POSITION_4, 4, 422_333);
|
||||||
|
perft(POSITION_4, 5, 15_833_292);
|
||||||
|
|
||||||
|
// Position 5
|
||||||
|
println!("Position 5");
|
||||||
|
perft(POSITION_5, 1, 44);
|
||||||
|
perft(POSITION_5, 2, 1_486);
|
||||||
|
perft(POSITION_5, 3, 62_379);
|
||||||
|
perft(POSITION_5, 4, 2_103_487);
|
||||||
|
|
||||||
|
// Position 6
|
||||||
|
println!("Position 6");
|
||||||
|
perft(POSITION_6, 1, 46);
|
||||||
|
perft(POSITION_6, 2, 2_079);
|
||||||
|
perft(POSITION_6, 3, 89_890);
|
||||||
|
perft(POSITION_6, 4, 3_894_594);
|
||||||
|
|
||||||
|
runner.summarise();
|
||||||
|
}
|
5
examples/single_perft_test.rs
Normal file
5
examples/single_perft_test.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use chs::{chess::perft::run_perft, constants::POSITION_6};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
run_perft("rnb2k1r/pp1Pbppp/2p5/q7/2B5/P7/1PP1NnPP/RNBQK2R w KQ - 1 9", 1, 9);
|
||||||
|
}
|
15
index.html
Normal file
15
index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<title>Chs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script src="/src-web/index.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "chs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"serve": "vite preview"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^18.11.7",
|
||||||
|
"typescript": "^4.8.4",
|
||||||
|
"vite": "^3.2.1",
|
||||||
|
"vite-plugin-solid": "^2.3.10"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fontsource/noto-sans-symbols-2": "^4.5.10",
|
||||||
|
"@solid-primitives/websocket": "^0.3.3",
|
||||||
|
"solid-devtools": "^0.20.1",
|
||||||
|
"solid-js": "^1.6.0",
|
||||||
|
"zod": "^3.19.1"
|
||||||
|
}
|
||||||
|
}
|
1076
pnpm-lock.yaml
Normal file
1076
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
61
src-web/App.tsx
Normal file
61
src-web/App.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { Component, createEffect, createSignal, Match, Switch } from 'solid-js';
|
||||||
|
import createWebsocket from '@solid-primitives/websocket';
|
||||||
|
import { wsUrl } from './constants';
|
||||||
|
import Spinner from './components/Spinner';
|
||||||
|
import Welcome from './components/Welcome';
|
||||||
|
import { Board as BoardData, Move, ServerChessEvent } from './events';
|
||||||
|
import Board from './components/Board';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gameId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const App: Component<Props> = (props) => {
|
||||||
|
const [board, setBoard] = createSignal<BoardData>(Array(64).fill(null));
|
||||||
|
const [possibleMoves, setPossibleMoves] = createSignal<Move[]>([]);
|
||||||
|
|
||||||
|
function handleEvent(e: MessageEvent<string>) {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
const event = ServerChessEvent.parse(data);
|
||||||
|
if (event.event === 'BoardUpdate') {
|
||||||
|
console.log(event.data.board);
|
||||||
|
setBoard(event.data.board);
|
||||||
|
} else if (event.event === 'PossibleMoves') {
|
||||||
|
console.log(event.data.moves);
|
||||||
|
setPossibleMoves(event.data.moves);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('board is now', board());
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('moves is now', possibleMoves());
|
||||||
|
});
|
||||||
|
|
||||||
|
const [connect, disconnect, send, state, socket] = createWebsocket(wsUrl(props.gameId), handleEvent, console.error);
|
||||||
|
|
||||||
|
function makeMove(move: Move) {
|
||||||
|
send(JSON.stringify({
|
||||||
|
event: 'MakeMove',
|
||||||
|
data: {
|
||||||
|
move,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Switch fallback={<><h1>Hello, World!</h1><Spinner /></>}>
|
||||||
|
<Match when={state() === WebSocket.CLOSED}>
|
||||||
|
<Welcome gameId={props.gameId} joinGame={() => connect()} />
|
||||||
|
</Match>
|
||||||
|
<Match when={state() === WebSocket.CONNECTING}>
|
||||||
|
<Spinner />
|
||||||
|
</Match>
|
||||||
|
<Match when={state() === WebSocket.OPEN}>
|
||||||
|
<Board board={board} moves={possibleMoves} makeMove={makeMove} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
62
src-web/components/Board.css
Normal file
62
src-web/components/Board.css
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
.board {
|
||||||
|
font-family: 'Noto Sans Symbols 2', sans-serif;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
.square {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
font-size: 48px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
padding: auto;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square.selectable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
background-color: #c8a2c8;
|
||||||
|
/* color: #5d375d; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
background-color: #5d375d;
|
||||||
|
/* color: #C8A2C8; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.borderSelected {
|
||||||
|
position: absolute;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border: 4px solid yellowgreen;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.borderTarget {
|
||||||
|
position: absolute;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
border: 4px solid yellowgreen;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
73
src-web/components/Board.tsx
Normal file
73
src-web/components/Board.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { Accessor, Component, createSignal, For, Show } from "solid-js";
|
||||||
|
import { Board as BoardData, Coordinate, Move, PieceType } from "../events";
|
||||||
|
import './Board.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
board: Accessor<BoardData>;
|
||||||
|
moves: Accessor<Move[]>;
|
||||||
|
makeMove: (move: Move) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PIECE_CHARS: Record<PieceType, string> = {
|
||||||
|
'king': '♔',
|
||||||
|
'queen': '♕',
|
||||||
|
'rook': '♜',
|
||||||
|
'knight': '♞',
|
||||||
|
'bishop': '♝',
|
||||||
|
'pawn': '♙',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Board: Component<Props> = (props) => {
|
||||||
|
const [selectedSquare, setSelectedSquare] = createSignal<number | null>(null);
|
||||||
|
|
||||||
|
const validMoves = () => {
|
||||||
|
const selected = selectedSquare();
|
||||||
|
if (selected !== null) {
|
||||||
|
return props.moves().filter(move => (move.from.rank * 8 + move.from.file) === selected);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div class="board">
|
||||||
|
{props.board().map((piece, i) => {
|
||||||
|
const coord: Coordinate = {
|
||||||
|
rank: Math.floor(i / 8),
|
||||||
|
file: i % 8,
|
||||||
|
};
|
||||||
|
const isLight = () => ((i % 8) + Math.floor(i / 8)) % 2 == 0;
|
||||||
|
const hasMoves = () => props.moves().find((move) => move.from.rank === coord.rank && move.from.file === coord.file) !== undefined;
|
||||||
|
const targetMove = () => validMoves().find(move => move.to.rank === coord.rank && move.to.file === coord.file);
|
||||||
|
const isTarget = () => targetMove() !== undefined;
|
||||||
|
|
||||||
|
return <div classList={{
|
||||||
|
square: true,
|
||||||
|
light: isLight(),
|
||||||
|
dark: !isLight(),
|
||||||
|
black: piece?.side === 'black',
|
||||||
|
white: piece?.side === 'white',
|
||||||
|
selectable: hasMoves() || isTarget(),
|
||||||
|
}} onClick={() => {
|
||||||
|
if (hasMoves()) {
|
||||||
|
if (selectedSquare() === i) {
|
||||||
|
setSelectedSquare(null);
|
||||||
|
} else {
|
||||||
|
setSelectedSquare(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const target = targetMove();
|
||||||
|
if (target && !target.promotions) {
|
||||||
|
props.makeMove(target);
|
||||||
|
setSelectedSquare(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Show when={selectedSquare() === i}><div class="borderSelected" /></Show>
|
||||||
|
<Show when={isTarget()}><div class="borderTarget" /></Show>
|
||||||
|
<Show when={piece !== null}>{PIECE_CHARS[piece!.ty]}</Show>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Board;
|
16
src-web/components/Button.css
Normal file
16
src-web/components/Button.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
.button {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
filter: brightness(.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
filter: brightness(.8);
|
||||||
|
}
|
16
src-web/components/Button.tsx
Normal file
16
src-web/components/Button.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { children, Component, JSX } from "solid-js";
|
||||||
|
import './Button.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClick?: () => void;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: Component<Props> = (props) => {
|
||||||
|
const c = children(() => props.children)
|
||||||
|
return <button class="button" onClick={props.onClick}>
|
||||||
|
{c()}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
0
src-web/components/Connecting.css
Normal file
0
src-web/components/Connecting.css
Normal file
11
src-web/components/Connecting.tsx
Normal file
11
src-web/components/Connecting.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Component } from 'solid-js';
|
||||||
|
import './Connecting.css';
|
||||||
|
import Spinner from './Spinner';
|
||||||
|
|
||||||
|
const Connecting: Component = () => {
|
||||||
|
return <div>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Connecting;
|
19
src-web/components/Spinner.css
Normal file
19
src-web/components/Spinner.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--spinner-size);
|
||||||
|
height: var(--spinner-size);
|
||||||
|
border: 4px solid;
|
||||||
|
border-color: transparent transparent var(--spinner-colour) var(--spinner-colour);
|
||||||
|
border-radius: 99999px;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: spinner-spin 500ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner-spin {
|
||||||
|
from {
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotateZ(360deg);
|
||||||
|
}
|
||||||
|
}
|
16
src-web/components/Spinner.tsx
Normal file
16
src-web/components/Spinner.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
import './Spinner.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
colour?: string;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Spinner: Component<Props> = (props) => {
|
||||||
|
return <div class="spinner" style={{
|
||||||
|
'--spinner-size': props.size ?? '32px',
|
||||||
|
'--spinner-colour': props.colour ?? '#f9027a',
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Spinner;
|
22
src-web/components/Welcome.tsx
Normal file
22
src-web/components/Welcome.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { Component } from "solid-js";
|
||||||
|
import Button from "./Button";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gameId: string;
|
||||||
|
joinGame: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Welcome: Component<Props> = (props) => {
|
||||||
|
console.log(props.gameId);
|
||||||
|
return <main>
|
||||||
|
<h1>Welcome</h1>
|
||||||
|
<div>
|
||||||
|
Game ID: <pre>{props.gameId}</pre>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => props.joinGame()}>
|
||||||
|
Join
|
||||||
|
</Button>
|
||||||
|
</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Welcome;
|
12
src-web/constants.ts
Normal file
12
src-web/constants.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// const WS_BASE = 'ws://localhost:3000/ws/';
|
||||||
|
|
||||||
|
export function wsUrl(id: string) {
|
||||||
|
if (import.meta.env.WS_BASE) {
|
||||||
|
return import.meta.env.WS_BASE + id;
|
||||||
|
} else {
|
||||||
|
const loc = window.location;
|
||||||
|
let newUri = loc.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
newUri += loc.host + '/ws/' + id;
|
||||||
|
return newUri;
|
||||||
|
}
|
||||||
|
}
|
59
src-web/events.ts
Normal file
59
src-web/events.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const Coordinate = z.object({
|
||||||
|
rank: z.number(),
|
||||||
|
file: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Coordinate = z.infer<typeof Coordinate>;
|
||||||
|
|
||||||
|
export const Side = z.enum(['white', 'black']);
|
||||||
|
export type Side = z.infer<typeof Side>;
|
||||||
|
|
||||||
|
export const PieceType = z.enum(['king', 'queen', 'rook', 'bishop', 'knight', 'pawn']);
|
||||||
|
export type PieceType = z.infer<typeof PieceType>;
|
||||||
|
|
||||||
|
export const Piece = z.object({
|
||||||
|
side: Side,
|
||||||
|
ty: PieceType,
|
||||||
|
});
|
||||||
|
export type Piece = z.infer<typeof Piece>;
|
||||||
|
|
||||||
|
export interface Move {
|
||||||
|
from: Coordinate;
|
||||||
|
to: Coordinate;
|
||||||
|
set_en_passant: Coordinate | null;
|
||||||
|
other: Move | null;
|
||||||
|
promotions: Piece[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Move: z.ZodType<Move> = z.lazy(() => z.object({
|
||||||
|
from: Coordinate,
|
||||||
|
to: Coordinate,
|
||||||
|
set_en_passant: z.nullable(Coordinate),
|
||||||
|
other: z.nullable(Move),
|
||||||
|
promotions: z.nullable(z.array(Piece)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Board = z.array(z.nullable(Piece));
|
||||||
|
|
||||||
|
export type Board = z.infer<typeof Board>;
|
||||||
|
|
||||||
|
export const BoardUpdateEvent = z.object({
|
||||||
|
board: Board,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BoardUpdateEvent = z.infer<typeof BoardUpdateEvent>;
|
||||||
|
|
||||||
|
export const PossibleMovesEvent = z.object({
|
||||||
|
moves: z.array(Move),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PossibleMovesEvent = z.infer<typeof PossibleMovesEvent>;
|
||||||
|
|
||||||
|
export const ServerChessEvent = z.discriminatedUnion("event", [
|
||||||
|
z.object({ event: z.literal('BoardUpdate'), data: BoardUpdateEvent }),
|
||||||
|
z.object({ event: z.literal('PossibleMoves'), data: PossibleMovesEvent }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type ServerChessEvent = z.infer<typeof ServerChessEvent>;
|
14
src-web/index.tsx
Normal file
14
src-web/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
/* @refresh reload */
|
||||||
|
import { render } from 'solid-js/web';
|
||||||
|
import 'solid-devtools';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
import './main.css';
|
||||||
|
import "@fontsource/noto-sans-symbols-2";
|
||||||
|
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
if (search.has('game_id')) {
|
||||||
|
render(() => <App gameId={search.get('game_id')!} />, document.getElementById('root') as HTMLElement);
|
||||||
|
} else {
|
||||||
|
console.error('no game id');
|
||||||
|
}
|
19
src-web/main.css
Normal file
19
src-web/main.css
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
26
src/assets.rs
Normal file
26
src/assets.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use axum::{response::{IntoResponse, Response}, body::{boxed, Full}, http::{header, StatusCode}};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "dist/"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
pub struct StaticFile<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> IntoResponse for StaticFile<T>
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let path = self.0.into();
|
||||||
|
|
||||||
|
match Asset::get(path.as_str()) {
|
||||||
|
Some(content) => {
|
||||||
|
let body = boxed(Full::from(content.data));
|
||||||
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||||
|
Response::builder().header(header::CONTENT_TYPE, mime.as_ref()).body(body).unwrap()
|
||||||
|
}
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
636
src/chess.rs
Normal file
636
src/chess.rs
Normal file
|
@ -0,0 +1,636 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::{FENParseError, NotationError};
|
||||||
|
|
||||||
|
pub mod mv;
|
||||||
|
pub mod perft;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct Coordinate {
|
||||||
|
rank: isize,
|
||||||
|
file: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coordinate {
|
||||||
|
pub fn is_board_position(self) -> bool {
|
||||||
|
self.rank >= 0 && self.rank < 8 && self.file >= 0 && self.file < 8
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_index(self) -> usize {
|
||||||
|
if !self.is_board_position() {
|
||||||
|
panic!("called to_index on a coordinate with out-of-bounds values: {self:?}");
|
||||||
|
}
|
||||||
|
(self.rank as usize) * 8 + (self.file as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_index(index: usize) -> Self {
|
||||||
|
let rank = (index / 8) as isize;
|
||||||
|
let file = (index % 8) as isize;
|
||||||
|
return Self { rank, file };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_algebraic(s: &str) -> Result<Self, NotationError> {
|
||||||
|
if s.len() != 2 {
|
||||||
|
return Err(NotationError::InvalidLength {
|
||||||
|
length: s.len(),
|
||||||
|
expected: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = s
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.to_digit(18)
|
||||||
|
.map(|v| v - 10)
|
||||||
|
.ok_or_else(|| NotationError::Other(s.to_owned()))? as isize;
|
||||||
|
|
||||||
|
let rank = s
|
||||||
|
.chars()
|
||||||
|
.skip(1)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.to_digit(9)
|
||||||
|
.map(|v| v - 1)
|
||||||
|
.ok_or_else(|| NotationError::Other(s.to_owned()))? as isize;
|
||||||
|
|
||||||
|
if file < 0 || rank < 0 {
|
||||||
|
return Err(NotationError::Other(s.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { file, rank })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn between(from: Coordinate, to: Coordinate) -> BetweenCoordsIter {
|
||||||
|
if from.rank != to.rank && from.file != to.file {
|
||||||
|
panic!("Coordinate::between must be passed two values in the same rank or file");
|
||||||
|
}
|
||||||
|
|
||||||
|
if from.rank != to.rank {
|
||||||
|
let min_rank = from.rank.min(to.rank);
|
||||||
|
let max_rank = from.rank.max(to.rank);
|
||||||
|
BetweenCoordsIter {
|
||||||
|
start_file: from.file,
|
||||||
|
start_rank: min_rank,
|
||||||
|
|
||||||
|
rank: true,
|
||||||
|
|
||||||
|
offset: 1,
|
||||||
|
max_offset: (max_rank - min_rank) - 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let min_file = from.file.min(to.file);
|
||||||
|
let max_file = from.file.max(to.file);
|
||||||
|
BetweenCoordsIter {
|
||||||
|
start_rank: from.rank,
|
||||||
|
start_file: min_file,
|
||||||
|
|
||||||
|
rank: false,
|
||||||
|
|
||||||
|
offset: 1,
|
||||||
|
max_offset: (max_file - min_file) - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FILES: [char; 8] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
||||||
|
|
||||||
|
impl Display for Coordinate {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if !self.is_board_position() {
|
||||||
|
panic!("cannot format non-board Coordinate in algebraic notation");
|
||||||
|
}
|
||||||
|
write!(f, "{}", FILES[self.file as usize])?;
|
||||||
|
write!(f, "{}", self.rank + 1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BetweenCoordsIter {
|
||||||
|
start_rank: isize,
|
||||||
|
start_file: isize,
|
||||||
|
|
||||||
|
rank: bool,
|
||||||
|
|
||||||
|
offset: isize,
|
||||||
|
max_offset: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for BetweenCoordsIter {
|
||||||
|
type Item = Coordinate;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.offset > self.max_offset {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rank = if self.rank {
|
||||||
|
self.start_rank + self.offset
|
||||||
|
} else {
|
||||||
|
self.start_rank
|
||||||
|
};
|
||||||
|
let file = if self.rank {
|
||||||
|
self.start_file
|
||||||
|
} else {
|
||||||
|
self.start_file + self.offset
|
||||||
|
};
|
||||||
|
|
||||||
|
self.offset += 1;
|
||||||
|
Some(Coordinate { rank, file })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Add<Coordinate> for Coordinate {
|
||||||
|
type Output = Coordinate;
|
||||||
|
|
||||||
|
fn add(self, rhs: Coordinate) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
file: self.file + rhs.file,
|
||||||
|
rank: self.rank + rhs.rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Mul<isize> for Coordinate {
|
||||||
|
type Output = Coordinate;
|
||||||
|
|
||||||
|
fn mul(self, rhs: isize) -> Self::Output {
|
||||||
|
Self {
|
||||||
|
rank: self.rank * rhs,
|
||||||
|
file: self.file * rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Board {
|
||||||
|
pub board: [Option<Piece>; 64],
|
||||||
|
pub to_move: Side,
|
||||||
|
pub castling: BySide<Castling>,
|
||||||
|
pub en_passant_target: Option<Coordinate>,
|
||||||
|
pub halfmove_clock: u32,
|
||||||
|
pub fullmove_number: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct Castling {
|
||||||
|
pub king: bool,
|
||||||
|
pub queen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// FEN parsing implementation
|
||||||
|
impl Board {
|
||||||
|
pub fn from_fen(fen: &str) -> Result<Self, FENParseError> {
|
||||||
|
let mut board = [None; 64];
|
||||||
|
let sections = fen.split(' ').collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if sections.len() < 6 {
|
||||||
|
return Err(FENParseError::NotEnoughSections);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::parse_fen_board(&mut board, sections[0])?;
|
||||||
|
|
||||||
|
let to_move = match sections[1] {
|
||||||
|
"w" => Side::White,
|
||||||
|
"b" => Side::Black,
|
||||||
|
c => return Err(NotationError::InvalidSide(c.to_owned()).into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let castling = Self::parse_fen_castling(sections[2])?;
|
||||||
|
|
||||||
|
let en_passant_target = match sections[3] {
|
||||||
|
"-" => None,
|
||||||
|
s => Some(Coordinate::parse_algebraic(s)?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let halfmove_clock = sections[4].parse::<u32>()?;
|
||||||
|
let fullmove_number = sections[5].parse::<u32>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
board,
|
||||||
|
to_move,
|
||||||
|
castling,
|
||||||
|
en_passant_target,
|
||||||
|
halfmove_clock,
|
||||||
|
fullmove_number,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_fen_castling(fen: &str) -> Result<BySide<Castling>, NotationError> {
|
||||||
|
let mut castling = BySide::<Castling>::default();
|
||||||
|
|
||||||
|
if fen != "-" {
|
||||||
|
for c in fen.chars() {
|
||||||
|
let side = Side::from(c);
|
||||||
|
let castling = castling.get_mut(side);
|
||||||
|
match c {
|
||||||
|
'q' | 'Q' => {
|
||||||
|
castling.queen = true;
|
||||||
|
}
|
||||||
|
'k' | 'K' => {
|
||||||
|
castling.king = true;
|
||||||
|
}
|
||||||
|
_ => return Err(NotationError::InvalidPiece(c)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(castling)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_fen_board(target: &mut [Option<Piece>; 64], board: &str) -> Result<(), FENParseError> {
|
||||||
|
enum State {
|
||||||
|
ParsingRank { file: usize },
|
||||||
|
WaitingForSlash,
|
||||||
|
EndOfString,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = State::ParsingRank { file: 0 };
|
||||||
|
let mut rank = 7;
|
||||||
|
|
||||||
|
for c in board.chars() {
|
||||||
|
match &mut state {
|
||||||
|
State::ParsingRank { file } => {
|
||||||
|
match c {
|
||||||
|
'0'..='8' => {
|
||||||
|
*file += c.to_digit(9).unwrap() as usize;
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
target[rank * 8 + *file] = Some(Piece::try_from(c)?);
|
||||||
|
*file += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *file == 8 {
|
||||||
|
state = if rank == 0 {
|
||||||
|
State::EndOfString
|
||||||
|
} else {
|
||||||
|
State::WaitingForSlash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::WaitingForSlash => {
|
||||||
|
if c != '/' {
|
||||||
|
return Err(FENParseError::ExpectedSlash);
|
||||||
|
} else {
|
||||||
|
state = State::ParsingRank { file: 0 };
|
||||||
|
rank -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
State::EndOfString => {
|
||||||
|
// This case should never be reached on a valid FEN string
|
||||||
|
return Err(FENParseError::BoardTooLarge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accessors
|
||||||
|
impl Board {
|
||||||
|
pub fn calc_check_state(&self) -> BySide<bool> {
|
||||||
|
let white_moves = {
|
||||||
|
let mut moves = vec![];
|
||||||
|
mv::generate_pseudolegal_captures(&Board {
|
||||||
|
to_move: Side::White,
|
||||||
|
..*self
|
||||||
|
}, &mut moves);
|
||||||
|
moves
|
||||||
|
};
|
||||||
|
let black_moves = {
|
||||||
|
let mut moves = vec![];
|
||||||
|
mv::generate_pseudolegal_captures(&Board {
|
||||||
|
to_move: Side::Black,
|
||||||
|
..*self
|
||||||
|
}, &mut moves);
|
||||||
|
moves
|
||||||
|
};
|
||||||
|
let black_checked = white_moves.into_iter().any(|m| {
|
||||||
|
self.board[m.to.to_index()] == Some(Side::Black | PieceType::King)
|
||||||
|
});
|
||||||
|
let white_checked = black_moves.into_iter().any(|m| {
|
||||||
|
self.board[m.to.to_index()] == Some(Side::White | PieceType::King)
|
||||||
|
});
|
||||||
|
return BySide {
|
||||||
|
black: black_checked,
|
||||||
|
white: white_checked,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, c: Coordinate) -> &Option<Piece> {
|
||||||
|
&self.board[c.to_index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, c: Coordinate) -> &mut Option<Piece> {
|
||||||
|
&mut self.board[c.to_index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_display(&self) {
|
||||||
|
for rank in (0..8).rev() {
|
||||||
|
for file in 0..8 {
|
||||||
|
if let Some(piece) = self.get(Coordinate { rank, file }) {
|
||||||
|
print!("{}", piece.to_char());
|
||||||
|
} else {
|
||||||
|
print!(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Side {
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Side {
|
||||||
|
pub fn other(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::White => Self::Black,
|
||||||
|
Self::Black => Self::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn back_rank(&self) -> isize {
|
||||||
|
match self {
|
||||||
|
Self::Black => 7,
|
||||||
|
Self::White => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pawn_rank(&self) -> isize {
|
||||||
|
match self {
|
||||||
|
Self::Black => 6,
|
||||||
|
Self::White => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<char> for Side {
|
||||||
|
fn from(c: char) -> Self {
|
||||||
|
if c.is_uppercase() {
|
||||||
|
Self::White
|
||||||
|
} else {
|
||||||
|
Self::Black
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum PieceType {
|
||||||
|
King,
|
||||||
|
Queen,
|
||||||
|
Rook,
|
||||||
|
Bishop,
|
||||||
|
Knight,
|
||||||
|
Pawn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PieceType {
|
||||||
|
pub fn to_char(self) -> char {
|
||||||
|
match self {
|
||||||
|
Self::King => 'k',
|
||||||
|
Self::Queen => 'q',
|
||||||
|
Self::Rook => 'r',
|
||||||
|
Self::Bishop => 'b',
|
||||||
|
Self::Knight => 'n',
|
||||||
|
Self::Pawn => 'p',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for PieceType {
|
||||||
|
type Error = NotationError;
|
||||||
|
|
||||||
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||||
|
match value.to_ascii_lowercase() {
|
||||||
|
'k' => Ok(Self::King),
|
||||||
|
'q' => Ok(Self::Queen),
|
||||||
|
'r' => Ok(Self::Rook),
|
||||||
|
'b' => Ok(Self::Bishop),
|
||||||
|
'n' => Ok(Self::Knight),
|
||||||
|
'p' => Ok(Self::Pawn),
|
||||||
|
_ => Err(NotationError::InvalidPiece(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct Piece {
|
||||||
|
pub side: Side,
|
||||||
|
pub ty: PieceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Piece {
|
||||||
|
pub fn to_char(&self) -> char {
|
||||||
|
match self.side {
|
||||||
|
Side::Black => self.ty.to_char(),
|
||||||
|
Side::White => self.ty.to_char().to_ascii_uppercase(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for Piece {
|
||||||
|
type Error = FENParseError;
|
||||||
|
|
||||||
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||||
|
let side = Side::from(value);
|
||||||
|
let ty = PieceType::try_from(value)?;
|
||||||
|
Ok(side | ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Piece {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.to_char())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::BitOr<PieceType> for Side {
|
||||||
|
type Output = Piece;
|
||||||
|
|
||||||
|
fn bitor(self, rhs: PieceType) -> Self::Output {
|
||||||
|
Piece {
|
||||||
|
side: self,
|
||||||
|
ty: rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
|
||||||
|
pub struct BySide<T> {
|
||||||
|
pub white: T,
|
||||||
|
pub black: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> BySide<T> {
|
||||||
|
pub fn get(&self, side: Side) -> &T {
|
||||||
|
match side {
|
||||||
|
Side::Black => &self.black,
|
||||||
|
Side::White => &self.white,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, side: Side) -> &mut T {
|
||||||
|
match side {
|
||||||
|
Side::Black => &mut self.black,
|
||||||
|
Side::White => &mut self.white,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
// === FEN LAYOUT PARSER ===
|
||||||
|
#[test]
|
||||||
|
fn start_position_board() {
|
||||||
|
let mut target = [None; 64];
|
||||||
|
let res =
|
||||||
|
Board::parse_fen_board(&mut target, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR");
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_position_board_extra() {
|
||||||
|
let mut target = [None; 64];
|
||||||
|
let res = Board::parse_fen_board(
|
||||||
|
&mut target,
|
||||||
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNRaaaaaaaaa",
|
||||||
|
);
|
||||||
|
assert!(matches!(res, Err(FENParseError::BoardTooLarge)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_position_board_invalid_piece() {
|
||||||
|
let mut target = [None; 64];
|
||||||
|
let res =
|
||||||
|
Board::parse_fen_board(&mut target, "rnbqkbnr/vvvvvvvv/8/8/8/8/PPPPPPPP/RNBQKBNR");
|
||||||
|
assert!(matches!(
|
||||||
|
res,
|
||||||
|
Err(FENParseError::InvalidNotation(NotationError::InvalidPiece(
|
||||||
|
_
|
||||||
|
)))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_position_board_rank_too_short() {
|
||||||
|
let mut target = [None; 64];
|
||||||
|
let res = Board::parse_fen_board(&mut target, "rnbqkbnr/pp/8/8/8/8/PPPPPPPP/RNBQKBNR");
|
||||||
|
assert!(matches!(
|
||||||
|
res,
|
||||||
|
Err(FENParseError::InvalidNotation(NotationError::InvalidPiece(
|
||||||
|
'/'
|
||||||
|
)))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === FULL FEN PARSER ===
|
||||||
|
#[test]
|
||||||
|
fn start_position() {
|
||||||
|
let board =
|
||||||
|
Board::from_fen(crate::constants::START_FEN).expect("FEN string should be parsed");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
board.board,
|
||||||
|
[
|
||||||
|
Some(White | Rook),
|
||||||
|
Some(White | Knight),
|
||||||
|
Some(White | Bishop),
|
||||||
|
Some(White | Queen),
|
||||||
|
Some(White | King),
|
||||||
|
Some(White | Bishop),
|
||||||
|
Some(White | Knight),
|
||||||
|
Some(White | Rook),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
Some(White | Pawn),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Pawn),
|
||||||
|
Some(Black | Rook),
|
||||||
|
Some(Black | Knight),
|
||||||
|
Some(Black | Bishop),
|
||||||
|
Some(Black | Queen),
|
||||||
|
Some(Black | King),
|
||||||
|
Some(Black | Bishop),
|
||||||
|
Some(Black | Knight),
|
||||||
|
Some(Black | Rook),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(board.to_move, White);
|
||||||
|
assert_eq!(
|
||||||
|
board.castling,
|
||||||
|
BySide {
|
||||||
|
white: Castling {
|
||||||
|
king: true,
|
||||||
|
queen: true,
|
||||||
|
},
|
||||||
|
black: Castling {
|
||||||
|
king: true,
|
||||||
|
queen: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(board.en_passant_target, None);
|
||||||
|
assert_eq!(board.halfmove_clock, 0);
|
||||||
|
assert_eq!(board.fullmove_number, 1);
|
||||||
|
}
|
||||||
|
}
|
404
src/chess/mv.rs
Normal file
404
src/chess/mv.rs
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use crate::{prelude::*, chess::Side};
|
||||||
|
use super::{Board, Coordinate, Piece, PieceType, Castling};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
|
pub struct Move {
|
||||||
|
pub from: Coordinate,
|
||||||
|
pub to: Coordinate,
|
||||||
|
pub set_en_passant: Option<Coordinate>,
|
||||||
|
pub other: Option<Box<Move>>,
|
||||||
|
pub promotions: Option<Vec<Piece>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Move {
|
||||||
|
pub fn new(from: Coordinate, to: Coordinate) -> Self {
|
||||||
|
Self {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
set_en_passant: None,
|
||||||
|
other: None,
|
||||||
|
promotions: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make(&self, board: &Board) -> Board {
|
||||||
|
let mut board = self.make_inner(board);
|
||||||
|
board.to_move = board.to_move.other();
|
||||||
|
board
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_inner(&self, board: &Board) -> Board {
|
||||||
|
let mut board = board.clone();
|
||||||
|
let piece = board.get(self.from).expect("cannot make a move with no piece");
|
||||||
|
if piece.ty == King {
|
||||||
|
*board.castling.get_mut(board.to_move) = Castling {
|
||||||
|
king: false,
|
||||||
|
queen: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let start_rank = board.to_move.back_rank();
|
||||||
|
|
||||||
|
if piece.ty == Rook && self.from.rank == start_rank {
|
||||||
|
if self.from.file == 0 {
|
||||||
|
board.castling.get_mut(board.to_move).queen = false;
|
||||||
|
}
|
||||||
|
if self.from.file == 7 {
|
||||||
|
board.castling.get_mut(board.to_move).king = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*board.get_mut(self.from) = None;
|
||||||
|
{
|
||||||
|
let captured_piece = board.get(self.to);
|
||||||
|
if matches!(captured_piece, Some(piece) if piece.ty == Rook && self.to.rank == board.to_move.other().back_rank()) {
|
||||||
|
let castling = board.castling.get_mut(board.to_move.other());
|
||||||
|
match self.to.file {
|
||||||
|
0 => castling.queen = false,
|
||||||
|
7 => castling.king = false,
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_piece = if let Some(promotions) = &self.promotions {
|
||||||
|
match promotions.len() {
|
||||||
|
0 => piece,
|
||||||
|
1 => promotions[0],
|
||||||
|
_ => panic!("tried to make a move with more than one promotion"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
piece
|
||||||
|
};
|
||||||
|
|
||||||
|
*board.get_mut(self.to) = Some(new_piece);
|
||||||
|
|
||||||
|
if piece.ty == Pawn {
|
||||||
|
if let Some(en_passant_target) = board.en_passant_target {
|
||||||
|
if en_passant_target == self.to {
|
||||||
|
*board.get_mut(Coordinate {
|
||||||
|
rank: self.from.rank,
|
||||||
|
file: self.to.file,
|
||||||
|
}) = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
board.en_passant_target = self.set_en_passant;
|
||||||
|
|
||||||
|
if let Some(other) = &self.other {
|
||||||
|
other.make_inner(&board)
|
||||||
|
} else {
|
||||||
|
board
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CARDINALS: [Coordinate; 4] = [
|
||||||
|
Coordinate { rank: 1, file: 0 },
|
||||||
|
Coordinate { rank: -1, file: 0 },
|
||||||
|
Coordinate { rank: 0, file: 1 },
|
||||||
|
Coordinate { rank: 0, file: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIAGONALS: [Coordinate; 4] = [
|
||||||
|
Coordinate { rank: 1, file: 1 },
|
||||||
|
Coordinate { rank: -1, file: -1 },
|
||||||
|
Coordinate { rank: -1, file: 1 },
|
||||||
|
Coordinate { rank: 1, file: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Wish there was a way to statically concatenate CARDIANALS and DIAGONALS so I didn't have to copy-paste these.
|
||||||
|
const ALL_DIRECTIONS: [Coordinate; 8] = [
|
||||||
|
Coordinate { rank: 1, file: 0 },
|
||||||
|
Coordinate { rank: -1, file: 0 },
|
||||||
|
Coordinate { rank: 0, file: 1 },
|
||||||
|
Coordinate { rank: 0, file: -1 },
|
||||||
|
Coordinate { rank: 1, file: 1 },
|
||||||
|
Coordinate { rank: -1, file: -1 },
|
||||||
|
Coordinate { rank: -1, file: 1 },
|
||||||
|
Coordinate { rank: 1, file: -1 },
|
||||||
|
];
|
||||||
|
// And no I can't be bothered to write a macro lol
|
||||||
|
|
||||||
|
const KNIGHT: [Coordinate; 8] = [
|
||||||
|
Coordinate { rank: 1, file: 2 },
|
||||||
|
Coordinate { rank: 2, file: 1 },
|
||||||
|
Coordinate { rank: -1, file: -2 },
|
||||||
|
Coordinate { rank: -2, file: -1 },
|
||||||
|
Coordinate { rank: -1, file: 2 },
|
||||||
|
Coordinate { rank: -2, file: 1 },
|
||||||
|
Coordinate { rank: 1, file: -2 },
|
||||||
|
Coordinate { rank: 2, file: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PAWN_CAPTURES: [Coordinate; 2] = [
|
||||||
|
Coordinate { rank: 0, file: 1 },
|
||||||
|
Coordinate { rank: 0, file: -1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
enum MoveResult {
|
||||||
|
NoCapture,
|
||||||
|
Invalid,
|
||||||
|
Capture,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_if<F: Fn(MoveResult) -> bool>(board: &Board, moves: &mut Vec<Move>, mv: Move, predicate: F) -> MoveResult {
|
||||||
|
if !mv.to.is_board_position() {
|
||||||
|
return MoveResult::Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_piece = board.board[mv.to.to_index()];
|
||||||
|
|
||||||
|
let result = if let Some(target_piece) = target_piece {
|
||||||
|
if target_piece.side == board.to_move {
|
||||||
|
MoveResult::Invalid
|
||||||
|
} else {
|
||||||
|
MoveResult::Capture
|
||||||
|
}
|
||||||
|
} else if matches!(board.get(mv.from), Some(piece) if piece.ty == Pawn) && matches!(board.en_passant_target, Some(target) if target == mv.to) {
|
||||||
|
MoveResult::Capture
|
||||||
|
} else {
|
||||||
|
MoveResult::NoCapture
|
||||||
|
};
|
||||||
|
|
||||||
|
if predicate(result) {
|
||||||
|
moves.push(mv);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_if_valid(board: &Board, moves: &mut Vec<Move>, mv: Move) -> MoveResult {
|
||||||
|
add_if(board, moves, mv, |r| r != MoveResult::Invalid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_if_not_capture(board: &Board, moves: &mut Vec<Move>, mv: Move) -> MoveResult {
|
||||||
|
add_if(board, moves, mv, |r| r == MoveResult::NoCapture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_if_capture(board: &Board, moves: &mut Vec<Move>, mv: Move) -> MoveResult {
|
||||||
|
add_if(board, moves, mv, |r| r == MoveResult::Capture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_castling_moves(board: &Board, moves: &mut Vec<Move>) {
|
||||||
|
fn test_move(board: &Board, king_from: Coordinate, king_to: Coordinate, rook_from: Coordinate, rook_to: Coordinate) -> Option<Move> {
|
||||||
|
let king = board.get(king_from);
|
||||||
|
let rook = board.get(rook_from);
|
||||||
|
let (king, rook) = match (king, rook) {
|
||||||
|
(Some(king), Some(rook)) => (*king, *rook),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
if king.ty != King || rook.ty != Rook {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if board.get(king_to).is_some() || board.get(rook_to).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for mid in Coordinate::between(king_from, king_to) {
|
||||||
|
if board.get(mid).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let test_state = Move::new(king_from, mid).make(&board);
|
||||||
|
if *test_state.calc_check_state().get(board.to_move) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for mid in Coordinate::between(rook_from, rook_to) {
|
||||||
|
if board.get(mid).is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Move {
|
||||||
|
from: king_from,
|
||||||
|
to: king_to,
|
||||||
|
other: Some(Box::new(Move::new(rook_from, rook_to))),
|
||||||
|
promotions: None,
|
||||||
|
set_en_passant: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let castling = board.castling.get(board.to_move);
|
||||||
|
|
||||||
|
// Cannot castle out of check
|
||||||
|
if (castling.queen || castling.king) && *board.calc_check_state().get(board.to_move) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rank = match board.to_move {
|
||||||
|
Black => 7,
|
||||||
|
White => 0,
|
||||||
|
};
|
||||||
|
let king = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 4,
|
||||||
|
};
|
||||||
|
if castling.queen {
|
||||||
|
let rook = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 0,
|
||||||
|
};
|
||||||
|
let king_target = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 2,
|
||||||
|
};
|
||||||
|
let rook_target = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 3,
|
||||||
|
};
|
||||||
|
if let Some(mv) = test_move(board, king, king_target, rook, rook_target) {
|
||||||
|
moves.push(mv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if castling.king {
|
||||||
|
let rook = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 7,
|
||||||
|
};
|
||||||
|
let king_target = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 6,
|
||||||
|
};
|
||||||
|
let rook_target = Coordinate {
|
||||||
|
rank,
|
||||||
|
file: 5,
|
||||||
|
};
|
||||||
|
if let Some(mv) = test_move(board, king, king_target, rook, rook_target) {
|
||||||
|
moves.push(mv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_pawn_moves(board: &Board, moves: &mut Vec<Move>) {
|
||||||
|
fn pawn_move(side: Side, from: Coordinate, to: Coordinate, set_en_passant: Option<Coordinate>) -> Move {
|
||||||
|
let promotion_rank = match side {
|
||||||
|
Black => 0,
|
||||||
|
White => 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
let promotions = if to.rank == promotion_rank {
|
||||||
|
Some(vec![
|
||||||
|
side | Queen,
|
||||||
|
side | Rook,
|
||||||
|
side | Bishop,
|
||||||
|
side | Knight,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mv = Move {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
other: None,
|
||||||
|
promotions,
|
||||||
|
set_en_passant,
|
||||||
|
};
|
||||||
|
|
||||||
|
mv
|
||||||
|
}
|
||||||
|
|
||||||
|
let (start_rank, direction) = match board.to_move {
|
||||||
|
Black => (48..56, Coordinate { rank: -1, file: 0 }),
|
||||||
|
White => (8..16, Coordinate { rank: 1, file: 0 }),
|
||||||
|
};
|
||||||
|
let pawn = board.to_move | Pawn;
|
||||||
|
for (index, piece) in board.board.iter().enumerate() {
|
||||||
|
match piece {
|
||||||
|
None => continue,
|
||||||
|
Some(p) if *p != pawn => continue,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let from = Coordinate::from_index(index);
|
||||||
|
let forward_res = add_if_not_capture(board, moves, pawn_move(board.to_move, from, from + direction, None));
|
||||||
|
if forward_res == MoveResult::NoCapture && start_rank.contains(&index) {
|
||||||
|
add_if_not_capture(board, moves, pawn_move(board.to_move, from, from + (direction * 2), Some(from + direction)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for capture in PAWN_CAPTURES {
|
||||||
|
add_if_capture(board, moves, pawn_move(board.to_move, from, from + direction + capture, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_line_moves(
|
||||||
|
board: &Board,
|
||||||
|
moves: &mut Vec<Move>,
|
||||||
|
ty: PieceType,
|
||||||
|
directions: &[Coordinate],
|
||||||
|
distance_limit: isize,
|
||||||
|
) {
|
||||||
|
let target_piece = board.to_move | ty;
|
||||||
|
for (index, piece) in board.board.iter().enumerate() {
|
||||||
|
if let Some(piece) = piece {
|
||||||
|
if piece != &target_piece {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let from = Coordinate::from_index(index);
|
||||||
|
for direction in directions {
|
||||||
|
let mut multiplier = 1;
|
||||||
|
while add_if_valid(
|
||||||
|
board,
|
||||||
|
moves,
|
||||||
|
Move::new(from, from + (*direction * multiplier)),
|
||||||
|
) == MoveResult::NoCapture
|
||||||
|
&& multiplier < distance_limit
|
||||||
|
{
|
||||||
|
multiplier += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `generate_pseudolegal`, but excludes castling moves
|
||||||
|
pub fn generate_pseudolegal_captures(board: &Board, moves: &mut Vec<Move>) {
|
||||||
|
generate_line_moves(board, moves, King, &ALL_DIRECTIONS, 1);
|
||||||
|
generate_line_moves(board, moves, Queen, &ALL_DIRECTIONS, 8);
|
||||||
|
generate_line_moves(board, moves, Rook, &CARDINALS, 8);
|
||||||
|
generate_line_moves(board, moves, Knight, &KNIGHT, 1);
|
||||||
|
generate_line_moves(board, moves, Bishop, &DIAGONALS, 8);
|
||||||
|
generate_pawn_moves(board, moves);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_pseudolegal(board: &Board, moves: &mut Vec<Move>) {
|
||||||
|
generate_pseudolegal_captures(board, moves);
|
||||||
|
generate_castling_moves(board, moves);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_legal(board: &Board) -> Vec<Move> {
|
||||||
|
let mut moves = vec![];
|
||||||
|
generate_pseudolegal(board, &mut moves);
|
||||||
|
moves.into_iter().filter(|mv| {
|
||||||
|
if mv.other.is_some() {
|
||||||
|
// Cannot castle out of check
|
||||||
|
if *board.calc_check_state().get(board.to_move) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let test_board = mv.make(board);
|
||||||
|
!*test_board.calc_check_state().get(board.to_move)
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::constants::START_FEN;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn start_position_pseudolegal() {
|
||||||
|
let board = Board::from_fen(START_FEN).expect("valid board");
|
||||||
|
let mut moves = vec![];
|
||||||
|
generate_pseudolegal(&board, &mut moves);
|
||||||
|
assert_eq!(moves.len(), 20);
|
||||||
|
}
|
||||||
|
}
|
66
src/chess/perft.rs
Normal file
66
src/chess/perft.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::prelude::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
fn perft_board(board: &Board, depth: u64, start_depth: u64) -> u64 {
|
||||||
|
if depth == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let mut moves = vec![];
|
||||||
|
crate::chess::mv::generate_pseudolegal(board, &mut moves);
|
||||||
|
|
||||||
|
let count = moves.into_par_iter().map(|mv| {
|
||||||
|
let mut count = 0;
|
||||||
|
if let Some(promotions) = mv.promotions {
|
||||||
|
for promotion in promotions {
|
||||||
|
let mv = Move {
|
||||||
|
promotions: Some(vec![promotion]),
|
||||||
|
other: mv.other.clone(),
|
||||||
|
..mv
|
||||||
|
};
|
||||||
|
let new_board = mv.make(board);
|
||||||
|
let check = new_board.calc_check_state();
|
||||||
|
if *check.get(board.to_move) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let this_count = perft_board(&new_board, depth - 1, start_depth);
|
||||||
|
count += this_count;
|
||||||
|
#[cfg(test)]
|
||||||
|
if depth == start_depth {
|
||||||
|
println!("{from}{to}: {this_count}", from = mv.from, to = mv.to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let new_board = mv.make(board);
|
||||||
|
if *new_board.calc_check_state().get(board.to_move) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let this_count = perft_board(&new_board, depth - 1, start_depth);
|
||||||
|
count += this_count;
|
||||||
|
#[cfg(test)]
|
||||||
|
if depth == start_depth {
|
||||||
|
println!("{from}{to}: {this_count}", from = mv.from, to = mv.to);
|
||||||
|
// println!("mv={mv:?} {piece:?}", piece = board.get(mv.from));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}).sum();
|
||||||
|
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_perft(fen: &str, depth: u64, expected_positions: u64) -> bool {
|
||||||
|
let board = Board::from_fen(fen).expect("failed to parse position");
|
||||||
|
|
||||||
|
println!("Running perft on position {fen:?} with depth {depth}, expecting {expected_positions} positions...");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
let positions = perft_board(&board, depth, depth);
|
||||||
|
if positions == expected_positions {
|
||||||
|
println!("Passed perft on position {fen:?} with depth {depth}, expecting {expected_positions} positions! ");
|
||||||
|
} else {
|
||||||
|
println!("Failed perft on position {fen:?} with depth {depth}, expecting {expected_positions} positions, found {positions}!");
|
||||||
|
}
|
||||||
|
|
||||||
|
positions == expected_positions
|
||||||
|
}
|
6
src/constants.rs
Normal file
6
src/constants.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
pub const START_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||||
|
pub const POSITION_2: &str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 0";
|
||||||
|
pub const POSITION_3: &str = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 0";
|
||||||
|
pub const POSITION_4: &str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1";
|
||||||
|
pub const POSITION_5: &str = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8";
|
||||||
|
pub const POSITION_6: &str = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10";
|
25
src/error.rs
Normal file
25
src/error.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum FENParseError {
|
||||||
|
#[error("not enough sections")]
|
||||||
|
NotEnoughSections,
|
||||||
|
#[error("expected slash")]
|
||||||
|
ExpectedSlash,
|
||||||
|
#[error("board too large")]
|
||||||
|
BoardTooLarge,
|
||||||
|
#[error("invalid notation: {0}")]
|
||||||
|
InvalidNotation(#[from] NotationError),
|
||||||
|
#[error("{0}")]
|
||||||
|
InvalidCount(#[from] std::num::ParseIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum NotationError {
|
||||||
|
#[error("expected length {length} for notation, expected: {expected}")]
|
||||||
|
InvalidLength { length: usize, expected: usize },
|
||||||
|
#[error("invalid piece character: {0}")]
|
||||||
|
InvalidPiece(char),
|
||||||
|
#[error("invalid side: {0}")]
|
||||||
|
InvalidSide(String),
|
||||||
|
#[error("{0:?} is invalid notation")]
|
||||||
|
Other(String),
|
||||||
|
}
|
144
src/game.rs
Normal file
144
src/game.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
use xtra::{prelude::*, WeakAddress};
|
||||||
|
|
||||||
|
use crate::{prelude::{Board, Move}, constants::START_FEN, chess::{Side, mv::generate_legal}, player::{Player, OutgoingPlayerEvent, IncomingPlayerEvent}};
|
||||||
|
|
||||||
|
#[derive(Actor)]
|
||||||
|
pub struct GameManager {
|
||||||
|
games: HashMap<Uuid, Address<ChessGame>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
games: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct JoinGame {
|
||||||
|
pub game_id: Uuid,
|
||||||
|
pub player: WeakAddress<Player>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum JoinGameResponse {
|
||||||
|
Success {
|
||||||
|
side: Side,
|
||||||
|
game: Address<ChessGame>,
|
||||||
|
},
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<JoinGame> for GameManager {
|
||||||
|
type Return = Option<JoinGameResponse>;
|
||||||
|
|
||||||
|
async fn handle(&mut self, join_game: JoinGame, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
if let Some(game) = self.games.get(&join_game.game_id) {
|
||||||
|
let res = game.send(join_game.clone()).await.ok()
|
||||||
|
.map(|r| match r {
|
||||||
|
Some(side) => JoinGameResponse::Success { side, game: game.clone() },
|
||||||
|
None => JoinGameResponse::Full,
|
||||||
|
});
|
||||||
|
if res.is_none() {
|
||||||
|
self.games.remove(&join_game.game_id);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
let game = ChessGame::new(join_game.player);
|
||||||
|
let game = xtra::spawn_tokio(game, Mailbox::unbounded());
|
||||||
|
self.games.insert(join_game.game_id, game.clone());
|
||||||
|
Some(JoinGameResponse::Success {
|
||||||
|
side: Side::White,
|
||||||
|
game,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Actor)]
|
||||||
|
pub struct ChessGame {
|
||||||
|
board: Board,
|
||||||
|
white: WeakAddress<Player>,
|
||||||
|
black: Option<WeakAddress<Player>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<JoinGame> for ChessGame {
|
||||||
|
type Return = Option<Side>;
|
||||||
|
|
||||||
|
async fn handle(&mut self, join_game: JoinGame, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
if self.black.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.black = Some(join_game.player);
|
||||||
|
self.broadcast_new_board();
|
||||||
|
self.send_possible_moves();
|
||||||
|
Some(Side::Black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct IncomingEvent {
|
||||||
|
pub data: IncomingPlayerEvent,
|
||||||
|
pub side: Side,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<IncomingEvent> for ChessGame {
|
||||||
|
type Return = ();
|
||||||
|
|
||||||
|
async fn handle(&mut self, incoming_event: IncomingEvent, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
match incoming_event.data {
|
||||||
|
IncomingPlayerEvent::MakeMove { mv } => {
|
||||||
|
if incoming_event.side != self.board.to_move {
|
||||||
|
tracing::warn!(?incoming_event.side, ?mv, "other player tried to make move");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let legal_moves = generate_legal(&self.board);
|
||||||
|
if legal_moves.contains(&mv) {
|
||||||
|
self.board = mv.make(&self.board);
|
||||||
|
self.broadcast_new_board();
|
||||||
|
self.send_possible_moves();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChessGame {
|
||||||
|
pub fn new(white: WeakAddress<Player>) -> Self {
|
||||||
|
Self {
|
||||||
|
board: Board::from_fen(START_FEN).expect("start fen is invalid"),
|
||||||
|
white,
|
||||||
|
black: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broadcast_new_board(&self) {
|
||||||
|
// TODO: Handle players disconnecting
|
||||||
|
tokio::spawn(self.white.send(OutgoingPlayerEvent::BoardUpdate {
|
||||||
|
board: self.board.board.to_vec(),
|
||||||
|
}));
|
||||||
|
if let Some(black) = &self.black {
|
||||||
|
tokio::spawn(black.send(OutgoingPlayerEvent::BoardUpdate {
|
||||||
|
board: self.board.board.to_vec(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_possible_moves(&self) {
|
||||||
|
let (current, other) = match &self.board.to_move {
|
||||||
|
Side::Black => (self.black.clone().unwrap(), self.white.clone()),
|
||||||
|
Side::White => (self.white.clone(), self.black.clone().unwrap()),
|
||||||
|
};
|
||||||
|
let moves = generate_legal(&self.board);
|
||||||
|
// TODO: handle player disconnects
|
||||||
|
tokio::spawn(current.send(OutgoingPlayerEvent::PossibleMoves { moves }));
|
||||||
|
tokio::spawn(other.send(OutgoingPlayerEvent::PossibleMoves { moves: Vec::new() }));
|
||||||
|
}
|
||||||
|
}
|
18
src/lib.rs
Normal file
18
src/lib.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub mod assets;
|
||||||
|
pub mod chess;
|
||||||
|
pub mod constants;
|
||||||
|
pub mod error;
|
||||||
|
pub mod game;
|
||||||
|
pub mod player;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
pub use crate::chess::Board;
|
||||||
|
pub use crate::chess::Piece;
|
||||||
|
|
||||||
|
pub use crate::chess::PieceType::*;
|
||||||
|
pub use crate::chess::Side::*;
|
||||||
|
|
||||||
|
pub use crate::chess::mv::Move;
|
||||||
|
}
|
29
src/main.rs
Normal file
29
src/main.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use axum::Extension;
|
||||||
|
use chs::{prelude::*, game::GameManager};
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
use xtra::Mailbox;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::filter::EnvFilter::new(
|
||||||
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "debug,hyper=info".into()),
|
||||||
|
))
|
||||||
|
.with(tracing_subscriber::fmt::layer())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
tracing::info!("Hello, world!");
|
||||||
|
|
||||||
|
let game_manager = GameManager::new();
|
||||||
|
let game_manager = xtra::spawn_tokio(game_manager, Mailbox::unbounded());
|
||||||
|
|
||||||
|
let app = chs::routes::routes()
|
||||||
|
.layer(Extension(game_manager))
|
||||||
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
|
axum::Server::bind(&format!("0.0.0.0:3000").parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
78
src/player.rs
Normal file
78
src/player.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use futures::{channel::mpsc::UnboundedSender, SinkExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use xtra::prelude::*;
|
||||||
|
|
||||||
|
use crate::{prelude::*, chess::Side, game::{ChessGame, IncomingEvent}};
|
||||||
|
|
||||||
|
#[derive(Actor)]
|
||||||
|
pub struct Player {
|
||||||
|
sink: UnboundedSender<OutgoingPlayerEvent>,
|
||||||
|
game: Option<GameInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GameInfo {
|
||||||
|
pub side: Side,
|
||||||
|
pub game: Address<ChessGame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub fn new(sink: UnboundedSender<OutgoingPlayerEvent>) -> Self {
|
||||||
|
Self {
|
||||||
|
sink,
|
||||||
|
game: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(tag = "event", content = "data")]
|
||||||
|
pub enum OutgoingPlayerEvent {
|
||||||
|
BoardUpdate {
|
||||||
|
board: Vec<Option<Piece>>,
|
||||||
|
},
|
||||||
|
PossibleMoves {
|
||||||
|
moves: Vec<Move>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<OutgoingPlayerEvent> for Player {
|
||||||
|
type Return = ();
|
||||||
|
|
||||||
|
async fn handle(&mut self, outgoing_event: OutgoingPlayerEvent, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
// TODO: Better error handling
|
||||||
|
self.sink.send(outgoing_event).await.expect("failed to send outgoing event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(tag = "event", content = "data")]
|
||||||
|
pub enum IncomingPlayerEvent {
|
||||||
|
MakeMove {
|
||||||
|
#[serde(rename = "move")]
|
||||||
|
mv: Move,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<IncomingPlayerEvent> for Player {
|
||||||
|
type Return = ();
|
||||||
|
|
||||||
|
async fn handle(&mut self, incoming_event: IncomingPlayerEvent, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
if let Some(game) = &self.game {
|
||||||
|
game.game.send(IncomingEvent {
|
||||||
|
data: incoming_event,
|
||||||
|
side: game.side,
|
||||||
|
}).await.expect("game disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handler<GameInfo> for Player {
|
||||||
|
type Return = ();
|
||||||
|
|
||||||
|
async fn handle(&mut self, game_info: GameInfo, _ctx: &mut Context<Self>) -> Self::Return {
|
||||||
|
self.game = Some(game_info);
|
||||||
|
}
|
||||||
|
}
|
121
src/routes.rs
Normal file
121
src/routes.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
use axum::{Router, extract::{WebSocketUpgrade, Path, ws::{Message, WebSocket}}, response::IntoResponse, routing::{get, get_service}, http::{StatusCode, Uri}, Extension};
|
||||||
|
use futures::{StreamExt, SinkExt, stream::{SplitStream, SplitSink}, channel::mpsc::{self, UnboundedSender}};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use xtra::{Mailbox, Address};
|
||||||
|
|
||||||
|
use crate::{player::{Player, IncomingPlayerEvent, OutgoingPlayerEvent, GameInfo}, game::{GameManager, JoinGame, JoinGameResponse}};
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
use crate::assets::StaticFile;
|
||||||
|
|
||||||
|
pub fn routes() -> Router {
|
||||||
|
let router = Router::new()
|
||||||
|
.route("/ws/:id", get(ws_handler));
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let router = router
|
||||||
|
.route("/", get(index))
|
||||||
|
.fallback(get(fallback));
|
||||||
|
|
||||||
|
router
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
async fn index() -> impl IntoResponse {
|
||||||
|
StaticFile("index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
async fn fallback(uri: Uri) -> impl IntoResponse {
|
||||||
|
StaticFile(uri.path().trim_start_matches('/').to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ws_handler(
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Extension(game_manager): Extension<Address<GameManager>>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
ws.on_upgrade(move |ws| handle_socket(ws, id, game_manager))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_socket(socket: WebSocket, id: Uuid, game_manager: Address<GameManager>) {
|
||||||
|
let (tx, rx) = socket.split();
|
||||||
|
|
||||||
|
let (tx, tx_task) = socket_send(tx);
|
||||||
|
|
||||||
|
let player = Player::new(tx);
|
||||||
|
let player = xtra::spawn_tokio(player, Mailbox::unbounded());
|
||||||
|
|
||||||
|
let res = game_manager.send(JoinGame { game_id: id, player: player.downgrade() }).await
|
||||||
|
.expect("game manager disconnected");
|
||||||
|
|
||||||
|
if let Some(res) = res {
|
||||||
|
if let JoinGameResponse::Success { side, game } = res {
|
||||||
|
player.send(GameInfo { side, game }).await.expect("player disconnected");
|
||||||
|
let rx_task = socket_recv(rx, player);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = rx_task => {},
|
||||||
|
_ = tx_task => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_send(mut tx: SplitSink<WebSocket, Message>) -> (UnboundedSender<OutgoingPlayerEvent>, JoinHandle<()>) {
|
||||||
|
let (message_tx, mut rx) = mpsc::unbounded();
|
||||||
|
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
while let Some(message) = rx.next().await {
|
||||||
|
match serde_json::to_string(&message) {
|
||||||
|
Ok(json) => {
|
||||||
|
if tx.send(Message::Text(json)).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(?e, ?message, "failed to encode outgoing message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(message_tx, task)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socket_recv(mut rx: SplitStream<WebSocket>, player: Address<Player>) -> JoinHandle<()> {
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
while let Some(msg) = rx.next().await {
|
||||||
|
if let Ok(msg) = msg {
|
||||||
|
match msg {
|
||||||
|
Message::Text(t) => {
|
||||||
|
let message = serde_json::from_str::<IncomingPlayerEvent>(&t);
|
||||||
|
match message {
|
||||||
|
Ok(message) => {
|
||||||
|
if player.send(message).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(?e, "client send invalid data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::Close(_) => {
|
||||||
|
tracing::info!("client disconnected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame => {
|
||||||
|
tracing::warn!(?frame, "client sent invalid frame type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::info!("client disconnected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task
|
||||||
|
}
|
48
tests/perft.rs
Normal file
48
tests/perft.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
macro_rules! perft_test {
|
||||||
|
($name:ident, $fen:expr, $depth:expr, $expected:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let result = chs::chess::perft::run_perft($fen, $depth, $expected);
|
||||||
|
assert!(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mod perft {
|
||||||
|
use chs::constants::*;
|
||||||
|
|
||||||
|
perft_test!(start_fen_depth_0, START_FEN, 0, 1);
|
||||||
|
perft_test!(start_fen_depth_1, START_FEN, 1, 20);
|
||||||
|
perft_test!(start_fen_depth_2, START_FEN, 2, 400);
|
||||||
|
perft_test!(start_fen_depth_3, START_FEN, 3, 8_902);
|
||||||
|
perft_test!(start_fen_depth_4, START_FEN, 4, 197_281);
|
||||||
|
perft_test!(start_fen_depth_5, START_FEN, 5, 4_865_609);
|
||||||
|
|
||||||
|
perft_test!(position_2_depth_1, POSITION_2, 1, 48);
|
||||||
|
perft_test!(position_2_depth_2, POSITION_2, 2, 2_039);
|
||||||
|
perft_test!(position_2_depth_3, POSITION_2, 3, 97_862);
|
||||||
|
perft_test!(position_2_depth_4, POSITION_2, 4, 4_085_603);
|
||||||
|
|
||||||
|
perft_test!(position_3_depth_1, POSITION_3, 1, 14);
|
||||||
|
perft_test!(position_3_depth_2, POSITION_3, 2, 191);
|
||||||
|
perft_test!(position_3_depth_3, POSITION_3, 3, 2_812);
|
||||||
|
perft_test!(position_3_depth_4, POSITION_3, 4, 43_238);
|
||||||
|
perft_test!(position_3_depth_5, POSITION_3, 5, 674_624);
|
||||||
|
perft_test!(position_3_depth_6, POSITION_3, 6, 11_030_083);
|
||||||
|
|
||||||
|
perft_test!(position_4_depth_1, POSITION_4, 1, 6);
|
||||||
|
perft_test!(position_4_depth_2, POSITION_4, 2, 264);
|
||||||
|
perft_test!(position_4_depth_3, POSITION_4, 3, 9_467);
|
||||||
|
perft_test!(position_4_depth_4, POSITION_4, 4, 422_333);
|
||||||
|
perft_test!(position_4_depth_5, POSITION_4, 5, 15_833_292);
|
||||||
|
|
||||||
|
perft_test!(position_5_depth_1, POSITION_5, 1, 44);
|
||||||
|
perft_test!(position_5_depth_2, POSITION_5, 2, 1_486);
|
||||||
|
perft_test!(position_5_depth_3, POSITION_5, 3, 62_379);
|
||||||
|
perft_test!(position_5_depth_4, POSITION_5, 4, 2_103_487);
|
||||||
|
|
||||||
|
perft_test!(position_6_depth_1, POSITION_6, 1, 46);
|
||||||
|
perft_test!(position_6_depth_2, POSITION_6, 2, 2_079);
|
||||||
|
perft_test!(position_6_depth_3, POSITION_6, 3, 89_890);
|
||||||
|
perft_test!(position_6_depth_4, POSITION_6, 4, 3_894_594);
|
||||||
|
}
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strict": true,
|
||||||
|
}
|
||||||
|
}
|
8
tsconfig.node.json
Normal file
8
tsconfig.node.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
21
vite.config.ts
Normal file
21
vite.config.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import solidPlugin from 'vite-plugin-solid';
|
||||||
|
import devtools from 'solid-devtools/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
solidPlugin(),
|
||||||
|
devtools({
|
||||||
|
// Will automatically add names when creating signals, memos, stores, or mutables
|
||||||
|
name: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
clientPort: parseInt(process.env.CLIENT_PORT || '5173'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue