nukes now reduce attacking troops and transport ships (same as rest of pop) (#350)

Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
Ilan Schemoul
2025-03-31 21:39:18 +02:00
committed by GitHub
parent 745017aee2
commit 11791719e4
8 changed files with 277 additions and 57 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) / Math.max(1, tilesOwned);
}
}
+67 -38
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,12 +41,55 @@ 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 {
return this.mg.owner(this.dst);
}
private tilesToDestroy(): Set<TileRef> {
const magnitude = this.mg.config().nukeMagnitudes(this.nuke.type());
const rand = new PseudoRandom(this.mg.ticks());
return this.mg.bfs(this.dst, (_, n: TileRef) => {
const d = this.mg.euclideanDist(this.dst, n);
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
});
}
private getAttackedTiles() {
const toDestroy = this.tilesToDestroy();
const attacked = new Map<Player, number>();
for (const tile of toDestroy) {
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
const mp = this.mg.player(owner.id());
const prev = attacked.get(mp) ?? 0;
attacked.set(mp, prev + 1);
}
}
return attacked;
}
private breakAlliances() {
for (const [other, tilesDestroyed] of this.getAttackedTiles()) {
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
// Mirv warheads shouldn't break alliances
const alliance = this.player.allianceWith(other);
if (alliance != null) {
this.player.breakAlliance(alliance);
}
if (other != this.player) {
other.updateRelation(this.player, -100);
}
}
}
}
tick(ticks: number): void {
if (this.nuke == null) {
const spawn = this.src ?? this.player.canBuild(this.type, this.dst);
@@ -91,6 +134,7 @@ export class NukeExecution implements Execution {
if (silo) {
silo.setCooldown(true);
}
return;
}
// make the nuke unactive if it was intercepted
@@ -100,6 +144,8 @@ export class NukeExecution implements Execution {
return;
}
this.breakAlliances();
if (this.waitTicks > 0) {
this.waitTicks--;
return;
@@ -149,55 +195,38 @@ 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.nuke.type());
const toDestroy = this.tilesToDestroy();
const rand = new PseudoRandom(this.mg.ticks());
const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => {
const d = this.mg.euclideanDist(this.dst, n);
return (d <= magnitude.inner || rand.chance(2)) && d <= magnitude.outer;
});
const attacked = new Map<Player, number>();
for (const tile of toDestroy) {
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
const mp = this.mg.player(owner.id());
mp.relinquish(tile);
mp.removeTroops((5 * mp.population()) / mp.numTilesOwned());
if (!attacked.has(mp)) {
attacked.set(mp, 0);
}
const prev = attacked.get(mp);
attacked.set(mp, prev + 1);
mp.removeTroops(
this.mg.config().nukeDeathFactor(mp.troops(), mp.numTilesOwned()),
);
mp.removeWorkers(
this.mg.config().nukeDeathFactor(mp.workers(), 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 (this.mg.isLand(tile)) {
this.mg.setFallout(tile, true);
}
}
for (const [other, tilesDestroyed] of attacked) {
if (tilesDestroyed > 100 && this.nuke.type() != UnitType.MIRVWarhead) {
// Mirv warheads shouldn't break alliances
const alliance = this.player.allianceWith(other);
if (alliance != null) {
this.player.breakAlliance(alliance);
}
if (other != this.player) {
other.updateRelation(this.player, -100);
}
}
}
for (const unit of this.mg.units()) {
if (
+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();
}
}