diff --git a/public/sounds/capture.webm b/public/sounds/capture.webm new file mode 100644 index 0000000..5b9a0bb Binary files /dev/null and b/public/sounds/capture.webm differ diff --git a/public/sounds/castle.webm b/public/sounds/castle.webm new file mode 100644 index 0000000..f601a9e Binary files /dev/null and b/public/sounds/castle.webm differ diff --git a/public/sounds/game-end.webm b/public/sounds/game-end.webm new file mode 100644 index 0000000..502eee4 Binary files /dev/null and b/public/sounds/game-end.webm differ diff --git a/public/sounds/game-start.webm b/public/sounds/game-start.webm new file mode 100644 index 0000000..0ac2714 Binary files /dev/null and b/public/sounds/game-start.webm differ diff --git a/public/sounds/illegal-move.webm b/public/sounds/illegal-move.webm new file mode 100644 index 0000000..7af7963 Binary files /dev/null and b/public/sounds/illegal-move.webm differ diff --git a/public/sounds/move-check.webm b/public/sounds/move-check.webm new file mode 100644 index 0000000..5ac91c0 Binary files /dev/null and b/public/sounds/move-check.webm differ diff --git a/public/sounds/move.webm b/public/sounds/move.webm new file mode 100644 index 0000000..4599fe2 Binary files /dev/null and b/public/sounds/move.webm differ diff --git a/public/sounds/promote.webm b/public/sounds/promote.webm new file mode 100644 index 0000000..108199b Binary files /dev/null and b/public/sounds/promote.webm differ diff --git a/src/hooks/useChessActions.ts b/src/hooks/useChessActions.ts index d8caa97..727c40e 100644 --- a/src/hooks/useChessActions.ts +++ b/src/hooks/useChessActions.ts @@ -1,4 +1,5 @@ import { setGameHeaders } from "@/lib/chess"; +import { playIllegalMoveSound, playSoundFromMove } from "@/lib/sounds"; import { Chess, Move } from "chess.js"; import { PrimitiveAtom, useAtom } from "jotai"; import { useCallback } from "react"; @@ -39,17 +40,23 @@ export const useChessActions = (chessAtom: PrimitiveAtom) => { const makeMove = useCallback( (move: { from: string; to: string; promotion?: string }): Move | null => { const newGame = copyGame(); - const result = newGame.move(move); - setGame(newGame); - - return result; + try { + const result = newGame.move(move); + setGame(newGame); + playSoundFromMove(result); + return result; + } catch { + playIllegalMoveSound(); + return null; + } }, [copyGame, setGame] ); const undoMove = useCallback(() => { const newGame = copyGame(); - newGame.undo(); + const move = newGame.undo(); + if (move) playSoundFromMove(move); setGame(newGame); }, [copyGame, setGame]); diff --git a/src/lib/chess.ts b/src/lib/chess.ts index adc1972..8216065 100644 --- a/src/lib/chess.ts +++ b/src/lib/chess.ts @@ -247,3 +247,8 @@ export const getStartingFen = ( return history[0].before; }; + +export const isCheck = (fen: string): boolean => { + const game = new Chess(fen); + return game.inCheck(); +}; diff --git a/src/lib/sounds.ts b/src/lib/sounds.ts new file mode 100644 index 0000000..5e0d648 --- /dev/null +++ b/src/lib/sounds.ts @@ -0,0 +1,35 @@ +import { Move } from "chess.js"; +import { getWhoIsCheckmated, isCheck } from "./chess"; + +const playSound = async (url: string) => { + const audio = new Audio(url); + await audio.play(); +}; + +export const playCaptureSound = () => playSound("/sounds/capture.webm"); +export const playCastleSound = () => playSound("/sounds/castle.webm"); +export const playGameEndSound = () => playSound("/sounds/game-end.webm"); +export const playGameStartSound = () => playSound("/sounds/game-start.webm"); +export const playIllegalMoveSound = () => + playSound("/sounds/illegal-move.webm"); +export const playMoveCheckSound = () => playSound("/sounds/move-check.webm"); +export const playMoveSound = () => playSound("/sounds/move.webm"); +export const playPromoteSound = () => playSound("/sounds/promote.webm"); + +export const playSoundFromMove = async (move: Move | null) => { + if (!move) { + playIllegalMoveSound(); + } else if (getWhoIsCheckmated(move.after)) { + playGameEndSound(); + } else if (isCheck(move.after)) { + playMoveCheckSound(); + } else if (move.promotion) { + playPromoteSound(); + } else if (move.captured) { + playCaptureSound(); + } else if (move.flags.includes("k") || move.flags.includes("q")) { + playCastleSound(); + } else { + playMoveSound(); + } +}; diff --git a/src/sections/analysis/board/index.tsx b/src/sections/analysis/board/index.tsx index c65546d..c26c5c6 100644 --- a/src/sections/analysis/board/index.tsx +++ b/src/sections/analysis/board/index.tsx @@ -40,17 +40,13 @@ export default function Board() { target: Square, piece: string ): boolean => { - try { - const result = makeBoardMove({ - from: source, - to: target, - promotion: piece[1]?.toLowerCase() ?? "q", - }); + const result = makeBoardMove({ + from: source, + to: target, + promotion: piece[1]?.toLowerCase() ?? "q", + }); - return !!result; - } catch { - return false; - } + return !!result; }; const handleSquareLeftClick = () => { diff --git a/src/sections/play/board/index.tsx b/src/sections/play/board/index.tsx index 393da1e..b8bbaad 100644 --- a/src/sections/play/board/index.tsx +++ b/src/sections/play/board/index.tsx @@ -70,17 +70,14 @@ export default function Board() { piece: string ): boolean => { if (!piece || piece[0] !== playerColor || !isGameInProgress) return false; - try { - const result = makeGameMove({ - from: source, - to: target, - promotion: piece[1]?.toLowerCase() ?? "q", - }); - return !!result; - } catch { - return false; - } + const result = makeGameMove({ + from: source, + to: target, + promotion: piece[1]?.toLowerCase() ?? "q", + }); + + return !!result; }; const isPieceDraggable = ({ piece }: { piece: string }): boolean => { diff --git a/src/sections/play/gameInProgress.tsx b/src/sections/play/gameInProgress.tsx index e09a5d7..8e353c2 100644 --- a/src/sections/play/gameInProgress.tsx +++ b/src/sections/play/gameInProgress.tsx @@ -2,6 +2,7 @@ import { Button, CircularProgress, Grid, Typography } from "@mui/material"; import { useAtom, useAtomValue } from "jotai"; import { gameAtom, isGameInProgressAtom } from "./states"; import { useEffect } from "react"; +import { playGameEndSound } from "@/lib/sounds"; export default function GameInProgress() { const game = useAtomValue(gameAtom); @@ -11,6 +12,11 @@ export default function GameInProgress() { if (game.isGameOver()) setIsGameInProgress(false); }, [game, setIsGameInProgress]); + const handleResign = () => { + playGameEndSound(); + setIsGameInProgress(false); + }; + if (!isGameInProgress) return null; return ( @@ -42,7 +48,7 @@ export default function GameInProgress() { xs={12} gap={2} > - diff --git a/src/sections/play/gameSettings/gameSettingsDialog.tsx b/src/sections/play/gameSettings/gameSettingsDialog.tsx index 87af625..277ba14 100644 --- a/src/sections/play/gameSettings/gameSettingsDialog.tsx +++ b/src/sections/play/gameSettings/gameSettingsDialog.tsx @@ -26,6 +26,7 @@ import { gameAtom, } from "../states"; import { useChessActions } from "@/hooks/useChessActions"; +import { playGameStartSound } from "@/lib/sounds"; interface Props { open: boolean; @@ -49,6 +50,7 @@ export default function GameSettingsDialog({ open, onClose }: Props) { blackName: playerColor === Color.Black ? "You" : `Stockfish level ${skillLevel}`, }); + playGameStartSound(); setIsGameInProgress(true); };