diff --git a/apps/web/src/routes/_components/main-menu/new-section.component.tsx b/apps/web/src/routes/_components/main-menu/new-section.component.tsx
index b04d16c..9e8733e 100644
--- a/apps/web/src/routes/_components/main-menu/new-section.component.tsx
+++ b/apps/web/src/routes/_components/main-menu/new-section.component.tsx
@@ -1,17 +1,20 @@
import { FC } from "react";
-import { Link } from "react-router";
+import { Link, useNavigate } from "react-router";
import { GameMode, MainMenuSection } from "../../../shared/enum";
-import { useMainMenuStore } from "../../_stores";
+import { useGameStore, useMainMenuStore } from "../../_stores";
export const NewGameSection: FC = () => {
+ const navigate = useNavigate();
+
+ const { reset: resetGame } = useGameStore();
const { setSection } = useMainMenuStore();
const GameModeOptions: {
label: string;
title: string;
mode: keyof typeof GameMode;
- action?: () => void;
+ action: () => void;
}[] = [
{
label: "AI",
@@ -25,7 +28,15 @@ export const NewGameSection: FC = () => {
title: "Play against another human player",
action: () => setSection(MainMenuSection.newGameHuman)
},
- { label: "Free Mode", mode: "free", title: "Play against yourself" },
+ {
+ label: "Free Mode",
+ mode: "free",
+ title: "Play against yourself",
+ action: () => {
+ navigate("/play?mode=free");
+ resetGame();
+ }
+ },
{
label: "Simulation",
mode: "simulation",
@@ -40,24 +51,18 @@ export const NewGameSection: FC = () => {
Choose your game mode:
- {GameModeOptions.map((option) => {
- const Component = option.action ? "button" : Link;
-
- return (
-
- {option.label}
-
- );
- })}
+ {GameModeOptions.map((option) => (
+
+ ))}
diff --git a/apps/web/src/routes/_hooks/use-game.hook.ts b/apps/web/src/routes/_hooks/use-game.hook.ts
deleted file mode 100644
index ae78d55..0000000
--- a/apps/web/src/routes/_hooks/use-game.hook.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import { useCallback, useRef, useState } from "react";
-import { Move } from "chess.js";
-import { register, RegisterModule } from "@quick-threejs/reactive";
-
-import {
- EngineGameUpdatedMessageEventPayload,
- MessageEventPayload
-} from "../../shared/types";
-import { GAME_UPDATED_TOKEN, PIECE_WILL_MOVE_TOKEN } from "../../shared/tokens";
-import { PlayerModel } from "../../shared/models";
-
-/** @description Game login worker location. */
-const workerLocation = new URL(
- "../../core/game/game.worker.ts",
- import.meta.url
-) as unknown as string;
-
-/** @description Provide resources about the chess game and the application logic. */
-export const useGame = () => {
- const [app, setApp] = useState();
-
- const state = useRef<{
- app?: RegisterModule;
- isPending: boolean;
- isReady: boolean;
- }>({ isPending: false, isReady: false });
-
- const [players, setPlayers] = useState([]);
-
- const handleGmeUpdate = useRef<
- | ((payload?: EngineGameUpdatedMessageEventPayload["value"]) => unknown)
- | null
- >();
-
- const onGameUpdate = useCallback(
- (
- callback: (
- payload: EngineGameUpdatedMessageEventPayload["value"]
- ) => unknown
- ) => {
- handleGmeUpdate.current = callback;
- },
- []
- );
-
- const performPieceMove = useCallback((move: Move) => {
- state.current.app?.worker()?.postMessage?.({
- token: PIECE_WILL_MOVE_TOKEN,
- value: move
- } satisfies MessageEventPayload);
- }, []);
-
- const handleMessages = useCallback(
- (payload: MessageEvent) => {
- if (!payload.data?.token) return;
-
- if (payload.data.token === GAME_UPDATED_TOKEN && payload.data?.value?.fen)
- handleGmeUpdate.current?.(payload.data.value);
- },
- []
- );
-
- const init = useCallback(() => {
- if (state.current.isPending || state.current.isReady) return;
- state.current.isPending = true;
-
- register({
- location: workerLocation,
- enableDebug: !!import.meta.env?.DEV,
- axesSizes: 5,
- gridSizes: 10,
- withMiniCamera: true,
- onReady: (_app) => {
- _app.worker()?.addEventListener("message", handleMessages);
-
- state.current.app = _app;
- state.current.isPending = false;
- state.current.isReady = true;
-
- setApp(_app);
- }
- });
- }, [state, handleMessages]);
-
- const createPlayer = useCallback(() => {
- const player = new PlayerModel();
- setPlayers((prev) => [...prev, player]);
-
- return player;
- }, []);
-
- const removePlayer = useCallback(
- (player: PlayerModel) =>
- setPlayers((prev) =>
- prev.filter((_player) => {
- if (_player !== player) return true;
-
- _player.unsubscribe();
- _player.complete();
-
- return false;
- })
- ),
- []
- );
-
- const dispose = useCallback(() => {
- app?.worker()?.removeEventListener("message", handleMessages);
- app?.dispose();
-
- state.current.app = undefined;
- state.current.isReady = false;
- state.current.isPending = false;
-
- setApp(undefined);
- }, [app, handleMessages]);
-
- return {
- app,
- state: state.current,
- init,
- players,
- createPlayer,
- removePlayer,
- performPieceMove,
- onGameUpdate,
- dispose
- };
-};
diff --git a/apps/web/src/routes/_stores/game.store.ts b/apps/web/src/routes/_stores/game.store.ts
index 83e154d..2061854 100644
--- a/apps/web/src/routes/_stores/game.store.ts
+++ b/apps/web/src/routes/_stores/game.store.ts
@@ -4,12 +4,14 @@ import { Properties } from "@quick-threejs/utils";
export interface GameStore {
app?: RegisterModule;
- setApp: (app?: RegisterModule) => void;
+ reset: () => void;
+ setApp: (app: RegisterModule | undefined) => void;
}
-export const gameInitialState: Properties = {};
+export const gameInitialState: Properties = { app: undefined };
export const useGameStore = create((set) => ({
...gameInitialState,
+ reset: () => set(() => ({ ...gameInitialState })),
setApp: (app?: RegisterModule) => set(() => ({ app }))
}));
diff --git a/apps/web/src/routes/play/_components/free-mode.component.tsx b/apps/web/src/routes/play/_components/free-mode.component.tsx
index 21609c9..df2f9fd 100644
--- a/apps/web/src/routes/play/_components/free-mode.component.tsx
+++ b/apps/web/src/routes/play/_components/free-mode.component.tsx
@@ -1,5 +1,134 @@
-import { FC, Fragment } from "react";
+import { Move } from "chess.js";
+import { FC, Fragment, useCallback, useEffect } from "react";
+import { merge } from "rxjs";
+
+import { useGameStore } from "../../_stores";
+import { PlayerModel } from "../../../shared/models";
+import {
+ GAME_UPDATED_TOKEN,
+ PIECE_WILL_MOVE_TOKEN
+} from "../../../shared/tokens";
+import {
+ EngineGameUpdatedMessageEventPayload,
+ MessageEventPayload
+} from "../../../shared/types";
+import { ColorSide, PlayerEntity } from "@chess-d/shared";
export const FreeModeComponent: FC = () => {
+ const { app } = useGameStore();
+
+ const performPieceMove = useCallback(
+ (move: Move) => {
+ app?.worker()?.postMessage?.({
+ token: PIECE_WILL_MOVE_TOKEN,
+ value: move
+ } satisfies MessageEventPayload);
+ },
+ [app]
+ );
+
+ const createPlayer = useCallback((identity: PlayerEntity) => {
+ const player = new PlayerModel();
+ player.color = identity.color as ColorSide;
+
+ return player;
+ }, []);
+
+ useEffect(() => {
+ const appGui = app?.gui();
+ const guiFreeMode = appGui.addFolder("Free Mode");
+
+ const _players = [
+ createPlayer({ color: ColorSide.white }),
+ createPlayer({ color: ColorSide.black })
+ ];
+
+ if (import.meta.env?.DEV) {
+ const guiPlayer1 = guiFreeMode.addFolder("Player 1");
+ const player1Positions = { piece: "p", from: "a2", to: "a3" };
+ guiPlayer1.add(player1Positions, "piece");
+ guiPlayer1.add(player1Positions, "from");
+ guiPlayer1.add(player1Positions, "to");
+ guiPlayer1.add(
+ {
+ "Perform Move": () => {
+ const move = {
+ color: _players[0]?.color,
+ from: player1Positions.from,
+ to: player1Positions.to,
+ piece: player1Positions.piece
+ } as Move;
+
+ _players[0]?.next({
+ token: "PLACED_PIECE",
+ value: { move }
+ });
+ }
+ },
+ "Perform Move"
+ );
+
+ const guiPlayer2 = guiFreeMode.addFolder("Player 2");
+ const player2Positions = { piece: "p", from: "a7", to: "a6" };
+ guiPlayer2.add(player2Positions, "piece");
+ guiPlayer2.add(player2Positions, "from");
+ guiPlayer2.add(player2Positions, "to");
+ guiPlayer2.add(
+ {
+ "Perform Move": () => {
+ const move = {
+ color: _players[1]?.color,
+ from: player2Positions.from,
+ to: player2Positions.to,
+ piece: player2Positions.piece
+ } as Move;
+
+ _players[1]?.next({
+ token: "PLACED_PIECE",
+ value: { move }
+ });
+ }
+ },
+ "Perform Move"
+ );
+ }
+
+ const handleMessages = (
+ payload: MessageEvent
+ ) => {
+ if (!payload.data?.token) return;
+
+ if (
+ payload.data.token === GAME_UPDATED_TOKEN &&
+ payload.data?.value?.fen
+ ) {
+ _players.forEach((player) => {
+ player.next({
+ token: "NOTIFIED",
+ value: payload.data.value
+ });
+ });
+ }
+ };
+
+ const playersSubscription = merge(..._players).subscribe((payload) => {
+ if (payload.token === "PLACED_PIECE" && payload.value?.move)
+ return performPieceMove(payload.value?.move);
+ });
+
+ app?.worker()?.addEventListener("message", handleMessages);
+
+ return () => {
+ playersSubscription.unsubscribe();
+ _players.forEach((player) => {
+ player.complete();
+ player.unsubscribe();
+ _players.shift();
+ });
+ app?.worker()?.removeEventListener("message", handleMessages);
+ guiFreeMode.destroy();
+ };
+ }, [app, createPlayer, performPieceMove]);
+
return ;
};
diff --git a/apps/web/src/routes/play/index.tsx b/apps/web/src/routes/play/index.tsx
index 1a537ac..37c8a9a 100644
--- a/apps/web/src/routes/play/index.tsx
+++ b/apps/web/src/routes/play/index.tsx
@@ -1,28 +1,66 @@
-import { FC, useEffect } from "react";
+import { register, RegisterModule } from "@quick-threejs/reactive";
+import { FC, useCallback, useEffect, useRef } from "react";
-import { useGame } from "../_hooks";
import { useGameStore } from "../_stores";
+import { FreeModeComponent } from "./_components/free-mode.component";
+
+/** @internal */
+const workerLocation = new URL(
+ "../../core/game/game.worker.ts",
+ import.meta.url
+) as unknown as string;
export const PlayRoute: FC = () => {
- const { setApp } = useGameStore();
- const {
- state: gameState,
- init: initGame,
- createPlayer: createFreePlayer,
- dispose: disposeGame
- } = useGame();
+ const { app, setApp } = useGameStore();
- useEffect(() => {
- if (gameState.app || gameState.isPending || gameState.isReady)
- return setApp(gameState.app);
+ const state = useRef<{
+ isPending: boolean;
+ isReady: boolean;
+ }>({ isPending: false, isReady: false });
+
+ const init = useCallback(
+ () =>
+ new Promise((resolve) => {
+ if (state.current.isPending || state.current.isReady) return;
+ state.current.isPending = true;
+
+ register({
+ location: workerLocation,
+ enableDebug: !!import.meta.env?.DEV,
+ axesSizes: 5,
+ gridSizes: 10,
+ withMiniCamera: true,
+ onReady: (_app) => {
+ state.current.isPending = false;
+ state.current.isReady = true;
+
+ resolve(_app);
+ }
+ });
+ }),
+ []
+ );
+
+ const dispose = useCallback(() => {
+ if (state.current.isPending || !state.current.isReady) return;
- initGame();
+ state.current.isPending = false;
+ state.current.isReady = false;
+
+ app?.dispose();
+
+ setApp(undefined);
+ }, [app, setApp]);
+
+ useEffect(() => {
+ if (!app) init().then(setApp);
return () => {
- if (gameState.app && !gameState.isPending && gameState.isReady)
- disposeGame();
+ if (app) dispose();
};
- }, [createFreePlayer, disposeGame, gameState, initGame, setApp]);
+ }, [app, init, dispose, setApp]);
+
+ if (!app) return null;
- return null;
+ return ;
};