Skip to content

Commit f6412a5

Browse files
Re-Enable HumansVsNations 🎉 (#2689)
## Description: **HumansVsNations is back!** The original PR had an issue: only the nations listed in the map’s `manifest.json` were being spawned, which resulted in completely unbalanced games. What did I change with this PR? - The number of humans and nations is now always the same. - If a map contains too many nations, we take a random subset. - If a map doesn’t contain enough nations, we dynamically add additional ones. These get random spawn locations, and their names are taken from the new name generator `NationNames.ts`. The name generator was taken from the closed PR #2245 (idea from @VariableVince). These changes apply to private lobbies and singleplayer as well. In singleplayer, you now simply play a 1-vs-1 against a nation. For public lobbies, we use 50% of the regular team-game player count. The remaining 50% are nations. We are also using the Hard difficulty for HumansVsNations. At the moment, it’s important that nations cheat a little because humans can donate troops, whereas nations cannot, at least not yet. In the future, we may make that work. We might need to adjust the difficulty or do fine-tuning depending on the humans’ win rate in production. Ideally, we want a ~50% win rate; otherwise, the mode may become boring. Over time, humans will likely develop strategies that nations can’t counter, in which case we’ll need to improve the nation AI. Here is a screenshot showing that the number of nations now matches the number of humans in the private lobby UI: <img width="806" height="304" alt="Screenshot 2025-12-25 004023" src="https://github.com/user-attachments/assets/cb4ac6f6-13cc-452c-8cc5-7a500670d7f2" /> The `PuplicLobby` display was a bit bugged for HumansVsNations: <img width="532" height="191" alt="Screenshot 2025-12-23 221832" src="https://github.com/user-attachments/assets/3950bcd9-0072-4c28-b1a0-83c0a24e9b8e" /> So I fixed it to look like this; <img width="532" height="195" alt="Screenshot 2025-12-23 224127" src="https://github.com/user-attachments/assets/690fc554-b607-4c8a-8b22-0c2912ee671a" /> ## 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 --------- Co-authored-by: iamlewis <lewismmmm@gmail.com>
1 parent 581fec5 commit f6412a5

File tree

9 files changed

+362
-22
lines changed

9 files changed

+362
-22
lines changed

‎resources/lang/en.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@
274274
"teams_Duos": "of 2 (Duos)",
275275
"teams_Trios": "of 3 (Trios)",
276276
"teams_Quads": "of 4 (Quads)",
277-
"teams_hvn": "Humans Vs Nations",
277+
"teams_hvn": "Humans vs Nations",
278+
"teams_hvn_detailed": "{num} Humans vs {num} Nations",
278279
"teams": "{num} teams",
279280
"players_per_team": "of {num}"
280281
},

‎src/client/HostLobbyModal.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -551,9 +551,9 @@ export class HostLobbyModal extends LitElement {
551551
: translateText("host_modal.players")
552552
}
553553
<span style="margin: 0 8px;">•</span>
554-
${this.disableNations ? 0 : this.nationCount}
554+
${this.getEffectiveNationCount()}
555555
${
556-
this.nationCount === 1
556+
this.getEffectiveNationCount() === 1
557557
? translateText("host_modal.nation_player")
558558
: translateText("host_modal.nation_players")
559559
}
@@ -564,7 +564,7 @@ export class HostLobbyModal extends LitElement {
564564
.clients=${this.clients}
565565
.lobbyCreatorClientID=${this.lobbyCreatorClientID}
566566
.teamCount=${this.teamCount}
567-
.nationCount=${this.disableNations ? 0 : this.nationCount}
567+
.nationCount=${this.getEffectiveNationCount()}
568568
.onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)}
569569
></lobby-team-view>
570570
</div>
@@ -876,6 +876,21 @@ export class HostLobbyModal extends LitElement {
876876
this.nationCount = 0;
877877
}
878878
}
879+
880+
/**
881+
* Returns the effective nation count for display purposes.
882+
* In HumansVsNations mode, this equals the number of human players.
883+
* Otherwise, it uses the manifest nation count (or 0 if nations are disabled).
884+
*/
885+
private getEffectiveNationCount(): number {
886+
if (this.disableNations) {
887+
return 0;
888+
}
889+
if (this.gameMode === GameMode.Team && this.teamCount === HumansVsNations) {
890+
return this.clients.length;
891+
}
892+
return this.nationCount;
893+
}
879894
}
880895

