From 71e5faf4ecb8f5d859ad4324bbe8732d97e846c8 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Mon, 16 Mar 2026 05:28:40 +0100 Subject: [PATCH] Rebalance HvN (#3433) ## Description: For the next v30 fix version imaege HvN balancing for the revamped difficulty steps of v30 sadly doesn't really work out... In medium difficulty games humans nearly always win (boring) In hard difficulty games humans usually lose It was intended differently... So lets get rid of medium difficulty HvN, always use hard difficulty and disable the donation-capability for public game nations. That will tune the human winrate towards a middle ground at about 65% I think. Which should be nice. Easier than in v29 (was frustrating sometimes) but not as easy as it's now. We can only test this in prod lol ## 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/core/execution/utils/AiAttackBehavior.ts | 6 +++ src/server/MapPlaylist.ts | 50 +++++++------------- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/src/core/execution/utils/AiAttackBehavior.ts b/src/core/execution/utils/AiAttackBehavior.ts index acdbfc662..c42301cc7 100644 --- a/src/core/execution/utils/AiAttackBehavior.ts +++ b/src/core/execution/utils/AiAttackBehavior.ts @@ -2,6 +2,7 @@ import { Difficulty, Game, GameMode, + GameType, HumansVsNations, Player, PlayerID, @@ -846,6 +847,11 @@ export class AiAttackBehavior { return false; } + // Don't donate in public games (To balance HvN) + if (this.game.config().gameConfig().gameType === GameType.Public) { + return false; + } + // Check if donating troops is allowed if (this.game.config().donateTroops() === false) { return false; diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index bb37e0d3c..3959fe68a 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -125,9 +125,6 @@ const MUTUALLY_EXCLUSIVE_MODIFIERS: [ModifierKey, ModifierKey][] = [ ["isHardNations", "startingGoldHigh"], ]; -// Probability of hard nations modifier in HumansVsNations games. -const HARD_NATIONS_HVN_PROBABILITY = 0.2; // 20% - export class MapPlaylist { private playlists: Record = { ffa: [], @@ -159,8 +156,8 @@ export class MapPlaylist { isRandomSpawn = false; } - // Hard nations modifier only applies when nations are present - if (mode === GameMode.Team && playerTeams !== HumansVsNations) { + // Hard nations modifier only applies when nations are present (not HvN, which is always hard) + if (mode === GameMode.Team) { isHardNations = false; } @@ -204,7 +201,10 @@ export class MapPlaylist { isAlliancesDisabled: false, }, startingGold, - difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium, + difficulty: + isHardNations || playerTeams === HumansVsNations + ? Difficulty.Hard + : Difficulty.Medium, infiniteGold: false, infiniteTroops: false, maxTimerValue: undefined, @@ -248,26 +248,15 @@ export class MapPlaylist { excludedModifiers.push("isRandomSpawn"); } - // Hard nations: excluded for non-HvN team modes (no nations present). - // For HumansVsNations: rolled independently (not via pool). - // For FFA: stays in the pool for normal ticket-based selection. - let hardNationsFromIndependentRoll: boolean | undefined; - let poolCountReduction = 0; - if (mode === GameMode.Team && playerTeams !== HumansVsNations) { - excludedModifiers.push("isHardNations"); - } else if (playerTeams === HumansVsNations) { + // Hard nations modifier only applies when nations are present (not HvN, which is always hard) + if (mode === GameMode.Team) { excludedModifiers.push("isHardNations"); + } + if (playerTeams === HumansVsNations) { excludedModifiers.push("startingGoldHigh"); // Nations are disabled if that modifier is active - hardNationsFromIndependentRoll = - Math.random() < HARD_NATIONS_HVN_PROBABILITY; - poolCountReduction = hardNationsFromIndependentRoll ? 1 : 0; } - const poolResult = this.getRandomSpecialGameModifiers( - excludedModifiers, - undefined, - poolCountReduction, - ); + const poolResult = this.getRandomSpecialGameModifiers(excludedModifiers); let { isCrowded, startingGold, @@ -275,9 +264,8 @@ export class MapPlaylist { isRandomSpawn, goldMultiplier, isAlliancesDisabled, + isHardNations, } = poolResult; - let isHardNations = - hardNationsFromIndependentRoll ?? poolResult.isHardNations; let crowdedMaxPlayers: number | undefined; if (isCrowded) { @@ -300,7 +288,6 @@ export class MapPlaylist { const fallback = this.getRandomSpecialGameModifiers( excludedModifiers, 1, - poolCountReduction, ); ({ isRandomSpawn, @@ -309,8 +296,7 @@ export class MapPlaylist { goldMultiplier, isAlliancesDisabled, } = fallback); - isHardNations = - hardNationsFromIndependentRoll ?? fallback.isHardNations; + ({ isHardNations } = fallback); } } } @@ -347,7 +333,10 @@ export class MapPlaylist { startingGold, goldMultiplier, disableAlliances: isAlliancesDisabled, - difficulty: isHardNations ? Difficulty.Hard : Difficulty.Medium, + difficulty: + isHardNations || playerTeams === HumansVsNations + ? Difficulty.Hard + : Difficulty.Medium, infiniteGold: false, infiniteTroops: false, maxTimerValue: undefined, @@ -502,10 +491,7 @@ export class MapPlaylist { isCompact: Math.random() < 0.05, // 5% chance isCrowded: Math.random() < 0.05, // 5% chance startingGold: Math.random() < 0.05 ? 5_000_000 : undefined, // 5% chance - isHardNations: - playerTeams === HumansVsNations - ? Math.random() < HARD_NATIONS_HVN_PROBABILITY - : Math.random() < 0.025, // 2.5% chance + isHardNations: Math.random() < 0.025, // 2.5% chance isAlliancesDisabled: false, }; }