diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index a9d605eb3..86b6197b5 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -6,6 +6,7 @@ import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { consolex } from "../core/Consolex"; import { Difficulty, + Duos, GameMapType, GameMode, mapCategories, @@ -28,7 +29,7 @@ export class HostLobbyModal extends LitElement { @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs = false; @state() private gameMode: GameMode = GameMode.FFA; - @state() private teamCount: number = 2; + @state() private teamCount: number | typeof Duos = 2; @state() private disableNukes: boolean = false; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @@ -194,7 +195,7 @@ export class HostLobbyModal extends LitElement { ${translateText("host_modal.team_count")}
- ${[2, 3, 4, 5, 6, 7].map( + ${[Duos, 2, 3, 4, 5, 6, 7].map( (o) => html`
- ${[2, 3, 4, 5, 6, 7].map( + ${["Duos", 2, 3, 4, 5, 6, 7].map( (o) => html`
{ const config = await getConfig(gameStart.config, null); const gameMap = await loadGameMap(gameStart.config.gameMap); - const game = createGame( - gameStart.players.map( - (p) => - new PlayerInfo( - p.flag, - p.clientID == clientID - ? sanitize(p.username) - : fixProfaneUsername(sanitize(p.username)), - PlayerType.Human, - p.clientID, - p.playerID, - ), - ), + const random = new PseudoRandom(simpleHash(gameStart.gameID)); + + const humans = gameStart.players.map( + (p) => + new PlayerInfo( + p.flag, + p.clientID == clientID + ? sanitize(p.username) + : fixProfaneUsername(sanitize(p.username)), + PlayerType.Human, + p.clientID, + p.playerID, + ), + ); + + const nations = gameStart.config.disableNPCs + ? [] + : gameMap.nationMap.nations.map( + (n) => + new Nation( + new Cell(n.coordinates[0], n.coordinates[1]), + n.strength, + new PlayerInfo( + n.flag || "", + n.name, + PlayerType.FakeHuman, + null, + random.nextID(), + ), + ), + ); + + const game: Game = createGame( + humans, + nations, gameMap.gameMap, gameMap.miniGameMap, - gameMap.nationMap, config, ); + const gr = new GameRunner( - game as Game, + game, new Executor(game, gameStart.gameID, clientID), callBack, ); diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 3882f74a1..b08fcfcfb 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -2,11 +2,11 @@ import { z } from "zod"; import { AllPlayers, Difficulty, + Duos, GameMapType, GameMode, GameType, PlayerType, - Team, UnitType, } from "./game/Game"; import { flattenedEmojiTable } from "./Util"; @@ -122,9 +122,11 @@ const GameConfigSchema = z.object({ infiniteTroops: z.boolean(), instantBuild: z.boolean(), maxPlayers: z.number().optional(), - numPlayerTeams: z.number().optional(), + playerTeams: z.union([z.number().optional(), z.literal(Duos)]), }); +export const TeamSchema = z.string(); + const SafeString = z .string() .regex( @@ -361,7 +363,7 @@ const ClientBaseMessageSchema = z.object({ export const ClientSendWinnerSchema = ClientBaseMessageSchema.extend({ type: z.literal("winner"), - winner: ID.or(z.nativeEnum(Team)).nullable(), + winner: z.union([ID, TeamSchema]).nullable(), allPlayersStats: AllPlayersStatsSchema, winnerType: z.enum(["player", "team"]), }); @@ -422,10 +424,7 @@ export const GameRecordSchema = z.object({ date: SafeString, num_turns: z.number(), turns: z.array(TurnSchema), - winner: z - .union([ID, z.nativeEnum(Team)]) - .nullable() - .optional(), + winner: z.union([ID, SafeString]).nullable().optional(), winnerType: z.enum(["player", "team"]).nullable().optional(), allPlayersStats: z.record(ID, PlayerStatsSchema), version: z.enum(["v0.0.1"]), diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 2e5d095d0..1442dd814 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -2,6 +2,7 @@ import { Colord } from "colord"; import { GameConfig, GameID } from "../Schemas"; import { Difficulty, + Duos, Game, GameMapType, GameMode, @@ -72,7 +73,7 @@ export interface Config { instantBuild(): boolean; numSpawnPhaseTurns(): number; userSettings(): UserSettings; - numPlayerTeams(): number; + playerTeams(): number | typeof Duos; startManpower(playerInfo: PlayerInfo): number; populationIncreaseRate(player: Player | PlayerView): number; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 226a893cd..4eabc348d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -1,5 +1,6 @@ import { Difficulty, + Duos, Game, GameMapType, GameMode, @@ -217,12 +218,14 @@ export class DefaultConfig implements Config { defensePostDefenseBonus(): number { return 5; } - numPlayerTeams(): number { - return this._gameConfig.numPlayerTeams ?? 0; + playerTeams(): number | typeof Duos { + return this._gameConfig.playerTeams ?? 0; } + spawnNPCs(): boolean { return !this._gameConfig.disableNPCs; } + disableNukes(): boolean { return this._gameConfig.disableNukes; } diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 83c56c7ea..9c4ac5d6c 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,7 +1,7 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; -import { PlayerType, Team, TerrainType } from "../game/Game"; +import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { @@ -43,24 +43,25 @@ export const pastelTheme = new (class implements Theme { teamColor(team: Team): Colord { switch (team) { - case Team.Blue: + case ColoredTeams.Blue: return blue; - case Team.Red: + case ColoredTeams.Red: return red; - case Team.Teal: + case ColoredTeams.Teal: return teal; - case Team.Purple: + case ColoredTeams.Purple: return purple; - case Team.Yellow: + case ColoredTeams.Yellow: return yellow; - case Team.Orange: + case ColoredTeams.Orange: return orange; - case Team.Green: + case ColoredTeams.Green: return green; - case Team.Bot: + case ColoredTeams.Bot: return botColor; + default: + return humanColors[simpleHash(team) % humanColors.length]; } - throw new Error(`Missing color for ${team}`); } territoryColor(player: PlayerView): Colord { diff --git a/src/core/configuration/PastelThemeDark.ts b/src/core/configuration/PastelThemeDark.ts index efc6bcd67..bc9dd8ddf 100644 --- a/src/core/configuration/PastelThemeDark.ts +++ b/src/core/configuration/PastelThemeDark.ts @@ -1,7 +1,7 @@ import { Colord, colord } from "colord"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; -import { PlayerType, Team, TerrainType } from "../game/Game"; +import { ColoredTeams, PlayerType, Team, TerrainType } from "../game/Game"; import { GameMap, TileRef } from "../game/GameMap"; import { PlayerView } from "../game/GameView"; import { @@ -43,24 +43,25 @@ export const pastelThemeDark = new (class implements Theme { teamColor(team: Team): Colord { switch (team) { - case Team.Blue: + case ColoredTeams.Blue: return blue; - case Team.Red: + case ColoredTeams.Red: return red; - case Team.Teal: + case ColoredTeams.Teal: return teal; - case Team.Purple: + case ColoredTeams.Purple: return purple; - case Team.Yellow: + case ColoredTeams.Yellow: return yellow; - case Team.Orange: + case ColoredTeams.Orange: return orange; - case Team.Green: + case ColoredTeams.Green: return green; - case Team.Bot: + case ColoredTeams.Bot: return botColor; + default: + return humanColors[simpleHash(team) % humanColors.length]; } - throw new Error(`Missing color for ${team}`); } territoryColor(player: PlayerView): Colord { diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 83795f90d..73bfb97e9 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,4 +1,4 @@ -import { Execution, Game, PlayerInfo, PlayerType } from "../game/Game"; +import { Execution, Game } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { ClientID, GameID, Intent, Turn } from "../Schemas"; import { simpleHash } from "../Util"; @@ -120,19 +120,7 @@ export class Executor { fakeHumanExecutions(): Execution[] { const execs = []; for (const nation of this.mg.nations()) { - execs.push( - new FakeHumanExecution( - this.gameID, - new PlayerInfo( - nation.flag || "", - nation.name, - PlayerType.FakeHuman, - null, - this.random.nextID(), - nation, - ), - ), - ); + execs.push(new FakeHumanExecution(this.gameID, nation)); } return execs; } diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 30e037c85..c9dfeb868 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -4,9 +4,9 @@ import { Difficulty, Execution, Game, + Nation, Player, PlayerID, - PlayerInfo, PlayerType, Relation, TerrainType, @@ -42,10 +42,10 @@ export class FakeHumanExecution implements Execution { constructor( gameID: GameID, - private playerInfo: PlayerInfo, + private nation: Nation, ) { this.random = new PseudoRandom( - simpleHash(playerInfo.id) + simpleHash(gameID), + simpleHash(nation.playerInfo.id) + simpleHash(gameID), ); this.heckleEmoji = ["🤡", "😡"].map((e) => flattenedEmojiTable.indexOf(e)); } @@ -102,15 +102,17 @@ export class FakeHumanExecution implements Execution { if (ticks % this.random.nextInt(5, 30) == 0) { const rl = this.randomLand(); if (rl == null) { - consolex.warn(`cannot spawn ${this.playerInfo.name}`); + consolex.warn(`cannot spawn ${this.nation.playerInfo.name}`); return; } - this.mg.addExecution(new SpawnExecution(this.playerInfo, rl)); + this.mg.addExecution(new SpawnExecution(this.nation.playerInfo, rl)); } return; } if (this.player == null) { - this.player = this.mg.players().find((p) => p.id() == this.playerInfo.id); + this.player = this.mg + .players() + .find((p) => p.id() == this.nation.playerInfo.id); if (this.player == null) { return; } @@ -552,7 +554,7 @@ export class FakeHumanExecution implements Execution { let tries = 0; while (tries < 50) { tries++; - const cell = this.playerInfo.nation.cell; + const cell = this.nation.spawnCell; const x = this.random.nextInt(cell.x - delta, cell.x + delta); const y = this.random.nextInt(cell.y - delta, cell.y + delta); if (!this.mg.isValidCoord(x, y)) { diff --git a/src/core/execution/WinCheckExecution.ts b/src/core/execution/WinCheckExecution.ts index a18521998..e40e3f0b7 100644 --- a/src/core/execution/WinCheckExecution.ts +++ b/src/core/execution/WinCheckExecution.ts @@ -1,5 +1,12 @@ import { GameEvent } from "../EventBus"; -import { Execution, Game, GameMode, Player, Team } from "../game/Game"; +import { + ColoredTeams, + Execution, + Game, + GameMode, + Player, + Team, +} from "../game/Game"; export class WinEvent implements GameEvent { constructor(public readonly winner: Player) {} @@ -66,7 +73,7 @@ export class WinCheckExecution implements Execution { this.mg.numLandTiles() - this.mg.numTilesWithFallout(); const percentage = (max[1] / numTilesWithoutFallout) * 100; if (percentage > this.mg.config().percentageTilesOwnedToWin()) { - if (max[0] == Team.Bot) return; + if (max[0] == ColoredTeams.Bot) return; this.mg.setWinner(max[0], this.mg.stats().stats()); console.log(`${max[0]} has won the game`); this.active = false; diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 369c01e98..cf563fe2a 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -37,16 +37,20 @@ export enum Difficulty { Impossible = "Impossible", } -export enum Team { - Red = "Red", - Blue = "Blue", - Teal = "Teal", - Purple = "Purple", - Yellow = "Yellow", - Orange = "Orange", - Green = "Green", - Bot = "Bot", -} +export type Team = string; + +export const Duos = "Duos" as const; + +export const ColoredTeams: Record = { + Red: "Red", + Blue: "Blue", + Teal: "Teal", + Purple: "Purple", + Yellow: "Yellow", + Orange: "Orange", + Green: "Green", + Bot: "Bot", +} as const; export enum GameMapType { World = "World", @@ -152,10 +156,9 @@ export enum Relation { export class Nation { constructor( - public readonly flag: string, - public readonly name: string, - public readonly cell: Cell, + public readonly spawnCell: Cell, public readonly strength: number, + public readonly playerInfo: PlayerInfo, ) {} } diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 772cf7d23..ccf269688 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -8,6 +8,8 @@ import { Alliance, AllianceRequest, Cell, + ColoredTeams, + Duos, EmojiMessage, Execution, Game, @@ -32,19 +34,18 @@ import { PlayerImpl } from "./PlayerImpl"; import { Stats } from "./Stats"; import { StatsImpl } from "./StatsImpl"; import { assignTeams } from "./TeamAssignment"; -import { NationMap } from "./TerrainMapLoader"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { UnitGrid } from "./UnitGrid"; import { UnitImpl } from "./UnitImpl"; export function createGame( humans: PlayerInfo[], + nations: Nation[], gameMap: GameMap, miniGameMap: GameMap, - nationMap: NationMap, config: Config, ): Game { - return new GameImpl(humans, gameMap, miniGameMap, nationMap, config); + return new GameImpl(humans, nations, gameMap, miniGameMap, config); } export type CellString = string; @@ -54,8 +55,6 @@ export class GameImpl implements Game { private unInitExecs: Execution[] = []; - private nations_: Nation[] = []; - _players: Map = new Map(); _playersBySmallID = []; @@ -75,51 +74,62 @@ export class GameImpl implements Game { private _stats: StatsImpl = new StatsImpl(); - private playerTeams: Team[] = [Team.Red, Team.Blue]; - private botTeam: Team = Team.Bot; + private playerTeams: Team[] = [ColoredTeams.Red, ColoredTeams.Blue]; + private botTeam: Team = ColoredTeams.Bot; constructor( private _humans: PlayerInfo[], + private _nations: Nation[], private _map: GameMap, private miniGameMap: GameMap, - nationMap: NationMap, private _config: Config, ) { this._terraNullius = new TerraNulliusImpl(); this._width = _map.width(); this._height = _map.height(); - this.nations_ = nationMap.nations.map( - (n) => - new Nation( - n.flag || "", - n.name, - new Cell(n.coordinates[0], n.coordinates[1]), - n.strength, - ), - ); this.unitGrid = new UnitGrid(this._map); if (_config.gameConfig().gameMode === GameMode.Team) { - const numPlayerTeams = _config.numPlayerTeams(); + this.populateTeams(); + } + this.addPlayers(); + } + + private populateTeams() { + if (this._config.playerTeams() === Duos) { + this.playerTeams = []; + const numTeams = Math.ceil( + (this._humans.length + this._nations.length) / 2, + ); + for (let i = 0; i < numTeams; i++) { + this.playerTeams.push("Team " + (i + 1)); + } + } else { + const numPlayerTeams = this._config.playerTeams() as number; if (numPlayerTeams < 2) throw new Error(`Too few teams: ${numPlayerTeams}`); - if (numPlayerTeams >= 3) this.playerTeams.push(Team.Teal); - if (numPlayerTeams >= 4) this.playerTeams.push(Team.Purple); - if (numPlayerTeams >= 5) this.playerTeams.push(Team.Yellow); - if (numPlayerTeams >= 6) this.playerTeams.push(Team.Orange); - if (numPlayerTeams >= 7) this.playerTeams.push(Team.Green); + if (numPlayerTeams >= 3) this.playerTeams.push(ColoredTeams.Teal); + if (numPlayerTeams >= 4) this.playerTeams.push(ColoredTeams.Purple); + if (numPlayerTeams >= 5) this.playerTeams.push(ColoredTeams.Yellow); + if (numPlayerTeams >= 6) this.playerTeams.push(ColoredTeams.Orange); + if (numPlayerTeams >= 7) this.playerTeams.push(ColoredTeams.Green); if (numPlayerTeams >= 8) throw new Error(`Too many teams: ${numPlayerTeams}`); } - this.addHumans(); } - private addHumans() { + private addPlayers() { if (this.config().gameConfig().gameMode != GameMode.Team) { this._humans.forEach((p) => this.addPlayer(p)); + this._nations.forEach((n) => this.addPlayer(n.playerInfo)); return; } - const playerToTeam = assignTeams(this._humans, this.playerTeams); + const isDuos = this.config().gameConfig().playerTeams === Duos; + const allPlayers = [ + ...this._humans, + ...this._nations.map((n) => n.playerInfo), + ]; + const playerToTeam = assignTeams(allPlayers, this.playerTeams); for (const [playerInfo, team] of playerToTeam.entries()) { if (team == "kicked") { console.warn(`Player ${playerInfo.name} was kicked from team`); @@ -180,7 +190,7 @@ export class GameImpl implements Game { return this.config().unitInfo(type); } nations(): Nation[] { - return this.nations_; + return this._nations; } createAllianceRequest(requestor: Player, recipient: Player): AllianceRequest { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index af24531d6..4da8695bf 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -20,6 +20,7 @@ import { Attack, BuildableUnit, Cell, + ColoredTeams, EmojiMessage, Gold, MessageType, @@ -606,7 +607,7 @@ export class PlayerImpl implements Player { if (this.team() == null || other.team() == null) { return false; } - if (this.team() == Team.Bot || other.team() == Team.Bot) { + if (this.team() == ColoredTeams.Bot || other.team() == ColoredTeams.Bot) { return false; } return this._team == other.team(); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 0eb003451..c655a1cf4 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -95,8 +95,8 @@ export class GameServer { if (gameConfig.gameMode != null) { this.gameConfig.gameMode = gameConfig.gameMode; } - if (gameConfig.numPlayerTeams != null) { - this.gameConfig.numPlayerTeams = gameConfig.numPlayerTeams; + if (gameConfig.playerTeams != null) { + this.gameConfig.playerTeams = gameConfig.playerTeams; } } diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index d828e4471..09f3dbbf5 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -1,133 +1,119 @@ -import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { Difficulty, GameMapType, GameMode, GameType } from "../core/game/Game"; +import { GameMapType, GameMode } from "../core/game/Game"; import { PseudoRandom } from "../core/PseudoRandom"; -import { GameConfig } from "../core/Schemas"; -import { logger } from "./Logger"; -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; +enum PlaylistType { + BigMaps, + SmallMaps, } +const random = new PseudoRandom(123); + export class MapPlaylist { - private mapsPlaylist: MapWithMode[] = []; + private gameModeRotation = [GameMode.FFA, GameMode.FFA, GameMode.Team]; + private currentGameModeIndex = 0; - public gameConfig(): GameConfig { - const { map, mode } = this.getNextMap(); + private mapsPlaylistBig: GameMapType[] = []; + private mapsPlaylistSmall: GameMapType[] = []; + private currentPlaylistCounter = 0; - 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, mode), - gameType: GameType.Public, - difficulty: Difficulty.Medium, - infiniteGold: false, - infiniteTroops: false, - instantBuild: false, - disableNPCs: mode == GameMode.Team, - disableNukes: false, - gameMode: mode, - numPlayerTeams: numPlayerTeams, - bots: 400, - } as GameConfig; + // Get the next map in rotation + public getNextMap(): GameMapType { + const playlistType: PlaylistType = this.getNextPlaylistType(); + const mapsPlaylist: GameMapType[] = this.getNextMapsPlayList(playlistType); + return mapsPlaylist.shift()!; } - 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()!; + 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); } - } - log.error("Failed to generate a valid map playlist"); + return this.mapsPlaylistBig; + + case PlaylistType.SmallMaps: + if (!(this.mapsPlaylistSmall.length > 0)) { + this.fillMapsPlaylist(playlistType, this.mapsPlaylistSmall); + } + return this.mapsPlaylistSmall; } - // Even if it failed, playlist will be partially populated. - return this.mapsPlaylist.shift()!; } - private shuffleMapsPlaylist(): boolean { - const maps: GameMapType[] = []; + private fillMapsPlaylist( + playlistType: PlaylistType, + mapsPlaylist: GameMapType[], + ): void { + const frequency = this.getFrequency(playlistType); Object.keys(GameMapType).forEach((key) => { - for (let i = 0; i < parseInt(frequency[key]); i++) { - maps.push(GameMapType[key]); + let count = parseInt(frequency[key]); + while (count > 0) { + mapsPlaylist.push(GameMapType[key]); + count--; } }); + do { + random.shuffleArray(mapsPlaylist); + } while (!this.allNonConsecutive(mapsPlaylist)); + } - const rand = new PseudoRandom(Date.now()); + // 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 ffa1: GameMapType[] = rand.shuffleArray([...maps]); - const ffa2: GameMapType[] = rand.shuffleArray([...maps]); - const ffa3: GameMapType[] = rand.shuffleArray([...maps]); - const team: GameMapType[] = rand.shuffleArray([...maps]); + 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, + }; + case PlaylistType.SmallMaps: + return { + World: 4, + EuropeClassic: 3, + Mena: 2, + Pangaea: 1, + Asia: 1, + Mars: 1, + BetweenTwoSeas: 2, + Japan: 2, + BlackSea: 1, + FaroeIslands: 2, + }; + } + } - 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, ffa3, GameMode.FFA)) { - return false; - } - if (!this.addNextMap(this.mapsPlaylist, team, GameMode.Team)) { + // 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]) { 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 2e04c5afe..d9e829bc2 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -5,7 +5,8 @@ import http from "http"; import path from "path"; import { fileURLToPath } from "url"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { GameInfo } from "../core/Schemas"; +import { Difficulty, GameMode, GameType } from "../core/game/Game"; +import { GameConfig, GameInfo } from "../core/Schemas"; import { generateID } from "../core/Util"; import { gatekeeper, LimiterType } from "./Gatekeeper"; import { logger } from "./Logger"; @@ -213,10 +214,40 @@ 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, gameMode), + 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 @@ -230,7 +261,7 @@ async function schedulePublicGame(playlist: MapPlaylist) { [config.adminHeader()]: config.adminToken(), }, body: JSON.stringify({ - gameConfig: playlist.gameConfig(), + gameConfig: defaultGameConfig, }), }, ); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 4c4c3ac4d..9f87fa923 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -164,7 +164,7 @@ export function startWorker() { disableNPCs: req.body.disableNPCs, disableNukes: req.body.disableNukes, gameMode: req.body.gameMode, - numPlayerTeams: req.body.numPlayerTeams, + playerTeams: req.body.playerTeams, }); res.status(200).json({ success: true }); }), diff --git a/tests/TeamAssignment.test.ts b/tests/TeamAssignment.test.ts index 340182e6f..331af56ef 100644 --- a/tests/TeamAssignment.test.ts +++ b/tests/TeamAssignment.test.ts @@ -1,7 +1,7 @@ -import { PlayerInfo, PlayerType, Team } from "../src/core/game/Game"; +import { ColoredTeams, PlayerInfo, PlayerType } from "../src/core/game/Game"; import { assignTeams } from "../src/core/game/TeamAssignment"; -const teams = [Team.Red, Team.Blue]; +const teams = [ColoredTeams.Red, ColoredTeams.Blue]; describe("assignTeams", () => { const createPlayer = (id: string, clan?: string): PlayerInfo => { @@ -27,10 +27,10 @@ describe("assignTeams", () => { const result = assignTeams(players, teams); // Check that players are assigned alternately - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Blue); - expect(result.get(players[2])).toEqual(Team.Red); - expect(result.get(players[3])).toEqual(Team.Blue); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Blue); + expect(result.get(players[2])).toEqual(ColoredTeams.Red); + expect(result.get(players[3])).toEqual(ColoredTeams.Blue); }); it("should keep clan members together on the same team", () => { @@ -44,10 +44,10 @@ describe("assignTeams", () => { const result = assignTeams(players, teams); // Check that clan members are on the same team - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Red); - expect(result.get(players[2])).toEqual(Team.Blue); - expect(result.get(players[3])).toEqual(Team.Blue); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Red); + expect(result.get(players[2])).toEqual(ColoredTeams.Blue); + expect(result.get(players[3])).toEqual(ColoredTeams.Blue); }); it("should handle mixed clan and non-clan players", () => { @@ -61,10 +61,10 @@ describe("assignTeams", () => { const result = assignTeams(players, teams); // Check that clan members are together and non-clan players balance teams - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Red); - expect(result.get(players[2])).toEqual(Team.Blue); - expect(result.get(players[3])).toEqual(Team.Blue); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Red); + expect(result.get(players[2])).toEqual(ColoredTeams.Blue); + expect(result.get(players[3])).toEqual(ColoredTeams.Blue); }); it("should kick players when teams are full", () => { @@ -80,14 +80,14 @@ describe("assignTeams", () => { const result = assignTeams(players, teams); // Check that players are kicked when teams are full - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Red); - expect(result.get(players[2])).toEqual(Team.Red); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Red); + expect(result.get(players[2])).toEqual(ColoredTeams.Red); expect(result.get(players[3])).toEqual("kicked"); - expect(result.get(players[4])).toEqual(Team.Blue); - expect(result.get(players[5])).toEqual(Team.Blue); + expect(result.get(players[4])).toEqual(ColoredTeams.Blue); + expect(result.get(players[5])).toEqual(ColoredTeams.Blue); }); it("should handle empty player list", () => { @@ -98,7 +98,7 @@ describe("assignTeams", () => { it("should handle single player", () => { const players = [createPlayer("1")]; const result = assignTeams(players, teams); - expect(result.get(players[0])).toEqual(Team.Red); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); }); it("should handle multiple clans with different sizes", () => { @@ -114,12 +114,12 @@ describe("assignTeams", () => { const result = assignTeams(players, teams); // Check that larger clans are assigned first - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Red); - expect(result.get(players[2])).toEqual(Team.Red); - expect(result.get(players[3])).toEqual(Team.Blue); - expect(result.get(players[4])).toEqual(Team.Blue); - expect(result.get(players[5])).toEqual(Team.Blue); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Red); + expect(result.get(players[2])).toEqual(ColoredTeams.Red); + expect(result.get(players[3])).toEqual(ColoredTeams.Blue); + expect(result.get(players[4])).toEqual(ColoredTeams.Blue); + expect(result.get(players[5])).toEqual(ColoredTeams.Blue); }); it("should distribute players among a larger number of teams", () => { @@ -141,28 +141,28 @@ describe("assignTeams", () => { ]; const result = assignTeams(players, [ - Team.Red, - Team.Blue, - Team.Teal, - Team.Purple, - Team.Yellow, - Team.Orange, - Team.Green, + ColoredTeams.Red, + ColoredTeams.Blue, + ColoredTeams.Teal, + ColoredTeams.Purple, + ColoredTeams.Yellow, + ColoredTeams.Orange, + ColoredTeams.Green, ]); - expect(result.get(players[0])).toEqual(Team.Red); - expect(result.get(players[1])).toEqual(Team.Red); + expect(result.get(players[0])).toEqual(ColoredTeams.Red); + expect(result.get(players[1])).toEqual(ColoredTeams.Red); expect(result.get(players[2])).toEqual("kicked"); - expect(result.get(players[3])).toEqual(Team.Blue); - expect(result.get(players[4])).toEqual(Team.Blue); - expect(result.get(players[5])).toEqual(Team.Teal); - expect(result.get(players[6])).toEqual(Team.Purple); - expect(result.get(players[7])).toEqual(Team.Yellow); - expect(result.get(players[8])).toEqual(Team.Orange); - expect(result.get(players[9])).toEqual(Team.Green); - expect(result.get(players[10])).toEqual(Team.Teal); - expect(result.get(players[11])).toEqual(Team.Purple); - expect(result.get(players[12])).toEqual(Team.Yellow); - expect(result.get(players[13])).toEqual(Team.Orange); + expect(result.get(players[3])).toEqual(ColoredTeams.Blue); + expect(result.get(players[4])).toEqual(ColoredTeams.Blue); + expect(result.get(players[5])).toEqual(ColoredTeams.Teal); + expect(result.get(players[6])).toEqual(ColoredTeams.Purple); + expect(result.get(players[7])).toEqual(ColoredTeams.Yellow); + expect(result.get(players[8])).toEqual(ColoredTeams.Orange); + expect(result.get(players[9])).toEqual(ColoredTeams.Green); + expect(result.get(players[10])).toEqual(ColoredTeams.Teal); + expect(result.get(players[11])).toEqual(ColoredTeams.Purple); + expect(result.get(players[12])).toEqual(ColoredTeams.Yellow); + expect(result.get(players[13])).toEqual(ColoredTeams.Orange); }); }); diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index 6e3c244bb..7f8b98a83 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -18,7 +18,6 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) { const miniGameMap = await genTerrainFromBin( String.fromCharCode.apply(null, miniMap), ); - const nationMap = { nations: [] }; // Configure the game const serverConfig = new TestServerConfig(); @@ -36,5 +35,5 @@ export async function setup(mapName: string, _gameConfig: GameConfig = {}) { const config = new TestConfig(serverConfig, gameConfig, new UserSettings()); // Create and return the game - return createGame([], gameMap, miniGameMap, nationMap, config); // TODO: !!! + return createGame([], [], gameMap, miniGameMap, config); }