diff --git a/src-web/App.tsx b/src-web/App.tsx index 7eeca2f..4f1c54b 100644 --- a/src-web/App.tsx +++ b/src-web/App.tsx @@ -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) => { const [board, setBoard] = createSignal(Array(64).fill(null)); const [possibleMoves, setPossibleMoves] = createSignal([]); const [side, setSide] = createSignal(null); + const [currentEvent, setCurrentEvent] = createSignal(null); function handleEvent(e: MessageEvent) { const data = JSON.parse(e.data); @@ -24,6 +26,10 @@ const App: Component = (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) => { setSide(null); setBoard(Array(64).fill(null)); setPossibleMoves([]); + setCurrentEvent(null); } function makeMove(move: Move) { @@ -57,7 +64,10 @@ const App: Component = (props) => {

Waiting for opponent...

}> - + + + + diff --git a/src-web/components/Popup.css b/src-web/components/Popup.css new file mode 100644 index 0000000..5d19603 --- /dev/null +++ b/src-web/components/Popup.css @@ -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; +} diff --git a/src-web/components/Popup.tsx b/src-web/components/Popup.tsx new file mode 100644 index 0000000..571b8bf --- /dev/null +++ b/src-web/components/Popup.tsx @@ -0,0 +1,20 @@ +import { Component } from "solid-js"; +import './Popup.css'; + +interface Props { + title: string; + message: string; +} + +const Popup: Component = (props) => { + console.log('popup opened', props.title, props.message); + + return +} + +export default Popup; diff --git a/src-web/events.ts b/src-web/events.ts index b939545..7769663 100644 --- a/src-web/events.ts +++ b/src-web/events.ts @@ -57,10 +57,19 @@ export const StartGameEvent = z.object({ export type StartGameEvent = z.infer; +export const GameEvent = z.object({ + title: z.string(), + message: z.string(), +}); + +export type GameEvent = z.infer; + 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; diff --git a/src/chess.rs b/src/chess.rs index 33af4ba..2446fc6 100644 --- a/src/chess.rs +++ b/src/chess.rs @@ -389,6 +389,15 @@ impl From 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 { diff --git a/src/constants.rs b/src/constants.rs index 6967b7a..ab8ef33 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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"; diff --git a/src/game.rs b/src/game.rs index 55576f4..22cd2f9 100644 --- a/src/game.rs +++ b/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![] })); + } } } diff --git a/src/player.rs b/src/player.rs index 6a02ce1..e0a37ef 100644 --- a/src/player.rs +++ b/src/player.rs @@ -31,6 +31,8 @@ pub enum OutgoingPlayerEvent { StartGame { side: Side }, BoardUpdate { board: Vec> }, PossibleMoves { moves: Vec }, + GameEvent { title: String, message: String }, + ClearGameEvent, } #[async_trait]