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
- `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:
### 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)})`,