- ${[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);
}