881896
async function createLobby(creatorClientID: string): Promise<GameInfo> {

‎src/client/PublicLobby.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export class PublicLobby extends LitElement {
135135
lobby.gameConfig.gameMode,
136136
teamCount,
137137
teamTotal,
138+
teamSize,
138139
);
139140
const teamDetailLabel = this.getTeamDetailLabel(
140141
lobby.gameConfig.gameMode,
@@ -241,7 +242,7 @@ export class PublicLobby extends LitElement {
241242
if (teamCount === Duos) return 2;
242243
if (teamCount === Trios) return 3;
243244
if (teamCount === Quads) return 4;
244-
if (teamCount === HumansVsNations) return Math.floor(maxPlayers / 2);
245+
if (teamCount === HumansVsNations) return maxPlayers;
245246
return undefined;
246247
}
247248
if (typeof teamCount === "number" && teamCount > 0) {
@@ -265,10 +266,13 @@ export class PublicLobby extends LitElement {
265266
gameMode: GameMode,
266267
teamCount: number | string | null,
267268
teamTotal: number | undefined,
269+
teamSize: number | undefined,
268270
): string {
269271
if (gameMode !== GameMode.Team) return translateText("game_mode.ffa");
270-
if (teamCount === HumansVsNations)
271-
return translateText("public_lobby.teams_hvn");
272+
if (teamCount === HumansVsNations && teamSize !== undefined)
273+
return translateText("public_lobby.teams_hvn_detailed", {
274+
num: teamSize,
275+
});
272276
const totalTeams =
273277
teamTotal ?? (typeof teamCount === "number" ? teamCount : 0);
274278
return translateText("public_lobby.teams", { num: totalTeams });
@@ -282,7 +286,11 @@ export class PublicLobby extends LitElement {
282286
): string | null {
283287
if (gameMode !== GameMode.Team) return null;
284288

285-
if (typeof teamCount === "string" && teamCount !== HumansVsNations) {
289+
if (typeof teamCount === "string" && teamCount === HumansVsNations) {
290+
return null;
291+
}
292+
293+
if (typeof teamCount === "string") {
286294
const teamKey = `public_lobby.teams_${teamCount}`;
287295
const maybeTranslated = translateText(teamKey);
288296
if (maybeTranslated !== teamKey) return maybeTranslated;

‎src/core/GameRunner.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Game,
1010
GameUpdates,
1111
NameViewData,
12-
Nation,
1312
Player,
1413
PlayerActions,
1514
PlayerBorderTiles,
@@ -26,6 +25,7 @@ import {
2625
GameUpdateType,
2726
GameUpdateViewData,
2827
} from "./game/GameUpdates";
28+
import { createNationsForGame } from "./game/NationUtils";
2929
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
3030
import { PseudoRandom } from "./PseudoRandom";
3131
import { ClientID, GameStartInfo, Turn } from "./Schemas";
@@ -56,15 +56,12 @@ export async function createGameRunner(
5656
);
5757
});
5858

59-
const nations = gameStart.config.disableNations
60-
? []
61-
: gameMap.nations.map(
62-
(n) =>
63-
new Nation(
64-
new Cell(n.coordinates[0], n.coordinates[1]),
65-
new PlayerInfo(n.name, PlayerType.Nation, null, random.nextID()),
66-
),
67-
);
59+
const nations = createNationsForGame(
60+
gameStart,
61+
gameMap.nations,
62+
humans.length,
63+
random,
64+
);
6865

6966
const game: Game = createGame(
7067
humans,

‎src/core/configuration/DefaultConfig.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ export abstract class DefaultServerConfig implements ServerConfig {
187187
p -= p % 4;
188188
break;
189189
case HumansVsNations:
190-
// For HumansVsNations, return the base team player count
190+
// Half the slots are for humans, the other half will get filled with nations
191+
p = Math.floor(p / 2);
191192
break;
192193
default:
193194
p -= p % numPlayerTeams;

‎src/core/execution/NationExecution.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,15 @@ export class NationExecution implements Execution {
116116
}
117117

118118
if (this.mg.inSpawnPhase()) {
119-
// select a tile near the position defined in the map manifest for the current nation
119+
// Place nations without a spawn cell (Dynamically created for HumansVsNations) randomly by SpawnExecution
120+
if (this.nation.spawnCell === undefined) {
121+
this.mg.addExecution(
122+
new SpawnExecution(this.gameID, this.nation.playerInfo),
123+
);
124+
return;
125+
}
126+
127+
// Select a tile near the position defined in the map manifest
120128
const rl = this.randomSpawnLand();
121129

122130
if (rl === null) {
@@ -194,6 +202,8 @@ export class NationExecution implements Execution {
194202
}
195203

196204
private randomSpawnLand(): TileRef | null {
205+
if (this.nation.spawnCell === undefined) throw new Error("not initialized");
206+
197207
const delta = 25;
198208
let tries = 0;
199209
while (tries < 50) {

‎src/core/game/Game.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export enum Relation {
312312

313313
export class Nation {
314314
constructor(
315-
public readonly spawnCell: Cell,
315+
public readonly spawnCell: Cell | undefined,
316316
public readonly playerInfo: PlayerInfo,
317317
) {}
318318
}

0 commit comments

Comments
 (0)