From e586db6d8f4578e8eb2961b6cd8be65179b662be Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 01:11:31 +0100
Subject: [PATCH 1/8] Re-Enable HumansVsNations
---
resources/lang/en.json | 3 +-
src/client/HostLobbyModal.ts | 21 ++++-
src/client/PublicLobby.ts | 16 +++-
src/core/GameRunner.ts | 103 +++++++++++++++++++++---
src/core/configuration/DefaultConfig.ts | 3 +-
src/core/execution/NationExecution.ts | 10 ++-
src/core/game/Game.ts | 2 +-
src/server/MapPlaylist.ts | 4 +-
8 files changed, 140 insertions(+), 22 deletions(-)
diff --git a/resources/lang/en.json b/resources/lang/en.json
index e5fc91da52..e9f28b542c 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -271,7 +271,8 @@
"teams_Duos": "of 2 (Duos)",
"teams_Trios": "of 3 (Trios)",
"teams_Quads": "of 4 (Quads)",
- "teams_hvn": "Humans Vs Nations",
+ "teams_hvn": "Humans vs Nations",
+ "teams_hvn_detailed": "{num} Humans vs {num} Nations",
"teams": "{num} teams",
"players_per_team": "of {num}"
},
diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts
index 09d8400482..64bc7b1757 100644
--- a/src/client/HostLobbyModal.ts
+++ b/src/client/HostLobbyModal.ts
@@ -549,9 +549,9 @@ export class HostLobbyModal extends LitElement {
: translateText("host_modal.players")
}
•
- ${this.disableNations ? 0 : this.nationCount}
+ ${this.getEffectiveNationCount()}
${
- this.nationCount === 1
+ this.getEffectiveNationCount() === 1
? translateText("host_modal.nation_player")
: translateText("host_modal.nation_players")
}
@@ -562,7 +562,7 @@ export class HostLobbyModal extends LitElement {
.clients=${this.clients}
.lobbyCreatorClientID=${this.lobbyCreatorClientID}
.teamCount=${this.teamCount}
- .nationCount=${this.disableNations ? 0 : this.nationCount}
+ .nationCount=${this.getEffectiveNationCount()}
.onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)}
>
@@ -872,6 +872,21 @@ export class HostLobbyModal extends LitElement {
this.nationCount = 0;
}
}
+
+ /**
+ * Returns the effective nation count for display purposes.
+ * In HumansVsNations mode, this equals the number of human players.
+ * Otherwise, it uses the manifest nation count (or 0 if nations are disabled).
+ */
+ private getEffectiveNationCount(): number {
+ if (this.disableNations) {
+ return 0;
+ }
+ if (this.gameMode === GameMode.Team && this.teamCount === HumansVsNations) {
+ return this.clients.length;
+ }
+ return this.nationCount;
+ }
}
async function createLobby(creatorClientID: string): Promise {
diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts
index 2e79119cb7..31937b279f 100644
--- a/src/client/PublicLobby.ts
+++ b/src/client/PublicLobby.ts
@@ -136,6 +136,7 @@ export class PublicLobby extends LitElement {
lobby.gameConfig.gameMode,
teamCount,
teamTotal,
+ teamSize,
);
const teamDetailLabel = this.getTeamDetailLabel(
lobby.gameConfig.gameMode,
@@ -215,7 +216,7 @@ export class PublicLobby extends LitElement {
if (teamCount === Duos) return 2;
if (teamCount === Trios) return 3;
if (teamCount === Quads) return 4;
- if (teamCount === HumansVsNations) return Math.floor(maxPlayers / 2);
+ if (teamCount === HumansVsNations) return maxPlayers;
return undefined;
}
if (typeof teamCount === "number" && teamCount > 0) {
@@ -239,10 +240,13 @@ export class PublicLobby extends LitElement {
gameMode: GameMode,
teamCount: number | string | null,
teamTotal: number | undefined,
+ teamSize: number | undefined,
): string {
if (gameMode !== GameMode.Team) return translateText("game_mode.ffa");
- if (teamCount === HumansVsNations)
- return translateText("public_lobby.teams_hvn");
+ if (teamCount === HumansVsNations && teamSize !== undefined)
+ return translateText("public_lobby.teams_hvn_detailed", {
+ num: teamSize,
+ });
const totalTeams =
teamTotal ?? (typeof teamCount === "number" ? teamCount : 0);
return translateText("public_lobby.teams", { num: totalTeams });
@@ -256,7 +260,11 @@ export class PublicLobby extends LitElement {
): string | null {
if (gameMode !== GameMode.Team) return null;
- if (typeof teamCount === "string" && teamCount !== HumansVsNations) {
+ if (typeof teamCount === "string" && teamCount === HumansVsNations) {
+ return null;
+ }
+
+ if (typeof teamCount === "string") {
const teamKey = `public_lobby.teams_${teamCount}`;
const maybeTranslated = translateText(teamKey);
if (maybeTranslated !== teamKey) return maybeTranslated;
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index ed8c8cd7b2..cdc6066c5c 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -1,13 +1,20 @@
import { placeName } from "../client/graphics/NameBoxCalculator";
import { getConfig } from "./configuration/ConfigLoader";
import { Executor } from "./execution/ExecutionManager";
+import {
+ BOT_NAME_PREFIXES,
+ BOT_NAME_SUFFIXES,
+} from "./execution/utils/BotNames";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
Attack,
Cell,
Game,
+ GameMode,
+ GameType,
GameUpdates,
+ HumansVsNations,
NameViewData,
Nation,
Player,
@@ -26,7 +33,10 @@ import {
GameUpdateType,
GameUpdateViewData,
} from "./game/GameUpdates";
-import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
+import {
+ loadTerrainMap as loadGameMap,
+ Nation as ManifestNation,
+} from "./game/TerrainMapLoader";
import { PseudoRandom } from "./PseudoRandom";
import { ClientID, GameStartInfo, Turn } from "./Schemas";
import { simpleHash } from "./Util";
@@ -55,15 +65,12 @@ export async function createGameRunner(
);
});
- const nations = gameStart.config.disableNations
- ? []
- : gameMap.nations.map(
- (n) =>
- new Nation(
- new Cell(n.coordinates[0], n.coordinates[1]),
- new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
- ),
- );
+ const nations = createNationsForGame(
+ gameStart,
+ gameMap.nations,
+ humans.length,
+ random,
+ );
const game: Game = createGame(
humans,
@@ -82,6 +89,82 @@ export async function createGameRunner(
return gr;
}
+/**
+ * Creates the nations array for a game, handling HumansVsNations mode specially.
+ * In HumansVsNations mode, the number of nations matches the number of human players to ensure fair gameplay.
+ */
+function createNationsForGame(
+ gameStart: GameStartInfo,
+ manifestNations: ManifestNation[],
+ numHumans: number,
+ random: PseudoRandom,
+): Nation[] {
+ // If nations are disabled, return empty array
+ if (gameStart.config.disableNations) {
+ return [];
+ }
+
+ const toNation = (n: ManifestNation): Nation =>
+ new Nation(
+ new Cell(n.coordinates[0], n.coordinates[1]),
+ new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
+ );
+
+ const isHumansVsNations =
+ gameStart.config.gameMode === GameMode.Team &&
+ gameStart.config.playerTeams === HumansVsNations;
+
+ // For non-HumansVsNations modes, use all manifest nations as before
+ if (!isHumansVsNations) {
+ return manifestNations.map(toNation);
+ }
+
+ // HumansVsNations mode: balance nation count to match human count
+ const isSingleplayer = gameStart.config.gameType === GameType.Singleplayer;
+ const targetNationCount = isSingleplayer ? 1 : numHumans;
+
+ if (targetNationCount === 0) {
+ return [];
+ }
+
+ // If we have enough manifest nations, use a subset
+ if (manifestNations.length >= targetNationCount) {
+ // Shuffle manifest nations to add variety
+ const shuffled = [...manifestNations];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ const j = random.nextInt(0, i + 1);
+ const temp = shuffled[i];
+ shuffled[i] = shuffled[j];
+ shuffled[j] = temp;
+ }
+ return shuffled.slice(0, targetNationCount).map(toNation);
+ }
+
+ // If we need more nations than defined in manifest, create additional ones
+ const nations: Nation[] = manifestNations.map(toNation);
+ const additionalCount = targetNationCount - manifestNations.length;
+ for (let i = 0; i < additionalCount; i++) {
+ const name = generateNationName(random);
+ nations.push(
+ new Nation(
+ undefined,
+ new PlayerInfo(name, PlayerType.Nation, null, random.nextID()),
+ ),
+ );
+ }
+
+ return nations;
+}
+
+/**
+ * Generates a nation name using the bot name prefixes and suffixes.
+ */
+function generateNationName(random: PseudoRandom): string {
+ const prefixIndex = random.nextInt(0, BOT_NAME_PREFIXES.length);
+ const suffixIndex = random.nextInt(0, BOT_NAME_SUFFIXES.length);
+ return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
+}
+
export class GameRunner {
private turns: Turn[] = [];
private currTurn = 0;
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index 85d1ec4664..18a4f1897e 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -207,7 +207,8 @@ export abstract class DefaultServerConfig implements ServerConfig {
p -= p % 4;
break;
case HumansVsNations:
- // For HumansVsNations, return the base team player count
+ // Half the slots are for humans, the other half will get filled with nations
+ p = Math.floor(p / 2);
break;
default:
p -= p % numPlayerTeams;
diff --git a/src/core/execution/NationExecution.ts b/src/core/execution/NationExecution.ts
index 53a39ffb95..50fbec07a8 100644
--- a/src/core/execution/NationExecution.ts
+++ b/src/core/execution/NationExecution.ts
@@ -119,7 +119,13 @@ export class NationExecution implements Execution {
}
if (this.mg.inSpawnPhase()) {
- // select a tile near the position defined in the map manifest for the current nation
+ // Place nations without a spawn cell (Dynamically created for HumansVsNations) randomly by SpawnExecution
+ if (this.nation.spawnCell === undefined) {
+ new SpawnExecution(this.gameID, this.nation.playerInfo);
+ return;
+ }
+
+ // Select a tile near the position defined in the map manifest
const rl = this.randomSpawnLand();
if (rl === null) {
@@ -250,6 +256,8 @@ export class NationExecution implements Execution {
}
private randomSpawnLand(): TileRef | null {
+ if (this.nation.spawnCell === undefined) throw new Error("not initialized");
+
const delta = 25;
let tries = 0;
while (tries < 50) {
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 9c5ef95ffd..418fee505f 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -310,7 +310,7 @@ export enum Relation {
export class Nation {
constructor(
- public readonly spawnCell: Cell,
+ public readonly spawnCell: Cell | undefined,
public readonly playerInfo: PlayerInfo,
) {}
}
diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts
index 5cacf3440a..dcf4b5564c 100644
--- a/src/server/MapPlaylist.ts
+++ b/src/server/MapPlaylist.ts
@@ -73,6 +73,7 @@ const TEAM_COUNTS = [
Duos,
Trios,
Quads,
+ HumansVsNations,
] as const satisfies TeamCountConfig[];
export class MapPlaylist {
@@ -94,7 +95,8 @@ export class MapPlaylist {
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams),
gameType: GameType.Public,
gameMapSize: GameMapSize.Normal,
- difficulty: Difficulty.Easy,
+ difficulty:
+ playerTeams === HumansVsNations ? Difficulty.Hard : Difficulty.Easy,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
From 6cf256ef93fde61f824331991824ffb1bf9ac3b0 Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 01:56:23 +0100
Subject: [PATCH 2/8] fix comment
---
src/core/GameRunner.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index cdc6066c5c..7dd1d13fe6 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -114,7 +114,7 @@ function createNationsForGame(
gameStart.config.gameMode === GameMode.Team &&
gameStart.config.playerTeams === HumansVsNations;
- // For non-HumansVsNations modes, use all manifest nations as before
+ // For non-HumansVsNations modes, simply use the manifest nations
if (!isHumansVsNations) {
return manifestNations.map(toNation);
}
From a60784c81a623ff54466675f4bbe9325ea6cb90b Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 02:02:58 +0100
Subject: [PATCH 3/8] Fix: Ensure nations without a spawn cell are added to
execution correctly (Lol it somehow worked without that)
---
src/core/execution/NationExecution.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/core/execution/NationExecution.ts b/src/core/execution/NationExecution.ts
index 50fbec07a8..d2a32f74a1 100644
--- a/src/core/execution/NationExecution.ts
+++ b/src/core/execution/NationExecution.ts
@@ -121,7 +121,9 @@ export class NationExecution implements Execution {
if (this.mg.inSpawnPhase()) {
// Place nations without a spawn cell (Dynamically created for HumansVsNations) randomly by SpawnExecution
if (this.nation.spawnCell === undefined) {
- new SpawnExecution(this.gameID, this.nation.playerInfo);
+ this.mg.addExecution(
+ new SpawnExecution(this.gameID, this.nation.playerInfo),
+ );
return;
}
From c933fbe8f3a00222049c3a2ec1bd48c7b5f248f7 Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 16:02:29 +0100
Subject: [PATCH 4/8] Added NationNames.ts
---
src/core/GameRunner.ts | 14 +-
src/core/execution/utils/NationNames.ts | 220 ++++++++++++++++++++++++
2 files changed, 221 insertions(+), 13 deletions(-)
create mode 100644 src/core/execution/utils/NationNames.ts
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 7dd1d13fe6..58bf7da436 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -1,10 +1,7 @@
import { placeName } from "../client/graphics/NameBoxCalculator";
import { getConfig } from "./configuration/ConfigLoader";
import { Executor } from "./execution/ExecutionManager";
-import {
- BOT_NAME_PREFIXES,
- BOT_NAME_SUFFIXES,
-} from "./execution/utils/BotNames";
+import { generateNationName } from "./execution/utils/NationNames";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
@@ -156,15 +153,6 @@ function createNationsForGame(
return nations;
}
-/**
- * Generates a nation name using the bot name prefixes and suffixes.
- */
-function generateNationName(random: PseudoRandom): string {
- const prefixIndex = random.nextInt(0, BOT_NAME_PREFIXES.length);
- const suffixIndex = random.nextInt(0, BOT_NAME_SUFFIXES.length);
- return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
-}
-
export class GameRunner {
private turns: Turn[] = [];
private currTurn = 0;
diff --git a/src/core/execution/utils/NationNames.ts b/src/core/execution/utils/NationNames.ts
new file mode 100644
index 0000000000..909d3d7f95
--- /dev/null
+++ b/src/core/execution/utils/NationNames.ts
@@ -0,0 +1,220 @@
+import { PseudoRandom } from "../../PseudoRandom";
+
+const PLURAL_NOUN = Symbol("plural!");
+const NOUN = Symbol("noun!");
+
+type NameTemplate = (string | typeof PLURAL_NOUN | typeof NOUN)[];
+
+const NAME_TEMPLATES: NameTemplate[] = [
+ ["World Famous", NOUN],
+ ["Famous", PLURAL_NOUN],
+ ["Comically Large", NOUN],
+ ["Comically Small", NOUN],
+ ["Massive", PLURAL_NOUN],
+ ["Friendly", NOUN],
+ ["Evil", NOUN],
+ ["Malicious", NOUN],
+ ["Spiteful", NOUN],
+ ["Suspicious", NOUN],
+ ["Canonically Evil", NOUN],
+ ["Limited Edition", NOUN],
+ ["Patent Pending", NOUN],
+ ["Patented", NOUN],
+ ["Space", NOUN],
+ ["Defend The", PLURAL_NOUN],
+ ["Anarchist", NOUN],
+ ["Republic of", PLURAL_NOUN],
+ ["Slippery", NOUN],
+ ["Wealthy", PLURAL_NOUN],
+ ["Certified", NOUN],
+ ["Dr", NOUN],
+ ["Runaway", NOUN],
+ ["Chrome", NOUN],
+ ["All New", NOUN],
+ ["Top Shelf", PLURAL_NOUN],
+ ["Invading", PLURAL_NOUN],
+ ["Loyal To", PLURAL_NOUN],
+ ["United States of", NOUN],
+ ["United States of", PLURAL_NOUN],
+ ["Flowing Rivers of", NOUN],
+ ["House of", PLURAL_NOUN],
+ ["Certified Organic", NOUN],
+ ["Unregulated", NOUN],
+
+ [NOUN, "For Hire"],
+ [PLURAL_NOUN, "That Bite"],
+ [PLURAL_NOUN, "Are Opps"],
+ [NOUN, "Hotel"],
+ [PLURAL_NOUN, "The Movie"],
+ [NOUN, "Corporation"],
+ [PLURAL_NOUN, "Inc"],
+ [NOUN, "Democracy"],
+ [NOUN, "Network"],
+ [NOUN, "Railway"],
+ [NOUN, "Congress"],
+ [NOUN, "Alliance"],
+ [NOUN, "Island"],
+ [NOUN, "Kingdom"],
+ [NOUN, "Empire"],
+ [NOUN, "Dynasty"],
+ [NOUN, "Cartel"],
+ [NOUN, "Cabal"],
+ [NOUN, "Land"],
+ [NOUN, "Oligarchy"],
+ [NOUN, "Nationalist"],
+ [NOUN, "State"],
+ [NOUN, "Duchy"],
+ [NOUN, "Ocean"],
+
+ ["Alternate", NOUN, "Universe"],
+ ["Famous", NOUN, "Collection"],
+ ["Supersonic", NOUN, "Spaceship"],
+ ["Secret", NOUN, "Agenda"],
+ ["Ballistic", NOUN, "Missile"],
+ ["The", PLURAL_NOUN, "are SPIES"],
+ ["Traveling", NOUN, "Circus"],
+ ["The", PLURAL_NOUN, "Lied"],
+ ["Sacred", NOUN, "Knowledge"],
+ ["Quantum", NOUN, "Computer"],
+ ["Hadron", NOUN, "Collider"],
+ ["Large", NOUN, "Obliterator"],
+ ["Interstellar", NOUN, "Cabal"],
+ ["Interstellar", NOUN, "Army"],
+ ["Interstellar", NOUN, "Pirates"],
+ ["Interstellar", NOUN, "Dynasty"],
+ ["Interstellar", NOUN, "Clan"],
+ ["Galactic", NOUN, "Smugglers"],
+ ["Galactic", NOUN, "Cabal"],
+ ["Galactic", NOUN, "Alliance"],
+ ["Galactic", NOUN, "Empire"],
+ ["Galactic", NOUN, "Army"],
+ ["Galactic", NOUN, "Crown"],
+ ["Galactic", NOUN, "Pirates"],
+ ["Galactic", NOUN, "Dynasty"],
+ ["Galactic", NOUN, "Clan"],
+ ["Alien", NOUN, "Army"],
+ ["Alien", NOUN, "Cabal"],
+ ["Alien", NOUN, "Alliance"],
+ ["Alien", NOUN, "Empire"],
+ ["Alien", NOUN, "Pirates"],
+ ["Alien", NOUN, "Clan"],
+ ["Grand", NOUN, "Empire"],
+ ["Grand", NOUN, "Dynasty"],
+ ["Grand", NOUN, "Army"],
+ ["Grand", NOUN, "Cabal"],
+ ["Grand", NOUN, "Alliance"],
+ ["Royal", NOUN, "Army"],
+ ["Royal", NOUN, "Cabal"],
+ ["Royal", NOUN, "Empire"],
+ ["Royal", NOUN, "Dynasty"],
+ ["Holy", NOUN, "Dynasty"],
+ ["Holy", NOUN, "Empire"],
+ ["Holy", NOUN, "Alliance"],
+ ["Eternal", NOUN, "Empire"],
+ ["Eternal", NOUN, "Cabal"],
+ ["Eternal", NOUN, "Alliance"],
+ ["Eternal", NOUN, "Dynasty"],
+ ["Invading", NOUN, "Cabal"],
+ ["Invading", NOUN, "Empire"],
+ ["Invading", NOUN, "Alliance"],
+ ["Immortal", NOUN, "Pirates"],
+ ["Shadow", NOUN, "Cabal"],
+ ["Secret", NOUN, "Dynasty"],
+ ["The Great", NOUN, "Army"],
+ ["The", NOUN, "Matrix"],
+];
+
+const NOUNS = [
+ "Snail",
+ "Cow",
+ "Giraffe",
+ "Donkey",
+ "Horse",
+ "Mushroom",
+ "Salad",
+ "Kitten",
+ "Fork",
+ "Apple",
+ "Pancake",
+ "Tree",
+ "Fern",
+ "Seashell",
+ "Turtle",
+ "Casserole",
+ "Gnome",
+ "Frog",
+ "Cheese",
+ "Mold",
+ "Clown",
+ "Boat",
+ "Robot",
+ "Millionaire",
+ "Billionaire",
+ "Pigeon",
+ "Fish",
+ "Bumblebee",
+ "Jelly",
+ "Wizard",
+ "Worm",
+ "Rat",
+ "Pumpkin",
+ "Zombie",
+ "Grass",
+ "Bear",
+ "Skunk",
+ "Sandwich",
+ "Butter",
+ "Soda",
+ "Pickle",
+ "Potato",
+ "Book",
+ "Friend",
+ "Feather",
+ "Flower",
+ "Oil",
+ "Train",
+ "Fan",
+ "Salmon",
+ "Cod",
+ "Sink",
+ "Villain",
+ "Bug",
+ "Car",
+ "Soup",
+ "Puppy",
+ "Rock",
+ "Stick",
+ "Succulent",
+ "Nerd",
+ "Mercenary",
+ "Ninja",
+ "Burger",
+ "Tomato",
+ "Penguin",
+];
+
+function pluralize(noun: string): string {
+ if (noun.endsWith("s")) {
+ return `${noun}es`;
+ }
+ return `${noun}s`;
+}
+
+export function generateNationName(random: PseudoRandom): string {
+ const template = NAME_TEMPLATES[random.nextInt(0, NAME_TEMPLATES.length)];
+ const noun = NOUNS[random.nextInt(0, NOUNS.length)];
+
+ const result: string[] = [];
+
+ for (const part of template) {
+ if (part === PLURAL_NOUN) {
+ result.push(pluralize(noun));
+ } else if (part === NOUN) {
+ result.push(noun);
+ } else {
+ result.push(part);
+ }
+ }
+
+ return result.join(" ");
+}
From 1d9c8f00719f25d14dee10c0241bd69843a43cba Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 16:10:32 +0100
Subject: [PATCH 5/8] Fix: Enhance pluralization logic for nouns in NationNames
utility
---
src/core/execution/utils/NationNames.ts | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/core/execution/utils/NationNames.ts b/src/core/execution/utils/NationNames.ts
index 909d3d7f95..dd215f8c1d 100644
--- a/src/core/execution/utils/NationNames.ts
+++ b/src/core/execution/utils/NationNames.ts
@@ -194,7 +194,19 @@ const NOUNS = [
];
function pluralize(noun: string): string {
- if (noun.endsWith("s")) {
+ if (
+ noun.endsWith("s") ||
+ noun.endsWith("ch") ||
+ noun.endsWith("sh") ||
+ noun.endsWith("x") ||
+ noun.endsWith("z")
+ ) {
+ return `${noun}es`;
+ }
+ if (noun.endsWith("y") && !"aeiou".includes(noun[noun.length - 2])) {
+ return `${noun.slice(0, -1)}ies`;
+ }
+ if (noun.endsWith("o")) {
return `${noun}es`;
}
return `${noun}s`;
From eb14bc354a03f9de29c00cf130ce99c1e2a9f720 Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Thu, 25 Dec 2025 16:17:08 +0100
Subject: [PATCH 6/8] Fix: Add irregular pluralization for nouns ending in "o"
in the pluralize function
---
src/core/execution/utils/NationNames.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/core/execution/utils/NationNames.ts b/src/core/execution/utils/NationNames.ts
index dd215f8c1d..79e682a50e 100644
--- a/src/core/execution/utils/NationNames.ts
+++ b/src/core/execution/utils/NationNames.ts
@@ -193,6 +193,9 @@ const NOUNS = [
"Penguin",
];
+// Words from NOUNS that need irregular "-oes" plural
+const O_TO_OES = new Set(["Potato", "Tomato"]);
+
function pluralize(noun: string): string {
if (
noun.endsWith("s") ||
@@ -206,7 +209,7 @@ function pluralize(noun: string): string {
if (noun.endsWith("y") && !"aeiou".includes(noun[noun.length - 2])) {
return `${noun.slice(0, -1)}ies`;
}
- if (noun.endsWith("o")) {
+ if (O_TO_OES.has(noun)) {
return `${noun}es`;
}
return `${noun}s`;
From 7de478b93169d27fc8163adbec6d83dd0dc2f253 Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Mon, 29 Dec 2025 01:24:47 +0100
Subject: [PATCH 7/8] Refactor: Move nation creation logic to NationUtils and
remove unused NationNames utility
---
src/core/GameRunner.ts | 78 +------------------
.../NationNames.ts => game/NationUtils.ts} | 75 +++++++++++++++++-
2 files changed, 75 insertions(+), 78 deletions(-)
rename src/core/{execution/utils/NationNames.ts => game/NationUtils.ts} (69%)
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 5e511f855b..d434ba49e3 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -1,19 +1,14 @@
import { placeName } from "../client/graphics/NameBoxCalculator";
import { getConfig } from "./configuration/ConfigLoader";
import { Executor } from "./execution/ExecutionManager";
-import { generateNationName } from "./execution/utils/NationNames";
import { WinCheckExecution } from "./execution/WinCheckExecution";
import {
AllPlayers,
Attack,
Cell,
Game,
- GameMode,
- GameType,
GameUpdates,
- HumansVsNations,
NameViewData,
- Nation,
Player,
PlayerActions,
PlayerBorderTiles,
@@ -30,10 +25,8 @@ import {
GameUpdateType,
GameUpdateViewData,
} from "./game/GameUpdates";
-import {
- loadTerrainMap as loadGameMap,
- Nation as ManifestNation,
-} from "./game/TerrainMapLoader";
+import { createNationsForGame } from "./game/NationUtils";
+import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
import { PseudoRandom } from "./PseudoRandom";
import { ClientID, GameStartInfo, Turn } from "./Schemas";
import { simpleHash } from "./Util";
@@ -87,73 +80,6 @@ export async function createGameRunner(
return gr;
}
-/**
- * Creates the nations array for a game, handling HumansVsNations mode specially.
- * In HumansVsNations mode, the number of nations matches the number of human players to ensure fair gameplay.
- */
-function createNationsForGame(
- gameStart: GameStartInfo,
- manifestNations: ManifestNation[],
- numHumans: number,
- random: PseudoRandom,
-): Nation[] {
- // If nations are disabled, return empty array
- if (gameStart.config.disableNations) {
- return [];
- }
-
- const toNation = (n: ManifestNation): Nation =>
- new Nation(
- new Cell(n.coordinates[0], n.coordinates[1]),
- new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
- );
-
- const isHumansVsNations =
- gameStart.config.gameMode === GameMode.Team &&
- gameStart.config.playerTeams === HumansVsNations;
-
- // For non-HumansVsNations modes, simply use the manifest nations
- if (!isHumansVsNations) {
- return manifestNations.map(toNation);
- }
-
- // HumansVsNations mode: balance nation count to match human count
- const isSingleplayer = gameStart.config.gameType === GameType.Singleplayer;
- const targetNationCount = isSingleplayer ? 1 : numHumans;
-
- if (targetNationCount === 0) {
- return [];
- }
-
- // If we have enough manifest nations, use a subset
- if (manifestNations.length >= targetNationCount) {
- // Shuffle manifest nations to add variety
- const shuffled = [...manifestNations];
- for (let i = shuffled.length - 1; i > 0; i--) {
- const j = random.nextInt(0, i + 1);
- const temp = shuffled[i];
- shuffled[i] = shuffled[j];
- shuffled[j] = temp;
- }
- return shuffled.slice(0, targetNationCount).map(toNation);
- }
-
- // If we need more nations than defined in manifest, create additional ones
- const nations: Nation[] = manifestNations.map(toNation);
- const additionalCount = targetNationCount - manifestNations.length;
- for (let i = 0; i < additionalCount; i++) {
- const name = generateNationName(random);
- nations.push(
- new Nation(
- undefined,
- new PlayerInfo(name, PlayerType.Nation, null, random.nextID()),
- ),
- );
- }
-
- return nations;
-}
-
export class GameRunner {
private turns: Turn[] = [];
private currTurn = 0;
diff --git a/src/core/execution/utils/NationNames.ts b/src/core/game/NationUtils.ts
similarity index 69%
rename from src/core/execution/utils/NationNames.ts
rename to src/core/game/NationUtils.ts
index 79e682a50e..baea6ef114 100644
--- a/src/core/execution/utils/NationNames.ts
+++ b/src/core/game/NationUtils.ts
@@ -1,4 +1,75 @@
-import { PseudoRandom } from "../../PseudoRandom";
+import { PseudoRandom } from "../PseudoRandom";
+import { GameStartInfo } from "../Schemas";
+import {
+ Cell,
+ GameMode,
+ GameType,
+ HumansVsNations,
+ Nation,
+ PlayerInfo,
+ PlayerType,
+} from "./Game";
+import { Nation as ManifestNation } from "./TerrainMapLoader";
+
+/**
+ * Creates the nations array for a game, handling HumansVsNations mode specially.
+ * In HumansVsNations mode, the number of nations matches the number of human players to ensure fair gameplay.
+ */
+export function createNationsForGame(
+ gameStart: GameStartInfo,
+ manifestNations: ManifestNation[],
+ numHumans: number,
+ random: PseudoRandom,
+): Nation[] {
+ if (gameStart.config.disableNations) {
+ return [];
+ }
+
+ const toNation = (n: ManifestNation): Nation =>
+ new Nation(
+ new Cell(n.coordinates[0], n.coordinates[1]),
+ new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
+ );
+
+ const isHumansVsNations =
+ gameStart.config.gameMode === GameMode.Team &&
+ gameStart.config.playerTeams === HumansVsNations;
+
+ // For non-HumansVsNations modes, simply use the manifest nations
+ if (!isHumansVsNations) {
+ return manifestNations.map(toNation);
+ }
+
+ // HumansVsNations mode: balance nation count to match human count
+ const isSingleplayer = gameStart.config.gameType === GameType.Singleplayer;
+ const targetNationCount = isSingleplayer ? 1 : numHumans;
+
+ if (targetNationCount === 0) {
+ return [];
+ }
+
+ // If we have enough manifest nations, use a subset
+ if (manifestNations.length >= targetNationCount) {
+ // Shuffle manifest nations to add variety
+ const shuffled = random.shuffleArray(manifestNations);
+ return shuffled.slice(0, targetNationCount).map(toNation);
+ }
+
+ // If we need more nations than defined in manifest, create additional ones
+ const nations: Nation[] = manifestNations.map(toNation);
+ const additionalCount = targetNationCount - manifestNations.length;
+ for (let i = 0; i < additionalCount; i++) {
+ const name = generateNationName(random);
+ nations.push(
+ new Nation(
+ undefined,
+ new PlayerInfo(name, PlayerType.Nation, null, random.nextID()),
+ ),
+ );
+ }
+
+ return nations;
+}
const PLURAL_NOUN = Symbol("plural!");
const NOUN = Symbol("noun!");
@@ -215,7 +286,7 @@ function pluralize(noun: string): string {
return `${noun}s`;
}
-export function generateNationName(random: PseudoRandom): string {
+function generateNationName(random: PseudoRandom): string {
const template = NAME_TEMPLATES[random.nextInt(0, NAME_TEMPLATES.length)];
const noun = NOUNS[random.nextInt(0, NOUNS.length)];
From bf32e9a7f291b4f5ceecc4ccd482afa2698d3b5f Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Mon, 29 Dec 2025 01:26:20 +0100
Subject: [PATCH 8/8] Refactor: Move generateNationName function to improve
code organization and readability
---
src/core/game/NationUtils.ts | 38 ++++++++++++++++++------------------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/src/core/game/NationUtils.ts b/src/core/game/NationUtils.ts
index baea6ef114..076ccfd72b 100644
--- a/src/core/game/NationUtils.ts
+++ b/src/core/game/NationUtils.ts
@@ -264,6 +264,25 @@ const NOUNS = [
"Penguin",
];
+function generateNationName(random: PseudoRandom): string {
+ const template = NAME_TEMPLATES[random.nextInt(0, NAME_TEMPLATES.length)];
+ const noun = NOUNS[random.nextInt(0, NOUNS.length)];
+
+ const result: string[] = [];
+
+ for (const part of template) {
+ if (part === PLURAL_NOUN) {
+ result.push(pluralize(noun));
+ } else if (part === NOUN) {
+ result.push(noun);
+ } else {
+ result.push(part);
+ }
+ }
+
+ return result.join(" ");
+}
+
// Words from NOUNS that need irregular "-oes" plural
const O_TO_OES = new Set(["Potato", "Tomato"]);
@@ -285,22 +304,3 @@ function pluralize(noun: string): string {
}
return `${noun}s`;
}
-
-function generateNationName(random: PseudoRandom): string {
- const template = NAME_TEMPLATES[random.nextInt(0, NAME_TEMPLATES.length)];
- const noun = NOUNS[random.nextInt(0, NOUNS.length)];
-
- const result: string[] = [];
-
- for (const part of template) {
- if (part === PLURAL_NOUN) {
- result.push(pluralize(noun));
- } else if (part === NOUN) {
- result.push(noun);
- } else {
- result.push(part);
- }
- }
-
- return result.join(" ");
-}