mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:10:42 +00:00
Fix "Better troop management for nations 🤖" (#4265)
## Description: There was a check missing... The troop management stuff should be disabled for team games because nations can expect donations in that case, and its mainly relevant for FFAs. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
This commit is contained in:
@@ -851,12 +851,15 @@ export class AiAttackBehavior {
|
||||
}
|
||||
|
||||
/**
|
||||
* For Hard & Impossible nations: returns true if `troops` is less than 20%
|
||||
* of the target's troop count, meaning the attack is too weak to be
|
||||
* worthwhile. Bots are exempt.
|
||||
* For Hard & Impossible nations in FFA: returns true if `troops` is less
|
||||
* than 20% of the target's troop count, meaning the attack is too weak to
|
||||
* be worthwhile. Bots and team games are exempt.
|
||||
*/
|
||||
private isAttackTooWeak(troops: number, target: Player): boolean {
|
||||
if (this.player.type() === PlayerType.Bot) return false;
|
||||
if (this.game.config().gameConfig().gameMode === GameMode.Team)
|
||||
return false;
|
||||
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
return (
|
||||
(difficulty === Difficulty.Hard ||
|
||||
@@ -866,14 +869,18 @@ export class AiAttackBehavior {
|
||||
}
|
||||
|
||||
/**
|
||||
* For Hard & Impossible nations: computes the max troops this nation can send
|
||||
* in an attack without letting its troop count drop below a fraction of its
|
||||
* strongest non-allied neighbor's troop count (Hard: 75%, Impossible: 90%).
|
||||
* Allied players and bot neighbors are not considered threats.
|
||||
* Bots are entirely exempt. Returns Infinity when no cap applies.
|
||||
* For Hard & Impossible nations in FFA: computes the max troops this nation
|
||||
* can send in an attack without letting its troop count drop below a
|
||||
* fraction of its strongest non-allied neighbor's troop count (Hard: 75%,
|
||||
* Impossible: 90%). Allied players and bot neighbors are not considered
|
||||
* threats. Bots and team games are entirely exempt. Returns Infinity when
|
||||
* no cap applies.
|
||||
*/
|
||||
private troopSendCap(): number {
|
||||
if (this.player.type() === PlayerType.Bot) return Infinity;
|
||||
if (this.game.config().gameConfig().gameMode === GameMode.Team)
|
||||
return Infinity;
|
||||
|
||||
const { difficulty } = this.game.config().gameConfig();
|
||||
let retainFraction: number;
|
||||
switch (difficulty) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AiAttackBehavior } from "../src/core/execution/utils/AiAttackBehavior";
|
||||
import {
|
||||
Difficulty,
|
||||
Game,
|
||||
GameMode,
|
||||
Player,
|
||||
PlayerInfo,
|
||||
PlayerType,
|
||||
@@ -406,4 +407,164 @@ describe("Hard/Impossible troop floor", () => {
|
||||
// No cap applies, so troops should be the full reserve amount
|
||||
expect(exec.startTroops).toBeGreaterThan(40_000);
|
||||
});
|
||||
|
||||
it("Team: troopSendCap returns Infinity — no cap in team games", async () => {
|
||||
// Same setup as Hard cap test but with GameMode.Team
|
||||
const testGame = await setup("big_plains", {
|
||||
difficulty: Difficulty.Hard,
|
||||
gameMode: GameMode.Team,
|
||||
playerTeams: 2,
|
||||
});
|
||||
|
||||
const attackerInfo = new PlayerInfo(
|
||||
"attacker",
|
||||
PlayerType.Nation,
|
||||
null,
|
||||
"attacker_id",
|
||||
);
|
||||
const neighborInfo = new PlayerInfo(
|
||||
"neighbor",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"neighbor_id",
|
||||
);
|
||||
const botInfo = new PlayerInfo(
|
||||
"target_bot",
|
||||
PlayerType.Bot,
|
||||
null,
|
||||
"bot_id",
|
||||
);
|
||||
testGame.addPlayer(attackerInfo);
|
||||
testGame.addPlayer(neighborInfo);
|
||||
testGame.addPlayer(botInfo);
|
||||
|
||||
const attacker = testGame.player("attacker_id");
|
||||
const neighbor = testGame.player("neighbor_id");
|
||||
const bot = testGame.player("bot_id");
|
||||
|
||||
let assigned = 0;
|
||||
testGame.map().forEachTile((tile) => {
|
||||
if (assigned >= 90) return;
|
||||
if (!testGame.map().isLand(tile)) return;
|
||||
const players = [attacker, neighbor, bot];
|
||||
players[assigned % 3].conquer(tile);
|
||||
assigned++;
|
||||
});
|
||||
bot.addTroops(100);
|
||||
|
||||
const mockEmoji = {
|
||||
maybeSendAttackEmoji: vi.fn(),
|
||||
sendEmoji: vi.fn(),
|
||||
} as any;
|
||||
const mockAlliance = { maybeBetray: vi.fn() } as any;
|
||||
|
||||
const behavior = new AiAttackBehavior(
|
||||
new PseudoRandom(42),
|
||||
testGame,
|
||||
attacker,
|
||||
0.5,
|
||||
0.3,
|
||||
0.2,
|
||||
mockAlliance,
|
||||
mockEmoji,
|
||||
);
|
||||
|
||||
// In FFA Hard, attacker with 100k and neighbor with 90k would cap
|
||||
// attack troops to 32.5k. In Team mode, troopSendCap returns Infinity
|
||||
// so the attack is not capped by neighbor strength.
|
||||
attacker.addTroops(100_000);
|
||||
neighbor.addTroops(90_000);
|
||||
|
||||
const addExecSpy = vi.spyOn(testGame, "addExecution");
|
||||
const result = behavior.sendAttack(bot);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const exec = addExecSpy.mock.calls.find(
|
||||
(c) => c[0].constructor.name === "AttackExecution",
|
||||
)?.[0] as any;
|
||||
expect(exec).toBeDefined();
|
||||
// In FFA Hard, troops would be capped to 32.5k. In Team mode, no cap.
|
||||
expect(exec.startTroops).toBeGreaterThan(32_500);
|
||||
});
|
||||
|
||||
it("Team: isAttackTooWeak returns false — weak attacks allowed in team games", async () => {
|
||||
// Same setup as the FFA "skips attack when capped troops are < 20%" test
|
||||
// but with GameMode.Team. In FFA Hard, the attack would be blocked.
|
||||
const testGame = await setup("big_plains", {
|
||||
difficulty: Difficulty.Hard,
|
||||
gameMode: GameMode.Team,
|
||||
playerTeams: 2,
|
||||
});
|
||||
|
||||
const attackerInfo = new PlayerInfo(
|
||||
"attacker",
|
||||
PlayerType.Nation,
|
||||
null,
|
||||
"attacker_id",
|
||||
);
|
||||
const neighborInfo = new PlayerInfo(
|
||||
"neighbor",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"neighbor_id",
|
||||
);
|
||||
testGame.addPlayer(attackerInfo);
|
||||
testGame.addPlayer(neighborInfo);
|
||||
|
||||
const attacker = testGame.player("attacker_id");
|
||||
const neighbor = testGame.player("neighbor_id");
|
||||
|
||||
// Add a strong human target sharing borders
|
||||
const targetInfo = new PlayerInfo(
|
||||
"strong_target",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"target_id",
|
||||
);
|
||||
testGame.addPlayer(targetInfo);
|
||||
const target = testGame.player("target_id");
|
||||
|
||||
let assigned = 0;
|
||||
testGame.map().forEachTile((tile) => {
|
||||
if (assigned >= 90) return;
|
||||
if (!testGame.map().isLand(tile)) return;
|
||||
const players = [attacker, neighbor, target];
|
||||
players[assigned % 3].conquer(tile);
|
||||
assigned++;
|
||||
});
|
||||
|
||||
const mockEmoji = {
|
||||
maybeSendAttackEmoji: vi.fn(),
|
||||
sendEmoji: vi.fn(),
|
||||
} as any;
|
||||
const mockAlliance = { maybeBetray: vi.fn() } as any;
|
||||
|
||||
const behavior = new AiAttackBehavior(
|
||||
new PseudoRandom(42),
|
||||
testGame,
|
||||
attacker,
|
||||
0.5,
|
||||
0.3,
|
||||
0.2,
|
||||
mockAlliance,
|
||||
mockEmoji,
|
||||
);
|
||||
|
||||
attacker.addTroops(100_000);
|
||||
neighbor.addTroops(100_000);
|
||||
target.addTroops(300_000);
|
||||
// In FFA Hard: troopSendCap = 25k, 20% of target = 60k → blocked.
|
||||
// In Team mode: isAttackTooWeak returns false, so the attack proceeds
|
||||
// even though troops would be below 20% of the target.
|
||||
|
||||
const addExecSpy = vi.spyOn(testGame, "addExecution");
|
||||
const result = behavior.sendAttack(target);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const exec = addExecSpy.mock.calls.find(
|
||||
(c) => c[0].constructor.name === "AttackExecution",
|
||||
)?.[0] as any;
|
||||
expect(exec).toBeDefined();
|
||||
expect(exec.startTroops).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user