Added a public game modifier system 😮 For more variety (#2801)

## Description:

Added a public game modifier system. It causes that

5% of public games are played on the compact version of the map
10% of public games have "Random Spawn" activated

Percentages can easily get changed via `DefaultConfig`.
We can also easily add more modifiers.
Modifiers can stack, so in rare cases you will play on a compact map
with random spawn 😄
More variety!

### "Compact Map" modifier implementation

- With the "Compact Map" modifier the lobby max player count gets
reduced to 25% and only 25% of the regular bots and only 25% of the
regular nations will spawn (because the map has only 25% of its regular
size)
- In private lobbies and singleplayer the nation reduction happens too
(When "Compact Map" is enabled).

### Restrictions

- Duos/Trios/Quads team modes do not get Random Spawn (defeats the
purpose)
- Maps with smallest player count < 50 do not get Compact Map in team
games (not enough players after the reduction to 25%). I have calculated
all the possible max player counts.

### How it looks like

Random Spawn modifier:

<img width="528" height="183" alt="Screenshot 2026-01-06 194959"
src="https://github.com/user-attachments/assets/2f729da9-80c3-4548-8205-71129da2a76a"
/>

Very rare case: Two modifiers at the same time and only 10 max players
have been chosen from `[GameMapType.FaroeIslands]: [20, 15, 10]`.
Because of the 75% reduction in player count only 3 players are allowed
(3 is the minimum). I think its funny that you can play a 1v1v1 in rare
occasions 😄

<img width="526" height="184" alt="Screenshot 2026-01-06 194938"
src="https://github.com/user-attachments/assets/834326eb-df03-41b7-b1db-1efa3f1013b5"
/>

### Funny side-effect

Team games with random spawn. That will be interesting. No more "Who is
better in donating troops to the frontline". Instead you have to heavily
coordinate with your teammates.

## 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:
FloPinguin
2026-01-07 04:37:58 +01:00
committed by GitHub
parent 387190b916
commit ebcb654825
10 changed files with 143 additions and 19 deletions
+6
View File
@@ -171,6 +171,12 @@ export const GameConfigSchema = z.object({
gameType: z.enum(GameType),
gameMode: z.enum(GameMode),
gameMapSize: z.enum(GameMapSize),
publicGameModifiers: z
.object({
isCompact: z.boolean(),
isRandomSpawn: z.boolean(),
})
.optional(),
disableNations: z.boolean(),
bots: z.number().int().min(0).max(400),
infiniteGold: z.boolean(),
+4
View File
@@ -7,6 +7,7 @@ import {
Gold,
Player,
PlayerInfo,
PublicGameModifiers,
Team,
TerraNullius,
Tick,
@@ -34,6 +35,7 @@ export interface ServerConfig {
map: GameMapType,
mode: GameMode,
numPlayerTeams: TeamCountConfig | undefined,
isCompactMap?: boolean,
): number;
numWorkers(): number;
workerIndex(gameID: GameID): number;
@@ -57,6 +59,8 @@ export interface ServerConfig {
stripePublishableKey(): string;
allowedFlares(): string[] | undefined;
enableMatchmaking(): boolean;
getRandomPublicGameModifiers(): PublicGameModifiers;
supportsCompactMapForTeams(map: GameMapType): boolean;
}
export interface NukeMagnitude {
+20
View File
@@ -12,6 +12,7 @@ import {
Player,
PlayerInfo,
PlayerType,
PublicGameModifiers,
Quads,
TerrainType,
TerraNullius,
@@ -176,11 +177,16 @@ export abstract class DefaultServerConfig implements ServerConfig {
map: GameMapType,
mode: GameMode,
numPlayerTeams: TeamCountConfig | undefined,
isCompactMap?: boolean,
): number {
const [l, m, s] = numPlayersConfig[map] ?? [50, 30, 20];
const r = Math.random();
const base = r < 0.3 ? l : r < 0.6 ? m : s;
let p = Math.min(mode === GameMode.Team ? Math.ceil(base * 1.5) : base, l);
// Apply compact map 75% player reduction
if (isCompactMap) {
p = Math.max(3, Math.floor(p * 0.25));
}
if (numPlayerTeams === undefined) return p;
switch (numPlayerTeams) {
case Duos:
@@ -218,6 +224,20 @@ export abstract class DefaultServerConfig implements ServerConfig {
enableMatchmaking(): boolean {
return false;
}
getRandomPublicGameModifiers(): PublicGameModifiers {
return {
isRandomSpawn: Math.random() < 0.1, // 10% chance
isCompact: Math.random() < 0.05, // 5% chance
};
}
supportsCompactMapForTeams(map: GameMapType): boolean {
// Maps with smallest player count < 50 don't support compact map in team games
// The smallest player count is the 3rd number in numPlayersConfig
const [, , smallest] = numPlayersConfig[map] ?? [50, 30, 20];
return smallest >= 50;
}
}
export class DefaultConfig implements Config {
+5
View File
@@ -193,6 +193,11 @@ export enum GameMapSize {
Normal = "Normal",
}
export interface PublicGameModifiers {
isCompact: boolean;
isRandomSpawn: boolean;
}
export interface UnitInfo {
cost: (game: Game, player: Player) => Gold;
// Determines if its owner changes when its tile is conquered.
+28 -2
View File
@@ -2,6 +2,7 @@ import { PseudoRandom } from "../PseudoRandom";
import { GameStartInfo } from "../Schemas";
import {
Cell,
GameMapSize,
GameMode,
GameType,
HumansVsNations,
@@ -14,6 +15,7 @@ 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.
* For compact maps, only 25% of the nations are used.
*/
export function createNationsForGame(
gameStart: GameStartInfo,
@@ -31,13 +33,23 @@ export function createNationsForGame(
new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
);
const isCompactMap = gameStart.config.gameMapSize === GameMapSize.Compact;
const isHumansVsNations =
gameStart.config.gameMode === GameMode.Team &&
gameStart.config.playerTeams === HumansVsNations;
// For non-HumansVsNations modes, simply use the manifest nations
// For compact maps, use only 25% of nations (minimum 1)
let effectiveNations = manifestNations;
if (isCompactMap && !isHumansVsNations) {
const targetCount = getCompactMapNationCount(manifestNations.length, true);
const shuffled = random.shuffleArray(manifestNations);
effectiveNations = shuffled.slice(0, targetCount);
}
// For non-HumansVsNations modes, simply use the effective nations
if (!isHumansVsNations) {
return manifestNations.map(toNation);
return effectiveNations.map(toNation);
}
// HumansVsNations mode: balance nation count to match human count
@@ -71,6 +83,20 @@ export function createNationsForGame(
return nations;
}
// For compact maps, only 25% of nations are used (minimum 1).
export function getCompactMapNationCount(
manifestNationCount: number,
isCompactMap: boolean,
): number {
if (manifestNationCount === 0) {
return 0;
}
if (isCompactMap) {
return Math.max(1, Math.floor(manifestNationCount * 0.25));
}
return manifestNationCount;
}
const PLURAL_NOUN = Symbol("plural!");
const NOUN = Symbol("noun!");