schedule game duration based on time of day

This commit is contained in:
Evan
2025-03-01 11:52:45 -08:00
parent 6496c2b5b7
commit a3ae44ddb1
7 changed files with 70 additions and 33 deletions
+2 -2
View File
@@ -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;
+8 -4
View File
@@ -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();
+2 -6
View File
@@ -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 {
+18 -11
View File
@@ -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<GameID, GameServer> = 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;
}
+6 -4
View File
@@ -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,
};
}
+17 -6
View File
@@ -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);
}
}
+17
View File
@@ -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;
}