mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-07 02:36:47 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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``;
|
||||
|
||||
@@ -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
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 &&
|
||||
+21
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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) {
|
||||
@@ -350,7 +350,7 @@ export enum TerrainType {
|
||||
export enum PlayerType {
|
||||
Bot = "BOT",
|
||||
Human = "HUMAN",
|
||||
FakeHuman = "FAKEHUMAN",
|
||||
Nation = "NATION",
|
||||
}
|
||||
|
||||
export interface Execution {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user