From 990eba61340f0b30dfa5f0611b4de8fe6eb08fee Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Tue, 12 May 2026 04:27:02 +0200 Subject: [PATCH] =?UTF-8?q?Improve=20MapPlaylist=20=F0=9F=8E=B2=20(#3904)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: ### 1. `SPECIAL_MODIFIER_POOL` rebalanced Ticket weights adjusted to roughly track the community "favorite modifier" poll Screenshot 2026-05-11 210740 - `isRandomSpawn`: 2 to 4 - `goldMultiplier`: 4 to 6 - `isWaterNukes`: 3 to 4 - `startingGold25M`: 1 to 3 - `startingGold5M`: 5 to 4 - `startingGold1M`: 3 to 2 ### 2. New `SPECIAL_TEAM_MAPS` config Replaces the hardcoded per-map branches in `getTeamCount` and `buildMapsList`. Each entry maps a `GameMapType` to its preferred `TeamCountConfig`. Shared constants: - `SPECIAL_TEAM_FORCE_CHANCE = 0.75` (probability of overriding the random team weights roll) - `SPECIAL_TEAM_FREQ_MULTIPLIER = 2` (frequency boost in the team playlist) Current entries: Baikal (2), FourIslands (4), Luna (2). Behavior preserved for the existing maps, but adding another special team map is now a one-line entry. ### 3. New `FULL_LAND_MAPS` config (TheBox, Alps) - Water nukes forced on 75% of the time in the special rotation (overrides `WATER_NUKES_BOOSTED_MAPS`, which still applies its 50% boost to FourIslands, Baikal, Luna, ArchipelagoSea). Because they make a lot of fun on these two maps. - The `isPortsDisabled` modifier is excluded unless water nukes is boosted on, since ports are pointless on full-land maps. Because this happened: image ### 4. Misc - Renamed `frequency` constant to `FREQUENCY` for consistency with other module-level constants. ### 5. Exclude `isNukesDisabled` on special team maps in team mode On `SPECIAL_TEAM_MAPS` (FourIslands, Baikal, Luna) in team mode, the `isNukesDisabled` modifier is now excluded from the pool. Otherwise an extreme warship spam will follow. ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- src/server/MapPlaylist.ts | 81 +++++++++++++++++++++++++----------- tests/MapConsistency.test.ts | 2 +- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index dabd79740..2128151ab 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -21,6 +21,7 @@ import { logger } from "./Logger"; import { getMapLandTiles } from "./MapLandTiles"; const log = logger.child({}); + const ARCADE_MAPS = new Set(mapCategories.arcade); const SPECIAL_ONLY_MAPS = new Set([GameMapType.ArchipelagoSea]); @@ -29,7 +30,7 @@ const MAX_PLAYER_COUNT = 125; // How many times each map should appear in the playlist. // Note: The Partial should eventually be removed for better type safety. -const frequency: Partial> = { +const FREQUENCY: Partial> = { Achiran: 5, Aegean: 6, Africa: 7, @@ -114,6 +115,17 @@ const TEAM_WEIGHTS: { config: TeamCountConfig; weight: number }[] = [ { config: HumansVsNations, weight: 20 }, ]; +// Maps with a preferred team count in team / special games. +// For these maps: team-playlist frequency is doubled, and the preferred +// team count overrides the random TEAM_WEIGHTS roll with SPECIAL_TEAM_FORCE_CHANCE. +const SPECIAL_TEAM_FORCE_CHANCE = 0.75; +const SPECIAL_TEAM_FREQ_MULTIPLIER = 2; +const SPECIAL_TEAM_MAPS: ReadonlyMap = new Map([ + [GameMapType.Baikal, 2], + [GameMapType.FourIslands, 4], + [GameMapType.Luna, 2], +]); + type ModifierKey = | "isRandomSpawn" | "isCompact" @@ -131,21 +143,22 @@ type ModifierKey = | "isWaterNukes"; // Each entry represents one "ticket" in the pool. More tickets = higher chance of selection. +// Weights are roughly informed by the community "favorite modifier" poll. const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ - ...Array(2).fill("isRandomSpawn"), + ...Array(4).fill("isRandomSpawn"), ...Array(4).fill("isCompact"), ...Array(2).fill("isCrowded"), ...Array(1).fill("isHardNations"), - ...Array(3).fill("startingGold1M"), - ...Array(5).fill("startingGold5M"), - ...Array(1).fill("startingGold25M"), - ...Array(4).fill("goldMultiplier"), + ...Array(2).fill("startingGold1M"), + ...Array(4).fill("startingGold5M"), + ...Array(3).fill("startingGold25M"), + ...Array(6).fill("goldMultiplier"), ...Array(1).fill("isAlliancesDisabled"), ...Array(1).fill("isPortsDisabled"), ...Array(1).fill("isNukesDisabled"), ...Array(1).fill("isSAMsDisabled"), ...Array(1).fill("isPeaceTime"), - ...Array(3).fill("isWaterNukes"), + ...Array(4).fill("isWaterNukes"), ]; // Maps where water nukes have a higher chance on top of the normal pool @@ -153,12 +166,18 @@ const SPECIAL_MODIFIER_POOL: ModifierKey[] = [ const WATER_NUKES_BOOSTED_MAPS: ReadonlySet = new Set([ GameMapType.FourIslands, GameMapType.Baikal, - GameMapType.Alps, - GameMapType.TheBox, GameMapType.Luna, GameMapType.ArchipelagoSea, ]); +// Maps that are entirely land. +// - Water nukes forced on 75% of the time (overrides WATER_NUKES_BOOSTED_MAPS) +// - The "ports disabled" modifier is only allowed when water nukes is on +const FULL_LAND_MAPS: ReadonlySet = new Set([ + GameMapType.TheBox, + GameMapType.Alps, +]); + // Modifiers that cannot be active at the same time. const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [ ["startingGold5M", "startingGold25M"], @@ -260,6 +279,13 @@ export class MapPlaylist { if (mode === GameMode.Team) { excludedModifiers.push("isHardNations"); } + + // On special team maps nukes-disabled makes cross-water attacks + // nearly impossible (extreme warship spam). + if (mode === GameMode.Team && SPECIAL_TEAM_MAPS.has(map)) { + excludedModifiers.push("isNukesDisabled"); + } + if (playerTeams === HumansVsNations) { excludedModifiers.push("startingGold25M"); // Nations are disabled if that modifier is active (Because of PVP immunity) excludedModifiers.push("isPeaceTime"); // Nations don't have PVP immunity @@ -267,12 +293,21 @@ export class MapPlaylist { // Boost water nukes chance // When boosted, water nukes is forced on and takes one modifier slot. - const boostWaterNukes = - WATER_NUKES_BOOSTED_MAPS.has(map) && Math.random() < 0.5; + const waterNukesBoostChance = FULL_LAND_MAPS.has(map) + ? 0.75 + : WATER_NUKES_BOOSTED_MAPS.has(map) + ? 0.5 + : 0; + const boostWaterNukes = Math.random() < waterNukesBoostChance; if (boostWaterNukes) { excludedModifiers.push("isWaterNukes", "isNukesDisabled"); } + // On full-land maps, ports-disabled is only allowed alongside water nukes + if (FULL_LAND_MAPS.has(map) && !boostWaterNukes) { + excludedModifiers.push("isPortsDisabled"); + } + const poolResult = this.getRandomSpecialGameModifiers( excludedModifiers, undefined, @@ -519,10 +554,10 @@ export class MapPlaylist { ) { return; } - let freq = frequency[key] ?? 0; - // Double frequency for Baikal and FourIslands in team games - if (type === "team" && (key === "Baikal" || key === "FourIslands")) { - freq *= 2; + let freq = FREQUENCY[key] ?? 0; + // Boost frequency for special team maps in the team playlist + if (type === "team" && SPECIAL_TEAM_MAPS.has(map)) { + freq *= SPECIAL_TEAM_FREQ_MULTIPLIER; } for (let i = 0; i < freq; i++) { maps.push(map); @@ -532,15 +567,13 @@ export class MapPlaylist { } private getTeamCount(map: GameMapType): TeamCountConfig { - // Override team count for specific maps (75% chance) - if (map === GameMapType.Baikal && Math.random() < 0.75) { - return 2; - } - if (map === GameMapType.FourIslands && Math.random() < 0.75) { - return 4; - } - if (map === GameMapType.Luna && Math.random() < 0.75) { - return 2; + // Override team count for specific maps + const forcedTeamCount = SPECIAL_TEAM_MAPS.get(map); + if ( + forcedTeamCount !== undefined && + Math.random() < SPECIAL_TEAM_FORCE_CHANCE + ) { + return forcedTeamCount; } const totalWeight = TEAM_WEIGHTS.reduce((sum, w) => sum + w.weight, 0); diff --git a/tests/MapConsistency.test.ts b/tests/MapConsistency.test.ts index 86b751ed0..ade0b7ee6 100644 --- a/tests/MapConsistency.test.ts +++ b/tests/MapConsistency.test.ts @@ -71,7 +71,7 @@ function getCategorizedMaps(): Set { function getFrequencyKeys(): Set { const content = fs.readFileSync(MAP_PLAYLIST, "utf8"); // Extract the frequency block - const freqMatch = content.match(/const frequency[\s\S]*?\{([\s\S]*?)\};/); + const freqMatch = content.match(/const FREQUENCY[\s\S]*?\{([\s\S]*?)\};/); if (!freqMatch) { throw new Error( `Failed to parse frequency record from MapPlaylist.ts (first 200 chars: ${content.slice(0, 200)})`,