mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Improve MapPlaylist 🎲 (#3904)
## Description: ### 1. `SPECIAL_MODIFIER_POOL` rebalanced Ticket weights adjusted to roughly track the community "favorite modifier" poll <img width="486" height="724" alt="Screenshot 2026-05-11 210740" src="https://github.com/user-attachments/assets/bb1d2461-beb3-41c0-8d7b-b604db5fc033" /> - `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: <img width="516" height="292" alt="image" src="https://github.com/user-attachments/assets/cd9ce31d-25d0-4b35-a8ba-bb3ec1c02b70" /> ### 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
This commit is contained in:
+57
-24
@@ -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>([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<Record<GameMapName, number>> = {
|
||||
const FREQUENCY: Partial<Record<GameMapName, number>> = {
|
||||
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<GameMapType, TeamCountConfig> = 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<ModifierKey>(2).fill("isRandomSpawn"),
|
||||
...Array<ModifierKey>(4).fill("isRandomSpawn"),
|
||||
...Array<ModifierKey>(4).fill("isCompact"),
|
||||
...Array<ModifierKey>(2).fill("isCrowded"),
|
||||
...Array<ModifierKey>(1).fill("isHardNations"),
|
||||
...Array<ModifierKey>(3).fill("startingGold1M"),
|
||||
...Array<ModifierKey>(5).fill("startingGold5M"),
|
||||
...Array<ModifierKey>(1).fill("startingGold25M"),
|
||||
...Array<ModifierKey>(4).fill("goldMultiplier"),
|
||||
...Array<ModifierKey>(2).fill("startingGold1M"),
|
||||
...Array<ModifierKey>(4).fill("startingGold5M"),
|
||||
...Array<ModifierKey>(3).fill("startingGold25M"),
|
||||
...Array<ModifierKey>(6).fill("goldMultiplier"),
|
||||
...Array<ModifierKey>(1).fill("isAlliancesDisabled"),
|
||||
...Array<ModifierKey>(1).fill("isPortsDisabled"),
|
||||
...Array<ModifierKey>(1).fill("isNukesDisabled"),
|
||||
...Array<ModifierKey>(1).fill("isSAMsDisabled"),
|
||||
...Array<ModifierKey>(1).fill("isPeaceTime"),
|
||||
...Array<ModifierKey>(3).fill("isWaterNukes"),
|
||||
...Array<ModifierKey>(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<GameMapType> = 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<GameMapType> = 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);
|
||||
|
||||
@@ -71,7 +71,7 @@ function getCategorizedMaps(): Set<string> {
|
||||
function getFrequencyKeys(): Set<string> {
|
||||
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)})`,
|
||||
|
||||
Reference in New Issue
Block a user