From 174e5a1fb4c33cc4466eb3b783411aaaf5265355 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Sat, 30 Dec 2023 17:04:16 +0100 Subject: [PATCH 01/17] add singleton service for timeout scheduling --- packages/server/src/games.ts | 15 +++++--- packages/server/src/index.ts | 6 ++++ packages/server/src/time-control.ts | 11 ++++++ packages/server/src/timeout.ts | 56 +++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 packages/server/src/timeout.ts diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index 44080579..4acb1537 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -8,6 +8,7 @@ import { import { ObjectId, WithId, Document } from "mongodb"; import { getDb } from "./db"; import { io } from "./socket_io"; +import { getTimeoutService } from './index'; import { HasTimeControlConfig, timeControlHandlerMap, @@ -42,14 +43,14 @@ export async function getGame(id: string): Promise { // TODO: db_game might be undefined if unknown ID is provided - console.log(db_game); + //console.log(db_game); const game = outwardFacingGame(db_game); // Legacy games don't have a players field // TODO: remove this code after doing proper db migration if (!game.players) { game.players = await BACKFILL_addEmptyPlayersArray(game); } - console.log(game); + //console.log(game); return game; } @@ -115,8 +116,14 @@ export async function playMove( HasTimeControlConfig(game.config) && ValidateTimeControlConfig(game.config.time_control) ) { - const timeHandler = new timeControlHandlerMap[game.variant](); - await timeHandler.handleMove(game, game_obj, move.player, move.move); + + if (game_obj.result !== '') { + getTimeoutService().clearGameTimeouts(game.id); + + } else { + const timeHandler = new timeControlHandlerMap[game.variant](); + await timeHandler.handleMove(game, game_obj, move.player, move.move); + } } gamesCollection() diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index f838990b..a9f7d358 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -18,6 +18,7 @@ import { Strategy as LocalStrategy } from "passport-local"; import { UserResponse } from "@ogfcommunity/variants-shared"; import { router as apiRouter } from "./api"; import * as socket_io from "./socket_io"; +import { TimeoutService } from "./timeout"; const LOCAL_ORIGIN = "http://localhost:3000"; @@ -130,3 +131,8 @@ const PORT = process.env.PORT || 3001; server.listen(PORT, () => { console.log(`listening on *:${PORT}`); }); + +const timeoutService = new TimeoutService(); +export function getTimeoutService(): TimeoutService { + return timeoutService; +} \ No newline at end of file diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 5e104a9a..4a0b1a3a 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -7,6 +7,8 @@ import { } from "@ogfcommunity/variants-shared"; import { gamesCollection } from "./games"; import { AbstractGame, GameResponse } from "@ogfcommunity/variants-shared"; +import { TimeoutService } from './timeout'; +import { getTimeoutService } from './index'; export function HasTimeControlConfig( game_config: unknown, @@ -51,6 +53,11 @@ export interface ITimeHandler { } class TimeHandlerSequentialMoves implements ITimeHandler { + private _timeoutService: TimeoutService + constructor() { + this._timeoutService = getTimeoutService() + } + async handleMove( game: GameResponse, game_obj: AbstractGame, @@ -98,12 +105,16 @@ class TimeHandlerSequentialMoves implements ITimeHandler { timestamp.getTime() - playerData.onThePlaySince.getTime(); } playerData.onThePlaySince = null; + this._timeoutService.clearPlayerTimeout(game.id, playerNr); + nextPlayers.forEach((player) => { if ( timeData.forPlayer[player] && timeData.forPlayer[player].remainingTimeMS ) { timeData.forPlayer[player].onThePlaySince = timestamp; + + this._timeoutService.scheduleTimeout(game.id, player, timeData.forPlayer[player].remainingTimeMS) } }); diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts new file mode 100644 index 00000000..b2c1ef58 --- /dev/null +++ b/packages/server/src/timeout.ts @@ -0,0 +1,56 @@ +type GameTimeouts = { + // timeout for this player, or null + // used to clear the timer + [player: number]: ReturnType | null +} + +export class TimeoutService { + private timeoutsByGame = new Map(); + + constructor() { + + } + + public initialize(): void { + + } + + private setPlayerTimeout(gameId: string, playerNr: number, timeout: ReturnType | null): void { + let gameTimeouts = this.timeoutsByGame.get(gameId); + + if (gameTimeouts === undefined) { + gameTimeouts = {}; + this.timeoutsByGame.set(gameId, gameTimeouts); + } + else { + const playerTimeout = gameTimeouts[playerNr] + if (playerTimeout) { + clearTimeout(playerTimeout) + } + } + + gameTimeouts[playerNr] = timeout; + } + + public clearGameTimeouts(gameId: string): void { + let gameTimeouts = this.timeoutsByGame.get(gameId) + + if (gameTimeouts !== undefined) { + Object.values(gameTimeouts).filter(t => t !== null).forEach(clearTimeout); + this.timeoutsByGame.delete(gameId) + } + } + + public clearPlayerTimeout(gameId: string, playerNr: number): void { + this.setPlayerTimeout(gameId, playerNr, null); + } + + public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { + let timeout = setTimeout(() => { + // TODO + console.log(`player nr ${playerNr} timed out`) + }, inTimeMs); + + this.setPlayerTimeout(gameId, playerNr, timeout); + } +} \ No newline at end of file From 9f2ff9156a53c00f452a3f9f5028a6b6dc0d05b4 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Sat, 30 Dec 2023 22:06:43 +0100 Subject: [PATCH 02/17] simplified time handler some (but less type safe), add init timeout --- packages/server/src/games.ts | 16 ++- packages/server/src/index.ts | 14 +-- packages/server/src/time-control.ts | 160 ++++++++++++++++++---------- packages/server/src/timeout.ts | 27 ++++- 4 files changed, 146 insertions(+), 71 deletions(-) diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index 4acb1537..6fbaee17 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -10,6 +10,7 @@ import { getDb } from "./db"; import { io } from "./socket_io"; import { getTimeoutService } from './index'; import { + GetInitialTimeControl, HasTimeControlConfig, timeControlHandlerMap, ValidateTimeControlConfig, @@ -36,6 +37,11 @@ export async function getGames( return (await games).map(outwardFacingGame); } +export async function getGamesWithTimeControl(): Promise { +const games = gamesCollection().find({ time_control: { $ne: null } }).toArray(); + return (await games).map(outwardFacingGame); +} + export async function getGame(id: string): Promise { const db_game = await gamesCollection().findOne({ _id: new ObjectId(id), @@ -43,14 +49,14 @@ export async function getGame(id: string): Promise { // TODO: db_game might be undefined if unknown ID is provided - //console.log(db_game); + console.log(db_game); const game = outwardFacingGame(db_game); // Legacy games don't have a players field // TODO: remove this code after doing proper db migration if (!game.players) { game.players = await BACKFILL_addEmptyPlayersArray(game); } - //console.log(game); + console.log(game); return game; } @@ -63,6 +69,7 @@ export async function createGame( variant: variant, moves: [] as MovesType[], config: config, + time_control: GetInitialTimeControl(variant, config) }; const result = await gamesCollection().insertOne(game); @@ -112,6 +119,7 @@ export async function playMove( const { player, move: new_move } = getOnlyMove(moves); game_obj.playMove(player, new_move); + let timeControl = game.time_control; if ( HasTimeControlConfig(game.config) && ValidateTimeControlConfig(game.config.time_control) @@ -122,12 +130,12 @@ export async function playMove( } else { const timeHandler = new timeControlHandlerMap[game.variant](); - await timeHandler.handleMove(game, game_obj, move.player, move.move); + timeControl = timeHandler.handleMove(game, game_obj, move.player, move.move); } } gamesCollection() - .updateOne({ _id: new ObjectId(game_id) }, { $push: { moves: moves } }) + .updateOne({ _id: new ObjectId(game_id) }, { $push: { moves: moves }, $set: { time_control: timeControl } }) .catch(console.log); game.moves.push(moves); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index a9f7d358..d416e9ea 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -37,8 +37,13 @@ passport.use( }), ); +const timeoutService = new TimeoutService(); +export function getTimeoutService(): TimeoutService { + return timeoutService; +} + // Initialize MongoDB -connectToDb().catch((e) => { +connectToDb().then(() => timeoutService.initialize()).catch((e) => { console.log("Unable to connect to the database."); console.log(e); }); @@ -130,9 +135,4 @@ if (isProd) { const PORT = process.env.PORT || 3001; server.listen(PORT, () => { console.log(`listening on *:${PORT}`); -}); - -const timeoutService = new TimeoutService(); -export function getTimeoutService(): TimeoutService { - return timeoutService; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 4a0b1a3a..c2f88b86 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -1,11 +1,10 @@ -import { ObjectId } from "mongodb"; import { TimeControlType, ITimeControlBase, ITimeControlConfig, IConfigWithTimeControl, + makeGameObject, } from "@ogfcommunity/variants-shared"; -import { gamesCollection } from "./games"; import { AbstractGame, GameResponse } from "@ogfcommunity/variants-shared"; import { TimeoutService } from './timeout'; import { getTimeoutService } from './index'; @@ -42,14 +41,25 @@ export function ValidateTimeControlBase( ); } +export function GetInitialTimeControl(variant: string, config: object): ITimeControlBase | null { + if (!HasTimeControlConfig(config)) return null; + + const handler = new timeControlHandlerMap[variant](); + return handler.initialState(variant, config); +} + // validation of the config should happen before this is called export interface ITimeHandler { + initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase; + handleMove( game: GameResponse, game_obj: AbstractGame, playerNr: number, move: string, - ): Promise; + ): ITimeControlBase; + + getMsUntilTimeout(game: GameResponse, playerNr: number): number; } class TimeHandlerSequentialMoves implements ITimeHandler { @@ -58,94 +68,134 @@ class TimeHandlerSequentialMoves implements ITimeHandler { this._timeoutService = getTimeoutService() } - async handleMove( - game: GameResponse, - game_obj: AbstractGame, - playerNr: number, - ): Promise { - if (!HasTimeControlConfig(game.config)) { - console.log("has no time control config"); + initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase { + const numPlayers = makeGameObject(variant, config).numPlayers(); - return; - } + const timeControl: ITimeControlBase = { + moveTimestamps: [], + forPlayer: {}, + }; - switch (game.config.time_control.type) { + for (let i = 0; i < numPlayers; i++) { + timeControl.forPlayer[i] = { + remainingTimeMS: config.time_control.mainTimeMS, + onThePlaySince: null + } + } + + switch (config.time_control.type) { case TimeControlType.Absolute: { - let timeData: ITimeControlBase = game.time_control; - if (timeData === undefined) { - timeData = { - moveTimestamps: [], - forPlayer: {}, - }; - } + // nothing to do + break; + } - if (timeData.forPlayer[playerNr] === undefined) { - timeData.forPlayer[playerNr] = { - remainingTimeMS: game.config.time_control.mainTimeMS, - onThePlaySince: undefined, - }; - } + case TimeControlType.Fischer: { + // nothing to do + break; + } - const nextPlayers = game_obj.nextToPlay(); + default: { + console.error('received config with invalid time control type'); + } + } - if (timeData.forPlayer[playerNr] === null) { - console.error(`game with id ${game.id} has defect time control data`); - return; - } + return timeControl + } - const playerData = timeData.forPlayer[playerNr]; + handleMove( + game: GameResponse, + game_obj: AbstractGame, + playerNr: number, + ): ITimeControlBase { + const config = game.config as IConfigWithTimeControl; + + switch (config.time_control.type) { + case TimeControlType.Absolute: { + const timeControl: ITimeControlBase = game.time_control; + const playerData = timeControl.forPlayer[playerNr]; const timestamp = new Date(); - timeData.moveTimestamps.push(timestamp); - if (playerData.onThePlaySince !== undefined) { - // with this construction, time will not be substracted before the first move of a player - // which is good imho - playerData.remainingTimeMS -= - timestamp.getTime() - playerData.onThePlaySince.getTime(); - } + timeControl.moveTimestamps.push(timestamp); playerData.onThePlaySince = null; this._timeoutService.clearPlayerTimeout(game.id, playerNr); + const nextPlayers = game_obj.nextToPlay(); nextPlayers.forEach((player) => { - if ( - timeData.forPlayer[player] && - timeData.forPlayer[player].remainingTimeMS - ) { - timeData.forPlayer[player].onThePlaySince = timestamp; - - this._timeoutService.scheduleTimeout(game.id, player, timeData.forPlayer[player].remainingTimeMS) - } + timeControl.forPlayer[player].onThePlaySince = timestamp; + this._timeoutService.scheduleTimeout(game.id, player, timeControl.forPlayer[player].remainingTimeMS) }); - await gamesCollection() - .updateOne( - { _id: new ObjectId(game.id) }, - { $set: { time_control: timeData } }, - ) - .catch(console.log); - break; + return timeControl; } case TimeControlType.Invalid: + throw Error(`game with id ${game.id} has invalid time control type`); + } + } + + getMsUntilTimeout(game: GameResponse, playerNr: number): number | null { + if (!HasTimeControlConfig(game.config)) { + console.log("has no time control config"); + + return null; + } + + switch (game.config.time_control.type) { + case TimeControlType.Absolute: + case TimeControlType.Fischer: + const times: ITimeControlBase = game.time_control; + + if (times === undefined || times.forPlayer === undefined) { + // old game with no moves + return null; + } + + const playerTime = times.forPlayer[playerNr] + + if (playerTime === undefined || playerTime.onThePlaySince === null) { + // player has not played a move yet + // or player is not on the play + return null; + } + + const timeoutTime = playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; + + return timeoutTime - new Date().getTime() + + default: { console.error(`game with id ${game.id} has invalid time control type`); return; + } } } } class TimeHandlerParallelMoves implements ITimeHandler { + + initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase { + throw Error( + "time control handler for parallel moves is not implemented yet", + ); + } + handleMove( _game: GameResponse, _game_obj: AbstractGame, _playerNr: number, _move: string, - ): Promise { + ): ITimeControlBase { console.log( "time control handler for parallel moves is not implemented yet", ); return; } + + getMsUntilTimeout(game: GameResponse, playerNr: number): number { + throw Error( + "time control handler for parallel moves is not implemented yet", + ); + } } export const timeControlHandlerMap: { diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index b2c1ef58..ade4532c 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -1,3 +1,9 @@ +import { getGamesWithTimeControl } from "./games"; +import { + makeGameObject, + } from "@ogfcommunity/variants-shared"; +import { timeControlHandlerMap } from "./time-control"; + type GameTimeouts = { // timeout for this player, or null // used to clear the timer @@ -8,11 +14,22 @@ export class TimeoutService { private timeoutsByGame = new Map(); constructor() { - } - public initialize(): void { + public async initialize(): Promise { + for (const game of (await getGamesWithTimeControl())) { + const game_object = makeGameObject(game.variant, game.config); + if (game_object.result !== '') { + // game is already finished + continue + } + const timeHandler = new timeControlHandlerMap[game.variant]() + for (const playerNr of game_object.nextToPlay()) { + const t = timeHandler.getMsUntilTimeout(game, playerNr) + this.scheduleTimeout(game.id, playerNr, t); + } + } } private setPlayerTimeout(gameId: string, playerNr: number, timeout: ReturnType | null): void { @@ -33,7 +50,7 @@ export class TimeoutService { } public clearGameTimeouts(gameId: string): void { - let gameTimeouts = this.timeoutsByGame.get(gameId) + const gameTimeouts = this.timeoutsByGame.get(gameId) if (gameTimeouts !== undefined) { Object.values(gameTimeouts).filter(t => t !== null).forEach(clearTimeout); @@ -46,9 +63,9 @@ export class TimeoutService { } public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { - let timeout = setTimeout(() => { + const timeout = setTimeout(() => { // TODO - console.log(`player nr ${playerNr} timed out`) + console.log(`player nr ${playerNr} timed out in game ${gameId}`) }, inTimeMs); this.setPlayerTimeout(gameId, playerNr, timeout); From a394c6546a10d65fe4d58f07dcdac8e14e7e34e9 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Sat, 30 Dec 2023 22:20:54 +0100 Subject: [PATCH 03/17] schedule timeout only after first move of player --- packages/server/src/time-control.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index c2f88b86..d3dcfec9 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -120,7 +120,8 @@ class TimeHandlerSequentialMoves implements ITimeHandler { playerData.onThePlaySince = null; this._timeoutService.clearPlayerTimeout(game.id, playerNr); - const nextPlayers = game_obj.nextToPlay(); + // filter out players who have not played any move yet + const nextPlayers = game_obj.nextToPlay().filter(playerNr => game.moves.some(move => playerNr in move)); nextPlayers.forEach((player) => { timeControl.forPlayer[player].onThePlaySince = timestamp; this._timeoutService.scheduleTimeout(game.id, player, timeControl.forPlayer[player].remainingTimeMS) From 85bdba758cbe0d5410ff402f14a26f860742d3bf Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Mon, 1 Jan 2024 20:26:25 +0100 Subject: [PATCH 04/17] add timeout move, add parallel move time control handler, handle timeout in variants --- packages/server/src/games.ts | 17 +- packages/server/src/time-control.ts | 149 +++++++++++++++--- packages/server/src/timeout.ts | 76 +++++++-- packages/shared/src/lib/utils.ts | 5 + .../src/time_control/time_control.types.ts | 11 ++ packages/shared/src/variants/baduk.ts | 8 +- .../badukWithAbstractBoard.ts | 8 +- packages/shared/src/variants/chess.ts | 12 ++ .../src/variants/fractional/fractional.ts | 31 +++- packages/shared/src/variants/parallel.ts | 39 ++++- 10 files changed, 304 insertions(+), 52 deletions(-) diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index 6fbaee17..66c103ad 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -49,14 +49,14 @@ export async function getGame(id: string): Promise { // TODO: db_game might be undefined if unknown ID is provided - console.log(db_game); + //console.log(db_game); const game = outwardFacingGame(db_game); // Legacy games don't have a players field // TODO: remove this code after doing proper db migration if (!game.players) { game.players = await BACKFILL_addEmptyPlayersArray(game); } - console.log(game); + //console.log(game); return game; } @@ -102,13 +102,13 @@ export async function playMove( throw Error("Game is already finished."); } - const move = getOnlyMove(moves); + const { player: playerNr, move: new_move } = getOnlyMove(moves); - if (!game.players || game.players[move.player] == null) { - throw Error(`Seat ${move.player} not occupied!`); + if (!game.players || game.players[playerNr] == null) { + throw Error(`Seat ${playerNr} not occupied!`); } - const expected_player = game.players[move.player]; + const expected_player = game.players[playerNr]; if (expected_player.id !== user_id) { throw Error( @@ -116,8 +116,7 @@ export async function playMove( ); } - const { player, move: new_move } = getOnlyMove(moves); - game_obj.playMove(player, new_move); + game_obj.playMove(playerNr, new_move); let timeControl = game.time_control; if ( @@ -130,7 +129,7 @@ export async function playMove( } else { const timeHandler = new timeControlHandlerMap[game.variant](); - timeControl = timeHandler.handleMove(game, game_obj, move.player, move.move); + timeControl = timeHandler.handleMove(game, game_obj, playerNr, new_move); } } diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index d3dcfec9..f1bda909 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -4,6 +4,7 @@ import { ITimeControlConfig, IConfigWithTimeControl, makeGameObject, + TimeControlParallel, } from "@ogfcommunity/variants-shared"; import { AbstractGame, GameResponse } from "@ogfcommunity/variants-shared"; import { TimeoutService } from './timeout'; @@ -111,16 +112,23 @@ class TimeHandlerSequentialMoves implements ITimeHandler { switch (config.time_control.type) { - case TimeControlType.Absolute: { - const timeControl: ITimeControlBase = game.time_control; + case TimeControlType.Absolute: + let timeControl: ITimeControlBase = game.time_control; + // could happen for old games + if (timeControl === undefined) { + timeControl = this.initialState(game.variant, config) + } const playerData = timeControl.forPlayer[playerNr]; const timestamp = new Date(); timeControl.moveTimestamps.push(timestamp); + if (!playerData.onThePlaySince === null) { + playerData.remainingTimeMS -= timestamp.getTime() - playerData.onThePlaySince.getTime(); + } playerData.onThePlaySince = null; this._timeoutService.clearPlayerTimeout(game.id, playerNr); - // filter out players who have not played any move yet + // time control starts only after the first move of player const nextPlayers = game_obj.nextToPlay().filter(playerNr => game.moves.some(move => playerNr in move)); nextPlayers.forEach((player) => { timeControl.forPlayer[player].onThePlaySince = timestamp; @@ -128,7 +136,6 @@ class TimeHandlerSequentialMoves implements ITimeHandler { }); return timeControl; - } case TimeControlType.Invalid: throw Error(`game with id ${game.id} has invalid time control type`); @@ -173,29 +180,133 @@ class TimeHandlerSequentialMoves implements ITimeHandler { } class TimeHandlerParallelMoves implements ITimeHandler { + private _timeoutService: TimeoutService; + constructor() { + this._timeoutService = getTimeoutService(); + } - initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase { - throw Error( - "time control handler for parallel moves is not implemented yet", - ); + initialState(variant: string, config: IConfigWithTimeControl): TimeControlParallel { + const numPlayers = makeGameObject(variant, config).numPlayers(); + + const timeControl: TimeControlParallel = { + moveTimestamps: [], + forPlayer: {}, + }; + + for (let i = 0; i < numPlayers; i++) { + timeControl.forPlayer[i] = { + remainingTimeMS: config.time_control.mainTimeMS, + onThePlaySince: null, + stagedMoveAt: null, + } + } + + switch (config.time_control.type) { + case TimeControlType.Absolute: { + // nothing to do + break; + } + + case TimeControlType.Fischer: { + // nothing to do + break; + } + + default: { + console.error('received config with invalid time control type'); + } + } + + return timeControl } handleMove( - _game: GameResponse, - _game_obj: AbstractGame, - _playerNr: number, - _move: string, + game: GameResponse, + game_obj: AbstractGame, + playerNr: number, ): ITimeControlBase { - console.log( - "time control handler for parallel moves is not implemented yet", - ); - return; + const config = game.config as IConfigWithTimeControl; + + switch (config.time_control.type) { + + case TimeControlType.Absolute: + let timeControl = game.time_control as TimeControlParallel; + // could happen for old games + if (timeControl === undefined) { + timeControl = this.initialState(game.variant, config) + } + + const timestamp = new Date(); + + timeControl.moveTimestamps.push(timestamp); + timeControl.forPlayer[playerNr].stagedMoveAt = timestamp; + this._timeoutService.clearPlayerTimeout(game.id, playerNr); + + // check if the round finishes with this move + // + // I'm not sure if this will always work as intended + // because this is the game object after the move has been played + // whereas we actually need the players that are on the play in the round that the move belongs to + if (game_obj.nextToPlay().every(player_nr => timeControl.forPlayer[player_nr].stagedMoveAt !== null)) { + for (let player_nr of game_obj.nextToPlay()) { + const playerData = timeControl.forPlayer[player_nr]; + + // time control starts only after first move of player + if (game.moves.filter(move => player_nr in move).length > (player_nr === playerNr ? 0 : 1)) { + playerData.remainingTimeMS -= playerData.stagedMoveAt.getTime() - playerData.onThePlaySince.getTime(); + } + + this._timeoutService.scheduleTimeout(game.id, player_nr, timeControl.forPlayer[player_nr].remainingTimeMS) + playerData.onThePlaySince = timestamp; + playerData.stagedMoveAt = null; + } + } + + return timeControl; + + case TimeControlType.Invalid: + throw Error(`game with id ${game.id} has invalid time control type`); + } } getMsUntilTimeout(game: GameResponse, playerNr: number): number { - throw Error( - "time control handler for parallel moves is not implemented yet", - ); + if (!HasTimeControlConfig(game.config)) { + console.log("has no time control config"); + + return null; + } + + switch (game.config.time_control.type) { + case TimeControlType.Absolute: + case TimeControlType.Fischer: + const times: TimeControlParallel = game.time_control as TimeControlParallel; + + if (times === undefined || times.forPlayer === undefined) { + // old game with no moves + return null; + } + + const playerTime = times.forPlayer[playerNr] + + if (playerTime === undefined || playerTime.onThePlaySince === null) { + // player has not played a move yet + // or player is not on the play + return null; + } + + if (playerTime.stagedMoveAt !== null) { + return null; + } + + const timeoutTime = playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; + + return timeoutTime - new Date().getTime() + + default: { + console.error(`game with id ${game.id} has invalid time control type`); + return; + } + } } } diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index ade4532c..9b7f5a72 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -1,12 +1,16 @@ -import { getGamesWithTimeControl } from "./games"; +import { gamesCollection, getGame, getGamesWithTimeControl } from "./games"; import { + MovesType, + getOnlyMove, makeGameObject, } from "@ogfcommunity/variants-shared"; import { timeControlHandlerMap } from "./time-control"; +import { ObjectId } from "mongodb"; +import { io } from "./socket_io"; type GameTimeouts = { // timeout for this player, or null - // used to clear the timer + // used to clear the timeout timer [player: number]: ReturnType | null } @@ -17,17 +21,32 @@ export class TimeoutService { } public async initialize(): Promise { + // I would like to improve this by only querying unfinished games from db + // but currently I think its not possible, because of the game result + // is not being stored in the db directly for (const game of (await getGamesWithTimeControl())) { const game_object = makeGameObject(game.variant, game.config); - if (game_object.result !== '') { - // game is already finished - continue - } - const timeHandler = new timeControlHandlerMap[game.variant]() - for (const playerNr of game_object.nextToPlay()) { - const t = timeHandler.getMsUntilTimeout(game, playerNr) - this.scheduleTimeout(game.id, playerNr, t); + try { + // check if game is already finished + game.moves.forEach((moves) => { + const { player, move } = getOnlyMove(moves); + game_object.playMove(player, move); + }); + + if (game_object.result !== '') { + continue + } + + const timeHandler = new timeControlHandlerMap[game.variant]() + for (const playerNr of game_object.nextToPlay()) { + const t = timeHandler.getMsUntilTimeout(game, playerNr) + this.scheduleTimeout(game.id, playerNr, t); + } + } + catch (error) { + console.error(error); + continue; } } } @@ -63,11 +82,40 @@ export class TimeoutService { } public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { - const timeout = setTimeout(() => { - // TODO - console.log(`player nr ${playerNr} timed out in game ${gameId}`) - }, inTimeMs); + const timeoutResolver = async () => { + console.log('timeout triggered') + const game = await getGame(gameId) + + const timeoutMove: MovesType = {[playerNr]: 'timeout'}; + game.moves.push(timeoutMove); + let timeControl = game.time_control; + + // this next part is somewhat duplicated from the playMove function + // which I don't like. But for parallel variants and consistency (move timestamps), + // the timeout move needs to be handled as well. + const game_object = makeGameObject(game.variant, game.config); + game.moves.forEach((moves) => { + const { player, move } = getOnlyMove(moves); + game_object.playMove(player, move); + }); + + if (game_object.result !== '') { + this.clearGameTimeouts(game.id); + + } else { + const timeHandler = new timeControlHandlerMap[game.variant](); + timeControl = timeHandler.handleMove(game, game_object, playerNr, 'timeout'); + } + + // TODO: improving the error handling would be great in future + await gamesCollection() + .updateOne({ _id: new ObjectId(gameId) }, { $push: { moves: timeoutMove }, $set: { time_control: timeControl } }) + .catch(console.log); + + io().emit(`game/${gameId}`, game) + } + const timeout = setTimeout(timeoutResolver, inTimeMs); this.setPlayerTimeout(gameId, playerNr, timeout); } } \ No newline at end of file diff --git a/packages/shared/src/lib/utils.ts b/packages/shared/src/lib/utils.ts index 8a057c04..7c285def 100644 --- a/packages/shared/src/lib/utils.ts +++ b/packages/shared/src/lib/utils.ts @@ -16,3 +16,8 @@ export function getOnlyMove(moves: MovesType): { const player = Number(players[0]); return { player, move: moves[player] }; } + +export type Participation = { + playerNr: number, + dropOutAtRound: number | null +}; \ No newline at end of file diff --git a/packages/shared/src/time_control/time_control.types.ts b/packages/shared/src/time_control/time_control.types.ts index 9fd24986..9024b6b9 100644 --- a/packages/shared/src/time_control/time_control.types.ts +++ b/packages/shared/src/time_control/time_control.types.ts @@ -29,3 +29,14 @@ export interface ITimeControlBase { [player: number]: IPerPlayerTimeControlBase; }; } + +export type PerPlayerTimeControlParallel = IPerPlayerTimeControlBase & { + stagedMoveAt: Date | null; +} + +export type TimeControlParallel = { + moveTimestamps: Date[]; + forPlayer: { + [player: number]: PerPlayerTimeControlParallel; + }; +} \ No newline at end of file diff --git a/packages/shared/src/variants/baduk.ts b/packages/shared/src/variants/baduk.ts index fac1d013..8c34bc7f 100644 --- a/packages/shared/src/variants/baduk.ts +++ b/packages/shared/src/variants/baduk.ts @@ -66,6 +66,12 @@ export class Baduk extends AbstractGame { return; } + if (move === "timeout") { + this.phase = "gameover" + this.result = player === 0 ? "W+T" : "B+T" + return; + } + if (move != "pass") { const decoded_move = Coordinate.fromSgfRepr(move); const { x, y } = decoded_move; @@ -94,7 +100,7 @@ export class Baduk extends AbstractGame { } override specialMoves() { - return { pass: "Pass", resign: "Resign" }; + return { pass: "Pass", resign: "Resign", timeout: "Timeout" }; } private playMoveInternal(move: Coordinate): void { diff --git a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts index e21d9e19..bfd0aa61 100644 --- a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts +++ b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts @@ -76,6 +76,12 @@ export class BadukWithAbstractBoard extends AbstractGame< return; } + if (move === "timeout") { + this.phase = "gameover" + this.result = player === 0 ? "W+T" : "B+T" + return; + } + const decoded_move = decodeMove(move); if (isOutOfBounds(decoded_move, this.board)) { throw Error( @@ -120,7 +126,7 @@ export class BadukWithAbstractBoard extends AbstractGame< } specialMoves() { - return { pass: "Pass", resign: "Resign" }; + return { pass: "Pass", resign: "Resign", timeout: "Timeout" }; } defaultConfig(): BadukWithAbstractBoardConfig { diff --git a/packages/shared/src/variants/chess.ts b/packages/shared/src/variants/chess.ts index 02bb0417..39c56448 100644 --- a/packages/shared/src/variants/chess.ts +++ b/packages/shared/src/variants/chess.ts @@ -10,6 +10,18 @@ export class ChessGame extends AbstractGame { private chess = new Chess(); playMove(player: number, move: string): void { + if (move === "resign") { + this.phase = "gameover"; + this.result = player === 0 ? "B+R" : "W+R"; + return; + } + + if (move === "timeout") { + this.phase = "gameover" + this.result = player === 0 ? "B+T" : "W+T"; + return; + } + if (player === 1 && "b" != this.chess.turn()) { throw Error("Not Black's turn"); } diff --git a/packages/shared/src/variants/fractional/fractional.ts b/packages/shared/src/variants/fractional/fractional.ts index e4d38216..13e3b2a8 100644 --- a/packages/shared/src/variants/fractional/fractional.ts +++ b/packages/shared/src/variants/fractional/fractional.ts @@ -4,6 +4,7 @@ import { AbstractBadukConfig, } from "../../lib/abstractBaduk/abstractBaduk"; import { FractionalStone } from "./fractionalStone"; +import { Participation } from "../../lib/utils"; export type Color = | "black" @@ -48,6 +49,8 @@ export class Fractional extends AbstractBaduk< FractionalState > { private stagedMoves: (FractionalIntersection | null)[]; + private playerParticipation = this.initializeParticipation(this.config.players.length); + private numberOfRounds: number = 0; constructor(config?: FractionalConfig) { super(config); @@ -55,6 +58,17 @@ export class Fractional extends AbstractBaduk< } playMove(p: number, m: string): void { + if (m === 'resign' || m === 'timeout') { + this.playerParticipation[p].dropOutAtRound = this.numberOfRounds; + + if (this.nextToPlay().length < 2) { + this.phase = 'gameover'; + // TODO: declare winner + } + + return; + } + const move = this.decodeMove(p, m); if (!move) { throw new Error(`Couldn't decode move ${{ player: p, move: m }}`); @@ -70,8 +84,8 @@ export class Fractional extends AbstractBaduk< if ( this.stagedMoves.every( - (stagedMove): stagedMove is FractionalIntersection => - stagedMove !== null, + (stagedMove, playerNr): stagedMove is FractionalIntersection => + stagedMove !== null || !this.nextToPlay().includes(playerNr), ) ) { this.intersections.forEach((intersection) => { @@ -95,6 +109,7 @@ export class Fractional extends AbstractBaduk< this.removeChains(false); this.stagedMoves = this.stagedMovesDefaults(); + this.numberOfRounds++; } } @@ -117,7 +132,9 @@ export class Fractional extends AbstractBaduk< } nextToPlay(): number[] { - return [...Array(this.config.players.length).keys()]; + return this.playerParticipation + .filter(x => x.dropOutAtRound === null || x.dropOutAtRound > this.numberOfRounds) + .map(p => p.playerNr) } numPlayers(): number { @@ -163,4 +180,12 @@ export class Fractional extends AbstractBaduk< null, ); } + + initializeParticipation(numPlayers: number): Participation[] { + const participation = new Array(numPlayers); + for (let i = 0; i < numPlayers; i++) { + participation[i] = {playerNr: i, dropOutAtRound: null}; + } + return participation; + } } diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index 5ab58089..304ca121 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -1,7 +1,7 @@ import { AbstractGame } from "../abstract_game"; import { Coordinate } from "../lib/coordinate"; import { Grid } from "../lib/grid"; -import { MovesType } from "../lib/utils"; +import { MovesType, Participation } from "../lib/utils"; export interface ParallelGoConfig { width: number; @@ -30,6 +30,8 @@ export class ParallelGo extends AbstractGame< private board: Grid; private staged: MovesType = {}; private last_round: MovesType = {}; + private playerParticipation = this.initializeParticipation(this.config.num_players); + private numberOfRounds: number = 0; constructor(config?: ParallelGoConfig) { super(config); @@ -51,7 +53,9 @@ export class ParallelGo extends AbstractGame< } nextToPlay(): number[] { - return [...Array(this.config.num_players).keys()]; + return this.playerParticipation + .filter(x => x.dropOutAtRound === null || x.dropOutAtRound > this.numberOfRounds) + .map(p => p.playerNr) } playMove(player: number, move: string): void { @@ -68,9 +72,24 @@ export class ParallelGo extends AbstractGame< } } + if (move === 'resign' || move === 'timeout') { + this.playerParticipation[player].dropOutAtRound = this.numberOfRounds; + + if (this.nextToPlay().length < 2) { + this.phase = 'gameover'; + // TODO: declare winner + } + + return; + } + + if (!this.nextToPlay().includes(player)) { + throw new Error('Not your turn') + } + this.staged[player] = move; - if (Object.entries(this.staged).length !== this.numPlayers()) { + if (this.nextToPlay().some(playerNr => !(playerNr in this.staged))) { // Don't play moves until everybody has staged a move return; } @@ -88,7 +107,7 @@ export class ParallelGo extends AbstractGame< const player = Number(player_str); this.board.at(decoded_move)?.push(player); }); - if (num_passes === this.numPlayers()) { + if (num_passes === this.nextToPlay().length) { this.phase = "gameover"; } @@ -107,6 +126,8 @@ export class ParallelGo extends AbstractGame< this.last_round = this.staged; this.staged = {}; + this.numberOfRounds++; + console.log(this.numberOfRounds) } numPlayers(): number { @@ -115,7 +136,7 @@ export class ParallelGo extends AbstractGame< specialMoves() { // TODO: support resign - return { pass: "Pass" }; + return { pass: "Pass", timeout: "Timeout" }; } replaceMultiColoredStonesWith(arr: number[]) { @@ -212,6 +233,14 @@ export class ParallelGo extends AbstractGame< group.stones.forEach((pos) => this.board.set(pos, [])); }); } + + initializeParticipation(numPlayers: number): Participation[] { + const participation = new Array(numPlayers); + for (let i = 0; i < numPlayers; i++) { + participation[i] = {playerNr: i, dropOutAtRound: null}; + } + return participation; + } } interface Group { From bffdd2aad17540764e1c188f5ce764f40fbe55ec Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Mon, 1 Jan 2024 22:13:55 +0100 Subject: [PATCH 05/17] timer works for parallel moves time control --- .../vue-client/src/components/GameTimer.vue | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/vue-client/src/components/GameTimer.vue b/packages/vue-client/src/components/GameTimer.vue index 74cafbf0..b45348a0 100644 --- a/packages/vue-client/src/components/GameTimer.vue +++ b/packages/vue-client/src/components/GameTimer.vue @@ -48,18 +48,32 @@ function resetTimer(): void { isDefined(props.time_control.onThePlaySince) && props.time_control.remainingTimeMS !== null ) { - isCountingDown.value = true; - const onThePlaySince: Date = new Date(props.time_control.onThePlaySince); - const now = new Date(); - time.value -= now.getTime() - onThePlaySince.getTime(); + // this is not ideal + if ( + "stagedMoveAt" in props.time_control && + props.time_control.stagedMoveAt !== null + ) { + isCountingDown.value = false; + const onThePlaySince = new Date(props.time_control.onThePlaySince); + const stagedMove = new Date(props.time_control.stagedMoveAt as Date); + time.value = Math.max( + 0, + time.value - (stagedMove.getTime() - onThePlaySince.getTime()) + ); + } else { + isCountingDown.value = true; + const onThePlaySince: Date = new Date(props.time_control.onThePlaySince); + const now = new Date(); + time.value = Math.max( + 0, + time.value - (now.getTime() - onThePlaySince.getTime()) + ); + } } else { isCountingDown.value = false; } - if ( - isDefined(props.time_control?.onThePlaySince) && - typeof window !== "undefined" - ) { + if (isCountingDown.value && typeof window !== "undefined") { timerIndex = window.setInterval(() => { if (time.value <= 0 && timerIndex !== null) { clearInterval(timerIndex); From 549c55d5896389649b44f68eea3f467dae05a5e7 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Mon, 1 Jan 2024 22:15:38 +0100 Subject: [PATCH 06/17] fix variants for parallel time control and timeout to work --- packages/server/src/time-control.ts | 3 +- packages/server/src/timeout.ts | 1 - .../src/variants/fractional/fractional.ts | 35 +++++++++++-------- packages/shared/src/variants/parallel.ts | 18 +++++----- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index f1bda909..52146d57 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -122,7 +122,8 @@ class TimeHandlerSequentialMoves implements ITimeHandler { const timestamp = new Date(); timeControl.moveTimestamps.push(timestamp); - if (!playerData.onThePlaySince === null) { + + if (playerData.onThePlaySince !== null) { playerData.remainingTimeMS -= timestamp.getTime() - playerData.onThePlaySince.getTime(); } playerData.onThePlaySince = null; diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 9b7f5a72..243bef68 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -83,7 +83,6 @@ export class TimeoutService { public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { const timeoutResolver = async () => { - console.log('timeout triggered') const game = await getGame(gameId) const timeoutMove: MovesType = {[playerNr]: 'timeout'}; diff --git a/packages/shared/src/variants/fractional/fractional.ts b/packages/shared/src/variants/fractional/fractional.ts index 13e3b2a8..4dd624fa 100644 --- a/packages/shared/src/variants/fractional/fractional.ts +++ b/packages/shared/src/variants/fractional/fractional.ts @@ -64,26 +64,31 @@ export class Fractional extends AbstractBaduk< if (this.nextToPlay().length < 2) { this.phase = 'gameover'; // TODO: declare winner + + return; } + } else { + // stage move - return; - } + const move = this.decodeMove(p, m); + if (!move) { + throw new Error(`Couldn't decode move ${{ player: p, move: m }}`); + } - const move = this.decodeMove(p, m); - if (!move) { - throw new Error(`Couldn't decode move ${{ player: p, move: m }}`); - } + if (move.intersection.stone) { + throw new Error( + `There is already a stone at intersection ${move.intersection.id}`, + ); + } - if (move.intersection.stone) { - throw new Error( - `There is already a stone at intersection ${move.intersection.id}`, - ); - } + if (!this.nextToPlay().includes(p)) { + throw new Error('Not your turn') + } - this.stagedMoves[move.player.index] = move.intersection; + this.stagedMoves[move.player.index] = move.intersection; + } - if ( - this.stagedMoves.every( + if (this.stagedMoves.every( (stagedMove, playerNr): stagedMove is FractionalIntersection => stagedMove !== null || !this.nextToPlay().includes(playerNr), ) @@ -94,7 +99,7 @@ export class Fractional extends AbstractBaduk< // place all moves and proceed to next round const playedIntersections = new Set(); - this.stagedMoves.forEach((intersection, playerId) => { + this.stagedMoves.filter(x => x !== null).forEach((intersection, playerId) => { playedIntersections.add(intersection); const colors = intersection.stone?.colors ?? diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index 304ca121..7a7cfec2 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -78,17 +78,20 @@ export class ParallelGo extends AbstractGame< if (this.nextToPlay().length < 2) { this.phase = 'gameover'; // TODO: declare winner + return } - - return; } - - if (!this.nextToPlay().includes(player)) { - throw new Error('Not your turn') + else + { + // stage move + + if (!this.nextToPlay().includes(player)) { + throw new Error('Not your turn') + } + + this.staged[player] = move; } - this.staged[player] = move; - if (this.nextToPlay().some(playerNr => !(playerNr in this.staged))) { // Don't play moves until everybody has staged a move return; @@ -127,7 +130,6 @@ export class ParallelGo extends AbstractGame< this.last_round = this.staged; this.staged = {}; this.numberOfRounds++; - console.log(this.numberOfRounds) } numPlayers(): number { From 29fc568200b1aaaf2fab84064d8c92fff8a9aaff Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Mon, 1 Jan 2024 23:12:53 +0100 Subject: [PATCH 07/17] add docs for time control and timeout service --- packages/server/src/time-control.ts | 28 ++++++++++++++++++++++++++++ packages/server/src/timeout.ts | 15 +++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 52146d57..5b9cb21d 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -10,6 +10,10 @@ import { AbstractGame, GameResponse } from "@ogfcommunity/variants-shared"; import { TimeoutService } from './timeout'; import { getTimeoutService } from './index'; +/** + * Validates whether game_config has type and + * properties for being a config with time control + */ export function HasTimeControlConfig( game_config: unknown, ): game_config is IConfigWithTimeControl { @@ -20,6 +24,10 @@ export function HasTimeControlConfig( ); } +/** + * Validates whether time_control_config has type and + * properties for being a time control config + */ export function ValidateTimeControlConfig( time_control_config: unknown, ): time_control_config is ITimeControlConfig { @@ -31,6 +39,10 @@ export function ValidateTimeControlConfig( ); } +/** + * Validates whether time_control has type and + * properties for being a basic time control data object + */ export function ValidateTimeControlBase( time_control: unknown, ): time_control is ITimeControlBase { @@ -42,6 +54,10 @@ export function ValidateTimeControlBase( ); } +/** + * Returns the initial state of a time control data object + * for a new game, if it has time control at all + */ export function GetInitialTimeControl(variant: string, config: object): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; @@ -51,8 +67,16 @@ export function GetInitialTimeControl(variant: string, config: object): ITimeCon // validation of the config should happen before this is called export interface ITimeHandler { + + /** + * Returns the initial state of a time control data object + * that this handler works with + */ initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase; + /** + * transition for the time control data when a move is played + */ handleMove( game: GameResponse, game_obj: AbstractGame, @@ -60,6 +84,10 @@ export interface ITimeHandler { move: string, ): ITimeControlBase; + /** + * Returns the time in milliseconds until this player + * times out, provided no moves are played + */ getMsUntilTimeout(game: GameResponse, playerNr: number): number; } diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 243bef68..6c5d02ae 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -20,6 +20,9 @@ export class TimeoutService { constructor() { } + /** + * initializes timeout schedules + */ public async initialize(): Promise { // I would like to improve this by only querying unfinished games from db // but currently I think its not possible, because of the game result @@ -51,6 +54,10 @@ export class TimeoutService { } } + /** + * stores a timeout-id or null for this player + game + * if an id was previously stored, the timeout is cleared + */ private setPlayerTimeout(gameId: string, playerNr: number, timeout: ReturnType | null): void { let gameTimeouts = this.timeoutsByGame.get(gameId); @@ -68,6 +75,9 @@ export class TimeoutService { gameTimeouts[playerNr] = timeout; } + /** + * clears the timeout-ids of this game + */ public clearGameTimeouts(gameId: string): void { const gameTimeouts = this.timeoutsByGame.get(gameId) @@ -81,6 +91,11 @@ export class TimeoutService { this.setPlayerTimeout(gameId, playerNr, null); } + /** + * schedules a timeout for this player + game + * timeout is resolved by adding a timeout move + * and applying the time control handler again + */ public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { const timeoutResolver = async () => { const game = await getGame(gameId) From e6b1c8dde4b676795cbd836f9bb89d0bc362bcb2 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Mon, 1 Jan 2024 23:27:45 +0100 Subject: [PATCH 08/17] lint --- packages/server/src/index.ts | 12 +- packages/server/src/time-control.ts | 138 +++++++++------ packages/server/src/timeout.ts | 251 +++++++++++++++------------- 3 files changed, 229 insertions(+), 172 deletions(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index d416e9ea..c9f88c14 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -43,10 +43,12 @@ export function getTimeoutService(): TimeoutService { } // Initialize MongoDB -connectToDb().then(() => timeoutService.initialize()).catch((e) => { - console.log("Unable to connect to the database."); - console.log(e); -}); +connectToDb() + .then(() => timeoutService.initialize()) + .catch((e) => { + console.log("Unable to connect to the database."); + console.log(e); + }); passport.use( "guest", @@ -135,4 +137,4 @@ if (isProd) { const PORT = process.env.PORT || 3001; server.listen(PORT, () => { console.log(`listening on *:${PORT}`); -}); \ No newline at end of file +}); diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 5b9cb21d..3592b6ae 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -7,11 +7,11 @@ import { TimeControlParallel, } from "@ogfcommunity/variants-shared"; import { AbstractGame, GameResponse } from "@ogfcommunity/variants-shared"; -import { TimeoutService } from './timeout'; -import { getTimeoutService } from './index'; +import { TimeoutService } from "./timeout"; +import { getTimeoutService } from "./index"; /** - * Validates whether game_config has type and + * Validates whether game_config has type and * properties for being a config with time control */ export function HasTimeControlConfig( @@ -25,7 +25,7 @@ export function HasTimeControlConfig( } /** - * Validates whether time_control_config has type and + * Validates whether time_control_config has type and * properties for being a time control config */ export function ValidateTimeControlConfig( @@ -40,7 +40,7 @@ export function ValidateTimeControlConfig( } /** - * Validates whether time_control has type and + * Validates whether time_control has type and * properties for being a basic time control data object */ export function ValidateTimeControlBase( @@ -58,7 +58,10 @@ export function ValidateTimeControlBase( * Returns the initial state of a time control data object * for a new game, if it has time control at all */ -export function GetInitialTimeControl(variant: string, config: object): ITimeControlBase | null { +export function GetInitialTimeControl( + variant: string, + config: object, +): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; const handler = new timeControlHandlerMap[variant](); @@ -67,12 +70,14 @@ export function GetInitialTimeControl(variant: string, config: object): ITimeCon // validation of the config should happen before this is called export interface ITimeHandler { - /** * Returns the initial state of a time control data object * that this handler works with */ - initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase; + initialState( + variant: string, + config: IConfigWithTimeControl, + ): ITimeControlBase; /** * transition for the time control data when a move is played @@ -92,12 +97,15 @@ export interface ITimeHandler { } class TimeHandlerSequentialMoves implements ITimeHandler { - private _timeoutService: TimeoutService + private _timeoutService: TimeoutService; constructor() { - this._timeoutService = getTimeoutService() + this._timeoutService = getTimeoutService(); } - initialState(variant: string, config: IConfigWithTimeControl): ITimeControlBase { + initialState( + variant: string, + config: IConfigWithTimeControl, + ): ITimeControlBase { const numPlayers = makeGameObject(variant, config).numPlayers(); const timeControl: ITimeControlBase = { @@ -108,10 +116,10 @@ class TimeHandlerSequentialMoves implements ITimeHandler { for (let i = 0; i < numPlayers; i++) { timeControl.forPlayer[i] = { remainingTimeMS: config.time_control.mainTimeMS, - onThePlaySince: null - } + onThePlaySince: null, + }; } - + switch (config.time_control.type) { case TimeControlType.Absolute: { // nothing to do @@ -124,11 +132,11 @@ class TimeHandlerSequentialMoves implements ITimeHandler { } default: { - console.error('received config with invalid time control type'); + console.error("received config with invalid time control type"); } } - return timeControl + return timeControl; } handleMove( @@ -139,32 +147,39 @@ class TimeHandlerSequentialMoves implements ITimeHandler { const config = game.config as IConfigWithTimeControl; switch (config.time_control.type) { - - case TimeControlType.Absolute: - let timeControl: ITimeControlBase = game.time_control; + case TimeControlType.Absolute: { + let timeControl = game.time_control; // could happen for old games if (timeControl === undefined) { - timeControl = this.initialState(game.variant, config) + timeControl = this.initialState(game.variant, config); } const playerData = timeControl.forPlayer[playerNr]; const timestamp = new Date(); timeControl.moveTimestamps.push(timestamp); - + if (playerData.onThePlaySince !== null) { - playerData.remainingTimeMS -= timestamp.getTime() - playerData.onThePlaySince.getTime(); + playerData.remainingTimeMS -= + timestamp.getTime() - playerData.onThePlaySince.getTime(); } playerData.onThePlaySince = null; this._timeoutService.clearPlayerTimeout(game.id, playerNr); // time control starts only after the first move of player - const nextPlayers = game_obj.nextToPlay().filter(playerNr => game.moves.some(move => playerNr in move)); + const nextPlayers = game_obj + .nextToPlay() + .filter((playerNr) => game.moves.some((move) => playerNr in move)); nextPlayers.forEach((player) => { timeControl.forPlayer[player].onThePlaySince = timestamp; - this._timeoutService.scheduleTimeout(game.id, player, timeControl.forPlayer[player].remainingTimeMS) + this._timeoutService.scheduleTimeout( + game.id, + player, + timeControl.forPlayer[player].remainingTimeMS, + ); }); return timeControl; + } case TimeControlType.Invalid: throw Error(`game with id ${game.id} has invalid time control type`); @@ -179,8 +194,8 @@ class TimeHandlerSequentialMoves implements ITimeHandler { } switch (game.config.time_control.type) { - case TimeControlType.Absolute: - case TimeControlType.Fischer: + case TimeControlType.Absolute: + case TimeControlType.Fischer: { const times: ITimeControlBase = game.time_control; if (times === undefined || times.forPlayer === undefined) { @@ -188,7 +203,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { return null; } - const playerTime = times.forPlayer[playerNr] + const playerTime = times.forPlayer[playerNr]; if (playerTime === undefined || playerTime.onThePlaySince === null) { // player has not played a move yet @@ -196,10 +211,11 @@ class TimeHandlerSequentialMoves implements ITimeHandler { return null; } - const timeoutTime = playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; - - return timeoutTime - new Date().getTime() + const timeoutTime = + playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; + return timeoutTime - new Date().getTime(); + } default: { console.error(`game with id ${game.id} has invalid time control type`); return; @@ -214,7 +230,10 @@ class TimeHandlerParallelMoves implements ITimeHandler { this._timeoutService = getTimeoutService(); } - initialState(variant: string, config: IConfigWithTimeControl): TimeControlParallel { + initialState( + variant: string, + config: IConfigWithTimeControl, + ): TimeControlParallel { const numPlayers = makeGameObject(variant, config).numPlayers(); const timeControl: TimeControlParallel = { @@ -227,9 +246,9 @@ class TimeHandlerParallelMoves implements ITimeHandler { remainingTimeMS: config.time_control.mainTimeMS, onThePlaySince: null, stagedMoveAt: null, - } + }; } - + switch (config.time_control.type) { case TimeControlType.Absolute: { // nothing to do @@ -242,11 +261,11 @@ class TimeHandlerParallelMoves implements ITimeHandler { } default: { - console.error('received config with invalid time control type'); + console.error("received config with invalid time control type"); } } - return timeControl + return timeControl; } handleMove( @@ -257,14 +276,13 @@ class TimeHandlerParallelMoves implements ITimeHandler { const config = game.config as IConfigWithTimeControl; switch (config.time_control.type) { - - case TimeControlType.Absolute: + case TimeControlType.Absolute: { let timeControl = game.time_control as TimeControlParallel; // could happen for old games if (timeControl === undefined) { - timeControl = this.initialState(game.variant, config) + timeControl = this.initialState(game.variant, config); } - + const timestamp = new Date(); timeControl.moveTimestamps.push(timestamp); @@ -276,22 +294,39 @@ class TimeHandlerParallelMoves implements ITimeHandler { // I'm not sure if this will always work as intended // because this is the game object after the move has been played // whereas we actually need the players that are on the play in the round that the move belongs to - if (game_obj.nextToPlay().every(player_nr => timeControl.forPlayer[player_nr].stagedMoveAt !== null)) { - for (let player_nr of game_obj.nextToPlay()) { + if ( + game_obj + .nextToPlay() + .every( + (player_nr) => + timeControl.forPlayer[player_nr].stagedMoveAt !== null, + ) + ) { + for (const player_nr of game_obj.nextToPlay()) { const playerData = timeControl.forPlayer[player_nr]; // time control starts only after first move of player - if (game.moves.filter(move => player_nr in move).length > (player_nr === playerNr ? 0 : 1)) { - playerData.remainingTimeMS -= playerData.stagedMoveAt.getTime() - playerData.onThePlaySince.getTime(); + if ( + game.moves.filter((move) => player_nr in move).length > + (player_nr === playerNr ? 0 : 1) + ) { + playerData.remainingTimeMS -= + playerData.stagedMoveAt.getTime() - + playerData.onThePlaySince.getTime(); } - this._timeoutService.scheduleTimeout(game.id, player_nr, timeControl.forPlayer[player_nr].remainingTimeMS) + this._timeoutService.scheduleTimeout( + game.id, + player_nr, + timeControl.forPlayer[player_nr].remainingTimeMS, + ); playerData.onThePlaySince = timestamp; playerData.stagedMoveAt = null; } } return timeControl; + } case TimeControlType.Invalid: throw Error(`game with id ${game.id} has invalid time control type`); @@ -306,16 +341,17 @@ class TimeHandlerParallelMoves implements ITimeHandler { } switch (game.config.time_control.type) { - case TimeControlType.Absolute: - case TimeControlType.Fischer: - const times: TimeControlParallel = game.time_control as TimeControlParallel; + case TimeControlType.Absolute: + case TimeControlType.Fischer: { + const times: TimeControlParallel = + game.time_control as TimeControlParallel; if (times === undefined || times.forPlayer === undefined) { // old game with no moves return null; } - const playerTime = times.forPlayer[playerNr] + const playerTime = times.forPlayer[playerNr]; if (playerTime === undefined || playerTime.onThePlaySince === null) { // player has not played a move yet @@ -327,9 +363,11 @@ class TimeHandlerParallelMoves implements ITimeHandler { return null; } - const timeoutTime = playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; + const timeoutTime = + playerTime.onThePlaySince.getTime() + playerTime.remainingTimeMS; - return timeoutTime - new Date().getTime() + return timeoutTime - new Date().getTime(); + } default: { console.error(`game with id ${game.id} has invalid time control type`); diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 6c5d02ae..23f11298 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -1,135 +1,152 @@ import { gamesCollection, getGame, getGamesWithTimeControl } from "./games"; import { - MovesType, - getOnlyMove, - makeGameObject, - } from "@ogfcommunity/variants-shared"; + MovesType, + getOnlyMove, + makeGameObject, +} from "@ogfcommunity/variants-shared"; import { timeControlHandlerMap } from "./time-control"; import { ObjectId } from "mongodb"; import { io } from "./socket_io"; type GameTimeouts = { - // timeout for this player, or null - // used to clear the timeout timer - [player: number]: ReturnType | null -} + // timeout for this player, or null + // used to clear the timeout timer + [player: number]: ReturnType | null; +}; export class TimeoutService { - private timeoutsByGame = new Map(); - - constructor() { - } - - /** - * initializes timeout schedules - */ - public async initialize(): Promise { - // I would like to improve this by only querying unfinished games from db - // but currently I think its not possible, because of the game result - // is not being stored in the db directly - for (const game of (await getGamesWithTimeControl())) { - const game_object = makeGameObject(game.variant, game.config); - - try { - // check if game is already finished - game.moves.forEach((moves) => { - const { player, move } = getOnlyMove(moves); - game_object.playMove(player, move); - }); - - if (game_object.result !== '') { - continue - } - - const timeHandler = new timeControlHandlerMap[game.variant]() - for (const playerNr of game_object.nextToPlay()) { - const t = timeHandler.getMsUntilTimeout(game, playerNr) - this.scheduleTimeout(game.id, playerNr, t); - } - } - catch (error) { - console.error(error); - continue; - } + private timeoutsByGame = new Map(); + + constructor() {} + + /** + * initializes timeout schedules + */ + public async initialize(): Promise { + // I would like to improve this by only querying unfinished games from db + // but currently I think its not possible, because of the game result + // is not being stored in the db directly + for (const game of await getGamesWithTimeControl()) { + const game_object = makeGameObject(game.variant, game.config); + + try { + // check if game is already finished + game.moves.forEach((moves) => { + const { player, move } = getOnlyMove(moves); + game_object.playMove(player, move); + }); + + if (game_object.result !== "") { + continue; } - } - - /** - * stores a timeout-id or null for this player + game - * if an id was previously stored, the timeout is cleared - */ - private setPlayerTimeout(gameId: string, playerNr: number, timeout: ReturnType | null): void { - let gameTimeouts = this.timeoutsByGame.get(gameId); - if (gameTimeouts === undefined) { - gameTimeouts = {}; - this.timeoutsByGame.set(gameId, gameTimeouts); - } - else { - const playerTimeout = gameTimeouts[playerNr] - if (playerTimeout) { - clearTimeout(playerTimeout) - } + const timeHandler = new timeControlHandlerMap[game.variant](); + for (const playerNr of game_object.nextToPlay()) { + const t = timeHandler.getMsUntilTimeout(game, playerNr); + this.scheduleTimeout(game.id, playerNr, t); } - - gameTimeouts[playerNr] = timeout; + } catch (error) { + console.error(error); + continue; + } } - - /** - * clears the timeout-ids of this game - */ - public clearGameTimeouts(gameId: string): void { - const gameTimeouts = this.timeoutsByGame.get(gameId) - - if (gameTimeouts !== undefined) { - Object.values(gameTimeouts).filter(t => t !== null).forEach(clearTimeout); - this.timeoutsByGame.delete(gameId) - } + } + + /** + * stores a timeout-id or null for this player + game + * if an id was previously stored, the timeout is cleared + */ + private setPlayerTimeout( + gameId: string, + playerNr: number, + timeout: ReturnType | null, + ): void { + let gameTimeouts = this.timeoutsByGame.get(gameId); + + if (gameTimeouts === undefined) { + gameTimeouts = {}; + this.timeoutsByGame.set(gameId, gameTimeouts); + } else { + const playerTimeout = gameTimeouts[playerNr]; + if (playerTimeout) { + clearTimeout(playerTimeout); + } } - public clearPlayerTimeout(gameId: string, playerNr: number): void { - this.setPlayerTimeout(gameId, playerNr, null); - } + gameTimeouts[playerNr] = timeout; + } - /** - * schedules a timeout for this player + game - * timeout is resolved by adding a timeout move - * and applying the time control handler again - */ - public scheduleTimeout(gameId: string, playerNr: number, inTimeMs: number): void { - const timeoutResolver = async () => { - const game = await getGame(gameId) - - const timeoutMove: MovesType = {[playerNr]: 'timeout'}; - game.moves.push(timeoutMove); - let timeControl = game.time_control; - - // this next part is somewhat duplicated from the playMove function - // which I don't like. But for parallel variants and consistency (move timestamps), - // the timeout move needs to be handled as well. - const game_object = makeGameObject(game.variant, game.config); - game.moves.forEach((moves) => { - const { player, move } = getOnlyMove(moves); - game_object.playMove(player, move); - }); - - if (game_object.result !== '') { - this.clearGameTimeouts(game.id); - - } else { - const timeHandler = new timeControlHandlerMap[game.variant](); - timeControl = timeHandler.handleMove(game, game_object, playerNr, 'timeout'); - } - - // TODO: improving the error handling would be great in future - await gamesCollection() - .updateOne({ _id: new ObjectId(gameId) }, { $push: { moves: timeoutMove }, $set: { time_control: timeControl } }) - .catch(console.log); - - io().emit(`game/${gameId}`, game) - } + /** + * clears the timeout-ids of this game + */ + public clearGameTimeouts(gameId: string): void { + const gameTimeouts = this.timeoutsByGame.get(gameId); - const timeout = setTimeout(timeoutResolver, inTimeMs); - this.setPlayerTimeout(gameId, playerNr, timeout); + if (gameTimeouts !== undefined) { + Object.values(gameTimeouts) + .filter((t) => t !== null) + .forEach(clearTimeout); + this.timeoutsByGame.delete(gameId); } -} \ No newline at end of file + } + + public clearPlayerTimeout(gameId: string, playerNr: number): void { + this.setPlayerTimeout(gameId, playerNr, null); + } + + /** + * schedules a timeout for this player + game + * timeout is resolved by adding a timeout move + * and applying the time control handler again + */ + public scheduleTimeout( + gameId: string, + playerNr: number, + inTimeMs: number, + ): void { + const timeoutResolver = async () => { + const game = await getGame(gameId); + + const timeoutMove: MovesType = { [playerNr]: "timeout" }; + game.moves.push(timeoutMove); + let timeControl = game.time_control; + + // this next part is somewhat duplicated from the playMove function + // which I don't like. But for parallel variants and consistency (move timestamps), + // the timeout move needs to be handled as well. + const game_object = makeGameObject(game.variant, game.config); + game.moves.forEach((moves) => { + const { player, move } = getOnlyMove(moves); + game_object.playMove(player, move); + }); + + if (game_object.result !== "") { + this.clearGameTimeouts(game.id); + } else { + const timeHandler = new timeControlHandlerMap[game.variant](); + timeControl = timeHandler.handleMove( + game, + game_object, + playerNr, + "timeout", + ); + } + + // TODO: improving the error handling would be great in future + await gamesCollection() + .updateOne( + { _id: new ObjectId(gameId) }, + { + $push: { moves: timeoutMove }, + $set: { time_control: timeControl }, + }, + ) + .catch(console.log); + + io().emit(`game/${gameId}`, game); + }; + + const timeout = setTimeout(timeoutResolver, inTimeMs); + this.setPlayerTimeout(gameId, playerNr, timeout); + } +} From 1c2996dd35f77a4aaa0b83cf5b9b7522ad684362 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Tue, 2 Jan 2024 23:00:46 +0100 Subject: [PATCH 09/17] catch errors in timeout service, fix time runs down after game ends --- packages/server/src/games.ts | 2 - packages/server/src/index.ts | 2 +- packages/server/src/time-control.ts | 22 +++++-- packages/server/src/timeout.ts | 52 ++++++++------- packages/shared/src/abstract_game.ts | 1 + packages/shared/src/variants/baduk.ts | 2 +- .../badukWithAbstractBoard.ts | 2 +- packages/shared/src/variants/chess.ts | 2 +- .../src/variants/fractional/fractional.ts | 66 +++++-------------- packages/shared/src/variants/parallel.ts | 6 +- 10 files changed, 69 insertions(+), 88 deletions(-) diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index 66c103ad..fd73c04b 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -49,14 +49,12 @@ export async function getGame(id: string): Promise { // TODO: db_game might be undefined if unknown ID is provided - //console.log(db_game); const game = outwardFacingGame(db_game); // Legacy games don't have a players field // TODO: remove this code after doing proper db migration if (!game.players) { game.players = await BACKFILL_addEmptyPlayersArray(game); } - //console.log(game); return game; } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index c9f88c14..e2e86868 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -20,7 +20,7 @@ import { router as apiRouter } from "./api"; import * as socket_io from "./socket_io"; import { TimeoutService } from "./timeout"; -const LOCAL_ORIGIN = "http://localhost:3000"; +const LOCAL_ORIGIN = "http://localhost:5173"; passport.use( new LocalStrategy(async function (username, password, callback) { diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 3592b6ae..5fff50ec 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -12,7 +12,7 @@ import { getTimeoutService } from "./index"; /** * Validates whether game_config has type and - * properties for being a config with time control + * properties for being a config with time control. */ export function HasTimeControlConfig( game_config: unknown, @@ -26,7 +26,7 @@ export function HasTimeControlConfig( /** * Validates whether time_control_config has type and - * properties for being a time control config + * properties for being a time control config. */ export function ValidateTimeControlConfig( time_control_config: unknown, @@ -41,7 +41,7 @@ export function ValidateTimeControlConfig( /** * Validates whether time_control has type and - * properties for being a basic time control data object + * properties for being a basic time control data object. */ export function ValidateTimeControlBase( time_control: unknown, @@ -56,7 +56,7 @@ export function ValidateTimeControlBase( /** * Returns the initial state of a time control data object - * for a new game, if it has time control at all + * for a new game, if it has time control at all. */ export function GetInitialTimeControl( variant: string, @@ -72,7 +72,7 @@ export function GetInitialTimeControl( export interface ITimeHandler { /** * Returns the initial state of a time control data object - * that this handler works with + * that this handler works with. */ initialState( variant: string, @@ -80,7 +80,9 @@ export interface ITimeHandler { ): ITimeControlBase; /** - * transition for the time control data when a move is played + * Transitions the time control data when a move is played. + * Schedules timeouts for next players, and cancels old + * scheduled timeouts if necessary. */ handleMove( game: GameResponse, @@ -91,7 +93,7 @@ export interface ITimeHandler { /** * Returns the time in milliseconds until this player - * times out, provided no moves are played + * times out, provided no moves are played. */ getMsUntilTimeout(game: GameResponse, playerNr: number): number; } @@ -283,8 +285,14 @@ class TimeHandlerParallelMoves implements ITimeHandler { timeControl = this.initialState(game.variant, config); } + const playerTimeControl = timeControl.forPlayer[playerNr]; const timestamp = new Date(); + if (playerTimeControl.onThePlaySince !== null && + timeControl.forPlayer[playerNr].remainingTimeMS <= timestamp.getTime() - playerTimeControl.onThePlaySince.getTime()) { + throw new Error("you can't change your move because it would reduce your remaining time to zero.") + } + timeControl.moveTimestamps.push(timestamp); timeControl.forPlayer[playerNr].stagedMoveAt = timestamp; this._timeoutService.clearPlayerTimeout(game.id, playerNr); diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 23f11298..ae3cdbd9 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -17,8 +17,6 @@ type GameTimeouts = { export class TimeoutService { private timeoutsByGame = new Map(); - constructor() {} - /** * initializes timeout schedules */ @@ -36,7 +34,7 @@ export class TimeoutService { game_object.playMove(player, move); }); - if (game_object.result !== "") { + if (game_object.result !== "" || game_object.phase == "gameover") { continue; } @@ -53,8 +51,8 @@ export class TimeoutService { } /** - * stores a timeout-id or null for this player + game - * if an id was previously stored, the timeout is cleared + * Stores a timeout-id or null for this player + game. + * If an id was previously stored, the corresponding timeout is cleared. */ private setPlayerTimeout( gameId: string, @@ -77,7 +75,7 @@ export class TimeoutService { } /** - * clears the timeout-ids of this game + * Clears the timeout-ids of this game. */ public clearGameTimeouts(gameId: string): void { const gameTimeouts = this.timeoutsByGame.get(gameId); @@ -95,9 +93,9 @@ export class TimeoutService { } /** - * schedules a timeout for this player + game - * timeout is resolved by adding a timeout move - * and applying the time control handler again + * Schedules a timeout for this player + game. + * Timeout is resolved by adding a timeout move + * and applying the time control handler again. */ public scheduleTimeout( gameId: string, @@ -114,22 +112,26 @@ export class TimeoutService { // this next part is somewhat duplicated from the playMove function // which I don't like. But for parallel variants and consistency (move timestamps), // the timeout move needs to be handled as well. - const game_object = makeGameObject(game.variant, game.config); - game.moves.forEach((moves) => { - const { player, move } = getOnlyMove(moves); - game_object.playMove(player, move); - }); - - if (game_object.result !== "") { - this.clearGameTimeouts(game.id); - } else { - const timeHandler = new timeControlHandlerMap[game.variant](); - timeControl = timeHandler.handleMove( - game, - game_object, - playerNr, - "timeout", - ); + try { + const game_object = makeGameObject(game.variant, game.config); + game.moves.forEach((moves) => { + const { player, move } = getOnlyMove(moves); + game_object.playMove(player, move); + }); + + if (game_object.result !== "" || game_object.phase === "gameover") { + this.clearGameTimeouts(game.id); + } else { + const timeHandler = new timeControlHandlerMap[game.variant](); + timeControl = timeHandler.handleMove( + game, + game_object, + playerNr, + "timeout", + ); + } + } catch (error) { + console.error(error) } // TODO: improving the error handling would be great in future diff --git a/packages/shared/src/abstract_game.ts b/packages/shared/src/abstract_game.ts index b6486ec0..b0243da2 100644 --- a/packages/shared/src/abstract_game.ts +++ b/packages/shared/src/abstract_game.ts @@ -27,6 +27,7 @@ export abstract class AbstractGame { /** * Returns the list of players that need to play a move the next round. + * Returns empty array when the game is finished. */ abstract nextToPlay(): number[]; diff --git a/packages/shared/src/variants/baduk.ts b/packages/shared/src/variants/baduk.ts index 8c34bc7f..c810f408 100644 --- a/packages/shared/src/variants/baduk.ts +++ b/packages/shared/src/variants/baduk.ts @@ -52,7 +52,7 @@ export class Baduk extends AbstractGame { } override nextToPlay(): number[] { - return [this.next_to_play]; + return this.phase === "gameover" ? [] : [this.next_to_play]; } override playMove(player: number, move: string): void { diff --git a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts index bfd0aa61..b0e7a089 100644 --- a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts +++ b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts @@ -52,7 +52,7 @@ export class BadukWithAbstractBoard extends AbstractGame< } nextToPlay(): number[] { - return [this.next_to_play]; + return this.phase === "gameover" ? [] : [this.next_to_play]; } playMove(player: number, move: string): void { diff --git a/packages/shared/src/variants/chess.ts b/packages/shared/src/variants/chess.ts index 39c56448..abdc983e 100644 --- a/packages/shared/src/variants/chess.ts +++ b/packages/shared/src/variants/chess.ts @@ -35,7 +35,7 @@ export class ChessGame extends AbstractGame { return { fen: this.chess.fen() }; } nextToPlay(): number[] { - return [this.chess.turn() == "b" ? 1 : 0]; + return this.phase === "gameover" ? [] : [this.chess.turn() == "b" ? 1 : 0]; } numPlayers(): number { return 2; diff --git a/packages/shared/src/variants/fractional/fractional.ts b/packages/shared/src/variants/fractional/fractional.ts index 4dd624fa..8d934f76 100644 --- a/packages/shared/src/variants/fractional/fractional.ts +++ b/packages/shared/src/variants/fractional/fractional.ts @@ -4,7 +4,6 @@ import { AbstractBadukConfig, } from "../../lib/abstractBaduk/abstractBaduk"; import { FractionalStone } from "./fractionalStone"; -import { Participation } from "../../lib/utils"; export type Color = | "black" @@ -49,8 +48,6 @@ export class Fractional extends AbstractBaduk< FractionalState > { private stagedMoves: (FractionalIntersection | null)[]; - private playerParticipation = this.initializeParticipation(this.config.players.length); - private numberOfRounds: number = 0; constructor(config?: FractionalConfig) { super(config); @@ -58,39 +55,23 @@ export class Fractional extends AbstractBaduk< } playMove(p: number, m: string): void { - if (m === 'resign' || m === 'timeout') { - this.playerParticipation[p].dropOutAtRound = this.numberOfRounds; - - if (this.nextToPlay().length < 2) { - this.phase = 'gameover'; - // TODO: declare winner - - return; - } - } else { - // stage move - - const move = this.decodeMove(p, m); - if (!move) { - throw new Error(`Couldn't decode move ${{ player: p, move: m }}`); - } - - if (move.intersection.stone) { - throw new Error( - `There is already a stone at intersection ${move.intersection.id}`, - ); - } - - if (!this.nextToPlay().includes(p)) { - throw new Error('Not your turn') - } - - this.stagedMoves[move.player.index] = move.intersection; + const move = this.decodeMove(p, m); + if (!move) { + throw new Error(`Couldn't decode move ${{ player: p, move: m }}`); } - if (this.stagedMoves.every( - (stagedMove, playerNr): stagedMove is FractionalIntersection => - stagedMove !== null || !this.nextToPlay().includes(playerNr), + if (move.intersection.stone) { + throw new Error( + `There is already a stone at intersection ${move.intersection.id}`, + ); + } + + this.stagedMoves[move.player.index] = move.intersection; + + if ( + this.stagedMoves.every( + (stagedMove): stagedMove is FractionalIntersection => + stagedMove !== null, ) ) { this.intersections.forEach((intersection) => { @@ -99,7 +80,7 @@ export class Fractional extends AbstractBaduk< // place all moves and proceed to next round const playedIntersections = new Set(); - this.stagedMoves.filter(x => x !== null).forEach((intersection, playerId) => { + this.stagedMoves.forEach((intersection, playerId) => { playedIntersections.add(intersection); const colors = intersection.stone?.colors ?? @@ -114,7 +95,6 @@ export class Fractional extends AbstractBaduk< this.removeChains(false); this.stagedMoves = this.stagedMovesDefaults(); - this.numberOfRounds++; } } @@ -137,9 +117,7 @@ export class Fractional extends AbstractBaduk< } nextToPlay(): number[] { - return this.playerParticipation - .filter(x => x.dropOutAtRound === null || x.dropOutAtRound > this.numberOfRounds) - .map(p => p.playerNr) + return this.phase === "gameover" ? [] : [...Array(this.config.players.length).keys()]; } numPlayers(): number { @@ -185,12 +163,4 @@ export class Fractional extends AbstractBaduk< null, ); } - - initializeParticipation(numPlayers: number): Participation[] { - const participation = new Array(numPlayers); - for (let i = 0; i < numPlayers; i++) { - participation[i] = {playerNr: i, dropOutAtRound: null}; - } - return participation; - } -} +} \ No newline at end of file diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index 7a7cfec2..1c267897 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -53,7 +53,7 @@ export class ParallelGo extends AbstractGame< } nextToPlay(): number[] { - return this.playerParticipation + return this.phase === "gameover" ? [] : this.playerParticipation .filter(x => x.dropOutAtRound === null || x.dropOutAtRound > this.numberOfRounds) .map(p => p.playerNr) } @@ -77,7 +77,9 @@ export class ParallelGo extends AbstractGame< if (this.nextToPlay().length < 2) { this.phase = 'gameover'; - // TODO: declare winner + if (this.nextToPlay().length === 1) { + this.result = `Player ${this.nextToPlay()[0]} wins!`; + } return } } From 875dffee27350d87c0f1208338af2b0cd0bf20dd Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Tue, 2 Jan 2024 23:23:20 +0100 Subject: [PATCH 10/17] fix game result for variant parallel --- packages/shared/src/variants/parallel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index 1c267897..7b3684a0 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -76,10 +76,11 @@ export class ParallelGo extends AbstractGame< this.playerParticipation[player].dropOutAtRound = this.numberOfRounds; if (this.nextToPlay().length < 2) { - this.phase = 'gameover'; if (this.nextToPlay().length === 1) { this.result = `Player ${this.nextToPlay()[0]} wins!`; } + + this.phase = 'gameover'; return } } From 86142dbcd7d5cb87807c193c021911dfbb0abf86 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Wed, 3 Jan 2024 16:54:10 +0100 Subject: [PATCH 11/17] fix linter errors --- packages/shared/src/lib/utils.ts | 6 +-- .../src/time_control/time_control.types.ts | 4 +- packages/shared/src/variants/baduk.ts | 12 ++--- .../badukWithAbstractBoard.ts | 12 ++--- packages/shared/src/variants/chess.ts | 2 +- .../src/variants/fractional/fractional.ts | 14 +++--- packages/shared/src/variants/parallel.ts | 47 ++++++++++--------- 7 files changed, 52 insertions(+), 45 deletions(-) diff --git a/packages/shared/src/lib/utils.ts b/packages/shared/src/lib/utils.ts index 7c285def..9f5f7992 100644 --- a/packages/shared/src/lib/utils.ts +++ b/packages/shared/src/lib/utils.ts @@ -18,6 +18,6 @@ export function getOnlyMove(moves: MovesType): { } export type Participation = { - playerNr: number, - dropOutAtRound: number | null -}; \ No newline at end of file + playerNr: number; + dropOutAtRound: number | null; +}; diff --git a/packages/shared/src/time_control/time_control.types.ts b/packages/shared/src/time_control/time_control.types.ts index 9024b6b9..3d4a5b8c 100644 --- a/packages/shared/src/time_control/time_control.types.ts +++ b/packages/shared/src/time_control/time_control.types.ts @@ -32,11 +32,11 @@ export interface ITimeControlBase { export type PerPlayerTimeControlParallel = IPerPlayerTimeControlBase & { stagedMoveAt: Date | null; -} +}; export type TimeControlParallel = { moveTimestamps: Date[]; forPlayer: { [player: number]: PerPlayerTimeControlParallel; }; -} \ No newline at end of file +}; diff --git a/packages/shared/src/variants/baduk.ts b/packages/shared/src/variants/baduk.ts index c810f408..4aea8b92 100644 --- a/packages/shared/src/variants/baduk.ts +++ b/packages/shared/src/variants/baduk.ts @@ -37,7 +37,7 @@ export class Baduk extends AbstractGame { constructor(config?: BadukConfig) { super(config); this.board = new Grid(this.config.width, this.config.height).fill( - Color.EMPTY, + Color.EMPTY ); } @@ -67,8 +67,8 @@ export class Baduk extends AbstractGame { } if (move === "timeout") { - this.phase = "gameover" - this.result = player === 0 ? "W+T" : "B+T" + this.phase = "gameover"; + this.result = player === 0 ? "W+T" : "B+T"; return; } @@ -78,12 +78,12 @@ export class Baduk extends AbstractGame { const color = this.board.at(decoded_move); if (color === undefined) { throw Error( - `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.config.width}x${this.config.height}`, + `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.config.width}x${this.config.height}` ); } if (color !== Color.EMPTY) { throw Error( - `Cannot place a stone on top of an existing stone. (${color} at (${x}, ${y}))`, + `Cannot place a stone on top of an existing stone. (${color} at (${x}, ${y}))` ); } @@ -166,7 +166,7 @@ export class Baduk extends AbstractGame { const black_points: number = board.reduce( count_color(Color.BLACK), - 0, + 0 ); const white_points: number = board.reduce(count_color(Color.WHITE), 0) + this.config.komi; diff --git a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts index b0e7a089..45c16b18 100644 --- a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts +++ b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts @@ -77,22 +77,22 @@ export class BadukWithAbstractBoard extends AbstractGame< } if (move === "timeout") { - this.phase = "gameover" - this.result = player === 0 ? "W+T" : "B+T" + this.phase = "gameover"; + this.result = player === 0 ? "W+T" : "B+T"; return; } const decoded_move = decodeMove(move); if (isOutOfBounds(decoded_move, this.board)) { throw Error( - `Move out of bounds. (move: ${decoded_move}, intersections: ${this.board.Intersections.length}`, + `Move out of bounds. (move: ${decoded_move}, intersections: ${this.board.Intersections.length}` ); } const intersection = this.board.Intersections[decoded_move]; if (intersection.StoneState.Color != Color.EMPTY) { throw Error( - `Cannot place a stone on top of an existing stone. (${intersection.StoneState.Color} at (${decoded_move}))`, + `Cannot place a stone on top of an existing stone. (${intersection.StoneState.Color} at (${decoded_move}))` ); } const player_color = player === 0 ? Color.BLACK : Color.WHITE; @@ -141,7 +141,7 @@ function decodeMove(move: string): number { /** Returns true if the group containing intersection has at least one liberty. */ function groupHasLiberties( intersection: Intersection, - board: BadukBoardAbstract, + board: BadukBoardAbstract ) { const color = intersection.StoneState.Color; const visited: { [key: string]: boolean } = {}; @@ -195,7 +195,7 @@ function floodFill(intersection: Intersection, target_color: Color): number { return intersection.Neighbours.map(helper).reduce( (acc, val) => acc + val, - 1, + 1 ); } diff --git a/packages/shared/src/variants/chess.ts b/packages/shared/src/variants/chess.ts index abdc983e..e3414f90 100644 --- a/packages/shared/src/variants/chess.ts +++ b/packages/shared/src/variants/chess.ts @@ -17,7 +17,7 @@ export class ChessGame extends AbstractGame { } if (move === "timeout") { - this.phase = "gameover" + this.phase = "gameover"; this.result = player === 0 ? "B+T" : "W+T"; return; } diff --git a/packages/shared/src/variants/fractional/fractional.ts b/packages/shared/src/variants/fractional/fractional.ts index 8d934f76..edd77c72 100644 --- a/packages/shared/src/variants/fractional/fractional.ts +++ b/packages/shared/src/variants/fractional/fractional.ts @@ -62,7 +62,7 @@ export class Fractional extends AbstractBaduk< if (move.intersection.stone) { throw new Error( - `There is already a stone at intersection ${move.intersection.id}`, + `There is already a stone at intersection ${move.intersection.id}` ); } @@ -71,7 +71,7 @@ export class Fractional extends AbstractBaduk< if ( this.stagedMoves.every( (stagedMove): stagedMove is FractionalIntersection => - stagedMove !== null, + stagedMove !== null ) ) { this.intersections.forEach((intersection) => { @@ -117,7 +117,9 @@ export class Fractional extends AbstractBaduk< } nextToPlay(): number[] { - return this.phase === "gameover" ? [] : [...Array(this.config.players.length).keys()]; + return this.phase === "gameover" + ? [] + : [...Array(this.config.players.length).keys()]; } numPlayers(): number { @@ -151,7 +153,7 @@ export class Fractional extends AbstractBaduk< private decodeMove(p: number, m: string): FractionalMove | null { const player = this.config.players[p]; const intersection = this.intersections.find( - (intersection) => intersection.id === Number.parseInt(m), + (intersection) => intersection.id === Number.parseInt(m) ); return player && intersection ? { player: { ...player, index: p }, intersection } @@ -160,7 +162,7 @@ export class Fractional extends AbstractBaduk< private stagedMovesDefaults(): (FractionalIntersection | null)[] { return new Array(this.numPlayers()).fill( - null, + null ); } -} \ No newline at end of file +} diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index 7b3684a0..edafe81a 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -30,7 +30,9 @@ export class ParallelGo extends AbstractGame< private board: Grid; private staged: MovesType = {}; private last_round: MovesType = {}; - private playerParticipation = this.initializeParticipation(this.config.num_players); + private playerParticipation = this.initializeParticipation( + this.config.num_players + ); private numberOfRounds: number = 0; constructor(config?: ParallelGoConfig) { @@ -53,9 +55,15 @@ export class ParallelGo extends AbstractGame< } nextToPlay(): number[] { - return this.phase === "gameover" ? [] : this.playerParticipation - .filter(x => x.dropOutAtRound === null || x.dropOutAtRound > this.numberOfRounds) - .map(p => p.playerNr) + return this.phase === "gameover" + ? [] + : this.playerParticipation + .filter( + (x) => + x.dropOutAtRound === null || + x.dropOutAtRound > this.numberOfRounds + ) + .map((p) => p.playerNr); } playMove(player: number, move: string): void { @@ -64,7 +72,7 @@ export class ParallelGo extends AbstractGame< const occupants = this.board.at(decoded_move); if (occupants === undefined) { throw Error( - `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.board.width}x${this.board.height}`, + `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.board.width}x${this.board.height}` ); } if (occupants.length !== 0) { @@ -72,30 +80,28 @@ export class ParallelGo extends AbstractGame< } } - if (move === 'resign' || move === 'timeout') { + if (move === "resign" || move === "timeout") { this.playerParticipation[player].dropOutAtRound = this.numberOfRounds; if (this.nextToPlay().length < 2) { if (this.nextToPlay().length === 1) { this.result = `Player ${this.nextToPlay()[0]} wins!`; } - - this.phase = 'gameover'; - return + + this.phase = "gameover"; + return; } - } - else - { + } else { // stage move - + if (!this.nextToPlay().includes(player)) { - throw new Error('Not your turn') + throw new Error("Not your turn"); } - + this.staged[player] = move; } - if (this.nextToPlay().some(playerNr => !(playerNr in this.staged))) { + if (this.nextToPlay().some((playerNr) => !(playerNr in this.staged))) { // Don't play moves until everybody has staged a move return; } @@ -125,8 +131,7 @@ export class ParallelGo extends AbstractGame< } this.removeGroupsIf( - ({ has_liberties, contains_staged }) => - !has_liberties && !contains_staged, + ({ has_liberties, contains_staged }) => !has_liberties && !contains_staged ); this.removeGroupsIf(({ has_liberties }) => !has_liberties); @@ -146,7 +151,7 @@ export class ParallelGo extends AbstractGame< replaceMultiColoredStonesWith(arr: number[]) { this.board = this.board.map((intersection) => - intersection.length > 1 ? [...arr] : intersection, + intersection.length > 1 ? [...arr] : intersection ); } @@ -162,7 +167,7 @@ export class ParallelGo extends AbstractGame< private getGroup( pos: Coordinate, color: number, - checked: Grid<{ [color: number]: boolean }>, + checked: Grid<{ [color: number]: boolean }> ): Group { if (this.isOutOfBounds(pos)) { return { stones: [], has_liberties: false, contains_staged: false }; @@ -242,7 +247,7 @@ export class ParallelGo extends AbstractGame< initializeParticipation(numPlayers: number): Participation[] { const participation = new Array(numPlayers); for (let i = 0; i < numPlayers; i++) { - participation[i] = {playerNr: i, dropOutAtRound: null}; + participation[i] = { playerNr: i, dropOutAtRound: null }; } return participation; } From d8e3e13a5f82432b975b05b78b3dd7c383e21022 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Wed, 3 Jan 2024 17:24:18 +0100 Subject: [PATCH 12/17] lint --- packages/server/src/games.ts | 17 ++++++++++------- packages/server/src/time-control.ts | 13 +++++++++---- packages/server/src/timeout.ts | 4 ++-- packages/shared/src/variants/baduk.ts | 8 ++++---- .../badukWithAbstractBoard.ts | 8 ++++---- .../src/variants/fractional/fractional.ts | 8 ++++---- packages/shared/src/variants/parallel.ts | 13 +++++++------ 7 files changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index fd73c04b..0efa7ff8 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -8,7 +8,7 @@ import { import { ObjectId, WithId, Document } from "mongodb"; import { getDb } from "./db"; import { io } from "./socket_io"; -import { getTimeoutService } from './index'; +import { getTimeoutService } from "./index"; import { GetInitialTimeControl, HasTimeControlConfig, @@ -38,7 +38,9 @@ export async function getGames( } export async function getGamesWithTimeControl(): Promise { -const games = gamesCollection().find({ time_control: { $ne: null } }).toArray(); + const games = gamesCollection() + .find({ time_control: { $ne: null } }) + .toArray(); return (await games).map(outwardFacingGame); } @@ -67,7 +69,7 @@ export async function createGame( variant: variant, moves: [] as MovesType[], config: config, - time_control: GetInitialTimeControl(variant, config) + time_control: GetInitialTimeControl(variant, config), }; const result = await gamesCollection().insertOne(game); @@ -121,10 +123,8 @@ export async function playMove( HasTimeControlConfig(game.config) && ValidateTimeControlConfig(game.config.time_control) ) { - - if (game_obj.result !== '') { + if (game_obj.result !== "") { getTimeoutService().clearGameTimeouts(game.id); - } else { const timeHandler = new timeControlHandlerMap[game.variant](); timeControl = timeHandler.handleMove(game, game_obj, playerNr, new_move); @@ -132,7 +132,10 @@ export async function playMove( } gamesCollection() - .updateOne({ _id: new ObjectId(game_id) }, { $push: { moves: moves }, $set: { time_control: timeControl } }) + .updateOne( + { _id: new ObjectId(game_id) }, + { $push: { moves: moves }, $set: { time_control: timeControl } }, + ) .catch(console.log); game.moves.push(moves); diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 5fff50ec..ab5da394 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -81,7 +81,7 @@ export interface ITimeHandler { /** * Transitions the time control data when a move is played. - * Schedules timeouts for next players, and cancels old + * Schedules timeouts for next players, and cancels old * scheduled timeouts if necessary. */ handleMove( @@ -288,9 +288,14 @@ class TimeHandlerParallelMoves implements ITimeHandler { const playerTimeControl = timeControl.forPlayer[playerNr]; const timestamp = new Date(); - if (playerTimeControl.onThePlaySince !== null && - timeControl.forPlayer[playerNr].remainingTimeMS <= timestamp.getTime() - playerTimeControl.onThePlaySince.getTime()) { - throw new Error("you can't change your move because it would reduce your remaining time to zero.") + if ( + playerTimeControl.onThePlaySince !== null && + timeControl.forPlayer[playerNr].remainingTimeMS <= + timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() + ) { + throw new Error( + "you can't change your move because it would reduce your remaining time to zero.", + ); } timeControl.moveTimestamps.push(timestamp); diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index ae3cdbd9..5274f6e4 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -118,7 +118,7 @@ export class TimeoutService { const { player, move } = getOnlyMove(moves); game_object.playMove(player, move); }); - + if (game_object.result !== "" || game_object.phase === "gameover") { this.clearGameTimeouts(game.id); } else { @@ -131,7 +131,7 @@ export class TimeoutService { ); } } catch (error) { - console.error(error) + console.error(error); } // TODO: improving the error handling would be great in future diff --git a/packages/shared/src/variants/baduk.ts b/packages/shared/src/variants/baduk.ts index 4aea8b92..3c5118b1 100644 --- a/packages/shared/src/variants/baduk.ts +++ b/packages/shared/src/variants/baduk.ts @@ -37,7 +37,7 @@ export class Baduk extends AbstractGame { constructor(config?: BadukConfig) { super(config); this.board = new Grid(this.config.width, this.config.height).fill( - Color.EMPTY + Color.EMPTY, ); } @@ -78,12 +78,12 @@ export class Baduk extends AbstractGame { const color = this.board.at(decoded_move); if (color === undefined) { throw Error( - `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.config.width}x${this.config.height}` + `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.config.width}x${this.config.height}`, ); } if (color !== Color.EMPTY) { throw Error( - `Cannot place a stone on top of an existing stone. (${color} at (${x}, ${y}))` + `Cannot place a stone on top of an existing stone. (${color} at (${x}, ${y}))`, ); } @@ -166,7 +166,7 @@ export class Baduk extends AbstractGame { const black_points: number = board.reduce( count_color(Color.BLACK), - 0 + 0, ); const white_points: number = board.reduce(count_color(Color.WHITE), 0) + this.config.komi; diff --git a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts index 45c16b18..09889ee4 100644 --- a/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts +++ b/packages/shared/src/variants/badukWithAbstractBoard/badukWithAbstractBoard.ts @@ -85,14 +85,14 @@ export class BadukWithAbstractBoard extends AbstractGame< const decoded_move = decodeMove(move); if (isOutOfBounds(decoded_move, this.board)) { throw Error( - `Move out of bounds. (move: ${decoded_move}, intersections: ${this.board.Intersections.length}` + `Move out of bounds. (move: ${decoded_move}, intersections: ${this.board.Intersections.length}`, ); } const intersection = this.board.Intersections[decoded_move]; if (intersection.StoneState.Color != Color.EMPTY) { throw Error( - `Cannot place a stone on top of an existing stone. (${intersection.StoneState.Color} at (${decoded_move}))` + `Cannot place a stone on top of an existing stone. (${intersection.StoneState.Color} at (${decoded_move}))`, ); } const player_color = player === 0 ? Color.BLACK : Color.WHITE; @@ -141,7 +141,7 @@ function decodeMove(move: string): number { /** Returns true if the group containing intersection has at least one liberty. */ function groupHasLiberties( intersection: Intersection, - board: BadukBoardAbstract + board: BadukBoardAbstract, ) { const color = intersection.StoneState.Color; const visited: { [key: string]: boolean } = {}; @@ -195,7 +195,7 @@ function floodFill(intersection: Intersection, target_color: Color): number { return intersection.Neighbours.map(helper).reduce( (acc, val) => acc + val, - 1 + 1, ); } diff --git a/packages/shared/src/variants/fractional/fractional.ts b/packages/shared/src/variants/fractional/fractional.ts index edd77c72..d99039d2 100644 --- a/packages/shared/src/variants/fractional/fractional.ts +++ b/packages/shared/src/variants/fractional/fractional.ts @@ -62,7 +62,7 @@ export class Fractional extends AbstractBaduk< if (move.intersection.stone) { throw new Error( - `There is already a stone at intersection ${move.intersection.id}` + `There is already a stone at intersection ${move.intersection.id}`, ); } @@ -71,7 +71,7 @@ export class Fractional extends AbstractBaduk< if ( this.stagedMoves.every( (stagedMove): stagedMove is FractionalIntersection => - stagedMove !== null + stagedMove !== null, ) ) { this.intersections.forEach((intersection) => { @@ -153,7 +153,7 @@ export class Fractional extends AbstractBaduk< private decodeMove(p: number, m: string): FractionalMove | null { const player = this.config.players[p]; const intersection = this.intersections.find( - (intersection) => intersection.id === Number.parseInt(m) + (intersection) => intersection.id === Number.parseInt(m), ); return player && intersection ? { player: { ...player, index: p }, intersection } @@ -162,7 +162,7 @@ export class Fractional extends AbstractBaduk< private stagedMovesDefaults(): (FractionalIntersection | null)[] { return new Array(this.numPlayers()).fill( - null + null, ); } } diff --git a/packages/shared/src/variants/parallel.ts b/packages/shared/src/variants/parallel.ts index edafe81a..773a7aae 100644 --- a/packages/shared/src/variants/parallel.ts +++ b/packages/shared/src/variants/parallel.ts @@ -31,7 +31,7 @@ export class ParallelGo extends AbstractGame< private staged: MovesType = {}; private last_round: MovesType = {}; private playerParticipation = this.initializeParticipation( - this.config.num_players + this.config.num_players, ); private numberOfRounds: number = 0; @@ -61,7 +61,7 @@ export class ParallelGo extends AbstractGame< .filter( (x) => x.dropOutAtRound === null || - x.dropOutAtRound > this.numberOfRounds + x.dropOutAtRound > this.numberOfRounds, ) .map((p) => p.playerNr); } @@ -72,7 +72,7 @@ export class ParallelGo extends AbstractGame< const occupants = this.board.at(decoded_move); if (occupants === undefined) { throw Error( - `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.board.width}x${this.board.height}` + `Move out of bounds. (move: ${decoded_move}, board dimensions: ${this.board.width}x${this.board.height}`, ); } if (occupants.length !== 0) { @@ -131,7 +131,8 @@ export class ParallelGo extends AbstractGame< } this.removeGroupsIf( - ({ has_liberties, contains_staged }) => !has_liberties && !contains_staged + ({ has_liberties, contains_staged }) => + !has_liberties && !contains_staged, ); this.removeGroupsIf(({ has_liberties }) => !has_liberties); @@ -151,7 +152,7 @@ export class ParallelGo extends AbstractGame< replaceMultiColoredStonesWith(arr: number[]) { this.board = this.board.map((intersection) => - intersection.length > 1 ? [...arr] : intersection + intersection.length > 1 ? [...arr] : intersection, ); } @@ -167,7 +168,7 @@ export class ParallelGo extends AbstractGame< private getGroup( pos: Coordinate, color: number, - checked: Grid<{ [color: number]: boolean }> + checked: Grid<{ [color: number]: boolean }>, ): Group { if (this.isOutOfBounds(pos)) { return { stones: [], has_liberties: false, contains_staged: false }; From adc0123cfa311888e33bb1cdeaf39afcd9fde74d Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Thu, 4 Jan 2024 10:23:44 +0100 Subject: [PATCH 13/17] bugfix parallel moves time control --- packages/server/src/time-control.ts | 46 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index ab5da394..9fbb62b3 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -15,7 +15,7 @@ import { getTimeoutService } from "./index"; * properties for being a config with time control. */ export function HasTimeControlConfig( - game_config: unknown, + game_config: unknown ): game_config is IConfigWithTimeControl { return ( game_config && @@ -29,7 +29,7 @@ export function HasTimeControlConfig( * properties for being a time control config. */ export function ValidateTimeControlConfig( - time_control_config: unknown, + time_control_config: unknown ): time_control_config is ITimeControlConfig { return ( time_control_config && @@ -44,7 +44,7 @@ export function ValidateTimeControlConfig( * properties for being a basic time control data object. */ export function ValidateTimeControlBase( - time_control: unknown, + time_control: unknown ): time_control is ITimeControlBase { return ( time_control && @@ -60,7 +60,7 @@ export function ValidateTimeControlBase( */ export function GetInitialTimeControl( variant: string, - config: object, + config: object ): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; @@ -76,7 +76,7 @@ export interface ITimeHandler { */ initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): ITimeControlBase; /** @@ -88,7 +88,7 @@ export interface ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string, + move: string ): ITimeControlBase; /** @@ -106,7 +106,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): ITimeControlBase { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -144,7 +144,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { handleMove( game: GameResponse, game_obj: AbstractGame, - playerNr: number, + playerNr: number ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -176,7 +176,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player, - timeControl.forPlayer[player].remainingTimeMS, + timeControl.forPlayer[player].remainingTimeMS ); }); @@ -234,7 +234,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): TimeControlParallel { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -274,6 +274,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, + move: string ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -288,18 +289,21 @@ class TimeHandlerParallelMoves implements ITimeHandler { const playerTimeControl = timeControl.forPlayer[playerNr]; const timestamp = new Date(); - if ( - playerTimeControl.onThePlaySince !== null && - timeControl.forPlayer[playerNr].remainingTimeMS <= - timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() - ) { - throw new Error( - "you can't change your move because it would reduce your remaining time to zero.", - ); + if (!Object.keys(game_obj.specialMoves()).includes(move)) { + timeControl.forPlayer[playerNr].stagedMoveAt = timestamp; + + if ( + playerTimeControl.onThePlaySince !== null && + timeControl.forPlayer[playerNr].remainingTimeMS <= + timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() + ) { + throw new Error( + "you can't change your move because it would reduce your remaining time to zero." + ); + } } timeControl.moveTimestamps.push(timestamp); - timeControl.forPlayer[playerNr].stagedMoveAt = timestamp; this._timeoutService.clearPlayerTimeout(game.id, playerNr); // check if the round finishes with this move @@ -312,7 +316,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { .nextToPlay() .every( (player_nr) => - timeControl.forPlayer[player_nr].stagedMoveAt !== null, + timeControl.forPlayer[player_nr].stagedMoveAt !== null ) ) { for (const player_nr of game_obj.nextToPlay()) { @@ -331,7 +335,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player_nr, - timeControl.forPlayer[player_nr].remainingTimeMS, + timeControl.forPlayer[player_nr].remainingTimeMS ); playerData.onThePlaySince = timestamp; playerData.stagedMoveAt = null; From 81f129e7430e8083ee67bd79d09701c2e38095ff Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Thu, 4 Jan 2024 10:24:18 +0100 Subject: [PATCH 14/17] lint --- packages/server/src/timeout.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 5274f6e4..8f28b3a2 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -57,7 +57,7 @@ export class TimeoutService { private setPlayerTimeout( gameId: string, playerNr: number, - timeout: ReturnType | null, + timeout: ReturnType | null ): void { let gameTimeouts = this.timeoutsByGame.get(gameId); @@ -100,7 +100,7 @@ export class TimeoutService { public scheduleTimeout( gameId: string, playerNr: number, - inTimeMs: number, + inTimeMs: number ): void { const timeoutResolver = async () => { const game = await getGame(gameId); @@ -127,7 +127,7 @@ export class TimeoutService { game, game_object, playerNr, - "timeout", + "timeout" ); } } catch (error) { @@ -141,7 +141,7 @@ export class TimeoutService { { $push: { moves: timeoutMove }, $set: { time_control: timeControl }, - }, + } ) .catch(console.log); From 22c91ef664ce4c9d0dd31ceec94dc565ebafe5d6 Mon Sep 17 00:00:00 2001 From: Martin Unger Date: Thu, 4 Jan 2024 10:27:25 +0100 Subject: [PATCH 15/17] lint --- packages/server/src/time-control.ts | 28 ++++++++++++++-------------- packages/server/src/timeout.ts | 8 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 9fbb62b3..aa59c82f 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -15,7 +15,7 @@ import { getTimeoutService } from "./index"; * properties for being a config with time control. */ export function HasTimeControlConfig( - game_config: unknown + game_config: unknown, ): game_config is IConfigWithTimeControl { return ( game_config && @@ -29,7 +29,7 @@ export function HasTimeControlConfig( * properties for being a time control config. */ export function ValidateTimeControlConfig( - time_control_config: unknown + time_control_config: unknown, ): time_control_config is ITimeControlConfig { return ( time_control_config && @@ -44,7 +44,7 @@ export function ValidateTimeControlConfig( * properties for being a basic time control data object. */ export function ValidateTimeControlBase( - time_control: unknown + time_control: unknown, ): time_control is ITimeControlBase { return ( time_control && @@ -60,7 +60,7 @@ export function ValidateTimeControlBase( */ export function GetInitialTimeControl( variant: string, - config: object + config: object, ): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; @@ -76,7 +76,7 @@ export interface ITimeHandler { */ initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): ITimeControlBase; /** @@ -88,7 +88,7 @@ export interface ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string + move: string, ): ITimeControlBase; /** @@ -106,7 +106,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): ITimeControlBase { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -144,7 +144,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { handleMove( game: GameResponse, game_obj: AbstractGame, - playerNr: number + playerNr: number, ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -176,7 +176,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player, - timeControl.forPlayer[player].remainingTimeMS + timeControl.forPlayer[player].remainingTimeMS, ); }); @@ -234,7 +234,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): TimeControlParallel { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -274,7 +274,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string + move: string, ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -298,7 +298,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() ) { throw new Error( - "you can't change your move because it would reduce your remaining time to zero." + "you can't change your move because it would reduce your remaining time to zero.", ); } } @@ -316,7 +316,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { .nextToPlay() .every( (player_nr) => - timeControl.forPlayer[player_nr].stagedMoveAt !== null + timeControl.forPlayer[player_nr].stagedMoveAt !== null, ) ) { for (const player_nr of game_obj.nextToPlay()) { @@ -335,7 +335,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player_nr, - timeControl.forPlayer[player_nr].remainingTimeMS + timeControl.forPlayer[player_nr].remainingTimeMS, ); playerData.onThePlaySince = timestamp; playerData.stagedMoveAt = null; diff --git a/packages/server/src/timeout.ts b/packages/server/src/timeout.ts index 8f28b3a2..5274f6e4 100644 --- a/packages/server/src/timeout.ts +++ b/packages/server/src/timeout.ts @@ -57,7 +57,7 @@ export class TimeoutService { private setPlayerTimeout( gameId: string, playerNr: number, - timeout: ReturnType | null + timeout: ReturnType | null, ): void { let gameTimeouts = this.timeoutsByGame.get(gameId); @@ -100,7 +100,7 @@ export class TimeoutService { public scheduleTimeout( gameId: string, playerNr: number, - inTimeMs: number + inTimeMs: number, ): void { const timeoutResolver = async () => { const game = await getGame(gameId); @@ -127,7 +127,7 @@ export class TimeoutService { game, game_object, playerNr, - "timeout" + "timeout", ); } } catch (error) { @@ -141,7 +141,7 @@ export class TimeoutService { { $push: { moves: timeoutMove }, $set: { time_control: timeControl }, - } + }, ) .catch(console.log); From 8202395d7df1aaffa2869fef9191bae520d5f50c Mon Sep 17 00:00:00 2001 From: merowin Date: Thu, 4 Jan 2024 21:06:24 +0100 Subject: [PATCH 16/17] pass needs to be staged by time handler --- packages/server/src/time-control.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index aa59c82f..4ada2e96 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -15,7 +15,7 @@ import { getTimeoutService } from "./index"; * properties for being a config with time control. */ export function HasTimeControlConfig( - game_config: unknown, + game_config: unknown ): game_config is IConfigWithTimeControl { return ( game_config && @@ -29,7 +29,7 @@ export function HasTimeControlConfig( * properties for being a time control config. */ export function ValidateTimeControlConfig( - time_control_config: unknown, + time_control_config: unknown ): time_control_config is ITimeControlConfig { return ( time_control_config && @@ -44,7 +44,7 @@ export function ValidateTimeControlConfig( * properties for being a basic time control data object. */ export function ValidateTimeControlBase( - time_control: unknown, + time_control: unknown ): time_control is ITimeControlBase { return ( time_control && @@ -60,7 +60,7 @@ export function ValidateTimeControlBase( */ export function GetInitialTimeControl( variant: string, - config: object, + config: object ): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; @@ -76,7 +76,7 @@ export interface ITimeHandler { */ initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): ITimeControlBase; /** @@ -88,7 +88,7 @@ export interface ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string, + move: string ): ITimeControlBase; /** @@ -106,7 +106,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): ITimeControlBase { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -144,7 +144,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { handleMove( game: GameResponse, game_obj: AbstractGame, - playerNr: number, + playerNr: number ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -176,7 +176,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player, - timeControl.forPlayer[player].remainingTimeMS, + timeControl.forPlayer[player].remainingTimeMS ); }); @@ -234,7 +234,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl, + config: IConfigWithTimeControl ): TimeControlParallel { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -274,7 +274,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string, + move: string ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -289,7 +289,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { const playerTimeControl = timeControl.forPlayer[playerNr]; const timestamp = new Date(); - if (!Object.keys(game_obj.specialMoves()).includes(move)) { + if (!(move === "resign") && !(move === "timeout")) { timeControl.forPlayer[playerNr].stagedMoveAt = timestamp; if ( @@ -298,7 +298,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() ) { throw new Error( - "you can't change your move because it would reduce your remaining time to zero.", + "you can't change your move because it would reduce your remaining time to zero." ); } } @@ -316,7 +316,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { .nextToPlay() .every( (player_nr) => - timeControl.forPlayer[player_nr].stagedMoveAt !== null, + timeControl.forPlayer[player_nr].stagedMoveAt !== null ) ) { for (const player_nr of game_obj.nextToPlay()) { @@ -335,7 +335,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player_nr, - timeControl.forPlayer[player_nr].remainingTimeMS, + timeControl.forPlayer[player_nr].remainingTimeMS ); playerData.onThePlaySince = timestamp; playerData.stagedMoveAt = null; From 921c5f15cef7d72b318c588743c3225007e7f4f2 Mon Sep 17 00:00:00 2001 From: merowin Date: Thu, 4 Jan 2024 21:20:37 +0100 Subject: [PATCH 17/17] lint --- packages/server/src/time-control.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/server/src/time-control.ts b/packages/server/src/time-control.ts index 4ada2e96..1a8202e5 100644 --- a/packages/server/src/time-control.ts +++ b/packages/server/src/time-control.ts @@ -15,7 +15,7 @@ import { getTimeoutService } from "./index"; * properties for being a config with time control. */ export function HasTimeControlConfig( - game_config: unknown + game_config: unknown, ): game_config is IConfigWithTimeControl { return ( game_config && @@ -29,7 +29,7 @@ export function HasTimeControlConfig( * properties for being a time control config. */ export function ValidateTimeControlConfig( - time_control_config: unknown + time_control_config: unknown, ): time_control_config is ITimeControlConfig { return ( time_control_config && @@ -44,7 +44,7 @@ export function ValidateTimeControlConfig( * properties for being a basic time control data object. */ export function ValidateTimeControlBase( - time_control: unknown + time_control: unknown, ): time_control is ITimeControlBase { return ( time_control && @@ -60,7 +60,7 @@ export function ValidateTimeControlBase( */ export function GetInitialTimeControl( variant: string, - config: object + config: object, ): ITimeControlBase | null { if (!HasTimeControlConfig(config)) return null; @@ -76,7 +76,7 @@ export interface ITimeHandler { */ initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): ITimeControlBase; /** @@ -88,7 +88,7 @@ export interface ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string + move: string, ): ITimeControlBase; /** @@ -106,7 +106,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): ITimeControlBase { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -144,7 +144,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { handleMove( game: GameResponse, game_obj: AbstractGame, - playerNr: number + playerNr: number, ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -176,7 +176,7 @@ class TimeHandlerSequentialMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player, - timeControl.forPlayer[player].remainingTimeMS + timeControl.forPlayer[player].remainingTimeMS, ); }); @@ -234,7 +234,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { initialState( variant: string, - config: IConfigWithTimeControl + config: IConfigWithTimeControl, ): TimeControlParallel { const numPlayers = makeGameObject(variant, config).numPlayers(); @@ -274,7 +274,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { game: GameResponse, game_obj: AbstractGame, playerNr: number, - move: string + move: string, ): ITimeControlBase { const config = game.config as IConfigWithTimeControl; @@ -298,7 +298,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { timestamp.getTime() - playerTimeControl.onThePlaySince.getTime() ) { throw new Error( - "you can't change your move because it would reduce your remaining time to zero." + "you can't change your move because it would reduce your remaining time to zero.", ); } } @@ -316,7 +316,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { .nextToPlay() .every( (player_nr) => - timeControl.forPlayer[player_nr].stagedMoveAt !== null + timeControl.forPlayer[player_nr].stagedMoveAt !== null, ) ) { for (const player_nr of game_obj.nextToPlay()) { @@ -335,7 +335,7 @@ class TimeHandlerParallelMoves implements ITimeHandler { this._timeoutService.scheduleTimeout( game.id, player_nr, - timeControl.forPlayer[player_nr].remainingTimeMS + timeControl.forPlayer[player_nr].remainingTimeMS, ); playerData.onThePlaySince = timestamp; playerData.stagedMoveAt = null;