feat: win conditions
This commit is contained in:
parent
c58753cfd3
commit
1b60f8aee3
8 changed files with 124 additions and 7 deletions
|
@ -1,10 +1,11 @@
|
|||
import { Component, createSignal, Match, Show, Switch } from 'solid-js';
|
||||
import { Component, createEffect, createSignal, Match, Show, 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, Side } from './events';
|
||||
import { Board as BoardData, GameEvent, Move, ServerChessEvent, Side } from './events';
|
||||
import Board from './components/Board';
|
||||
import Popup from './components/Popup';
|
||||
|
||||
interface Props {
|
||||
gameId: string;
|
||||
|
@ -14,6 +15,7 @@ const App: Component<Props> = (props) => {
|
|||
const [board, setBoard] = createSignal<BoardData>(Array(64).fill(null));
|
||||
const [possibleMoves, setPossibleMoves] = createSignal<Move[]>([]);
|
||||
const [side, setSide] = createSignal<Side | null>(null);
|
||||
const [currentEvent, setCurrentEvent] = createSignal<GameEvent | null>(null);
|
||||
|
||||
function handleEvent(e: MessageEvent<string>) {
|
||||
const data = JSON.parse(e.data);
|
||||
|
@ -24,6 +26,10 @@ const App: Component<Props> = (props) => {
|
|||
setPossibleMoves(event.data.moves);
|
||||
} else if (event.event === 'StartGame') {
|
||||
setSide(event.data.side);
|
||||
} else if (event.event === 'GameEvent') {
|
||||
setCurrentEvent(event.data);
|
||||
} else if (event.event === 'ClearGameEvent') {
|
||||
setCurrentEvent(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +40,7 @@ const App: Component<Props> = (props) => {
|
|||
setSide(null);
|
||||
setBoard(Array(64).fill(null));
|
||||
setPossibleMoves([]);
|
||||
setCurrentEvent(null);
|
||||
}
|
||||
|
||||
function makeMove(move: Move) {
|
||||
|
@ -57,7 +64,10 @@ const App: Component<Props> = (props) => {
|
|||
<h1>Waiting for opponent...</h1>
|
||||
<Spinner />
|
||||
</>}>
|
||||
<Board board={board} moves={possibleMoves} makeMove={makeMove} />
|
||||
<Board board={board} moves={possibleMoves} makeMove={makeMove} side={side()!} />
|
||||
</Show>
|
||||
<Show when={currentEvent() !== null}>
|
||||
<Popup {...currentEvent()!} />
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
|
21
src-web/components/Popup.css
Normal file
21
src-web/components/Popup.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.popup {
|
||||
min-width: 512px;
|
||||
max-width: 100vw;
|
||||
/* min-height: 480px; */
|
||||
|
||||
background-color: #000000DD;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background-color: #00000088;
|
||||
}
|
20
src-web/components/Popup.tsx
Normal file
20
src-web/components/Popup.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Component } from "solid-js";
|
||||
import './Popup.css';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const Popup: Component<Props> = (props) => {
|
||||
console.log('popup opened', props.title, props.message);
|
||||
|
||||
return <div class="popup-container">
|
||||
<section class="popup">
|
||||
<h2>{props.title}</h2>
|
||||
<p>{props.message}</p>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Popup;
|
|
@ -57,10 +57,19 @@ export const StartGameEvent = z.object({
|
|||
|
||||
export type StartGameEvent = z.infer<typeof StartGameEvent>;
|
||||
|
||||
export const GameEvent = z.object({
|
||||
title: z.string(),
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
export type GameEvent = z.infer<typeof GameEvent>;
|
||||
|
||||
export const ServerChessEvent = z.discriminatedUnion("event", [
|
||||
z.object({ event: z.literal('BoardUpdate'), data: BoardUpdateEvent }),
|
||||
z.object({ event: z.literal('PossibleMoves'), data: PossibleMovesEvent }),
|
||||
z.object({ event: z.literal('StartGame'), data: StartGameEvent })
|
||||
z.object({ event: z.literal('StartGame'), data: StartGameEvent }),
|
||||
z.object({ event: z.literal('GameEvent'), data: GameEvent }),
|
||||
z.object({ event: z.literal('ClearGameEvent') }),
|
||||
]);
|
||||
|
||||
export type ServerChessEvent = z.infer<typeof ServerChessEvent>;
|
||||
|
|
|
@ -389,6 +389,15 @@ impl From<char> for Side {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for Side {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
Self::White => "White",
|
||||
Self::Black => "Black",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PieceType {
|
||||
|
|
|
@ -5,3 +5,6 @@ pub const POSITION_4: &str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1
|
|||
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";
|
||||
|
||||
pub const CHECKMATE_POSITION: &str = "7k/3Q4/5K2/8/8/8/8/8 w - - 0 0";
|
||||
pub const STALEMATE_POSITION: &str = "7k/R7/8/8/4K3/8/8/4R3 w - - 0 0";
|
||||
|
|
49
src/game.rs
49
src/game.rs
|
@ -173,8 +173,51 @@ impl ChessGame {
|
|||
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() }));
|
||||
|
||||
if moves.is_empty() {
|
||||
tracing::info!("no legal moves available!");
|
||||
let check = self.board.calc_check_state();
|
||||
if *check.get(self.board.to_move) {
|
||||
tracing::info!("checkmate!");
|
||||
self.checkmate(self.board.to_move.other());
|
||||
} else {
|
||||
tracing::info!("stalemate!");
|
||||
self.stalemate_no_legal(self.board.to_move);
|
||||
}
|
||||
self.game_end();
|
||||
} else {
|
||||
// TODO: handle player disconnects
|
||||
tokio::spawn(current.send(OutgoingPlayerEvent::PossibleMoves { moves }));
|
||||
tokio::spawn(other.send(OutgoingPlayerEvent::PossibleMoves { moves: Vec::new() }));
|
||||
}
|
||||
}
|
||||
|
||||
fn checkmate(&self, winner: Side) {
|
||||
let event = OutgoingPlayerEvent::GameEvent {
|
||||
title: "Checkmate!".to_owned(),
|
||||
message: format!("{winner} wins!")
|
||||
};
|
||||
tokio::spawn(self.white.send(event.clone()));
|
||||
if let Some(black) = &self.black {
|
||||
tokio::spawn(black.send(event));
|
||||
}
|
||||
}
|
||||
|
||||
fn stalemate_no_legal(&self, no_moves: Side) {
|
||||
let event = OutgoingPlayerEvent::GameEvent {
|
||||
title: "Stalemate".to_owned(),
|
||||
message: format!("{no_moves} has no legal moves to make!")
|
||||
};
|
||||
tokio::spawn(self.white.send(event.clone()));
|
||||
if let Some(black) = &self.black {
|
||||
tokio::spawn(black.send(event));
|
||||
}
|
||||
}
|
||||
|
||||
fn game_end(&self) {
|
||||
tokio::spawn(self.white.send(OutgoingPlayerEvent::PossibleMoves { moves: vec![] }));
|
||||
if let Some(black) = &self.black {
|
||||
tokio::spawn(black.send(OutgoingPlayerEvent::PossibleMoves { moves: vec![] }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ pub enum OutgoingPlayerEvent {
|
|||
StartGame { side: Side },
|
||||
BoardUpdate { board: Vec<Option<Piece>> },
|
||||
PossibleMoves { moves: Vec<Move> },
|
||||
GameEvent { title: String, message: String },
|
||||
ClearGameEvent,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
Loading…
Reference in a new issue