Cleanup nations (Part 1) 🧹 (#2637)

## Description:

1. Using the wording `"Nation"`, `"FakeHuman"` and `"NPC"` at the same
time is confusing.
So I renamed every mention of `"FakeHuman"` and `"NPC"` in the entire
project to `"Nation"`. Just like they are called ingame.

2. `BotBehavior.ts` was originally intended for sharing the logic
between nations and bots.
But at the moment, the logic there isn't really shared and it's
basically just about attacking.
So I renamed `BotBehavior.ts` to `AiAttackBehavior.ts`. I use "Ai" to
indicate that this file is used by bots AND nations.

3. Moved `execuction/utils/AllianceBehavior.ts` to
`execuction/nation/NationAllianceBehavior.ts` to make sure everybody
understands that this file is not about alliances in general. It's just
about nations and how they handle alliances.

4. Removed `difficultyModifier` from `DefaultConfig`. It's unused and I
think we usually want to finetune the difficulty instead of using that
method.

5. Added `assertNever` in all `switch (difficulty)` default cases.

## 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
2025-12-19 01:20:23 +01:00
committed by GitHub
parent f60aef65e1
commit 4d5bb7a835
26 changed files with 290 additions and 278 deletions
+13 -13
View File
@@ -38,7 +38,7 @@ export class HostLobbyModal extends LitElement {
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs = false;
@state() private disableNations = false;
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: TeamCountConfig = 2;
@state() private bots: number = 400;
@@ -358,17 +358,17 @@ export class HostLobbyModal extends LitElement {
)
? html`
<label
for="disable-npcs"
class="option-card ${this.disableNPCs
for="disable-nations"
class="option-card ${this.disableNations
? "selected"
: ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
id="disable-nations"
@change=${this.handleDisableNationsChange}
.checked=${this.disableNations}
/>
<div class="option-card-title">
${translateText("host_modal.disable_nations")}
@@ -556,7 +556,7 @@ export class HostLobbyModal extends LitElement {
: translateText("host_modal.players")
}
<span style="margin: 0 8px;">•</span>
${this.disableNPCs ? 0 : this.nationCount}
${this.disableNations ? 0 : this.nationCount}
${
this.nationCount === 1
? translateText("host_modal.nation_player")
@@ -569,7 +569,7 @@ export class HostLobbyModal extends LitElement {
.clients=${this.clients}
.lobbyCreatorClientID=${this.lobbyCreatorClientID}
.teamCount=${this.teamCount}
.nationCount=${this.disableNPCs ? 0 : this.nationCount}
.nationCount=${this.disableNations ? 0 : this.nationCount}
.onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)}
></lobby-team-view>
</div>
@@ -735,9 +735,9 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
private async handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
console.log(`updating disable npcs to ${this.disableNPCs}`);
private async handleDisableNationsChange(e: Event) {
this.disableNations = Boolean((e.target as HTMLInputElement).checked);
console.log(`updating disable nations to ${this.disableNations}`);
this.putGameConfig();
}
@@ -779,10 +779,10 @@ export class HostLobbyModal extends LitElement {
...(this.gameMode === GameMode.Team &&
this.teamCount === HumansVsNations
? {
disableNPCs: false,
disableNations: false,
}
: {
disableNPCs: this.disableNPCs,
disableNations: this.disableNations,
}),
maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
+12 -10
View File
@@ -36,7 +36,7 @@ export class SinglePlayerModal extends LitElement {
};
@state() private selectedMap: GameMapType = GameMapType.World;
@state() private selectedDifficulty: Difficulty = Difficulty.Medium;
@state() private disableNPCs: boolean = false;
@state() private disableNations: boolean = false;
@state() private bots: number = 400;
@state() private infiniteGold: boolean = false;
@state() private infiniteTroops: boolean = false;
@@ -261,15 +261,17 @@ export class SinglePlayerModal extends LitElement {
)
? html`
<label
for="singleplayer-modal-disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
for="singleplayer-modal-disable-nations"
class="option-card ${this.disableNations
? "selected"
: ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="singleplayer-modal-disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
id="singleplayer-modal-disable-nations"
@change=${this.handleDisableNationsChange}
.checked=${this.disableNations}
/>
<div class="option-card-title">
${translateText("single_modal.disable_nations")}
@@ -491,8 +493,8 @@ export class SinglePlayerModal extends LitElement {
this.maxTimerValue = value;
}
private handleDisableNPCsChange(e: Event) {
this.disableNPCs = Boolean((e.target as HTMLInputElement).checked);
private handleDisableNationsChange(e: Event) {
this.disableNations = Boolean((e.target as HTMLInputElement).checked);
}
private handleGameModeSelection(value: GameMode) {
@@ -591,10 +593,10 @@ export class SinglePlayerModal extends LitElement {
...(this.gameMode === GameMode.Team &&
this.teamCount === HumansVsNations
? {
disableNPCs: false,
disableNations: false,
}
: {
disableNPCs: this.disableNPCs,
disableNations: this.disableNations,
}),
},
lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP
@@ -260,11 +260,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
.map((a) => a.troops)
.reduce((a, b) => a + b, 0);
if (
player.type() === PlayerType.FakeHuman &&
myPlayer !== null &&
!isAllied
) {
if (player.type() === PlayerType.Nation && myPlayer !== null && !isAllied) {
const relation =
this.playerProfile?.relations[myPlayer.smallID()] ?? Relation.Neutral;
const relationClass = this.getRelationClass(relation);
@@ -299,7 +295,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer {
case PlayerType.Bot:
playerType = translateText("player_type.bot");
break;
case PlayerType.FakeHuman:
case PlayerType.Nation:
playerType = translateText("player_type.nation");
break;
case PlayerType.Human:
+2 -2
View File
@@ -276,7 +276,7 @@ export class PlayerPanel extends LitElement implements Layer {
private identityChipProps(type: PlayerType) {
switch (type) {
case PlayerType.FakeHuman:
case PlayerType.Nation:
return {
labelKey: "player_type.nation",
aria: "Nation player",
@@ -388,7 +388,7 @@ export class PlayerPanel extends LitElement implements Layer {
}
private renderRelationPillIfNation(other: PlayerView, my: PlayerView) {
if (other.type() !== PlayerType.FakeHuman) return html``;
if (other.type() !== PlayerType.Nation) return html``;
if (other.isTraitor()) return html``;
if (my?.isAlliedWith && my.isAlliedWith(other)) return html``;
if (!this.otherProfile || !my) return html``;
+5 -5
View File
@@ -55,7 +55,7 @@ export async function createGameRunner(
);
});
const nations = gameStart.config.disableNPCs
const nations = gameStart.config.disableNations
? []
: gameMap.nations.map(
(n) =>
@@ -63,7 +63,7 @@ export async function createGameRunner(
new Cell(n.coordinates[0], n.coordinates[1]),
new PlayerInfo(
n.name,
PlayerType.FakeHuman,
PlayerType.Nation,
null,
random.nextID(),
n.strength,
@@ -110,8 +110,8 @@ export class GameRunner {
...this.execManager.spawnBots(this.game.config().numBots()),
);
}
if (this.game.config().spawnNPCs()) {
this.game.addExecution(...this.execManager.fakeHumanExecutions());
if (this.game.config().spawnNations()) {
this.game.addExecution(...this.execManager.nationExecutions());
}
this.game.addExecution(new WinCheckExecution());
}
@@ -160,7 +160,7 @@ export class GameRunner {
.players()
.filter(
(p) =>
p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman,
p.type() === PlayerType.Human || p.type() === PlayerType.Nation,
)
.forEach(
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
+1 -1
View File
@@ -164,7 +164,7 @@ export const GameConfigSchema = z.object({
gameType: z.enum(GameType),
gameMode: z.enum(GameMode),
gameMapSize: z.enum(GameMapSize),
disableNPCs: z.boolean(),
disableNations: z.boolean(),
bots: z.number().int().min(0).max(400),
infiniteGold: z.boolean(),
infiniteTroops: z.boolean(),
+1 -3
View File
@@ -1,7 +1,6 @@
import { Colord } from "colord";
import { JWK } from "jose";
import {
Difficulty,
Game,
GameMapType,
GameMode,
@@ -82,7 +81,7 @@ export interface Config {
theme(): Theme;
percentageTilesOwnedToWin(): number;
numBots(): number;
spawnNPCs(): boolean;
spawnNations(): boolean;
isUnitDisabled(unitType: UnitType): boolean;
bots(): number;
infiniteGold(): boolean;
@@ -159,7 +158,6 @@ export interface Config {
defensePostDefenseBonus(): number;
defensePostSpeedBonus(): number;
falloutDefenseModifier(percentOfFallout: number): number;
difficultyModifier(difficulty: Difficulty): number;
warshipPatrolRange(): number;
warshipShellAttackRate(): number;
warshipTargettingRange(): number;
+13 -20
View File
@@ -287,19 +287,6 @@ export class DefaultConfig implements Config {
return this._userSettings;
}
difficultyModifier(difficulty: Difficulty): number {
switch (difficulty) {
case Difficulty.Easy:
return 1;
case Difficulty.Medium:
return 3;
case Difficulty.Hard:
return 9;
case Difficulty.Impossible:
return 18;
}
}
cityTroopIncrease(): number {
return 250_000;
}
@@ -332,8 +319,8 @@ export class DefaultConfig implements Config {
return this._gameConfig.playerTeams ?? 0;
}
spawnNPCs(): boolean {
return !this._gameConfig.disableNPCs;
spawnNations(): boolean {
return !this._gameConfig.disableNations;
}
isUnitDisabled(unitType: UnitType): boolean {
@@ -712,7 +699,7 @@ export class DefaultConfig implements Config {
mag *= 0.8;
}
if (
attacker.type() === PlayerType.FakeHuman &&
attacker.type() === PlayerType.Nation &&
defender.type() === PlayerType.Bot
) {
mag *= 0.8;
@@ -816,9 +803,9 @@ export class DefaultConfig implements Config {
}
useNationStrengthForStartManpower(): boolean {
// Currently disabled: FakeHumans became harder to play against due to AI improvements
// Currently disabled: Nations became harder to play against due to AI improvements
// nation strength multiplier was unintentionally disabled during those AI improvements (playerInfo.nation was undefined),
// Re-enabling this without rebalancing FakeHuman difficulty elsewhere may make them overpowered
// Re-enabling this without rebalancing Nation difficulty elsewhere may make them overpowered
return false;
}
@@ -826,7 +813,7 @@ export class DefaultConfig implements Config {
if (playerInfo.playerType === PlayerType.Bot) {
return 10_000;
}
if (playerInfo.playerType === PlayerType.FakeHuman) {
if (playerInfo.playerType === PlayerType.Nation) {
const strength = this.useNationStrengthForStartManpower()
? (playerInfo.nationStrength ?? 1)
: 1;
@@ -840,6 +827,8 @@ export class DefaultConfig implements Config {
return 31_250 * strength;
case Difficulty.Impossible:
return 37_500 * strength;
default:
assertNever(this._gameConfig.difficulty);
}
}
return this.infiniteTroops() ? 1_000_000 : 25_000;
@@ -873,6 +862,8 @@ export class DefaultConfig implements Config {
return maxTroops * 1.25;
case Difficulty.Impossible:
return maxTroops * 1.5;
default:
assertNever(this._gameConfig.difficulty);
}
}
@@ -888,7 +879,7 @@ export class DefaultConfig implements Config {
toAdd *= 0.6;
}
if (player.type() === PlayerType.FakeHuman) {
if (player.type() === PlayerType.Nation) {
switch (this._gameConfig.difficulty) {
case Difficulty.Easy:
toAdd *= 0.95;
@@ -902,6 +893,8 @@ export class DefaultConfig implements Config {
case Difficulty.Impossible:
toAdd *= 1.1;
break;
default:
assertNever(this._gameConfig.difficulty);
}
}
+10 -10
View File
@@ -2,7 +2,7 @@ import { Execution, Game, Player } from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution";
import { BotBehavior } from "./utils/BotBehavior";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class BotExecution implements Execution {
private active = true;
@@ -10,7 +10,7 @@ export class BotExecution implements Execution {
private mg: Game;
private neighborsTerraNullius = true;
private behavior: BotBehavior | null = null;
private attackBehavior: AiAttackBehavior | null = null;
private attackRate: number;
private attackTick: number;
private triggerRatio: number;
@@ -42,8 +42,8 @@ export class BotExecution implements Execution {
return;
}
if (this.behavior === null) {
this.behavior = new BotBehavior(
if (this.attackBehavior === null) {
this.attackBehavior = new AiAttackBehavior(
this.random,
this.mg,
this.bot,
@@ -53,7 +53,7 @@ export class BotExecution implements Execution {
);
// Send an attack on the first tick
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
@@ -81,10 +81,10 @@ export class BotExecution implements Execution {
}
private maybeAttack() {
if (this.behavior === null) {
if (this.attackBehavior === null) {
throw new Error("not initialized");
}
const toAttack = this.behavior.getNeighborTraitorToAttack();
const toAttack = this.attackBehavior.getNeighborTraitorToAttack();
if (toAttack !== null) {
const odds = this.bot.isFriendly(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
@@ -95,20 +95,20 @@ export class BotExecution implements Execution {
this.bot.breakAlliance(alliance);
}
this.behavior.sendAttack(toAttack);
this.attackBehavior.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
if (this.bot.sharesBorderWith(this.mg.terraNullius())) {
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
this.neighborsTerraNullius = false;
}
this.behavior.attackRandomTarget();
this.attackBehavior.attackRandomTarget();
}
isActive(): boolean {
+1 -1
View File
@@ -43,7 +43,7 @@ export class EmojiExecution implements Execution {
if (
emojiString === "🖕" &&
this.recipient !== AllPlayers &&
this.recipient.type() === PlayerType.FakeHuman
this.recipient.type() === PlayerType.Nation
) {
this.recipient.updateRelation(this.requestor, -100);
}
+3 -3
View File
@@ -16,9 +16,9 @@ import { DonateTroopsExecution } from "./DonateTroopExecution";
import { EmbargoAllExecution } from "./EmbargoAllExecution";
import { EmbargoExecution } from "./EmbargoExecution";
import { EmojiExecution } from "./EmojiExecution";
import { FakeHumanExecution } from "./FakeHumanExecution";
import { MarkDisconnectedExecution } from "./MarkDisconnectedExecution";
import { MoveWarshipExecution } from "./MoveWarshipExecution";
import { NationExecution } from "./NationExecution";
import { NoOpExecution } from "./NoOpExecution";
import { QuickChatExecution } from "./QuickChatExecution";
import { RetreatExecution } from "./RetreatExecution";
@@ -136,10 +136,10 @@ export class Executor {
return new PlayerSpawner(this.mg, this.gameID).spawnPlayers();
}
fakeHumanExecutions(): Execution[] {
nationExecutions(): Execution[] {
const execs: Execution[] = [];
for (const nation of this.mg.nations()) {
execs.push(new FakeHumanExecution(this.gameID, nation));
execs.push(new NationExecution(this.gameID, nation));
}
return execs;
}
@@ -20,19 +20,19 @@ import { GameID } from "../Schemas";
import { boundingBoxTiles, calculateBoundingBox, simpleHash } from "../Util";
import { ConstructionExecution } from "./ConstructionExecution";
import { MirvExecution } from "./MIRVExecution";
import { NationAllianceBehavior } from "./nation/NationAllianceBehavior";
import { structureSpawnTileValue } from "./nation/structureSpawnTileValue";
import { NukeExecution } from "./NukeExecution";
import { SpawnExecution } from "./SpawnExecution";
import { TransportShipExecution } from "./TransportShipExecution";
import { calculateTerritoryCenter, closestTwoTiles } from "./Util";
import { AllianceBehavior } from "./utils/AllianceBehavior";
import { BotBehavior } from "./utils/BotBehavior";
import { AiAttackBehavior } from "./utils/AiAttackBehavior";
export class FakeHumanExecution implements Execution {
export class NationExecution implements Execution {
private active = true;
private random: PseudoRandom;
private behavior: BotBehavior | null = null; // Shared behavior logic for both bots and fakehumans
private allianceBehavior: AllianceBehavior | null = null;
private attackBehavior: AiAttackBehavior | null = null;
private allianceBehavior: NationAllianceBehavior | null = null;
private mg: Game;
private player: Player | null = null;
@@ -73,7 +73,7 @@ export class FakeHumanExecution implements Execution {
constructor(
gameID: GameID,
private nation: Nation, // Nation contains PlayerInfo with PlayerType.FakeHuman
private nation: Nation, // Nation contains PlayerInfo with PlayerType.Nation
) {
this.random = new PseudoRandom(
simpleHash(nation.playerInfo.id) + simpleHash(gameID),
@@ -176,9 +176,9 @@ export class FakeHumanExecution implements Execution {
return;
}
if (this.behavior === null || this.allianceBehavior === null) {
if (this.attackBehavior === null || this.allianceBehavior === null) {
// Player is unavailable during init()
this.behavior = new BotBehavior(
this.attackBehavior = new AiAttackBehavior(
this.random,
this.mg,
this.player,
@@ -186,14 +186,14 @@ export class FakeHumanExecution implements Execution {
this.reserveRatio,
this.expandRatio,
);
this.allianceBehavior = new AllianceBehavior(
this.allianceBehavior = new NationAllianceBehavior(
this.random,
this.mg,
this.player,
);
// Send an attack on the first tick
this.behavior.forceSendAttack(this.mg.terraNullius());
this.attackBehavior.forceSendAttack(this.mg.terraNullius());
return;
}
@@ -209,7 +209,7 @@ export class FakeHumanExecution implements Execution {
private maybeAttack() {
if (
this.player === null ||
this.behavior === null ||
this.attackBehavior === null ||
this.allianceBehavior === null
) {
throw new Error("not initialized");
@@ -240,7 +240,7 @@ export class FakeHumanExecution implements Execution {
(t) => !this.mg.hasOwner(t) && !this.mg.hasFallout(t),
);
if (hasNonNukedTerraNullius) {
this.behavior.sendAttack(this.mg.terraNullius());
this.attackBehavior.sendAttack(this.mg.terraNullius());
return;
}
@@ -257,11 +257,13 @@ export class FakeHumanExecution implements Execution {
this.allianceBehavior.maybeSendAllianceRequests(borderingEnemies);
}
this.behavior.assistAllies();
this.attackBehavior.assistAllies();
this.behavior.attackBestTarget(borderingFriends, borderingEnemies);
this.attackBehavior.attackBestTarget(borderingFriends, borderingEnemies);
this.maybeSendNuke(this.behavior.findBestNukeTarget(borderingEnemies));
this.maybeSendNuke(
this.attackBehavior.findBestNukeTarget(borderingEnemies),
);
}
private maybeSendNuke(other: Player | null) {
@@ -271,7 +273,7 @@ export class FakeHumanExecution implements Execution {
silos.length === 0 ||
this.player.gold() < this.cost(UnitType.AtomBomb) ||
other === null ||
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to fakehumans and humans)
other.type() === PlayerType.Bot || // Don't nuke bots (as opposed to nations and humans)
this.player.isOnSameTeam(other)
) {
return;
@@ -340,7 +342,7 @@ export class FakeHumanExecution implements Execution {
const tick = this.mg.ticks();
this.lastNukeSent.push([tick, tile]);
this.mg.addExecution(new NukeExecution(nukeType, this.player, tile));
this.behavior?.maybeSendEmoji(targetPlayer);
this.attackBehavior?.maybeSendEmoji(targetPlayer);
}
private nukeTileScore(tile: TileRef, silos: Unit[], targets: Unit[]): number {
@@ -683,7 +685,7 @@ export class FakeHumanExecution implements Execution {
return false;
}
if (this.random.chance(FakeHumanExecution.MIRV_HESITATION_ODDS)) {
if (this.random.chance(NationExecution.MIRV_HESITATION_ODDS)) {
this.triggerMIRVCooldown();
return false;
}
@@ -735,7 +737,7 @@ export class FakeHumanExecution implements Execution {
.map((x) => x.numTilesOwned())
.reduce((a, b) => a + b, 0);
const teamShare = teamTerritory / totalLand;
if (teamShare >= FakeHumanExecution.VICTORY_DENIAL_TEAM_THRESHOLD) {
if (teamShare >= NationExecution.VICTORY_DENIAL_TEAM_THRESHOLD) {
// Only consider the largest team member as the target when team exceeds threshold
let largestMember: Player | null = null;
let largestTiles = -1;
@@ -754,7 +756,7 @@ export class FakeHumanExecution implements Execution {
}
} else {
const share = p.numTilesOwned() / totalLand;
if (share >= FakeHumanExecution.VICTORY_DENIAL_INDIVIDUAL_THRESHOLD)
if (share >= NationExecution.VICTORY_DENIAL_INDIVIDUAL_THRESHOLD)
severity = share;
}
if (severity > 0) {
@@ -780,13 +782,13 @@ export class FakeHumanExecution implements Execution {
const topPlayer = allPlayers[0];
if (topPlayer.cityCount <= FakeHumanExecution.STEAMROLL_MIN_LEADER_CITIES)
if (topPlayer.cityCount <= NationExecution.STEAMROLL_MIN_LEADER_CITIES)
return null;
const secondHighest = allPlayers[1].cityCount;
const threshold =
secondHighest * FakeHumanExecution.STEAMROLL_CITY_GAP_MULTIPLIER;
secondHighest * NationExecution.STEAMROLL_CITY_GAP_MULTIPLIER;
if (topPlayer.cityCount >= threshold) {
return validTargets.some((p) => p === topPlayer.p) ? topPlayer.p : null;
@@ -852,7 +854,7 @@ export class FakeHumanExecution implements Execution {
private maybeSendMIRV(enemy: Player): void {
if (this.player === null) throw new Error("not initialized");
this.behavior?.maybeSendEmoji(enemy);
this.attackBehavior?.maybeSendEmoji(enemy);
const centerTile = this.calculateTerritoryCenter(enemy);
if (centerTile && this.player.canBuild(UnitType.MIRV, centerTile)) {
@@ -878,7 +880,7 @@ export class FakeHumanExecution implements Execution {
}
private removeOldMIRVEvents() {
const maxAge = FakeHumanExecution.MIRV_COOLDOWN_TICKS;
const maxAge = NationExecution.MIRV_COOLDOWN_TICKS;
const tick = this.mg.ticks();
while (
this.lastMIRVSent.length > 0 &&
@@ -6,10 +6,11 @@ import {
Relation,
} from "../../game/Game";
import { PseudoRandom } from "../../PseudoRandom";
import { assertNever } from "../../Util";
import { AllianceExtensionExecution } from "../alliance/AllianceExtensionExecution";
import { AllianceRequestExecution } from "../alliance/AllianceRequestExecution";
export class AllianceBehavior {
export class NationAllianceBehavior {
constructor(
private random: PseudoRandom,
private game: Game,
@@ -52,8 +53,10 @@ export class AllianceBehavior {
return this.random.chance(30);
case Difficulty.Hard:
return this.random.chance(25);
default:
case Difficulty.Impossible:
return this.random.chance(20);
default:
assertNever(difficulty);
}
};
@@ -116,8 +119,10 @@ export class AllianceBehavior {
return this.random.chance(20); // 5% chance to be confused on medium
case Difficulty.Hard:
return this.random.chance(40); // 2.5% chance to be confused on hard
default:
case Difficulty.Impossible:
return false; // No confusion on impossible
default:
assertNever(difficulty);
}
}
@@ -137,7 +142,7 @@ export class AllianceBehavior {
this.game.config().maxTroops(otherPlayer) >
this.game.config().maxTroops(this.player) * 2
);
default: {
case Difficulty.Impossible: {
// On impossible we check for multiple factors and try to not mess with stronger players (we want to steamroll over weaklings)
const otherHasMoreTroops =
otherPlayer.troops() > this.player.troops() * 1.5;
@@ -150,6 +155,8 @@ export class AllianceBehavior {
otherPlayer.numTilesOwned() > this.player.numTilesOwned() * 1.5;
return otherHasMoreTroops || otherHasMoreMaxTroops || otherHasMoreTiles;
}
default:
assertNever(difficulty);
}
}
@@ -160,7 +167,8 @@ export class AllianceBehavior {
return false; // On easy we never think we have enough alliances
case Difficulty.Medium:
return this.player.alliances().length >= this.random.nextInt(5, 8);
default: {
case Difficulty.Hard:
case Difficulty.Impossible: {
// On hard and impossible we try to not ally with all our neighbors (If we have 3+ neighbors)
const borderingPlayers = this.player
.neighbors()
@@ -181,6 +189,8 @@ export class AllianceBehavior {
}
return this.player.alliances().length >= this.random.nextInt(2, 5);
}
default:
assertNever(difficulty);
}
}
@@ -195,11 +205,13 @@ export class AllianceBehavior {
this.player.relation(otherPlayer) === Relation.Friendly &&
this.random.nextInt(0, 100) >= 17
);
default:
case Difficulty.Impossible:
return (
this.player.relation(otherPlayer) === Relation.Friendly &&
this.random.nextInt(0, 100) >= 33
);
default:
assertNever(difficulty);
}
}
@@ -222,11 +234,13 @@ export class AllianceBehavior {
otherPlayer.troops() >
this.player.troops() * (this.random.nextInt(75, 85) / 100)
);
default:
case Difficulty.Impossible:
return (
otherPlayer.troops() >
this.player.troops() * (this.random.nextInt(80, 90) / 100)
);
default:
assertNever(difficulty);
}
}
}
@@ -9,6 +9,7 @@ import {
} from "../../game/Game";
import { PseudoRandom } from "../../PseudoRandom";
import {
assertNever,
boundingBoxCenter,
calculateBoundingBoxCenter,
flattenedEmojiTable,
@@ -26,7 +27,7 @@ const EMOJI_TARGET_ME = (["🥺", "💀"] as const).map(emojiId);
const EMOJI_TARGET_ALLY = (["🕊️", "👎"] as const).map(emojiId);
const EMOJI_HECKLE = (["🤡", "😡"] as const).map(emojiId);
export class BotBehavior {
export class AiAttackBehavior {
private botAttackTroopsSent: number = 0;
private readonly lastEmojiSent = new Map<Player, Tick>();
@@ -233,8 +234,11 @@ export class BotBehavior {
case Difficulty.Hard:
return 4;
// On impossible difficulty, attack as much bots as possible in parallel
default:
case Difficulty.Impossible: {
return 100;
}
default:
assertNever(difficulty);
}
}
@@ -380,7 +384,7 @@ export class BotBehavior {
if (!neighbor.isPlayer()) continue;
if (this.player.isFriendly(neighbor)) continue;
if (
neighbor.type() === PlayerType.FakeHuman ||
neighbor.type() === PlayerType.Nation ||
neighbor.type() === PlayerType.Human
) {
if (this.random.chance(2) || difficulty === Difficulty.Easy) {
+1 -1
View File
@@ -350,7 +350,7 @@ export enum TerrainType {
export enum PlayerType {
Bot = "BOT",
Human = "HUMAN",
FakeHuman = "FAKEHUMAN",
Nation = "NATION",
}
export interface Execution {
+1 -1
View File
@@ -444,7 +444,7 @@ export class PlayerImpl implements Player {
markTraitor(): void {
this.markedTraitorTick = this.mg.ticks();
this._betrayalCount++; // Keep count for FakeHumans too
this._betrayalCount++; // Keep count for Nations too
// Record stats (only for real Humans)
this.mg.stats().betray(this);
+2 -2
View File
@@ -59,7 +59,7 @@ export function assignTeams(
// Then, assign non-clan players to balance teams
let nationPlayers = noClanPlayers.filter(
(player) => player.playerType === PlayerType.FakeHuman,
(player) => player.playerType === PlayerType.Nation,
);
if (nationPlayers.length > 0) {
// Shuffle only nations to randomize their team assignment
@@ -67,7 +67,7 @@ export function assignTeams(
nationPlayers = random.shuffleArray(nationPlayers);
}
const otherPlayers = noClanPlayers.filter(
(player) => player.playerType !== PlayerType.FakeHuman,
(player) => player.playerType !== PlayerType.Nation,
);
for (const player of otherPlayers.concat(nationPlayers)) {
+1 -1
View File
@@ -65,7 +65,7 @@ export class GameManager {
gameType: GameType.Private,
gameMapSize: GameMapSize.Normal,
difficulty: Difficulty.Medium,
disableNPCs: false,
disableNations: false,
infiniteGold: false,
infiniteTroops: false,
maxTimerValue: undefined,
+2 -2
View File
@@ -90,8 +90,8 @@ export class GameServer {
if (gameConfig.difficulty !== undefined) {
this.gameConfig.difficulty = gameConfig.difficulty;
}
if (gameConfig.disableNPCs !== undefined) {
this.gameConfig.disableNPCs = gameConfig.disableNPCs;
if (gameConfig.disableNations !== undefined) {
this.gameConfig.disableNations = gameConfig.disableNations;
}
if (gameConfig.bots !== undefined) {
this.gameConfig.bots = gameConfig.bots;
+1 -1
View File
@@ -99,7 +99,7 @@ export class MapPlaylist {
maxTimerValue: undefined,
instantBuild: false,
randomSpawn: false,
disableNPCs: mode === GameMode.Team && playerTeams !== HumansVsNations,
disableNations: mode === GameMode.Team && playerTeams !== HumansVsNations,
gameMode: mode,
playerTeams,
bots: 400,