import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { Difficulty, Duos, GameMapName, GameMapSize, GameMapType, GameMode, GameType, HumansVsNations, Quads, Trios, } from "../core/game/Game"; import { PseudoRandom } from "../core/PseudoRandom"; import { GameConfig, TeamCountConfig } from "../core/Schemas"; import { logger } from "./Logger"; const log = logger.child({}); const config = getServerConfigFromServer(); // How many times each map should appear in the playlist. // Note: The Partial should eventually be removed for better type safety. const frequency: Partial> = { Africa: 7, Asia: 6, Australia: 4, Achiran: 5, Baikal: 5, BetweenTwoSeas: 5, BlackSea: 6, Britannia: 5, DeglaciatedAntarctica: 4, EastAsia: 5, Europe: 3, EuropeClassic: 3, FalklandIslands: 4, FaroeIslands: 4, FourIslands: 4, GatewayToTheAtlantic: 5, GulfOfStLawrence: 4, Halkidiki: 4, Iceland: 4, Italia: 6, Japan: 6, Lisbon: 4, Manicouagan: 4, Mars: 3, Mena: 6, Montreal: 6, NewYorkCity: 3, NorthAmerica: 5, Pangaea: 5, Pluto: 6, SouthAmerica: 5, StraitOfGibraltar: 5, Svalmel: 8, World: 8, Lemnos: 3, TwoLakes: 6, StraitOfHormuz: 4, Surrounded: 4, }; interface MapWithMode { map: GameMapType; mode: GameMode; } const TEAM_COUNTS = [ 2, 3, 4, 5, 6, 7, Duos, Trios, Quads, HumansVsNations, ] as const satisfies TeamCountConfig[]; export class MapPlaylist { private mapsPlaylist: MapWithMode[] = []; constructor(private disableTeams: boolean = false) {} public gameConfig(): GameConfig { const { map, mode } = this.getNextMap(); const playerTeams = mode === GameMode.Team ? this.getTeamCount() : undefined; // Create the default public game config (from your GameManager) return { donateGold: mode === GameMode.Team, donateTroops: mode === GameMode.Team, gameMap: map, maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams), gameType: GameType.Public, gameMapSize: GameMapSize.Normal, difficulty: playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Easy, infiniteGold: false, infiniteTroops: false, maxTimerValue: undefined, instantBuild: false, randomSpawn: false, disableNations: mode === GameMode.Team && playerTeams !== HumansVsNations, gameMode: mode, playerTeams, bots: 400, spawnImmunityDuration: 5 * 10, disabledUnits: [], } satisfies GameConfig; } private getTeamCount(): TeamCountConfig { return TEAM_COUNTS[Math.floor(Math.random() * TEAM_COUNTS.length)]; } 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()!; } } log.error("Failed to generate a valid map playlist"); } // Even if it failed, playlist will be partially populated. return this.mapsPlaylist.shift()!; } private shuffleMapsPlaylist(): boolean { const maps: GameMapType[] = []; (Object.keys(GameMapType) as GameMapName[]).forEach((key) => { for (let i = 0; i < (frequency[key] ?? 0); i++) { maps.push(GameMapType[key]); } }); const rand = new PseudoRandom(Date.now()); const ffa1: GameMapType[] = rand.shuffleArray([...maps]); const team1: GameMapType[] = rand.shuffleArray([...maps]); const ffa2: GameMapType[] = rand.shuffleArray([...maps]); this.mapsPlaylist = []; for (let i = 0; i < maps.length; i++) { if (!this.addNextMap(this.mapsPlaylist, ffa1, GameMode.FFA)) { return false; } if (!this.disableTeams) { if (!this.addNextMap(this.mapsPlaylist, team1, GameMode.Team)) { return false; } } if (!this.addNextMap(this.mapsPlaylist, ffa2, GameMode.FFA)) { 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; } }