diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index d2594de55..31dc35cd4 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -88,6 +88,8 @@ export class PublicLobby extends LitElement { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; + const playersRemainingBeforeMax = + lobby.gameConfig.maxPlayers - lobby.numClients; return html` - ${lobby.numClients} - ${lobby.numClients === 1 ? "Player" : "Players"} waiting + ${lobby.numClients} / ${lobby.gameConfig.maxPlayers} players + waiting @@ -129,6 +131,12 @@ export class PublicLobby extends LitElement { ${timeDisplay} + + + Game starts when ${playersRemainingBeforeMax} more players join + or in ${timeDisplay} seconds. + + diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index fcf6a106a..ebbf61329 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -110,6 +110,7 @@ const GameConfigSchema = z.object({ infiniteGold: z.boolean(), infiniteTroops: z.boolean(), instantBuild: z.boolean(), + maxPlayers: z.number().optional(), }); const SafeString = z diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index b4ef2161d..b23234f90 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -90,7 +90,8 @@ function getServerConfig(gameEnv: string) { export interface ServerConfig { turnIntervalMs(): number; gameCreationRate(highTraffic: boolean): number; - lobbyLifetime(highTraffic): number; + lobbyLifetime(highTraffic: boolean): number; + lobbyMaxPlayers(): number; discordRedirectURI(): string; numWorkers(): number; workerIndex(gameID: GameID): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 88c5c214b..d11971df0 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -54,6 +54,9 @@ export abstract class DefaultServerConfig implements ServerConfig { return 50 * 1000; } } + lobbyMaxPlayers(): number { + return Math.random() < 0.1 ? 100 : 35; + } lobbyLifetime(highTraffic: boolean): number { return this.gameCreationRate(highTraffic) * 2; } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 964f7ad56..f88184a5f 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -20,6 +20,10 @@ export class DevServerConfig extends DefaultServerConfig { return 5 * 1000; } + lobbyMaxPlayers(): number { + return Math.random() < 0.5 ? 2 : 3; + } + discordRedirectURI(): string { return "http://localhost:3000/auth/callback"; } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 1fc0f0443..337395d3f 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -380,7 +380,14 @@ export class GameServer { } } - if (now - this.createdAt < this.config.lobbyLifetime(this.highTraffic)) { + const msSinceCreation = now - this.createdAt; + const lessThanLifetime = + msSinceCreation < this.config.lobbyLifetime(this.highTraffic); + const notEnoughPlayers = + this.gameConfig.gameType == GameType.Public && + this.gameConfig.maxPlayers && + this.activeClients.length < this.gameConfig.maxPlayers; + if (lessThanLifetime && notEnoughPlayers) { return GamePhase.Lobby; } const warmupOver = diff --git a/src/server/Master.ts b/src/server/Master.ts index d112089c2..930149ec8 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -8,7 +8,7 @@ import { GameEnv, getServerConfigFromServer, } from "../core/configuration/Config"; -import { GameInfo } from "../core/Schemas"; +import { GameConfig, GameInfo } from "../core/Schemas"; import path from "path"; import rateLimit from "express-rate-limit"; import { fileURLToPath } from "url"; @@ -199,7 +199,7 @@ async function fetchLobbies(): Promise { }); lobbyInfos.forEach((l) => { - if (l.msUntilStart <= 250) { + if (l.msUntilStart <= 250 || l.gameConfig.maxPlayers == l.numClients) { publicLobbyIDs.delete(l.gameID); } }); @@ -217,6 +217,7 @@ async function schedulePublicGame() { // Create the default public game config (from your GameManager) const defaultGameConfig = { gameMap: getNextMap(), + maxPlayers: config.lobbyMaxPlayers(), gameType: GameType.Public, difficulty: Difficulty.Medium, infiniteGold: false, @@ -224,7 +225,7 @@ async function schedulePublicGame() { instantBuild: false, disableNPCs: false, bots: 400, - }; + } as GameConfig; const workerPath = config.workerPath(gameID);