Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ VIRTUAL_ROBOTS=false

# Determines if robots should start on the board rather than home
START_ROBOTS_AT_DEFAULT=false

# How long before a command times out and pauses the game
MOVE_TIMEOUT=60000

# number of retries before an error
MAX_RETRIES=2;
6 changes: 3 additions & 3 deletions src/client/game/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ export function Game(): JSX.Element {
"game-state",
async () => {
return get("/game-state").then((gameState) => {
setChess(new ChessEngine(gameState.state.position));
setChess(new ChessEngine(gameState.position));
setPause(gameState.pause);
if (gameState.state.gameEndReason !== undefined) {
setGameInterruptedReason(gameState.state.gameEndReason);
if (gameState.gameEndReason !== undefined) {
setGameInterruptedReason(gameState.gameEndReason);
}
return gameState.state;
});
Expand Down
32 changes: 28 additions & 4 deletions src/server/api/game-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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 "./pauseHandler";
import { gamePaused, setPaused } from "./pauseHandler";

type GameState = {
type?: "puzzle" | "human" | "computer";
Expand Down Expand Up @@ -156,7 +156,15 @@ export class HumanGameManager extends GameManager {

console.log("running executor");
console.dir(command, { depth: null });
await executor.execute(command);
await executor.execute(command).catch((reason) => {
setPaused(true);
console.log(reason);
this.chess.undo();
this.socketManager.sendToAll(
new GameHoldMessage(GameHoldReason.GAME_PAUSED),
);
return;
});
console.log("executor done");

if (ids && DO_SAVES) {
Expand Down Expand Up @@ -280,7 +288,15 @@ export class ComputerGameManager extends GameManager {
this.socketManager.sendToAll(new MoveMessage(message.move));
this.chess.makeMove(message.move);

await executor.execute(command);
await executor.execute(command).catch((reason) => {
setPaused(true);
console.log(reason);
this.chess.undo();
this.socketManager.sendToAll(
new GameHoldMessage(GameHoldReason.GAME_PAUSED),
);
return;
});

if (DO_SAVES) {
SaveManager.saveGame(
Expand Down Expand Up @@ -370,7 +386,15 @@ export class PuzzleGameManager extends GameManager {

console.log("running executor");
console.dir(command, { depth: null });
await executor.execute(command);
await executor.execute(command).catch((reason) => {
setPaused(true);
console.log(reason);
this.chess.undo();
this.socketManager.sendToAll(
new GameHoldMessage(GameHoldReason.GAME_PAUSED),
);
return;
});
console.log("executor done");

//if there is another move, make it
Expand Down
3 changes: 2 additions & 1 deletion src/server/api/save-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { Side } from "../../common/game-types";
2;

// Save files contain a date in ms and pgn string
export interface iSave {
Expand Down Expand Up @@ -56,7 +57,7 @@ export class SaveManager {
game: pgn,
pos: fen,
robotPos: Array.from(robots),
oldPos: "",
oldPos: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
oldRobotPos: Array<[string, string]>(),
};
const oldGame = SaveManager.loadGame(hostId + "+" + clientID);
Expand Down
113 changes: 102 additions & 11 deletions src/server/command/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { gamePaused } from "../api/pauseHandler";
import { robotManager } from "../robot/robot-manager";
import { MAX_RETRIES, MOVE_TIMEOUT } from "../utils/env";

/**
* An command which operates on one or more robots.
Expand All @@ -11,6 +12,11 @@ export interface Command {
*/
requirements: Set<object>;

/**
* used for time calculations
*/
height: number;

/**
* Executes the command.
*/
Expand Down Expand Up @@ -45,6 +51,8 @@ export interface Reversible<T extends Reversible<T>> {
export abstract class CommandBase implements Command {
protected _requirements: Set<object> = new Set();

protected _height: number = 1;

public abstract execute(): Promise<void>;

public then(next: Command): SequentialCommandGroup {
Expand All @@ -55,6 +63,14 @@ export abstract class CommandBase implements Command {
return new ParallelCommandGroup([this, new WaitCommand(seconds)]);
}

public get height(): number {
return this._height;
}

public set height(height: number) {
this._height = height;
}

public get requirements(): Set<object> {
return this._requirements;
}
Expand All @@ -72,6 +88,7 @@ export abstract class CommandBase implements Command {
* Note this class redirects the execute implementation to executeRobot.
*/
export abstract class RobotCommand extends CommandBase {
commandIsCompleted = false;
constructor(public readonly robotId: string) {
super();
// TO DISCUSS: idk if its possible for a robot object to change between adding it as a requrement and executing the command but if it is, adding the robot object as a requirement semi defeats the purpose of using robot ids everywhere
Expand All @@ -86,6 +103,8 @@ export abstract class RobotCommand extends CommandBase {
export class WaitCommand extends CommandBase {
constructor(public readonly durationSec: number) {
super();
//in case there is a long wait that isn't accounted for in the regular timeout
this.height = durationSec / MOVE_TIMEOUT;
}
public async execute(): Promise<void> {
return new Promise((resolve) =>
Expand All @@ -112,45 +131,117 @@ function isReversable(obj): obj is Reversible<typeof obj> {
* Executes one or more commands in parallel.
*/
export class ParallelCommandGroup extends CommandGroup {
constructor(public readonly commands: Command[]) {
super(commands);
let max = 1;
for (let x = 0; x < commands.length; x++) {
if (commands[x].height > max) {
max = commands[x].height;
}
}
this.height = max;
}

public async execute(): Promise<void> {
const promises = this.commands
.map((move) => {
if (!gamePaused) return move.execute();
if (!gamePaused) return move.execute().catch();
else return new Promise<void>(() => {}).catch();
})
.filter(Boolean);
return Promise.all(promises).then(null);
if (promises) {
return timeoutRetry(
Promise.all(promises),
MAX_RETRIES,
this.height,
0,
"Parallel Group Error",
) as Promise<void>;
}
}
public async reverse(): Promise<void> {
const promises = this.commands.map((move) => {
if (isReversable(move)) {
move.reverse();
move.reverse().catch();
}
});
return Promise.all(promises).then(null);
return Promise.all(promises).catch().then(null);
}
}

/**
* Executes one or more commands in sequence, one after another.
*/
export class SequentialCommandGroup extends CommandGroup {
constructor(public readonly commands: Command[]) {
super(commands);
let sum = 0;
for (let x = 0; x < commands.length; x++) {
sum += commands[x].height;
}
this.height = sum;
}

public async execute(): Promise<void> {
let promise = Promise.resolve();

for (const command of this.commands) {
promise = promise.then(() => {
if (!gamePaused) return command.execute();
else return Promise.resolve();
});
promise = promise
.then(() => {
if (!gamePaused) return command.execute().catch();
})
.catch();
}
return promise;

return timeoutRetry(
promise,
MAX_RETRIES,
this.height,
0,
"Sequential Group Error",
) as Promise<void>;
}

public async reverse(): Promise<void> {
let promise = Promise.resolve();
for (const command of this.commands) {
if (isReversable(command)) {
promise = promise.then(() => command.reverse());
promise = promise.then(() => command.reverse().catch());
}
}
return promise;
return promise.catch();
}
}

export function timeoutRetry(
promise: Promise<unknown>,
maxRetries: number,
height: number,
count: number,
debugInfo?: string,
): typeof promise {
const timeout = new Promise<void>((_, rej) => {
//time for each move to execute plus time to handle errors
setTimeout(
() => {
rej("Move Timeout");
},
height * MOVE_TIMEOUT * maxRetries * 1.1,
);
}).catch();
return Promise.race([promise, timeout]).catch((reason) => {
if (reason.indexOf("Move Timeout") >= 0) {
if (count < MAX_RETRIES) {
return timeoutRetry(
promise,
maxRetries,
height,
count + 1,
debugInfo,
).catch();
} else {
throw `${reason} failed at height: ${height.toString()} with error: ${debugInfo} \\`;
}
}
});
}
20 changes: 9 additions & 11 deletions src/server/command/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,15 @@ export class CommandExecutor {
* @returns - The command to execute.
*/
public async finishExecution(): Promise<void> {
return Promise.all(
this.runningCommands.map((command) => {
command.execute().finally(() => {
this.oldCommands.unshift(command);
const index = this.runningCommands.indexOf(command);
if (index >= 0) {
this.runningCommands.splice(index, 1);
}
});
}),
).then();
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() {
Expand Down
Loading