mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 22:41:57 +00:00
e1125e0c37
## Description: This PR fixes an exploit that allows the player to request alliances to Nations, mostly in impossible mode, during spawn phase, with high chances for it to be accepted due to troop count parity. Nations now reject alliance requests during the spawn phase. `GameImpl.executeNextTick()` initializes ALL pending `unInitExecs` in one batch on the first post-spawn tick ( `numSpawnPhaseTurns() + 1` ). So every alliance request submitted during spawn phase is guaranteed to be created with `createdAt = numSpawnPhaseTurns() + 1` on the very first post-spawn tick. Therefore, we check for alliance requests created on the very first post-spawn tick and reject those. ## 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: deshack_82603 --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
196 lines
5.4 KiB
TypeScript
196 lines
5.4 KiB
TypeScript
import { NationAllianceBehavior } from "../src/core/execution/nation/NationAllianceBehavior";
|
|
import { NationEmojiBehavior } from "../src/core/execution/nation/NationEmojiBehavior";
|
|
import {
|
|
AllianceRequest,
|
|
Game,
|
|
Player,
|
|
PlayerInfo,
|
|
PlayerType,
|
|
Tick,
|
|
} from "../src/core/game/Game";
|
|
import { PseudoRandom } from "../src/core/PseudoRandom";
|
|
import { setup } from "./util/Setup";
|
|
|
|
let game: Game;
|
|
let player: Player;
|
|
let requestor: Player;
|
|
let allianceBehavior: NationAllianceBehavior;
|
|
|
|
describe("AllianceBehavior.handleAllianceRequests", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("big_plains", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
});
|
|
|
|
const playerInfo = new PlayerInfo(
|
|
"player_id",
|
|
PlayerType.Bot,
|
|
null,
|
|
"player_id",
|
|
);
|
|
const requestorInfo = new PlayerInfo(
|
|
"requestor_id",
|
|
PlayerType.Human,
|
|
null,
|
|
"requestor_id",
|
|
);
|
|
|
|
game.addPlayer(playerInfo);
|
|
game.addPlayer(requestorInfo);
|
|
|
|
player = game.player("player_id");
|
|
requestor = game.player("requestor_id");
|
|
|
|
// Use a fixed random seed for deterministic behavior
|
|
const random = new PseudoRandom(46);
|
|
|
|
allianceBehavior = new NationAllianceBehavior(
|
|
random,
|
|
game,
|
|
player,
|
|
new NationEmojiBehavior(random, game, player),
|
|
);
|
|
|
|
while (game.inSpawnPhase()) {
|
|
game.executeNextTick();
|
|
}
|
|
});
|
|
|
|
function setupAllianceRequest({
|
|
isTraitor = false,
|
|
relationDelta = 2,
|
|
numTilesPlayer = 10,
|
|
numTilesRequestor = 10,
|
|
alliancesCount = 0,
|
|
createdAtTick = game.ticks() + 1,
|
|
} = {}) {
|
|
if (isTraitor) requestor.markTraitor();
|
|
|
|
player.updateRelation(requestor, relationDelta);
|
|
requestor.updateRelation(player, relationDelta);
|
|
|
|
game.map().forEachTile((tile) => {
|
|
if (game.map().isLand(tile)) {
|
|
if (numTilesPlayer > 0) {
|
|
player.conquer(tile);
|
|
numTilesPlayer--;
|
|
} else if (numTilesRequestor > 0) {
|
|
requestor.conquer(tile);
|
|
numTilesRequestor--;
|
|
}
|
|
}
|
|
});
|
|
|
|
vi.spyOn(player, "alliances").mockReturnValue(new Array(alliancesCount));
|
|
|
|
const mockRequest = {
|
|
requestor: () => requestor,
|
|
recipient: () => player,
|
|
createdAt: () => createdAtTick as unknown as Tick,
|
|
accept: vi.fn(),
|
|
reject: vi.fn(),
|
|
} as unknown as AllianceRequest;
|
|
|
|
vi.spyOn(player, "incomingAllianceRequests").mockReturnValue([mockRequest]);
|
|
|
|
return mockRequest;
|
|
}
|
|
|
|
test("should reject alliance created on first post-spawn tick", () => {
|
|
const cutoff = game.config().numSpawnPhaseTurns() + 1;
|
|
const request = setupAllianceRequest({ createdAtTick: cutoff });
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).not.toHaveBeenCalled();
|
|
expect(request.reject).toHaveBeenCalled();
|
|
});
|
|
|
|
test("should accept alliance when all conditions are met", () => {
|
|
const request = setupAllianceRequest({});
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).toHaveBeenCalled();
|
|
expect(request.reject).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should reject alliance if requestor is a traitor", () => {
|
|
const request = setupAllianceRequest({ isTraitor: true });
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).not.toHaveBeenCalled();
|
|
expect(request.reject).toHaveBeenCalled();
|
|
});
|
|
|
|
test("should reject alliance if relation is hostile", () => {
|
|
const request = setupAllianceRequest({ relationDelta: -2 });
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).not.toHaveBeenCalled();
|
|
expect(request.reject).toHaveBeenCalled();
|
|
});
|
|
|
|
test("should accept alliance if requestor is much larger (> 3 times size of recipient)", () => {
|
|
const request = setupAllianceRequest({
|
|
numTilesRequestor: 40,
|
|
});
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).toHaveBeenCalled();
|
|
expect(request.reject).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should reject alliance if player has too many alliances", () => {
|
|
const request = setupAllianceRequest({ alliancesCount: 10 });
|
|
|
|
allianceBehavior.handleAllianceRequests();
|
|
|
|
expect(request.accept).not.toHaveBeenCalled();
|
|
expect(request.reject).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("AllianceBehavior.handleAllianceExtensionRequests", () => {
|
|
let mockGame: any;
|
|
let mockPlayer: any;
|
|
let mockAlliance: any;
|
|
let mockHuman: any;
|
|
let mockRandom: any;
|
|
let allianceBehavior: NationAllianceBehavior;
|
|
|
|
beforeEach(() => {
|
|
mockGame = { addExecution: vi.fn() };
|
|
mockHuman = { id: vi.fn(() => "human_id") };
|
|
mockAlliance = {
|
|
onlyOneAgreedToExtend: vi.fn(() => true),
|
|
other: vi.fn(() => mockHuman),
|
|
};
|
|
mockRandom = { chance: vi.fn() };
|
|
|
|
mockPlayer = {
|
|
alliances: vi.fn(() => [mockAlliance]),
|
|
relation: vi.fn(),
|
|
id: vi.fn(() => "bot_id"),
|
|
type: vi.fn(() => PlayerType.Nation),
|
|
};
|
|
|
|
allianceBehavior = new NationAllianceBehavior(
|
|
mockRandom,
|
|
mockGame,
|
|
mockPlayer,
|
|
new NationEmojiBehavior(mockRandom, mockGame, mockPlayer),
|
|
);
|
|
});
|
|
|
|
it("should NOT request extension if onlyOneAgreedToExtend is false (no expiration yet or both already agreed)", () => {
|
|
mockAlliance.onlyOneAgreedToExtend.mockReturnValue(false);
|
|
allianceBehavior.handleAllianceExtensionRequests();
|
|
expect(mockGame.addExecution).not.toHaveBeenCalled();
|
|
});
|
|
});
|