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.
This commit is contained in:
ilan schemoul
2025-03-26 23:20:04 +01:00
committed by Evan
parent f12d14fd22
commit 94dcb67a40
8 changed files with 232 additions and 34 deletions
+9
View File
@@ -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 {
+22 -1
View File
@@ -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;
}
}
+22 -15
View File
@@ -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);
}
+114
View File
@@ -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);
});
});
+4 -10
View File
@@ -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();
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

+50 -1
View File
@@ -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;
}
}
+11 -7
View File
@@ -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();
}
}