From a872c3f3f8ca231c6dbcf993bd2f0ce2c6c5bf03 Mon Sep 17 00:00:00 2001 From: ymmot239 Date: Mon, 22 Sep 2025 10:21:25 -0500 Subject: [PATCH 1/4] advanced stop procedure added a stop for promises in progress also added some hotfixes for pause --- src/client/game/admin-dialogs.tsx | 2 +- src/client/game/game.tsx | 2 +- src/server/api/api.ts | 3 +-- src/server/command/command.ts | 5 +++-- src/server/robot/robot-manager.ts | 4 ++++ src/server/robot/robot.ts | 6 ++++++ 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/client/game/admin-dialogs.tsx b/src/client/game/admin-dialogs.tsx index b5e6346f..bc050016 100644 --- a/src/client/game/admin-dialogs.tsx +++ b/src/client/game/admin-dialogs.tsx @@ -58,7 +58,7 @@ export function NotificationDialog(props: NotificationDialogProps) { className={buttonColor()} intent="primary" onClick={() => { - close(); + setIsOpen(false); }} /> ); diff --git a/src/client/game/game.tsx b/src/client/game/game.tsx index aa19a575..8e4ab1ee 100644 --- a/src/client/game/game.tsx +++ b/src/client/game/game.tsx @@ -175,7 +175,7 @@ export function Game(): JSX.Element { : null; /** make moves by making a copy of the chessboard and sending the move message */ - const handleMove = paused? + const handleMove = !paused? (move: Move): void => { setChess(chess.copy(move)); sendMessage(new MoveMessage(move)); diff --git a/src/server/api/api.ts b/src/server/api/api.ts index 65f90f8e..c9853b8f 100644 --- a/src/server/api/api.ts +++ b/src/server/api/api.ts @@ -553,18 +553,17 @@ apiRouter.get("/get-puzzles", (_, res) => { */ apiRouter.get("/pause-game", (_, res) => { gamePaused.flag = true; + robotManager.stopAllRobots(); socketManager.sendToAll(new GameHoldMessage(GameHoldReason.GAME_PAUSED)); return res.send({ message: "success" }); }); /** * Unpause the game - * Resumes any leftover commands queued in the command executor * Todo: add authentication instead of an exposed unpause call */ apiRouter.get("/unpause-game", async (_, res) => { gamePaused.flag = false; - await executor.finishExecution(); socketManager.sendToAll(new GameHoldMessage(GameHoldReason.GAME_UNPAUSED)); return res.send({ message: "success" }); }); diff --git a/src/server/command/command.ts b/src/server/command/command.ts index e955bada..75ce3949 100644 --- a/src/server/command/command.ts +++ b/src/server/command/command.ts @@ -1,3 +1,4 @@ +import { gamePaused } from "../api/managers"; import { robotManager } from "../robot/robot-manager"; /** @@ -112,7 +113,7 @@ function isReversable(obj): obj is Reversible { */ export class ParallelCommandGroup extends CommandGroup { public async execute(): Promise { - const promises = this.commands.map((move) => move.execute()); + const promises = this.commands.map((move) => {gamePaused.flag?null:move.execute()}); return Promise.all(promises).then(null); } public async reverse(): Promise { @@ -132,7 +133,7 @@ export class SequentialCommandGroup extends CommandGroup { public async execute(): Promise { let promise = Promise.resolve(); for (const command of this.commands) { - promise = promise.then(() => command.execute()); + promise = promise.then(() => {gamePaused?null:command.execute()}); } return promise; } diff --git a/src/server/robot/robot-manager.ts b/src/server/robot/robot-manager.ts index fd72e426..5945bf23 100644 --- a/src/server/robot/robot-manager.ts +++ b/src/server/robot/robot-manager.ts @@ -99,6 +99,10 @@ export class RobotManager { } indicesToIds.set(indices.toString(), robotId); } + + stopAllRobots(){ + Array.from(this.idsToRobots.values()).forEach((robot)=>{robot.sendStopPacket()}) + } } export const robotManager = new RobotManager( diff --git a/src/server/robot/robot.ts b/src/server/robot/robot.ts index 7540ec82..2571a158 100644 --- a/src/server/robot/robot.ts +++ b/src/server/robot/robot.ts @@ -186,4 +186,10 @@ export class Robot { timeDeltaMs: timeDeltaMs, }); } + + public async sendStopPacket():Promise{ + await this.tunnel!.send({ + type: PacketType.ESTOP, + }); + } } From 9fccb54b898647863717224480162e321317ff98 Mon Sep 17 00:00:00 2001 From: ymmot239 Date: Mon, 22 Sep 2025 18:12:31 -0500 Subject: [PATCH 2/4] added robot saves saved game state as well as robot positions in save files --- src/server/api/game-manager.ts | 7 +++++++ src/server/api/save-manager.ts | 24 ++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/server/api/game-manager.ts b/src/server/api/game-manager.ts index 870ee267..a9b58791 100644 --- a/src/server/api/game-manager.ts +++ b/src/server/api/game-manager.ts @@ -26,6 +26,7 @@ import { SaveManager } from "./save-manager"; import { materializePath } from "../robot/path-materializer"; import { DO_SAVES } from "../utils/env"; import { executor } from "../command/executor"; +import { robotManager } from "../robot/robot-manager"; import { gamePaused } from "./managers"; type GameState = { @@ -160,6 +161,8 @@ export class HumanGameManager extends GameManager { this.hostSide, -1, this.chess.pgn, + this.chess.fen, + robotManager.getIndicesToIds(), ); } else { SaveManager.saveGame( @@ -168,6 +171,8 @@ export class HumanGameManager extends GameManager { oppositeSide(this.hostSide), -1, this.chess.pgn, + this.chess.fen, + robotManager.getIndicesToIds(), ); } } @@ -278,6 +283,8 @@ export class ComputerGameManager extends GameManager { this.hostSide, this.difficulty, this.chess.pgn, + this.chess.fen, + robotManager.getIndicesToIds(), ); } diff --git a/src/server/api/save-manager.ts b/src/server/api/save-manager.ts index 02f8ec59..b54b91fd 100644 --- a/src/server/api/save-manager.ts +++ b/src/server/api/save-manager.ts @@ -10,7 +10,7 @@ * Modified: 9/20/24 */ -import { Side } from "../../common/game-types"; +import { Side } from "../../common/game-types";2 // Save files contain a date in ms and pgn string export interface iSave { @@ -19,6 +19,10 @@ export interface iSave { hostWhite: boolean; aiDifficulty: number; game: string; + pos:string; + robotPos: Array<[string,string]>; + oldPos: string; + oldRobotPos: Array<[string,string]>; } export class SaveManager { @@ -27,9 +31,9 @@ export class SaveManager { * * Finds or creates a save file * Save name is host id + client id - * Saves include the game start time and save pgn as json - * - * Input: host id, client id, game pgn + * Saves include the game start time and save fen as json + * Robots are saved as a map of indices to robots + * Input: host id, client id, game fen, robots * Output: boolean competed */ public static saveGame( @@ -38,16 +42,28 @@ export class SaveManager { hostSide: Side, aiDiff: number, pgn: string, + fen: string, + robots: Map, ) { const day = new Date().getTime(); const side = hostSide === Side.WHITE; + console.log(JSON.stringify(robots)); const saveContents = { host: hostId, date: day, hostWhite: side, aiDifficulty: aiDiff, game: pgn, + pos: fen, + robotPos: Array.from(robots), + oldPos: "", + oldRobotPos: Array<[string,string]>(), }; + const oldGame = SaveManager.loadGame(hostId + "+" + clientID); + if (oldGame && oldGame.pos !== null) { + saveContents.oldPos = oldGame.game; + saveContents.oldRobotPos = oldGame.robotPos; + } return FileManager.writeFile(hostId + "+" + clientID, saveContents); } From c2d7b5ee29831bcf95f296e75ad8b22da1b9e80c Mon Sep 17 00:00:00 2001 From: ymmot239 Date: Mon, 22 Sep 2025 20:04:15 -0500 Subject: [PATCH 3/4] Added pause rollback allowed pause to rollback the robot positions. Also allowed saveGame to load from a saved positions, so hopefully less errors --- src/client/game/game.tsx | 27 ++++++++++-------- src/server/api/api.ts | 46 +++++++++++++++++++++---------- src/server/api/game-manager.ts | 2 +- src/server/api/managers.ts | 2 +- src/server/api/save-manager.ts | 11 ++++---- src/server/command/command.ts | 8 ++++-- src/server/command/executor.ts | 18 ++++++------ src/server/robot/robot-manager.ts | 6 ++-- src/server/robot/robot.ts | 2 +- 9 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/client/game/game.tsx b/src/client/game/game.tsx index 420e5e2a..5adac3a5 100644 --- a/src/client/game/game.tsx +++ b/src/client/game/game.tsx @@ -42,7 +42,7 @@ function getMessageHandler( setGameInterruptedReason: Dispatch, setGameEndedReason: Dispatch, setGameHoldReason: Dispatch, - setPaused : Dispatch, + setPaused: Dispatch, ): MessageHandler { return (message) => { if (message instanceof MoveMessage) { @@ -65,9 +65,9 @@ function getMessageHandler( setGameEndedReason(message.reason); } else if (message instanceof GameHoldMessage) { setGameHoldReason(message.reason); - if(message.reason === GameHoldReason.GAME_PAUSED){ + if (message.reason === GameHoldReason.GAME_PAUSED) { setPaused(true); - } else if(message.reason === GameHoldReason.GAME_UNPAUSED){ + } else if (message.reason === GameHoldReason.GAME_UNPAUSED) { setPaused(false); } } @@ -161,27 +161,30 @@ export function Game(): JSX.Element { : null : null; - const gamePauseDialog = + const gamePauseDialog = gameHoldReason !== undefined ? gameHoldReason === GameHoldReason.GAME_PAUSED ? - : null - : null; + : null + : null; - const gameUnpauseDialog = + const gameUnpauseDialog = gameHoldReason !== undefined ? gameHoldReason === GameHoldReason.GAME_UNPAUSED ? - - : null - : null; + + : null + : null; /** make moves by making a copy of the chessboard and sending the move message */ - const handleMove = !paused? + const handleMove = + !paused ? (move: Move): void => { setChess(chess.copy(move)); sendMessage(new MoveMessage(move)); } - : (move:Move):void => {move;}; //send a do-nothing function if game is paused + : (move: Move): void => { + move; + }; //send a do-nothing function if game is paused // return the chessboard wrapper, navbar, and potential end dialog return ( diff --git a/src/server/api/api.ts b/src/server/api/api.ts index da3017ea..a1490fe9 100644 --- a/src/server/api/api.ts +++ b/src/server/api/api.ts @@ -51,12 +51,12 @@ import { SpinRadiansCommand, } from "../command/move-command"; import { GridIndices } from "../robot/grid-indices"; -import { puzzles, type PuzzleComponents } from "./puzzles"; import { moveAllRobotsHomeToDefaultOptimized, moveAllRobotsToDefaultPositions, - moveAllRobotsFromBoardToHome, } from "../robot/path-materializer"; +import type { PuzzleComponents } from "./puzzles"; +import { puzzles } from "./puzzles"; import { tcpServer } from "./tcp-interface"; import { robotManager } from "../robot/robot-manager"; import { executor } from "../command/executor"; @@ -76,7 +76,7 @@ async function setupDefaultRobotPositions( moveAllRobotsToDefaultPositions(defaultPositionsMap); await executor.execute(command); } else { - setAllRobotsToDefaultPositions(defaultPositionsMap); + moveAllRobotsToDefaultPositions(defaultPositionsMap); } } else { if (isMoving) { @@ -186,6 +186,17 @@ apiRouter.get("/client-information", async (req, res) => { ), ); } + const robotPos = new Map( + oldSave!.robotPos?.map<[string, GridIndices]>((obj) => [ + obj[1], + new GridIndices( + parseInt(obj[0].split(", ")[0]), + parseInt(obj[0].split(", ")[1]), + ), + ]), + ); + console.log(robotPos); + setAllRobotsToDefaultPositions(robotPos); } /** * Note the client currently redirects to home from the game over screen @@ -285,7 +296,6 @@ apiRouter.post("/start-puzzle-game", async (req, res) => { const fen = puzzle.fen; const moves = puzzle.moves; const difficulty = puzzle.rating; - const tooltip = puzzle.tooltip; if (puzzle.robotDefaultPositions) { // Convert puzzle.robotDefaultPositions from Record to Map @@ -322,7 +332,7 @@ apiRouter.post("/start-puzzle-game", async (req, res) => { new ChessEngine(), socketManager, fen, - tooltip, + "", moves, difficulty, ), @@ -331,15 +341,6 @@ apiRouter.post("/start-puzzle-game", async (req, res) => { return res.send({ message: "success" }); }); -/** - * Returns robots to home after a game ends. - */ -apiRouter.post("/return-home", async (_req, res) => { - const command = moveAllRobotsFromBoardToHome(); - await executor.execute(command); - return res.send({ message: "success" }); -}); - /** * Returns all registered robot ids */ @@ -575,6 +576,23 @@ apiRouter.get("/pause-game", (_, res) => { */ apiRouter.get("/unpause-game", async (_, res) => { gamePaused.flag = false; + const ids = clientManager.getIds(); + if (ids) { + const oldSave = SaveManager.loadGame(ids[0]); + gameManager?.chess.loadFen(oldSave!.oldPos); + setAllRobotsToDefaultPositions( + new Map( + oldSave!.oldRobotPos?.map<[string, GridIndices]>((obj) => [ + obj[1], + new GridIndices( + parseInt(obj[0].split(", ")[0]), + parseInt(obj[0].split(", ")[1]), + ), + ]), + ), + ); + socketManager.sendToAll(new SetChessMessage(oldSave!.oldPos)); + } socketManager.sendToAll(new GameHoldMessage(GameHoldReason.GAME_UNPAUSED)); return res.send({ message: "success" }); }); diff --git a/src/server/api/game-manager.ts b/src/server/api/game-manager.ts index 2b312f23..2d36d0e6 100644 --- a/src/server/api/game-manager.ts +++ b/src/server/api/game-manager.ts @@ -220,7 +220,7 @@ export class HumanGameManager extends GameManager { SaveManager.endGame(ids[0], ids[1]); else SaveManager.endGame(ids[1], ids[0]); } - } + } } } diff --git a/src/server/api/managers.ts b/src/server/api/managers.ts index c0b7f994..fb41e9f1 100644 --- a/src/server/api/managers.ts +++ b/src/server/api/managers.ts @@ -9,7 +9,7 @@ import { SocketManager } from "./socket-manager"; export const socketManager = new SocketManager({}); export const clientManager = new ClientManager(socketManager); export let gameManager: GameManager | null = null; -export const gamePaused = {flag:false}; +export const gamePaused = { flag: false }; export function setGameManager(manager: GameManager) { gameManager = manager; diff --git a/src/server/api/save-manager.ts b/src/server/api/save-manager.ts index b54b91fd..5f77f233 100644 --- a/src/server/api/save-manager.ts +++ b/src/server/api/save-manager.ts @@ -10,7 +10,8 @@ * Modified: 9/20/24 */ -import { Side } from "../../common/game-types";2 +import { Side } from "../../common/game-types"; +2; // Save files contain a date in ms and pgn string export interface iSave { @@ -19,10 +20,10 @@ export interface iSave { hostWhite: boolean; aiDifficulty: number; game: string; - pos:string; - robotPos: Array<[string,string]>; + pos: string; + robotPos: Array<[string, string]>; oldPos: string; - oldRobotPos: Array<[string,string]>; + oldRobotPos: Array<[string, string]>; } export class SaveManager { @@ -57,7 +58,7 @@ export class SaveManager { pos: fen, robotPos: Array.from(robots), oldPos: "", - oldRobotPos: Array<[string,string]>(), + oldRobotPos: Array<[string, string]>(), }; const oldGame = SaveManager.loadGame(hostId + "+" + clientID); if (oldGame && oldGame.pos !== null) { diff --git a/src/server/command/command.ts b/src/server/command/command.ts index 75ce3949..6c8796c8 100644 --- a/src/server/command/command.ts +++ b/src/server/command/command.ts @@ -113,7 +113,9 @@ function isReversable(obj): obj is Reversible { */ export class ParallelCommandGroup extends CommandGroup { public async execute(): Promise { - const promises = this.commands.map((move) => {gamePaused.flag?null:move.execute()}); + const promises = this.commands.map((move) => { + gamePaused.flag ? null : move.execute(); + }); return Promise.all(promises).then(null); } public async reverse(): Promise { @@ -133,7 +135,9 @@ export class SequentialCommandGroup extends CommandGroup { public async execute(): Promise { let promise = Promise.resolve(); for (const command of this.commands) { - promise = promise.then(() => {gamePaused?null:command.execute()}); + promise = promise.then(() => { + gamePaused.flag ? null : command.execute(); + }); } return promise; } diff --git a/src/server/command/executor.ts b/src/server/command/executor.ts index c2e8d35e..28b85c0f 100644 --- a/src/server/command/executor.ts +++ b/src/server/command/executor.ts @@ -43,14 +43,14 @@ export class CommandExecutor { public async execute(command: Command): Promise { this.checkRequirements(command); this.runningCommands.push(command); - if(!gamePaused.flag){ + if (!gamePaused.flag) { return command.execute().finally(() => { this.oldCommands.unshift(command); const index = this.runningCommands.indexOf(command); if (index >= 0) { this.runningCommands.splice(index, 1); } - }) + }); } } @@ -59,19 +59,19 @@ export class CommandExecutor { * mainly used to finish the backlog from a paused game * @returns - The command to execute. */ - public async finishExecution() : Promise { - return this.runningCommands.forEach((command) =>{ - command.execute().finally(()=>{ + public async finishExecution(): Promise { + return this.runningCommands.forEach((command) => { + command.execute().finally(() => { this.oldCommands.unshift(command); const index = this.runningCommands.indexOf(command); if (index >= 0) { this.runningCommands.splice(index, 1); } - }) - }) + }); + }); } - public clearExecution(){ + public clearExecution() { this.runningCommands = []; } @@ -83,7 +83,7 @@ export class CommandExecutor { return this.oldCommands; } - public clearOldCommands(){ + public clearOldCommands() { this.oldCommands = []; } } diff --git a/src/server/robot/robot-manager.ts b/src/server/robot/robot-manager.ts index 5945bf23..32daecfc 100644 --- a/src/server/robot/robot-manager.ts +++ b/src/server/robot/robot-manager.ts @@ -100,8 +100,10 @@ export class RobotManager { indicesToIds.set(indices.toString(), robotId); } - stopAllRobots(){ - Array.from(this.idsToRobots.values()).forEach((robot)=>{robot.sendStopPacket()}) + stopAllRobots() { + Array.from(this.idsToRobots.values()).forEach((robot) => { + robot.sendStopPacket(); + }); } } diff --git a/src/server/robot/robot.ts b/src/server/robot/robot.ts index 2571a158..62556b36 100644 --- a/src/server/robot/robot.ts +++ b/src/server/robot/robot.ts @@ -187,7 +187,7 @@ export class Robot { }); } - public async sendStopPacket():Promise{ + public async sendStopPacket(): Promise { await this.tunnel!.send({ type: PacketType.ESTOP, }); From e166cf95143b9ad75f5e405fe3f0f84a97a1bcb5 Mon Sep 17 00:00:00 2001 From: ymmot239 Date: Mon, 22 Sep 2025 20:29:46 -0500 Subject: [PATCH 4/4] pause on reload Forced clients to be paused even on join or reload. --- src/client/game/game.tsx | 16 +++++------- src/server/api/api.ts | 47 ++++++++++++++++++++-------------- src/server/api/save-manager.ts | 2 +- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/src/client/game/game.tsx b/src/client/game/game.tsx index 5adac3a5..c10d8741 100644 --- a/src/client/game/game.tsx +++ b/src/client/game/game.tsx @@ -104,11 +104,12 @@ export function Game(): JSX.Element { "game-state", async () => { return get("/game-state").then((gameState) => { - setChess(new ChessEngine(gameState.position)); - if (gameState.gameEndReason !== undefined) { - setGameInterruptedReason(gameState.gameEndReason); + setChess(new ChessEngine(gameState.state.position)); + setPause(gameState.pause); + if (gameState.state.gameEndReason !== undefined) { + setGameInterruptedReason(gameState.state.gameEndReason); } - return gameState; + return gameState.state; }); }, false, @@ -161,12 +162,7 @@ export function Game(): JSX.Element { : null : null; - const gamePauseDialog = - gameHoldReason !== undefined ? - gameHoldReason === GameHoldReason.GAME_PAUSED ? - - : null - : null; + const gamePauseDialog = paused ? : null; const gameUnpauseDialog = gameHoldReason !== undefined ? diff --git a/src/server/api/api.ts b/src/server/api/api.ts index a1490fe9..7dd28429 100644 --- a/src/server/api/api.ts +++ b/src/server/api/api.ts @@ -221,7 +221,10 @@ apiRouter.get("/game-state", (req, res) => { return res.status(400).send({ message: "No game is currently active" }); } const clientType = clientManager.getClientType(req.cookies.id); - return res.send(gameManager.getGameState(clientType)); + return res.send({ + state: gameManager.getGameState(clientType), + pause: gamePaused.flag, + }); }); /** @@ -575,26 +578,32 @@ apiRouter.get("/pause-game", (_, res) => { * Todo: add authentication instead of an exposed unpause call */ apiRouter.get("/unpause-game", async (_, res) => { - gamePaused.flag = false; - const ids = clientManager.getIds(); - if (ids) { - const oldSave = SaveManager.loadGame(ids[0]); - gameManager?.chess.loadFen(oldSave!.oldPos); - setAllRobotsToDefaultPositions( - new Map( - oldSave!.oldRobotPos?.map<[string, GridIndices]>((obj) => [ - obj[1], - new GridIndices( - parseInt(obj[0].split(", ")[0]), - parseInt(obj[0].split(", ")[1]), - ), - ]), - ), + if (gamePaused.flag) { + gamePaused.flag = false; + const ids = clientManager.getIds(); + if (ids) { + const oldSave = SaveManager.loadGame(ids[0]); + gameManager?.chess.loadFen(oldSave!.oldPos); + setAllRobotsToDefaultPositions( + new Map( + oldSave!.oldRobotPos?.map<[string, GridIndices]>((obj) => [ + obj[1], + new GridIndices( + parseInt(obj[0].split(", ")[0]), + parseInt(obj[0].split(", ")[1]), + ), + ]), + ), + ); + socketManager.sendToAll(new SetChessMessage(oldSave!.oldPos)); + } + socketManager.sendToAll( + new GameHoldMessage(GameHoldReason.GAME_UNPAUSED), ); - socketManager.sendToAll(new SetChessMessage(oldSave!.oldPos)); + return res.send({ message: "success" }); + } else { + return res.send({ message: "game not paused" }); } - socketManager.sendToAll(new GameHoldMessage(GameHoldReason.GAME_UNPAUSED)); - return res.send({ message: "success" }); }); /** diff --git a/src/server/api/save-manager.ts b/src/server/api/save-manager.ts index 5f77f233..570bd958 100644 --- a/src/server/api/save-manager.ts +++ b/src/server/api/save-manager.ts @@ -62,7 +62,7 @@ export class SaveManager { }; const oldGame = SaveManager.loadGame(hostId + "+" + clientID); if (oldGame && oldGame.pos !== null) { - saveContents.oldPos = oldGame.game; + saveContents.oldPos = oldGame.pos; saveContents.oldRobotPos = oldGame.robotPos; } return FileManager.writeFile(hostId + "+" + clientID, saveContents);