From 68621f326a84b2247cc10edd117d7d34803d6707 Mon Sep 17 00:00:00 2001 From: Ilan Schemoul Date: Fri, 21 Mar 2025 18:17:33 +0100 Subject: [PATCH] sam do not target twice same nuke (#270) --- src/core/configuration/Config.ts | 2 + src/core/configuration/DefaultConfig.ts | 8 ++ src/core/execution/ConstructionExecution.ts | 2 +- src/core/execution/NukeExecution.ts | 2 +- src/core/execution/SAMLauncherExecution.ts | 34 +++-- src/core/execution/SAMMissileExecution.ts | 26 ++-- src/core/game/Game.ts | 3 + src/core/game/UnitImpl.ts | 11 +- tests/SAM.test.ts | 132 ++++++++++++++++++++ tests/util/TestConfig.ts | 6 +- tests/util/utils.ts | 7 +- 11 files changed, 200 insertions(+), 33 deletions(-) create mode 100644 tests/SAM.test.ts diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 0116b6992..e0cfc2d3a 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -46,6 +46,8 @@ export interface ServerConfig { } export interface Config { + samHittingChance(): number; + samCooldown(): Tick; spawnImmunityDuration(): Tick; serverConfig(): ServerConfig; gameConfig(): GameConfig; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 2a9bece6f..f3e1612f3 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -89,6 +89,14 @@ export class DefaultConfig implements Config { private _userSettings: UserSettings, ) {} + samHittingChance(): number { + return 0.8; + } + + samCooldown(): Tick { + return 100; + } + traitorDefenseDebuff(): number { return 0.8; } diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index efe416033..0f5914f1d 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -55,7 +55,7 @@ export class ConstructionExecution implements Execution { } const spawnTile = this.player.canBuild(this.constructionType, this.tile); if (spawnTile == false) { - consolex.warn(`cannot build ${UnitType.Construction}`); + consolex.warn(`cannot build ${this.constructionType}`); this.active = false; return; } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index f27c47b9d..3efc43525 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -87,7 +87,7 @@ export class NukeExecution implements Execution { // make the nuke unactive if it was intercepted if (!this.nuke.isActive()) { - consolex.warn(`Nuke destroyed before reaching target`); + consolex.log(`Nuke destroyed before reaching target`); this.active = false; return; } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 5f6eb89ea..810e9a5e6 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -7,6 +7,7 @@ import { Unit, PlayerID, UnitType, + MessageType, } from "../game/Game"; import { manhattanDistFN, TileRef } from "../game/GameMap"; import { SAMMissileExecution } from "./SAMMissileExecution"; @@ -22,7 +23,6 @@ export class SAMLauncherExecution implements Execution { private searchRangeRadius = 75; - private missileAttackRate = 75; // 7.5 seconds private lastMissileAttack = 0; private pseudoRandom: PseudoRandom; @@ -100,22 +100,38 @@ export class SAMLauncherExecution implements Execution { const cooldown = this.lastMissileAttack != 0 && - this.mg.ticks() - this.lastMissileAttack <= this.missileAttackRate; - if (this.post.isSamCooldown() != cooldown) { - this.post.setSamCooldown(cooldown); + this.mg.ticks() - this.lastMissileAttack <= + this.mg.config().samCooldown(); + + if (this.post.isSamCooldown() && !cooldown) { + this.post.setSamCooldown(false); } - if (this.target != null) { - if (!this.post.isSamCooldown()) { - this.lastMissileAttack = this.mg.ticks(); + if ( + this.target && + !this.post.isSamCooldown() && + !this.target.targetedBySAM() + ) { + this.lastMissileAttack = this.mg.ticks(); + this.post.setSamCooldown(true); + const random = this.pseudoRandom.next(); + const hit = random < this.mg.config().samHittingChance(); + + this.lastMissileAttack = this.mg.ticks(); + if (!hit) { + this.mg.displayMessage( + `Missile failed to intercept ${this.target.type()}`, + MessageType.ERROR, + this.post.owner().id(), + ); + } else { + this.target.setTargetedBySAM(true); this.mg.addExecution( new SAMMissileExecution( this.post.tile(), this.post.owner(), this.post, this.target, - this.mg, - this.pseudoRandom.next(), ), ); } diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 9a110c3be..1ea88acd4 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -15,20 +15,19 @@ export class SAMMissileExecution implements Execution { private active = true; private pathFinder: PathFinder; private SAMMissile: Unit; + private mg: Game; constructor( private spawn: TileRef, private _owner: Player, private ownerUnit: Unit, private target: Unit, - private mg: Game, - private pseudoRandom: number, private speed: number = 12, - private hittingChance: number = 0.75, ) {} init(mg: Game, ticks: number): void { this.pathFinder = PathFinder.Mini(mg, 2000, true, 10); + this.mg = mg; } tick(ticks: number): void { @@ -63,22 +62,13 @@ export class SAMMissileExecution implements Execution { ); switch (result.type) { case PathFindResultType.Completed: + this.mg.displayMessage( + `Missile intercepted ${this.target.type()}`, + MessageType.SUCCESS, + this._owner.id(), + ); this.active = false; - if (this.pseudoRandom < this.hittingChance) { - this.target.delete(); - - this.mg.displayMessage( - `Missile succesfully intercepted ${this.target.type()}`, - MessageType.SUCCESS, - this._owner.id(), - ); - } else { - this.mg.displayMessage( - `Missile failed to target ${this.target.type()}`, - MessageType.ERROR, - this._owner.id(), - ); - } + this.target.delete(); this.SAMMissile.delete(false); return; case PathFindResultType.NextTile: diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 77c9a7d94..d61143fa8 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -244,6 +244,9 @@ export interface Unit { setMoveTarget(cell: TileRef): void; moveTarget(): TileRef | null; + setTargetedBySAM(targeted: boolean): void; + targetedBySAM(): boolean; + // Mutations setTroops(troops: number): void; delete(displayerMessage?: boolean): void; diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index aaa783508..d31e6b111 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -15,10 +15,11 @@ export class UnitImpl implements Unit { // Currently only warship use it private _target: Unit = null; private _moveTarget: TileRef = null; + private _targetedBySAM = false; private _constructionType: UnitType = undefined; - private _isSamCooldown: boolean; + private _isSamCooldown: boolean = false; private _dstPort: Unit | null = null; // Only for trade ships private _detonationDst: TileRef | null = null; // Only for nukes private _warshipTarget: Unit | null = null; @@ -204,4 +205,12 @@ export class UnitImpl implements Unit { moveTarget(): TileRef | null { return this._moveTarget; } + + setTargetedBySAM(targeted: boolean): void { + this._targetedBySAM = targeted; + } + + targetedBySAM(): boolean { + return this._targetedBySAM; + } } diff --git a/tests/SAM.test.ts b/tests/SAM.test.ts new file mode 100644 index 000000000..3e033c61c --- /dev/null +++ b/tests/SAM.test.ts @@ -0,0 +1,132 @@ +import { + Game, + Player, + PlayerInfo, + PlayerType, + UnitType, +} from "../src/core/game/Game"; +import { SpawnExecution } from "../src/core/execution/SpawnExecution"; +import { setup } from "./util/Setup"; +import { constructionExecution } from "./util/utils"; +import { NukeExecution } from "../src/core/execution/NukeExecution"; +import { TileRef } from "../src/core/game/GameMap"; + +let game: Game; +let attacker: Player; +let defender: Player; + +function attackerBuildsNuke( + source: TileRef, + target: TileRef, + initialize = true, +) { + game.addExecution( + new NukeExecution(UnitType.AtomBomb, attacker.id(), target, source), + ); + if (initialize) { + game.executeNextTick(); + game.executeNextTick(); + } +} + +function defenderBuildsSam(x: number, y: number) { + constructionExecution(game, defender.id(), x, y, UnitType.SAMLauncher); +} + +describe("SAM", () => { + beforeEach(async () => { + game = await setup("Plains", { infiniteGold: true, instantBuild: true }); + const defender_info = new PlayerInfo( + "us", + "defender_id", + PlayerType.Human, + null, + "defender_id", + ); + const attacker_info = new PlayerInfo( + "fr", + "attacker_id", + PlayerType.Human, + null, + "attacker_id", + ); + game.addPlayer(defender_info, 1000); + game.addPlayer(attacker_info, 1000); + + game.addExecution( + new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)), + new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + defender = game.player("defender_id"); + attacker = game.player("attacker_id"); + + constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo); + }); + + test("one sam should take down one nuke", async () => { + defenderBuildsSam(1, 1); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + }); + + test("sam should only get one nuke at a time", async () => { + defenderBuildsSam(1, 1); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1), false); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + }); + + test("sam should cooldown as long as configured", async () => { + defenderBuildsSam(1, 1); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + + for (let i = 0; i < game.config().samCooldown() - 1; i++) { + game.executeNextTick(); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe( + true, + ); + } + + game.executeNextTick(); + expect(defender.units(UnitType.SAMLauncher)[0].isSamCooldown()).toBe(false); + }); + + test("two sams should not target twice same nuke", async () => { + defenderBuildsSam(1, 1); + defenderBuildsSam(1, 2); + attackerBuildsNuke(game.ref(7, 7), game.ref(1, 1)); + + expect(defender.units(UnitType.SAMLauncher)).toHaveLength(2); + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); + + game.executeNextTick(); + game.executeNextTick(); + + expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); + const sams = defender.units(UnitType.SAMLauncher); + // Only one sam must have shot + expect( + (sams[0].isSamCooldown() && !sams[1].isSamCooldown()) || + (sams[1].isSamCooldown() && !sams[0].isSamCooldown()), + ).toBe(true); + }); +}); diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts index e48a63162..1e52658c0 100644 --- a/tests/util/TestConfig.ts +++ b/tests/util/TestConfig.ts @@ -1,3 +1,7 @@ import { DefaultConfig } from "../../src/core/configuration/DefaultConfig"; -export class TestConfig extends DefaultConfig {} +export class TestConfig extends DefaultConfig { + samHittingChance(): number { + return 1; + } +} diff --git a/tests/util/utils.ts b/tests/util/utils.ts index 17ca1a447..e3175423e 100644 --- a/tests/util/utils.ts +++ b/tests/util/utils.ts @@ -15,9 +15,12 @@ export function constructionExecution( unit: UnitType, ) { game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit)); - // Init + // Init exec game.executeNextTick(); - // Exec + // Exec construction execution game.executeNextTick(); + // Add the execution related to the building + game.executeNextTick(); + // First tick of the execution of the constructed structure/unit game.executeNextTick(); }