Skip to content

179 server create chess puzzle framework #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
70aae37
Short update
ymmot239 Oct 18, 2024
4509c90
intermediate push
ymmot239 Oct 25, 2024
f10e501
Merge branch 'main' of https://github.com/Comet-Robotics/chessBot int…
ymmot239 Oct 25, 2024
cdc3039
fix object problems
LQNguyen05-max Oct 26, 2024
c01eaec
Merge branch '179-server-create-chess-puzzle-framework' of https://gi…
ymmot239 Oct 26, 2024
0ca599e
Added game ended message handler
ymmot239 Oct 26, 2024
e992406
Finished puzzle page
ymmot239 Nov 4, 2024
60651c9
Puzzle cleanup
ymmot239 Nov 4, 2024
f2cf906
added ratings
ymmot239 Nov 5, 2024
8207d23
readability changes
ymmot239 Nov 5, 2024
d41f7b9
navbar fix
ymmot239 Nov 5, 2024
27bba3d
Update puzzles.json
ymmot239 Nov 5, 2024
c9e4728
Update setup-base.tsx
ymmot239 Nov 5, 2024
c3586d1
Merge branch 'main' into 179-server-create-chess-puzzle-framework
ymmot239 Nov 9, 2024
5191b5f
fixed merge mistakes
ymmot239 Nov 9, 2024
f21eb54
Added spectator support
ymmot239 Nov 9, 2024
5f286a8
Update game-manager.ts
ymmot239 Nov 12, 2024
92614b4
Added new puzzle
ymmot239 Mar 24, 2025
9794573
Merge branch 'main' into 179-server-create-chess-puzzle-framework
ymmot239 Mar 24, 2025
dcc13e3
Update game-manager.ts
ymmot239 Mar 24, 2025
aa681e9
Merge branch 'main' into 179-server-create-chess-puzzle-framework
ymmot239 Mar 24, 2025
67155ca
Load Fen
ymmot239 Mar 24, 2025
3bdc67f
Merge branch 'main' into 179-server-create-chess-puzzle-framework
democat3457 Apr 9, 2025
641eb83
Re-add removed comments
democat3457 Apr 9, 2025
436a4f9
Merge remote-tracking branch 'origin/main' into 179-server-create-che…
jasonappah May 12, 2025
aa97663
Rename AI props
jasonappah May 13, 2025
4878053
Remove unused branch
jasonappah May 13, 2025
8c9abc6
address nits
jasonappah May 13, 2025
5e416bc
Lint/Format
jasonappah May 13, 2025
f6bf221
added functionality
ymmot239 May 13, 2025
752b2a1
colin said i should get it done
jasonappah May 13, 2025
99014cc
bye
jasonappah May 13, 2025
cb0a913
Add comment to set chess message
jasonappah May 13, 2025
7b4f97d
format.
jasonappah May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { useQuery } from "@tanstack/react-query";
/**
* A wrapper around `useQuery` which causes the wrapped query to be trigged once each time the page renders.
*/
export function useEffectQuery(
export function useEffectQuery<T>(
queryKey: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
queryFn: () => Promise<any>,
queryFn: () => Promise<T>,
retry?: boolean | number,
) {
// id guarantees query is the same every time
Expand Down
8 changes: 6 additions & 2 deletions src/client/game/game-end-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ function gameOverIcon(reason: GameEndReason, side: Side) {
// check which side won
const whiteWon =
reason === GameFinishedReason.BLACK_CHECKMATED ||
reason === GameInterruptedReason.BLACK_RESIGNED;
reason === GameInterruptedReason.BLACK_RESIGNED ||
reason === GameFinishedReason.PUZZLE_SOLVED;
const blackWon =
reason === GameFinishedReason.WHITE_CHECKMATED ||
reason === GameInterruptedReason.WHITE_RESIGNED;
reason === GameInterruptedReason.WHITE_RESIGNED ||
reason === GameFinishedReason.PUZZLE_SOLVED;

// checks which side is asking and assigns win/lost accordingly
const won = side === Side.WHITE ? whiteWon : blackWon;
Expand Down Expand Up @@ -133,6 +135,8 @@ function gameOverMessage(reason: GameEndReason) {
return "Checkmate - White Wins";
case GameFinishedReason.STALEMATE:
return "Draw - Stalemate";
case GameFinishedReason.PUZZLE_SOLVED:
return "Puzzle Solved";
case GameFinishedReason.THREEFOLD_REPETITION:
return "Draw - Threefold Repetition";
case GameFinishedReason.INSUFFICIENT_MATERIAL:
Expand Down
27 changes: 24 additions & 3 deletions src/client/game/game.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Dispatch, useState } from "react";

import {
GameEndMessage,
GameFinishedMessage,
GameHoldMessage,
GameInterruptedMessage,
SetChessMessage,
} from "../../common/message/game-message";
import { MoveMessage } from "../../common/message/game-message";
import {
Expand Down Expand Up @@ -35,14 +37,28 @@ function getMessageHandler(
chess: ChessEngine,
setChess: Dispatch<ChessEngine>,
setGameInterruptedReason: Dispatch<GameInterruptedReason>,
setGameEndedReason: Dispatch<GameEndReason>,
setGameHoldReason: Dispatch<GameHoldReason>,
): MessageHandler {
return (message) => {
if (message instanceof MoveMessage) {
// Must be a new instance of ChessEngine to trigger UI redraw
setChess(chess.copy(message.move));
// short wait so the pieces don't teleport into place
setTimeout(() => {
setChess(chess.copy(message.move));
}, 500);
} else if (message instanceof SetChessMessage) {
const fen = message.chess;
if (fen) {
setTimeout(() => {
chess.loadFen(fen);
setChess(chess.copy());
}, 500);
}
} else if (message instanceof GameInterruptedMessage) {
setGameInterruptedReason(message.reason);
} else if (message instanceof GameEndMessage) {
setGameEndedReason(message.reason);
} else if (message instanceof GameHoldMessage) {
setGameHoldReason(message.reason);
}
Expand All @@ -57,6 +73,7 @@ export function Game(): JSX.Element {
const [chess, setChess] = useState(new ChessEngine());
const [gameInterruptedReason, setGameInterruptedReason] =
useState<GameInterruptedReason>();
const [gameEndedReason, setGameEndedReason] = useState<GameEndReason>();
const [gameHoldReason, setGameHoldReason] = useState<GameHoldReason>();
Comment on lines 74 to 77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your problem to handle in this PR, but since I am looking at this code consciously, in general it would be nice to store all these reasons in one state hook and union the types. my current assumption is that there should only be one of a interrupted/ended/hold reason at a given time. if my assumption is wrong this is a useless comment :)

const [rotation, setRotation] = useState<number>(0);

Expand All @@ -66,6 +83,7 @@ export function Game(): JSX.Element {
chess,
setChess,
setGameInterruptedReason,
setGameEndedReason,
setGameHoldReason,
),
);
Expand Down Expand Up @@ -103,7 +121,9 @@ export function Game(): JSX.Element {
// check if the game has ended or been interrupted
let gameEndReason: GameEndReason | undefined = undefined;
const gameFinishedReason = chess.getGameFinishedReason();
if (gameFinishedReason !== undefined) {
if (gameEndedReason !== undefined) {
gameEndReason = gameEndedReason;
} else if (gameFinishedReason !== undefined) {
sendMessage(new GameFinishedMessage(gameFinishedReason));
gameEndReason = gameFinishedReason;
} else if (gameInterruptedReason !== undefined) {
Expand All @@ -115,7 +135,6 @@ export function Game(): JSX.Element {
gameEndReason !== undefined ?
<GameEndDialog reason={gameEndReason} side={side} />
: null;

const gameOfferDialog =
gameHoldReason !== undefined ?
gameHoldReason === GameHoldReason.DRAW_CONFIRMATION ?
Expand All @@ -142,6 +161,8 @@ export function Game(): JSX.Element {
<NavbarMenu
sendMessage={sendMessage}
side={side}
difficulty={data.difficulty}
aiDifficulty={data.aiDifficulty}
setRotation={setRotation}
/>
<div id="body-container" className={bgColor()}>
Expand Down
84 changes: 55 additions & 29 deletions src/client/game/navbar-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import "../colors.css";
interface NavbarMenuProps {
sendMessage: SendMessage;
side: Side;
difficulty?: string;
aiDifficulty?: number;
setRotation: Dispatch<React.SetStateAction<number>>; //set state type
}

Expand All @@ -40,6 +42,20 @@ interface NavbarMenuProps {
export function NavbarMenu(props: NavbarMenuProps): JSX.Element {
// Store react router state for game
const navigate = useNavigate();
const difficultyButton =
props.difficulty ?
<Button minimal disabled text={"rating: " + props.difficulty} />
: null;

const aiArray = ["Baby", "Beginner", "Intermediate", "Advances"];
const aiDifficultyButton =
props.aiDifficulty ?
<Button
minimal
disabled
text={"AI Difficulty: " + aiArray[props.aiDifficulty]}
/>
: null;

/** create navbar rotate button */
const rotateButton =
Expand All @@ -54,43 +70,53 @@ export function NavbarMenu(props: NavbarMenuProps): JSX.Element {
});
}}
/>
: "";
: undefined;

const resignButton =
props.side === Side.SPECTATOR ?
undefined
: <Button
icon="flag"
variant="minimal"
text="Resign"
intent="danger"
onClick={async () => {
props.sendMessage(
new GameInterruptedMessage(
props.side === Side.WHITE ?
GameInterruptedReason.WHITE_RESIGNED
: GameInterruptedReason.BLACK_RESIGNED,
),
);
}}
/>;

const drawButton =
props.side === Side.SPECTATOR ?
undefined
: <Button
icon="pause"
variant="minimal"
text="Draw"
intent="danger"
onClick={async () => {
props.sendMessage(
new GameHoldMessage(GameHoldReason.DRAW_CONFIRMATION),
);
}}
/>;

return (
<Navbar className={bgColor()}>
<NavbarGroup>
<NavbarHeading className={textColor()}>ChessBot</NavbarHeading>
<NavbarDivider />
<Button
icon="flag"
variant="minimal"
text="Resign"
intent="danger"
onClick={async () => {
props.sendMessage(
new GameInterruptedMessage(
props.side === Side.WHITE ?
GameInterruptedReason.WHITE_RESIGNED
: GameInterruptedReason.BLACK_RESIGNED,
),
);
}}
/>
<Button
icon="pause"
variant="minimal"
text="Draw"
intent="danger"
onClick={async () => {
props.sendMessage(
new GameHoldMessage(
GameHoldReason.DRAW_CONFIRMATION,
),
);
}}
/>
{resignButton}
{drawButton}
</NavbarGroup>
<NavbarGroup align="right">
{difficultyButton}
{aiDifficultyButton}
{rotateButton}
<Button
icon={darkModeIcon()}
Expand Down
8 changes: 0 additions & 8 deletions src/client/puzzle/puzzle.tsx

This file was deleted.

76 changes: 76 additions & 0 deletions src/client/puzzle/select-puzzle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Button, MenuItem } from "@blueprintjs/core";
import { ItemRenderer, Select } from "@blueprintjs/select";
import { post } from "../api";
import { useNavigate } from "react-router-dom";
import { PuzzleComponents } from "../../server/api/api";

const renderPuzzleOptions: ItemRenderer<string> = (
puzzleNumber,
{ modifiers, handleFocus, handleClick },
) => {
return (
<MenuItem
key={puzzleNumber}
active={modifiers.active}
roleStructure="listoption"
text={puzzleNumber}
onFocus={handleFocus}
onClick={handleClick}
/>
);
};

interface SelectPuzzleProps {
puzzles: Record<string, PuzzleComponents>;
selectedPuzzle: string | undefined;
onPuzzleSelected: (puzzle: string) => void;
}

export function SelectPuzzle(props: SelectPuzzleProps) {
const navigate = useNavigate();
const hasSelection = props.selectedPuzzle !== undefined;

const submit = (
<Button
text="Play"
icon="arrow-right"
intent="primary"
onClick={async () => {
if (props.selectedPuzzle && props.puzzles) {
//convert puzzle to map and send to start puzzles
const puzzle = props.puzzles as Record<
string,
PuzzleComponents
>;
const promise = post("/start-puzzle-game", {
puzzle: JSON.stringify(puzzle[props.selectedPuzzle]),
});
promise.then(() => {
navigate("/game");
});
}
}}
/>
);
return (
<>
<Select<string>
items={[...Object.keys(props.puzzles)]}
itemRenderer={renderPuzzleOptions}
onItemSelect={props.onPuzzleSelected}
filterable={false}
popoverProps={{ minimal: true }}
>
<Button
text={
hasSelection ?
props.selectedPuzzle
: "Select a puzzle..."
}
endIcon="double-caret-vertical"
/>
</Select>
{submit}
</>
);
}
40 changes: 40 additions & 0 deletions src/client/puzzle/setup-puzzle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState } from "react";
import { SetupBase } from "../setup/setup-base";
import { SelectPuzzle } from "./select-puzzle";
import { NonIdealState, Spinner } from "@blueprintjs/core";
import { get, useEffectQuery } from "../api";
import { Navigate } from "react-router-dom";
import { PuzzleComponents } from "../../server/api/api";

export function SetupPuzzle() {
const [selectedPuzzle, setSelectedPuzzle] = useState<string | undefined>();

//get puzzles from api
const { isPending, data, isError } = useEffectQuery(
"get-puzzles",
async () =>
(await get("/get-puzzles")) as Record<string, PuzzleComponents>,
false,
);

if (isPending) {
return (
<NonIdealState
icon={<Spinner intent="primary" />}
title="Loading..."
/>
);
} else if (isError || data === undefined) {
return <Navigate to="/home" />;
}

return (
<SetupBase>
<SelectPuzzle
puzzles={data}
selectedPuzzle={selectedPuzzle}
onPuzzleSelected={setSelectedPuzzle}
/>
</SetupBase>
);
}
5 changes: 0 additions & 5 deletions src/client/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { createBrowserRouter } from "react-router-dom";
import { Setup } from "./setup/setup";
import { Debug } from "./debug/debug";
import { Game } from "./game/game";
import { Puzzle } from "./puzzle/puzzle";
import { Lobby } from "./setup/lobby";
import { Home } from "./home";
import { Debug2 } from "./debug/debug2";
Expand Down Expand Up @@ -33,10 +32,6 @@ export const router = createBrowserRouter([
path: "/lobby",
element: <Lobby />,
},
{
path: "/puzzle",
element: <Puzzle />,
},
{
path: "/game",
element: <Game />,
Expand Down
4 changes: 4 additions & 0 deletions src/client/setup/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Dispatch, useState } from "react";
import { SetupGame } from "./setup-game";
import { Navigate, useNavigate } from "react-router-dom";
import { ClientType, GameType } from "../../common/client-types";
import { SetupPuzzle } from "../puzzle/setup-puzzle";
import { get, useEffectQuery } from "../api";
import {
allSettings,
Expand Down Expand Up @@ -68,6 +69,9 @@ export function Setup(): JSX.Element {
}
/>
: null}
{setupType === SetupType.PUZZLE ?
<SetupPuzzle />
: null}
</SetupBase>
);
} else {
Expand Down
Loading