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(" "); -}