From a3ae44ddb13c69a16cfd56f982ee5507f2a6e28b Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 1 Mar 2025 11:52:45 -0800 Subject: [PATCH] schedule game duration based on time of day --- src/core/configuration/Config.ts | 4 ++-- src/core/configuration/DefaultConfig.ts | 12 ++++++---- src/core/configuration/DevConfig.ts | 8 ++----- src/server/GameManager.ts | 29 +++++++++++++++---------- src/server/GameServer.ts | 10 +++++---- src/server/Master.ts | 23 +++++++++++++++----- src/server/Util.ts | 17 +++++++++++++++ 7 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 src/server/Util.ts diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 06aca3a35..ad70b48ec 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -63,8 +63,8 @@ export function getServerConfig(): ServerConfig { export interface ServerConfig { turnIntervalMs(): number; - gameCreationRate(): number; - lobbyLifetime(): number; + gameCreationRate(highTraffic: boolean): number; + lobbyLifetime(highTraffic): number; discordRedirectURI(): string; numWorkers(): number; workerIndex(gameID: GameID): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index a0055a185..4d60b2984 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -30,11 +30,15 @@ export abstract class DefaultServerConfig implements ServerConfig { turnIntervalMs(): number { return 100; } - gameCreationRate(): number { - return 30 * 1000; + gameCreationRate(highTraffic: boolean): number { + if (highTraffic) { + return 30 * 1000; + } else { + return 60 * 1000; + } } - lobbyLifetime(): number { - return 1 * 60 * 1000; + lobbyLifetime(highTraffic: boolean): number { + return this.gameCreationRate(highTraffic) * 2; } workerIndex(gameID: GameID): number { return simpleHash(gameID) % this.numWorkers(); diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index ac0dd5c16..2bc1a0c15 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -8,12 +8,8 @@ export class DevServerConfig extends DefaultServerConfig { env(): GameEnv { return GameEnv.Dev; } - gameCreationRate(): number { - return 10 * 1000; - } - - lobbyLifetime(): number { - return 10 * 1000; + gameCreationRate(highTraffic: boolean): number { + return 5 * 1000; } discordRedirectURI(): string { diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 56b7032d4..c494f6843 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -3,6 +3,7 @@ import { GameConfig, GameID } from "../core/Schemas"; import { Client } from "./Client"; import { GamePhase, GameServer } from "./GameServer"; import { Difficulty, GameMapType, GameType } from "../core/game/Game"; +import { isHighTrafficTime } from "./Util"; export class GameManager { private games: Map = new Map(); @@ -25,17 +26,23 @@ export class GameManager { } createGame(id: GameID, gameConfig: GameConfig | undefined) { - const game = new GameServer(id, Date.now(), this.config, { - gameMap: GameMapType.World, - gameType: GameType.Private, - difficulty: Difficulty.Medium, - disableNPCs: false, - infiniteGold: false, - infiniteTroops: false, - instantBuild: false, - bots: 400, - ...gameConfig, - }); + const game = new GameServer( + id, + Date.now(), + isHighTrafficTime(), + this.config, + { + gameMap: GameMapType.World, + gameType: GameType.Private, + difficulty: Difficulty.Medium, + disableNPCs: false, + infiniteGold: false, + infiniteTroops: false, + instantBuild: false, + bots: 400, + ...gameConfig, + }, + ); this.games.set(id, game); return game; } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 1f4cfef93..f7a60c0e7 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -53,6 +53,7 @@ export class GameServer { constructor( public readonly id: string, public readonly createdAt: number, + public readonly highTraffic: boolean, private config: ServerConfig, public gameConfig: GameConfig, ) {} @@ -203,7 +204,7 @@ export class GameServer { return this._startTime; } else { //game hasn't started yet, only works for public games - return this.createdAt + this.config.lobbyLifetime(); + return this.createdAt + this.config.lobbyLifetime(this.highTraffic); } } @@ -371,11 +372,12 @@ export class GameServer { } } - if (now - this.createdAt < this.config.lobbyLifetime()) { + if (now - this.createdAt < this.config.lobbyLifetime(this.highTraffic)) { return GamePhase.Lobby; } const warmupOver = - now > this.createdAt + this.config.lobbyLifetime() + 30 * 1000; + now > + this.createdAt + this.config.lobbyLifetime(this.highTraffic) + 30 * 1000; if (noActive && warmupOver && noRecentPings) { return GamePhase.Finished; } @@ -396,7 +398,7 @@ export class GameServer { })), gameConfig: this.gameConfig, msUntilStart: this.isPublic() - ? this.createdAt + this.config.lobbyLifetime() + ? this.createdAt + this.config.lobbyLifetime(this.highTraffic) : undefined, }; } diff --git a/src/server/Master.ts b/src/server/Master.ts index 96393c657..29e40d685 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -9,6 +9,7 @@ import { GameInfo } from "../core/Schemas"; import path from "path"; import rateLimit from "express-rate-limit"; import { fileURLToPath } from "url"; +import { isHighTrafficTime } from "./Util"; const config = getServerConfig(); const readyWorkers = new Set(); @@ -62,19 +63,29 @@ export async function startMaster() { console.log( `Worker ${workerId} is ready. (${readyWorkers.size}/${config.numWorkers()} ready)`, ); - // Start scheduling when all workers are ready if (readyWorkers.size === config.numWorkers()) { console.log("All workers ready, starting game scheduling"); - // let the workers start up + + // Safe implementation of dynamic interval + let timeoutId = null; + const scheduleLobbies = () => { - schedulePublicGame().catch((error) => { - console.error("Error scheduling public game:", error); - }); + schedulePublicGame() + .catch((error) => { + console.error("Error scheduling public game:", error); + }) + .finally(() => { + // Schedule next run with the current config value + const currentLifetime = config.lobbyLifetime(isHighTrafficTime()); + timeoutId = setTimeout(scheduleLobbies, currentLifetime); + }); }; + // Run first execution immediately scheduleLobbies(); - setInterval(scheduleLobbies, config.gameCreationRate()); + + // Regular interval for fetching lobbies setInterval(() => fetchLobbies(), 250); } } diff --git a/src/server/Util.ts b/src/server/Util.ts new file mode 100644 index 000000000..72b180a34 --- /dev/null +++ b/src/server/Util.ts @@ -0,0 +1,17 @@ +export function isHighTrafficTime(): boolean { + // More traffic from 4am to 4pm + const now = new Date(); + + // Convert current time to PST (America/Los_Angeles timezone) + // Using a more compatible approach + const formatter = new Intl.DateTimeFormat("en-US", { + timeZone: "America/Los_Angeles", + hour: "numeric", + hour12: false, + }); + + const formattedTime = formatter.format(now); + const hourPST = parseInt(formattedTime.split(":")[0], 10); + + return hourPST >= 4 && hourPST < 16; +}