diff --git a/src/core/PseudoRandom.ts b/src/core/PseudoRandom.ts index 985de26fd..ca217cc68 100644 --- a/src/core/PseudoRandom.ts +++ b/src/core/PseudoRandom.ts @@ -48,7 +48,7 @@ export class PseudoRandom { return this.nextInt(0, odds) == 0; } - shuffleArray(array: any[]) { + shuffleArray(array: any[]): any[] { for (let i = array.length - 1; i >= 0; i--) { const j = Math.floor(this.nextInt(0, i + 1)); [array[i], array[j]] = [array[j], array[i]]; diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index 822c9abb1..ef742d827 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -1,120 +1,129 @@ -import { GameMapType, GameMode } from "../core/game/Game"; +import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; +import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; import { PseudoRandom } from "../core/PseudoRandom"; +import { GameConfig } from "../core/Schemas"; +import { logger } from "./Logger"; -enum PlaylistType { - BigMaps, - SmallMaps, +const log = logger.child({}); + +const config = getServerConfigFromServer(); + +const frequency = { + World: 3, + Europe: 2, + Africa: 2, + Australia: 1, + NorthAmerica: 1, + Britannia: 1, + GatewayToTheAtlantic: 1, + Iceland: 1, + SouthAmerica: 1, + KnownWorld: 1, + DeglaciatedAntarctica: 1, + EuropeClassic: 1, + Mena: 1, + Pangaea: 1, + Asia: 1, + Mars: 1, + BetweenTwoSeas: 1, + Japan: 1, + BlackSea: 1, + FaroeIslands: 1, +}; + +interface MapWithMode { + map: GameMapType; + mode: GameMode; } -const random = new PseudoRandom(123); - export class MapPlaylist { - private gameModeRotation = [GameMode.FFA, GameMode.FFA, GameMode.Team]; - private currentGameModeIndex = 0; + private mapsPlaylist: MapWithMode[] = []; - private mapsPlaylistBig: GameMapType[] = []; - private mapsPlaylistSmall: GameMapType[] = []; - private currentPlaylistCounter = 0; + public gameConfig(): GameConfig { + const { map, mode } = this.getNextMap(); - // Get the next map in rotation - public getNextMap(): GameMapType { - const playlistType: PlaylistType = this.getNextPlaylistType(); - const mapsPlaylist: GameMapType[] = this.getNextMapsPlayList(playlistType); - return mapsPlaylist.shift()!; + const numPlayerTeams = + mode === GameMode.Team ? 2 + Math.floor(Math.random() * 5) : undefined; + + // Create the default public game config (from your GameManager) + return { + gameMap: map, + maxPlayers: config.lobbyMaxPlayers(map), + gameType: GameType.Public, + difficulty: Difficulty.Medium, + infiniteGold: false, + infiniteTroops: false, + instantBuild: false, + disableNPCs: mode == GameMode.Team, + disableNukes: false, + gameMode: mode, + playerTeams: numPlayerTeams, + bots: 400, + } as GameConfig; } - public getNextGameMode(): GameMode { - const nextGameMode = this.gameModeRotation[this.currentGameModeIndex]; - this.currentGameModeIndex = - (this.currentGameModeIndex + 1) % this.gameModeRotation.length; - return nextGameMode; - } - - private getNextMapsPlayList(playlistType: PlaylistType): GameMapType[] { - switch (playlistType) { - case PlaylistType.BigMaps: - if (!(this.mapsPlaylistBig.length > 0)) { - this.fillMapsPlaylist(playlistType, this.mapsPlaylistBig); + private getNextMap(): MapWithMode { + if (this.mapsPlaylist.length === 0) { + const numAttempts = 10000; + for (let i = 0; i < numAttempts; i++) { + if (this.shuffleMapsPlaylist()) { + log.info(`Generated map playlist in ${i} attempts`); + return this.mapsPlaylist.shift()!; } - return this.mapsPlaylistBig; - - case PlaylistType.SmallMaps: - if (!(this.mapsPlaylistSmall.length > 0)) { - this.fillMapsPlaylist(playlistType, this.mapsPlaylistSmall); - } - return this.mapsPlaylistSmall; + } + log.error("Failed to generate a valid map playlist"); } + // Even if it failed, playlist will be partially populated. + return this.mapsPlaylist.shift()!; } - private fillMapsPlaylist( - playlistType: PlaylistType, - mapsPlaylist: GameMapType[], - ): void { - const frequency = this.getFrequency(playlistType); + private shuffleMapsPlaylist(): boolean { + const maps: GameMapType[] = []; Object.keys(GameMapType).forEach((key) => { - let count = parseInt(frequency[key]); - while (count > 0) { - mapsPlaylist.push(GameMapType[key]); - count--; + for (let i = 0; i < parseInt(frequency[key]); i++) { + maps.push(GameMapType[key]); } }); - do { - random.shuffleArray(mapsPlaylist); - } while (!this.allNonConsecutive(mapsPlaylist)); - } - // Specifically controls how the playlists rotate. - private getNextPlaylistType(): PlaylistType { - switch (this.currentPlaylistCounter) { - case 0: - case 1: - this.currentPlaylistCounter++; - return PlaylistType.BigMaps; - case 2: - this.currentPlaylistCounter = 0; - return PlaylistType.SmallMaps; - } - } + const rand = new PseudoRandom(Date.now()); - private getFrequency(playlistType: PlaylistType) { - switch (playlistType) { - // Big Maps are those larger than ~2.5 mil pixels - case PlaylistType.BigMaps: - return { - Europe: 2, - NorthAmerica: 1, - Africa: 2, - Britannia: 1, - GatewayToTheAtlantic: 2, - Australia: 2, - Iceland: 2, - SouthAmerica: 1, - KnownWorld: 2, - DeglaciatedAntarctica: 2, - }; - case PlaylistType.SmallMaps: - return { - World: 4, - EuropeClassic: 3, - Mena: 2, - Pangaea: 1, - Asia: 1, - Mars: 1, - BetweenTwoSeas: 2, - Japan: 2, - BlackSea: 1, - FaroeIslands: 2, - }; - } - } + const ffa1: GameMapType[] = rand.shuffleArray([...maps]); + const ffa2: GameMapType[] = rand.shuffleArray([...maps]); + const team: GameMapType[] = rand.shuffleArray([...maps]); - // Check for consecutive duplicates in the maps array - private allNonConsecutive(maps: GameMapType[]): boolean { - for (let i = 0; i < maps.length - 1; i++) { - if (maps[i] === maps[i + 1]) { + this.mapsPlaylist = []; + for (let i = 0; i < maps.length; i++) { + if (!this.addNextMap(this.mapsPlaylist, ffa1, GameMode.FFA)) { + return false; + } + if (!this.addNextMap(this.mapsPlaylist, ffa2, GameMode.FFA)) { + return false; + } + if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) { return false; } } return true; } + + private addNextMap( + playlist: MapWithMode[], + nextEls: GameMapType[], + mode: GameMode, + ): boolean { + const nonConsecutiveNum = 5; + const lastEls = playlist + .slice(playlist.length - nonConsecutiveNum) + .map((m) => m.map); + for (let i = 0; i < nextEls.length; i++) { + const next = nextEls[i]; + if (lastEls.includes(next)) { + continue; + } + nextEls.splice(i, 1); + playlist.push({ map: next, mode: mode }); + return true; + } + return false; + } } diff --git a/src/server/Master.ts b/src/server/Master.ts index 89c14f2b8..bcab69fff 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -5,8 +5,7 @@ import http from "http"; import path from "path"; import { fileURLToPath } from "url"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { Difficulty, GameMode, GameType } from "../core/game/Game"; -import { GameConfig, GameInfo } from "../core/Schemas"; +import { GameInfo } from "../core/Schemas"; import { generateID } from "../core/Util"; import { gatekeeper, LimiterType } from "./Gatekeeper"; import { logger } from "./Logger"; @@ -214,40 +213,11 @@ async function fetchLobbies(): Promise { return publicLobbyIDs.size; } -let lastGameMode: GameMode = GameMode.FFA; - // Function to schedule a new public game async function schedulePublicGame(playlist: MapPlaylist) { const gameID = generateID(); - const map = playlist.getNextMap(); publicLobbyIDs.add(gameID); - if (lastGameMode == GameMode.FFA) { - lastGameMode = GameMode.Team; - } else { - lastGameMode = GameMode.FFA; - } - - const gameMode = playlist.getNextGameMode(); - const numPlayerTeams = - gameMode === GameMode.Team ? 2 + Math.floor(Math.random() * 5) : undefined; - - // Create the default public game config (from your GameManager) - const defaultGameConfig: GameConfig = { - gameMap: map, - maxPlayers: config.lobbyMaxPlayers(map), - gameType: GameType.Public, - difficulty: Difficulty.Medium, - infiniteGold: false, - infiniteTroops: false, - instantBuild: false, - disableNPCs: gameMode == GameMode.Team, - disableNukes: false, - gameMode, - playerTeams: numPlayerTeams, - bots: 400, - }; - const workerPath = config.workerPath(gameID); // Send request to the worker to start the game @@ -261,7 +231,7 @@ async function schedulePublicGame(playlist: MapPlaylist) { [config.adminHeader()]: config.adminToken(), }, body: JSON.stringify({ - gameConfig: defaultGameConfig, + gameConfig: playlist.gameConfig(), }), }, );