diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 77ad21327..65ceb7ec1 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -91,6 +91,11 @@ export class TransportShipExecution implements Execution { } } + if (this.target === this.attacker) { + this.active = false; + return; + } + if (this.target.isPlayer() && !this.attacker.canAttackPlayer(this.target)) { this.active = false; return; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index d9b477b4e..5ac30a257 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -776,6 +776,9 @@ export class PlayerImpl implements Player { } canDonateGold(recipient: Player): boolean { + if (recipient === this) { + return false; + } if ( !this.isAlive() || !recipient.isAlive() || @@ -803,6 +806,9 @@ export class PlayerImpl implements Player { } canDonateTroops(recipient: Player): boolean { + if (recipient === this) { + return false; + } if ( !this.isAlive() || !recipient.isAlive() || @@ -830,6 +836,9 @@ export class PlayerImpl implements Player { } donateTroops(recipient: Player, troops: number): boolean { + // Defense-in-depth: canDonateTroops already checks this, but guard here too + // to prevent self-donation if the method is called directly. + if (recipient === this) return false; if (troops <= 0) return false; const removed = this.removeTroops(troops); if (removed === 0) return false; @@ -847,6 +856,9 @@ export class PlayerImpl implements Player { } donateGold(recipient: Player, gold: Gold): boolean { + // Defense-in-depth: canDonateGold already checks this, but guard here too + // to prevent self-donation if the method is called directly. + if (recipient === this) return false; if (gold <= 0n) return false; const removed = this.removeGold(gold); if (removed === 0n) return false; @@ -969,6 +981,9 @@ export class PlayerImpl implements Player { } isFriendly(other: Player, treatAFKFriendly: boolean = false): boolean { + if (other === this) { + return true; + } if (other.isDisconnected() && !treatAFKFriendly) { return false; } diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts index 65ecfa44c..a376de974 100644 --- a/tests/Attack.test.ts +++ b/tests/Attack.test.ts @@ -527,4 +527,19 @@ describe("Attack immunity", () => { constructionExecution(game, playerA, 0, 11, UnitType.AtomBomb, 3); expect(playerA.units(UnitType.AtomBomb)).toHaveLength(1); }); + + test("Should abort TransportShipExecution when target is the attacker itself", async () => { + // Wait for spawn immunity to end to ensure it doesn't prematurely abort the execution + waitForImmunityToEnd(); + + // playerA tries to send a transport ship targeting one of playerA's own tiles (spawn tile at 7, 0) + const selfTarget = game.ref(7, 0); + const exec = new TransportShipExecution(playerA, selfTarget, 10); + game.addExecution(exec); + game.executeNextTick(); + + // Verify it aborted immediately: active is false, and no transport ship unit spawned + expect(exec.isActive()).toBe(false); + expect(playerA.units(UnitType.TransportShip)).toHaveLength(0); + }); }); diff --git a/tests/Donate.test.ts b/tests/Donate.test.ts index d9ff2c0c7..107e75ac0 100644 --- a/tests/Donate.test.ts +++ b/tests/Donate.test.ts @@ -239,3 +239,51 @@ describe("Donate Gold to a non ally", () => { expect(recipient.gold() >= recipientGoldBefore).toBe(true); }); }); + +describe("Self donation prevention", () => { + it("Should evaluate isFriendly(this) to true but disallow donating to self", async () => { + const game = await setup("ocean_and_land", { + infiniteGold: false, + infiniteTroops: false, + donateGold: true, + donateTroops: true, + }); + const gameID: GameID = "game_id"; + + // Create a player with team=0/null (default/FFA) + const playerInfo = new PlayerInfo( + "player_self", + PlayerType.Human, + null, + "self_id", + ); + game.addPlayer(playerInfo); + + const player = game.player(playerInfo.id); + const spawnA = game.ref(0, 10); + + game.addExecution(new SpawnExecution(gameID, playerInfo, spawnA)); + game.executeNextTick(); + + // Assert player.isFriendly(player) === true + expect(player.isFriendly(player)).toBe(true); + + // Assert canDonateGold and canDonateTroops return false for self + expect(player.canDonateGold(player)).toBe(false); + expect(player.canDonateTroops(player)).toBe(false); + + // Try executing DonateGoldExecution and DonateTroopsExecution on self + player.addGold(1000n); + player.addTroops(1000); + const goldBefore = player.gold(); + const troopsBefore = player.troops(); + + game.addExecution(new DonateGoldExecution(player, player.id(), 500)); + game.addExecution(new DonateTroopsExecution(player, player.id(), 500)); + game.executeNextTick(); + + // Verify no changes occurred to gold or troops (execution failed/aborted) + expect(player.gold()).toBeGreaterThanOrEqual(goldBefore); + expect(player.troops()).toBeGreaterThanOrEqual(troopsBefore); + }); +});