mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:10:42 +00:00
Fix Race conditions on alliances (#1605)
## Description: Players received "traitor" debuff when alliances were formed after attacks started, creating an unfair race condition. the problem was mentioned here https://discord.com/channels/1284581928254701718/1399115120486912100 ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: Kipstzz --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import { FlatBinaryHeap } from "./utils/FlatBinaryHeap"; // adjust path if neede
|
||||
const malusForRetreat = 25;
|
||||
export class AttackExecution implements Execution {
|
||||
private breakAlliance = false;
|
||||
private wasAlliedAtInit = false; // Store alliance state at initialization
|
||||
private active: boolean = true;
|
||||
private toConquer = new FlatBinaryHeap();
|
||||
|
||||
@@ -147,8 +148,9 @@ export class AttackExecution implements Execution {
|
||||
}
|
||||
|
||||
if (this.target.isPlayer()) {
|
||||
if (this._owner.isAlliedWith(this.target)) {
|
||||
// No updates should happen in init.
|
||||
// Store the alliance state at initialization time to prevent race conditions
|
||||
this.wasAlliedAtInit = this._owner.isAlliedWith(this.target);
|
||||
if (this.wasAlliedAtInit) {
|
||||
this.breakAlliance = true;
|
||||
}
|
||||
this.target.updateRelation(this._owner, -80);
|
||||
@@ -226,8 +228,13 @@ export class AttackExecution implements Execution {
|
||||
this.breakAlliance = false;
|
||||
this._owner.breakAlliance(alliance);
|
||||
}
|
||||
if (targetPlayer && this._owner.isAlliedWith(targetPlayer)) {
|
||||
if (
|
||||
targetPlayer &&
|
||||
this._owner.isAlliedWith(targetPlayer) &&
|
||||
!this.wasAlliedAtInit
|
||||
) {
|
||||
// In this case a new alliance was created AFTER the attack started.
|
||||
// We should retreat to avoid the attacker becoming a traitor.
|
||||
this.retreat();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,3 +112,147 @@ describe("Attack", () => {
|
||||
expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Attack race condition with alliance requests", () => {
|
||||
it("should not mark attacker as traitor when alliance is formed after attack starts", async () => {
|
||||
const game = await setup("ocean_and_land", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
|
||||
const playerAInfo = new PlayerInfo(
|
||||
"playerA",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"playerA_id",
|
||||
);
|
||||
const playerBInfo = new PlayerInfo(
|
||||
"playerB",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"playerB_id",
|
||||
);
|
||||
|
||||
game.addPlayer(playerAInfo);
|
||||
game.addPlayer(playerBInfo);
|
||||
|
||||
const playerA = game.player(playerAInfo.id);
|
||||
const playerB = game.player(playerBInfo.id);
|
||||
|
||||
// Spawn both players
|
||||
const spawnA = game.ref(0, 10);
|
||||
const spawnB = game.ref(0, 15);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(playerAInfo, spawnA),
|
||||
new SpawnExecution(playerBInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Player A sends alliance request to Player B
|
||||
const allianceRequest = playerA.createAllianceRequest(playerB);
|
||||
expect(allianceRequest).not.toBeNull();
|
||||
|
||||
// Player A attacks Player B
|
||||
const attackExecution = new AttackExecution(
|
||||
null,
|
||||
playerA,
|
||||
playerB.id(),
|
||||
null,
|
||||
);
|
||||
game.addExecution(attackExecution);
|
||||
|
||||
// Player B counter-attacks Player A
|
||||
const counterAttackExecution = new AttackExecution(
|
||||
null,
|
||||
playerB,
|
||||
playerA.id(),
|
||||
null,
|
||||
);
|
||||
game.addExecution(counterAttackExecution);
|
||||
|
||||
// Player B accepts the alliance request
|
||||
if (allianceRequest) {
|
||||
allianceRequest.accept();
|
||||
}
|
||||
|
||||
// Execute a few ticks to process the attacks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Player A should not be marked as traitor because the alliance was formed after the attack started
|
||||
expect(playerA.isTraitor()).toBe(false);
|
||||
|
||||
// The attacks should have retreated due to the alliance being formed
|
||||
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
||||
expect(playerB.outgoingAttacks()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should mark attacker as traitor when alliance existed before attack", async () => {
|
||||
const game = await setup("ocean_and_land", {
|
||||
infiniteGold: true,
|
||||
instantBuild: true,
|
||||
infiniteTroops: true,
|
||||
});
|
||||
|
||||
const playerAInfo = new PlayerInfo(
|
||||
"playerA",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"playerA_id",
|
||||
);
|
||||
const playerBInfo = new PlayerInfo(
|
||||
"playerB",
|
||||
PlayerType.Human,
|
||||
null,
|
||||
"playerB_id",
|
||||
);
|
||||
|
||||
game.addPlayer(playerAInfo);
|
||||
game.addPlayer(playerBInfo);
|
||||
|
||||
const playerA = game.player(playerAInfo.id);
|
||||
const playerB = game.player(playerBInfo.id);
|
||||
|
||||
// Spawn both players
|
||||
const spawnA = game.ref(0, 10);
|
||||
const spawnB = game.ref(0, 15);
|
||||
|
||||
game.addExecution(
|
||||
new SpawnExecution(playerAInfo, spawnA),
|
||||
new SpawnExecution(playerBInfo, spawnB),
|
||||
);
|
||||
|
||||
while (game.inSpawnPhase()) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Create an alliance between Player A and Player B
|
||||
const allianceRequest = playerA.createAllianceRequest(playerB);
|
||||
if (allianceRequest) {
|
||||
allianceRequest.accept();
|
||||
}
|
||||
|
||||
// Player A attacks Player B (should break the alliance)
|
||||
const attackExecution = new AttackExecution(
|
||||
null,
|
||||
playerA,
|
||||
playerB.id(),
|
||||
null,
|
||||
);
|
||||
game.addExecution(attackExecution);
|
||||
|
||||
// Execute a few ticks to process the attack
|
||||
for (let i = 0; i < 10; i++) {
|
||||
game.executeNextTick();
|
||||
}
|
||||
|
||||
// Player A should be marked as traitor because they attacked an ally
|
||||
expect(playerA.isTraitor()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user