sam do not target twice same nuke (#270)

This commit is contained in:
Ilan Schemoul
2025-03-21 18:17:33 +01:00
committed by GitHub
parent d4ddffe1eb
commit 68621f326a
11 changed files with 200 additions and 33 deletions
+2
View File
@@ -46,6 +46,8 @@ export interface ServerConfig {
}
export interface Config {
samHittingChance(): number;
samCooldown(): Tick;
spawnImmunityDuration(): Tick;
serverConfig(): ServerConfig;
gameConfig(): GameConfig;
+8
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+25 -9
View File
@@ -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(),
),
);
}
+8 -18
View File
@@ -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:
+3
View File
@@ -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;
+10 -1
View File
@@ -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;
}
}
+132
View File
@@ -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);
});
});
+5 -1
View File
@@ -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;
}
}
+5 -2
View File
@@ -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();
}