diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 0449c4c216..5bc7cdc7b9 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -149,19 +149,46 @@ export class MapPlaylist { team: [], }; - public async gameConfig(type: PublicGameType): Promise { + public async gameConfigNotInUse( + type: PublicGameType, + notInUse: (config: GameConfig) => boolean, + ): Promise { + const maxAttempts = 6; + let attempts = 0; + + // Capture playlist length because isCompact decision depends on it in buildConfig, and + // cycleNextMap could modify playlist length via generateNewPlaylist + this.ensurePlaylistPopulated(type); + const playlistLength = this.playlists[type].length; + + while (true) { + const map = this.peekNextMap(type); + const config = await this.buildConfig(type, map, playlistLength); + attempts++; + + if (notInUse(config) || attempts >= maxAttempts) { + this.consumeNextMap(type); + return config; + } + + this.cycleNextMap(type); + } + } + + private async buildConfig( + type: PublicGameType, + map: GameMapType, + playlistLength: number = this.playlists[type].length, + ): Promise { if (type === "special") { - return this.getSpecialConfig(); + return this.buildSpecialConfig(map); } const mode = type === "ffa" ? GameMode.FFA : GameMode.Team; - const map = this.getNextMap(type); - const playerTeams = mode === GameMode.Team ? this.getTeamCount(map) : undefined; - let isCompact: boolean | undefined = - this.playlists[type].length % 3 === 0 || undefined; + let isCompact: boolean | undefined = playlistLength % 3 === 0 || undefined; if ( isCompact && mode === GameMode.Team && @@ -199,9 +226,8 @@ export class MapPlaylist { } satisfies GameConfig; } - private async getSpecialConfig(): Promise { + private async buildSpecialConfig(map: GameMapType): Promise { const mode = Math.random() < 0.5 ? GameMode.FFA : GameMode.Team; - const map = this.getNextMap("special"); const playerTeams = mode === GameMode.Team ? this.getTeamCount(map) : undefined; @@ -401,12 +427,28 @@ export class MapPlaylist { } satisfies GameConfig; } - private getNextMap(type: PublicGameType): GameMapType { - const playlist = this.playlists[type]; - if (playlist.length === 0) { - playlist.push(...this.generateNewPlaylist(type)); + private ensurePlaylistPopulated(type: PublicGameType): void { + if (this.playlists[type].length === 0) { + this.playlists[type].push(...this.generateNewPlaylist(type)); + } + } + + private peekNextMap(type: PublicGameType): GameMapType { + this.ensurePlaylistPopulated(type); + return this.playlists[type][0]; + } + + private consumeNextMap(type: PublicGameType): GameMapType { + this.ensurePlaylistPopulated(type); + return this.playlists[type].shift()!; + } + + private cycleNextMap(type: PublicGameType): void { + const map = this.consumeNextMap(type); + + if (!this.addNextMapNonConsecutive(this.playlists[type], [map])) { + this.playlists[type].push(...this.generateNewPlaylist(type)); } - return playlist.shift()!; } private generateNewPlaylist(type: PublicGameType): GameMapType[] { diff --git a/src/server/MasterLobbyService.ts b/src/server/MasterLobbyService.ts index 9285b8a912..6cc8511ce6 100644 --- a/src/server/MasterLobbyService.ts +++ b/src/server/MasterLobbyService.ts @@ -133,6 +133,22 @@ export class MasterLobbyService { private async maybeScheduleLobby() { const lobbiesByType = this.getAllLobbies(); + const activeConfigs = Object.values(lobbiesByType) + .map((lobbies) => lobbies[0]?.gameConfig) + .filter((c) => c !== undefined); + + const activeMaps = new Set(activeConfigs.map((c) => c.gameMap)); + const activeNumTeams = new Set( + activeConfigs + .filter((c) => c.playerTeams !== undefined) + .map((c) => String(c.playerTeams)), + ); + const activeMaxPlayers = new Set( + activeConfigs + .filter((c) => c.maxPlayers !== undefined) + .map((c) => c.maxPlayers), + ); + for (const type of Object.keys(lobbiesByType) as PublicGameType[]) { const lobbies = lobbiesByType[type]; @@ -153,10 +169,35 @@ export class MasterLobbyService { continue; } + const gameConfig = await this.playlist.gameConfigNotInUse(type, (c) => { + if (activeMaps.has(c.gameMap)) return false; + + if ( + c.playerTeams !== undefined && + activeNumTeams.has(String(c.playerTeams)) + ) { + return false; + } + + if (c.maxPlayers !== undefined && activeMaxPlayers.has(c.maxPlayers)) { + return false; + } + + return true; + }); + + activeMaps.add(gameConfig.gameMap); + if (gameConfig.playerTeams !== undefined) { + activeNumTeams.add(String(gameConfig.playerTeams)); + } + if (gameConfig.maxPlayers !== undefined) { + activeMaxPlayers.add(gameConfig.maxPlayers); + } + this.sendMessageToWorker({ type: "createGame", gameID: generateID(), - gameConfig: await this.playlist.gameConfig(type), + gameConfig, publicGameType: type, } satisfies MasterCreateGame); }