From 94dcb67a40eda2d31b145940c2ad4633c6cd0b7d Mon Sep 17 00:00:00 2001 From: ilan schemoul Date: Wed, 26 Mar 2025 23:20:04 +0100 Subject: [PATCH] nukes now reduce attacking troops and transport ships (same as rest of pop) Also made NukeExecution build in two separate steps (build then execute) as for other execution. Otherwise it would instantly explode if I set high speeds. high speeds in necessary for some tests. It implied changing a bit some tests. This includes removing the test that nukes.length == 0 in missile silo which had nothing to do there as we are just checking cooldown. One test should test only one thing. Here it was breaking because I changed the NukeExecution which made no sense. --- src/core/configuration/Config.ts | 9 ++ src/core/configuration/DefaultConfig.ts | 23 ++++- src/core/execution/NukeExecution.ts | 37 ++++---- tests/Attack.test.ts | 114 ++++++++++++++++++++++++ tests/MissileSilo.test.ts | 14 +-- tests/testdata/ocean_and_land.png | Bin 0 -> 120 bytes tests/util/TestConfig.ts | 51 ++++++++++- tests/util/utils.ts | 18 ++-- 8 files changed, 232 insertions(+), 34 deletions(-) create mode 100644 tests/Attack.test.ts create mode 100755 tests/testdata/ocean_and_land.png diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 95b5d7554..9dc185805 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -8,6 +8,7 @@ import { PlayerInfo, TerraNullius, Tick, + Unit, UnitInfo, UnitType, } from "../game/Game"; @@ -45,6 +46,11 @@ export interface ServerConfig { r2SecretKey(): string; } +export interface NukeMagnitude { + inner: number; + outer: number; +} + export interface Config { samHittingChance(): number; spawnImmunityDuration(): Tick; @@ -112,6 +118,9 @@ export interface Config { difficultyModifier(difficulty: Difficulty): number; // 0-1 traitorDefenseDebuff(): number; + nukeMagnitudes(unitType: UnitType): NukeMagnitude; + defaultNukeSpeed(): number; + nukeDeathFactor(humans: number, tilesOwned: number): number; } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 03517c79b..6f9587f5b 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -10,6 +10,7 @@ import { TerrainType, TerraNullius, Tick, + Unit, UnitInfo, UnitType, } from "../game/Game"; @@ -18,7 +19,7 @@ import { PlayerView } from "../game/GameView"; import { UserSettings } from "../game/UserSettings"; import { GameConfig, GameID } from "../Schemas"; import { assertNever, simpleHash, within } from "../Util"; -import { Config, GameEnv, ServerConfig, Theme } from "./Config"; +import { Config, GameEnv, NukeMagnitude, ServerConfig, Theme } from "./Config"; import { pastelTheme } from "./PastelTheme"; import { pastelThemeDark } from "./PastelThemeDark"; @@ -592,4 +593,24 @@ export class DefaultConfig implements Config { } return adjustment; } + + nukeMagnitudes(unitType: UnitType): NukeMagnitude { + switch (unitType) { + case UnitType.MIRVWarhead: + return { inner: 25, outer: 30 }; + case UnitType.AtomBomb: + return { inner: 12, outer: 30 }; + case UnitType.HydrogenBomb: + return { inner: 80, outer: 100 }; + } + } + + defaultNukeSpeed(): number { + return 4; + } + + // Humans can be population, soldiers attacking, soldiers in boat etc. + nukeDeathFactor(humans: number, tilesOwned: number): number { + return (5 * humans) / tilesOwned; + } } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 2935377ef..b9225957a 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -27,7 +27,7 @@ export class NukeExecution implements Execution { private senderID: PlayerID, private dst: TileRef, private src?: TileRef, - private speed: number = 4, + private speed: number = -1, private waitTicks = 0, ) {} @@ -41,6 +41,9 @@ export class NukeExecution implements Execution { this.mg = mg; this.player = mg.player(this.senderID); this.random = new PseudoRandom(ticks); + if (this.speed == -1) { + this.speed = this.mg.config().defaultNukeSpeed(); + } } public target(): Player | TerraNullius { @@ -91,6 +94,7 @@ export class NukeExecution implements Execution { if (silo) { silo.setCooldown(true); } + return; } // make the nuke unactive if it was intercepted @@ -149,19 +153,7 @@ export class NukeExecution implements Execution { } private detonate() { - let magnitude; - switch (this.type) { - case UnitType.MIRVWarhead: - magnitude = { inner: 25, outer: 30 }; - break; - case UnitType.AtomBomb: - magnitude = { inner: 12, outer: 30 }; - break; - case UnitType.HydrogenBomb: - magnitude = { inner: 80, outer: 100 }; - break; - } - + const magnitude = this.mg.config().nukeMagnitudes(this.type); const rand = new PseudoRandom(this.mg.ticks()); const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => { const d = this.mg.euclideanDist(this.dst, n); @@ -174,7 +166,22 @@ export class NukeExecution implements Execution { if (owner.isPlayer()) { const mp = this.mg.player(owner.id()); mp.relinquish(tile); - mp.removeTroops((5 * mp.population()) / mp.numTilesOwned()); + mp.removeTroops( + this.mg.config().nukeDeathFactor(mp.population(), mp.numTilesOwned()), + ); + mp.outgoingAttacks().forEach((attack) => { + const deaths = this.mg + .config() + .nukeDeathFactor(attack.troops(), mp.numTilesOwned()); + attack.setTroops(attack.troops() - deaths); + }); + mp.units(UnitType.TransportShip).forEach((attack) => { + const deaths = this.mg + .config() + .nukeDeathFactor(attack.troops(), mp.numTilesOwned()); + attack.setTroops(attack.troops() - deaths); + }); + if (!attacked.has(mp)) { attacked.set(mp, 0); } diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts new file mode 100644 index 000000000..f964b8b9d --- /dev/null +++ b/tests/Attack.test.ts @@ -0,0 +1,114 @@ +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 { TransportShipExecution } from "../src/core/execution/TransportShipExecution"; +import { TileRef } from "../src/core/game/GameMap"; +import { AttackExecution } from "../src/core/execution/AttackExecution"; +import { TestConfig } from "./util/TestConfig"; + +let game: Game; +let attacker: Player; +let defender: Player; +let defenderSpawn: TileRef; +let attackerSpawn: TileRef; + +function sendBoat(target: TileRef, troops: number) { + game.addExecution( + new TransportShipExecution(defender.id(), null, target, troops), + ); +} + +describe("Attack", () => { + beforeEach(async () => { + game = await setup("ocean_and_land", { + infiniteGold: true, + instantBuild: true, + infiniteTroops: true, + }); + const attackerInfo = new PlayerInfo( + "us", + "attacker dude", + PlayerType.Human, + null, + "attacker_id", + ); + game.addPlayer(attackerInfo, 1000); + const defenderInfo = new PlayerInfo( + "us", + "defender dude", + PlayerType.Human, + null, + "defender_id", + ); + game.addPlayer(defenderInfo, 1000); + + defenderSpawn = game.ref(0, 15); + attackerSpawn = game.ref(0, 10); + + game.addExecution( + new SpawnExecution(game.player(attackerInfo.id).info(), attackerSpawn), + new SpawnExecution(game.player(defenderInfo.id).info(), defenderSpawn), + ); + + while (game.inSpawnPhase()) { + game.executeNextTick(); + } + + attacker = game.player(attackerInfo.id); + defender = game.player(defenderInfo.id); + + game.addExecution(new AttackExecution(100, defender.id(), null)); + game.executeNextTick(); + while (defender.outgoingAttacks().length > 0) { + game.executeNextTick(); + } + + (game.config() as TestConfig).setDefaultNukeSpeed(50); + }); + + test("Nuke reduce attacking troop counts", async () => { + // Not building exactly spawn to it's better protected from attacks (but still + // on defender territory) + constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo); + expect(defender.units(UnitType.MissileSilo)).toHaveLength(1); + game.addExecution(new AttackExecution(100, attacker.id(), defender.id())); + constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3); + const nuke = defender.units(UnitType.AtomBomb)[0]; + expect(nuke.isActive()).toBe(true); + + expect(attacker.outgoingAttacks()).toHaveLength(1); + expect(attacker.outgoingAttacks()[0].troops()).toBe(98); + + // Make the nuke go kaboom + game.executeNextTick(); + expect(nuke.isActive()).toBe(false); + expect(attacker.outgoingAttacks()[0].troops()).not.toBe(97); + expect(attacker.outgoingAttacks()[0].troops()).toBeLessThan(90); + }); + + test("Nuke reduce attacking boat troop count", async () => { + constructionExecution(game, defender.id(), 1, 1, UnitType.MissileSilo); + expect(defender.units(UnitType.MissileSilo)).toHaveLength(1); + + sendBoat(game.ref(15, 8), 100); + + constructionExecution(game, defender.id(), 0, 15, UnitType.AtomBomb, 3); + const nuke = defender.units(UnitType.AtomBomb)[0]; + expect(nuke.isActive()).toBe(true); + + const ship = defender.units(UnitType.TransportShip)[0]; + expect(ship.troops()).toBe(100); + + game.executeNextTick(); + + expect(nuke.isActive()).toBe(false); + expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90); + }); +}); diff --git a/tests/MissileSilo.test.ts b/tests/MissileSilo.test.ts index 707f295a1..1a6c00795 100644 --- a/tests/MissileSilo.test.ts +++ b/tests/MissileSilo.test.ts @@ -56,12 +56,11 @@ describe("MissileSilo", () => { test("missilesilo should launch nuke", async () => { attackerBuildsNuke(null, game.ref(7, 7)); expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); - //because of initilization the nuke already moved so it should be at 204 when starting from 101 - expect(attacker.units(UnitType.AtomBomb)[0].tile()).toBe( - game.map().ref(5, 1), + expect(attacker.units(UnitType.AtomBomb)[0].tile()).not.toBe( + game.map().ref(7, 7), ); - for (let i = 0; i < 3; i++) { + for (let i = 0; i < 5; i++) { game.executeNextTick(); } expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); @@ -79,12 +78,7 @@ describe("MissileSilo", () => { attackerBuildsNuke(null, game.ref(50, 50)); expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1); - for (let i = 0; i < 24; i++) { - game.executeNextTick(); - } - expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0); - - for (let i = 0; i < game.config().SiloCooldown() - 25; i++) { + for (let i = 0; i < game.config().SiloCooldown() - 1; i++) { game.executeNextTick(); expect(attacker.units(UnitType.MissileSilo)[0].isCooldown()).toBeTruthy(); } diff --git a/tests/testdata/ocean_and_land.png b/tests/testdata/ocean_and_land.png new file mode 100755 index 0000000000000000000000000000000000000000..43d7ef32d7d57abdb6e8658f08c8ee15ecb6ea33 GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|j67W&LoEE0 zpZuNve?BAsk259{1?Ss5Uct!2EY7y!fckA#=6nX7!ijHN1DOSWPJZOT!f?OBz2^F< Ryv;z}44$rjF6*2UngDHAB}@PS literal 0 HcmV?d00001 diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts index 5a3493440..40427a90e 100644 --- a/tests/util/TestConfig.ts +++ b/tests/util/TestConfig.ts @@ -1,7 +1,17 @@ +import { NukeMagnitude } from "../../src/core/configuration/Config"; import { DefaultConfig } from "../../src/core/configuration/DefaultConfig"; +import { + Game, + Player, + TerraNullius, + Tick, + UnitType, +} from "../../src/core/game/Game"; +import { TileRef } from "../../src/core/game/GameMap"; export class TestConfig extends DefaultConfig { - _proximityBonusPortsNb: number = 0; + private _proximityBonusPortsNb: number = 0; + private _defaultNukeSpeed: number = 4; samHittingChance(): number { return 1; @@ -19,4 +29,43 @@ export class TestConfig extends DefaultConfig { setProximityBonusPortsNb(nb: number): void { this._proximityBonusPortsNb = nb; } + + nukeMagnitudes(_: UnitType): NukeMagnitude { + return { inner: 1, outer: 1 }; + } + + setDefaultNukeSpeed(speed: number): void { + this._defaultNukeSpeed = speed; + } + + defaultNukeSpeed(): number { + return this._defaultNukeSpeed; + } + + spawnImmunityDuration(): Tick { + return 0; + } + + attackLogic( + gm: Game, + attackTroops: number, + attacker: Player, + defender: Player | TerraNullius, + tileToConquer: TileRef, + ): { + attackerTroopLoss: number; + defenderTroopLoss: number; + tilesPerTickUsed: number; + } { + return { attackerTroopLoss: 1, defenderTroopLoss: 1, tilesPerTickUsed: 1 }; + } + + attackTilesPerTick( + attackTroops: number, + attacker: Player, + defender: Player | TerraNullius, + numAdjacentTilesWithEnemy: number, + ): number { + return 1; + } } diff --git a/tests/util/utils.ts b/tests/util/utils.ts index e3175423e..34210b9d8 100644 --- a/tests/util/utils.ts +++ b/tests/util/utils.ts @@ -13,14 +13,18 @@ export function constructionExecution( x: number, y: number, unit: UnitType, + ticks = 4, ) { game.addExecution(new ConstructionExecution(playerID, game.ref(x, y), unit)); - // Init exec - game.executeNextTick(); + + // 4 ticks by default as it usually goes like this + // Init of construction execution // 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(); + // Tick of construction execution which adds the execution related to the building/unit + // First tick of the execution of the constructed building/unit + // (sometimes step 3 and 4 are merged in one) + + for (let i = 0; i < ticks; i++) { + game.executeNextTick(); + } }