implement mirv

This commit is contained in:
Evan
2025-02-03 19:17:08 -08:00
parent e750afcd65
commit b643a6357b
10 changed files with 278 additions and 50 deletions
+22 -1
View File
@@ -81,7 +81,7 @@ export class UnitLayer implements Layer {
this.canvas.width = this.game.width();
this.canvas.height = this.game.height();
for (const unit of this.game.units()) {
// this.onUnitEvent(new UnitEvent(unit, unit.tile()))
this.onUnitEvent(unit);
}
}
@@ -112,8 +112,12 @@ export class UnitLayer implements Layer {
case UnitType.TradeShip:
this.handleTradeShipEvent(unit);
break;
case UnitType.MIRVWarhead:
this.handleMIRVWarhead(unit);
break;
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
case UnitType.MIRV:
this.handleNuke(unit);
break;
}
@@ -228,6 +232,23 @@ export class UnitLayer implements Layer {
}
}
private handleMIRVWarhead(unit: UnitView) {
const rel = this.relationship(unit);
this.clearCell(this.game.x(unit.lastTile()), this.game.y(unit.lastTile()));
if (unit.isActive()) {
// Paint area
this.paintCell(
this.game.x(unit.tile()),
this.game.y(unit.tile()),
rel,
this.theme.borderColor(unit.owner().info()),
255
);
}
}
private handleTradeShipEvent(unit: UnitView) {
const rel = this.relationship(unit);
@@ -28,7 +28,7 @@ interface BuildItemDisplay {
const buildTable: BuildItemDisplay[][] = [
[
{ unitType: UnitType.AtomBomb, icon: atomBombIcon },
{ unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon },
{ unitType: UnitType.MIRV, icon: hydrogenBombIcon },
{ unitType: UnitType.Warship, icon: warshipIcon },
{ unitType: UnitType.Port, icon: portIcon },
{ unitType: UnitType.MissileSilo, icon: missileSiloIcon },
+10 -5
View File
@@ -119,6 +119,16 @@ export class DefaultConfig implements Config {
cost: () => 5_000_000,
territoryBound: false,
};
case UnitType.MIRV:
return {
cost: () => 5_000_000,
territoryBound: false,
};
case UnitType.MIRVWarhead:
return {
cost: () => 0,
territoryBound: false,
};
case UnitType.TradeShip:
return {
cost: () => 0,
@@ -330,16 +340,11 @@ export class DefaultConfig implements Config {
populationIncreaseRate(player: Player): number {
let max = this.maxPopulation(player);
// const thing = Math.sqrt(player.population() + player.population() * player.workers())
let toAdd = 10 + Math.pow(player.population(), 0.73) / 4;
const ratio = 1 - player.population() / max;
toAdd *= ratio;
if (player.type() == PlayerType.FakeHuman) {
toAdd *= 1.0;
}
if (player.type() == PlayerType.Bot) {
toAdd *= 0.7;
}
+6 -6
View File
@@ -45,10 +45,10 @@ export class DevConfig extends DefaultConfig {
// return 5000
// }
numBots(): number {
return 0;
}
spawnNPCs(): boolean {
return false;
}
// numBots(): number {
// return 0;
// }
// spawnNPCs(): boolean {
// return false;
// }
}
+6 -1
View File
@@ -37,6 +37,7 @@ import { MissileSiloExecution } from "./MissileSiloExecution";
import { DefensePostExecution } from "./DefensePostExecution";
import { CityExecution } from "./CityExecution";
import { TileRef } from "../game/GameMap";
import { MirvExecution } from "./MIRVExecution";
export class Executor {
// private random = new PseudoRandom(999)
@@ -113,6 +114,11 @@ export class Executor {
intent.player,
this.mg.ref(intent.x, intent.y)
);
case UnitType.MIRV:
return new MirvExecution(
intent.player,
this.mg.ref(intent.x, intent.y)
);
case UnitType.Warship:
return new WarshipExecution(
intent.player,
@@ -155,7 +161,6 @@ export class Executor {
fakeHumanExecutions(): Execution[] {
const execs = [];
for (const nation of this.mg.nations()) {
console.log(`got nation: ${nation.name}`);
execs.push(
new FakeHumanExecution(
this.gameID,
+160
View File
@@ -0,0 +1,160 @@
import { nextTick } from "process";
import {
Cell,
Execution,
Game,
Player,
PlayerID,
Unit,
UnitType,
TerraNullius,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { PseudoRandom } from "../PseudoRandom";
import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
import { simpleHash } from "../Util";
import { NukeExecution } from "./NukeExecution";
export class MirvExecution implements Execution {
private player: Player;
private active = true;
private mg: Game;
private nuke: Unit;
private mirvRange = 350;
private warheadCount = 1000;
// private warheadRange = 5;
private random: PseudoRandom;
private pathFinder: PathFinder;
private targetPlayer: Player | TerraNullius;
constructor(private senderID: PlayerID, private dst: TileRef) {}
init(mg: Game, ticks: number): void {
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.senderID));
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
this.player = mg.player(this.senderID);
this.targetPlayer = this.mg.owner(this.dst);
}
tick(ticks: number): void {
if (this.nuke == null) {
const spawn = this.player.canBuild(UnitType.MIRV, this.dst);
if (spawn == false) {
consolex.warn(`cannot build MIRV`);
this.active = false;
return;
}
this.nuke = this.player.buildUnit(UnitType.MIRV, 0, spawn);
}
for (let i = 0; i < 4; i++) {
const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst);
switch (result.type) {
case PathFindResultType.Completed:
this.nuke.move(result.tile);
this.separate();
this.active = false;
return;
case PathFindResultType.NextTile:
this.nuke.move(result.tile);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
consolex.warn(
`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`
);
this.active = false;
return;
}
}
}
private separate() {
const dsts: TileRef[] = [this.dst];
let attempts = 1000;
while (attempts > 0 && dsts.length < this.warheadCount) {
attempts--;
const potential = this.randomLand(this.dst);
if (potential == null) {
continue;
}
dsts.push(potential);
}
console.log(`dsts: ${dsts.length}`);
for (const dst of dsts) {
this.mg.addExecution(
new NukeExecution(
UnitType.MIRVWarhead,
this.senderID,
dst,
this.nuke.tile(),
this.random.nextInt(5, 9)
)
);
}
if (this.targetPlayer.isPlayer()) {
const alliance = this.player.allianceWith(this.targetPlayer);
if (alliance != null) {
this.player.breakAlliance(alliance);
}
if (this.targetPlayer != this.player) {
this.targetPlayer.updateRelation(this.player, -100);
}
}
this.nuke.delete(false);
}
randomLand(ref: TileRef): TileRef | null {
let tries = 0;
while (tries < 25) {
tries++;
const x = this.random.nextInt(
this.mg.x(ref) - this.mirvRange,
this.mg.x(ref) + this.mirvRange
);
const y = this.random.nextInt(
this.mg.y(ref) - this.mirvRange,
this.mg.y(ref) + this.mirvRange
);
if (!this.mg.isValidCoord(x, y)) {
continue;
}
const tile = this.mg.ref(x, y);
if (!this.mg.isLand(tile)) {
continue;
}
if (this.mg.euclideanDist(tile, ref) > this.mirvRange) {
continue;
}
if (this.mg.owner(tile) != this.targetPlayer) {
continue;
}
return tile;
}
return null;
}
owner(): Player {
return this.player;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+65 -35
View File
@@ -1,4 +1,3 @@
import { nextTick } from "process";
import {
Cell,
Execution,
@@ -9,32 +8,33 @@ import {
UnitType,
TerraNullius,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { PseudoRandom } from "../PseudoRandom";
import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class NukeExecution implements Execution {
private player: Player;
private active = true;
private mg: Game;
private nuke: Unit;
private pathFinder: PathFinder;
private random: PseudoRandom;
constructor(
private type: UnitType.AtomBomb | UnitType.HydrogenBomb,
private type:
| UnitType.AtomBomb
| UnitType.HydrogenBomb
| UnitType.MIRVWarhead,
private senderID: PlayerID,
private dst: TileRef,
private src?: TileRef,
private speed: number = 4
) {}
init(mg: Game, ticks: number): void {
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
this.player = mg.player(this.senderID);
this.random = new PseudoRandom(ticks);
}
public target(): Player | TerraNullius {
@@ -43,7 +43,7 @@ export class NukeExecution implements Execution {
tick(ticks: number): void {
if (this.nuke == null) {
const spawn = this.player.canBuild(this.type, this.dst);
const spawn = this.src ?? this.player.canBuild(this.type, this.dst);
if (spawn == false) {
consolex.warn(`cannot build Nuke`);
this.active = false;
@@ -52,33 +52,60 @@ export class NukeExecution implements Execution {
this.nuke = this.player.buildUnit(this.type, 0, spawn);
}
for (let i = 0; i < 4; i++) {
const result = this.pathFinder.nextTile(this.nuke.tile(), this.dst);
switch (result.type) {
case PathFindResultType.Completed:
this.nuke.move(result.tile);
this.detonate();
return;
case PathFindResultType.NextTile:
this.nuke.move(result.tile);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
consolex.warn(
`nuke cannot find path from ${this.nuke.tile()} to ${this.dst}`,
);
this.active = false;
return;
for (let i = 0; i < this.speed; i++) {
const x = this.mg.x(this.nuke.tile());
const y = this.mg.y(this.nuke.tile());
const dstX = this.mg.x(this.dst);
const dstY = this.mg.y(this.dst);
// If we've reached the destination, detonate
if (x === dstX && y === dstY) {
this.detonate();
return;
}
// Calculate next position
let nextX = x;
let nextY = y;
const ratio = Math.floor(
1 + Math.abs(dstY - y) / (Math.abs(dstX - x) + 1)
);
if (this.random.chance(ratio) && x != dstX) {
if (x < dstX) nextX++;
else if (x > dstX) nextX--;
} else {
if (y < dstY) nextY++;
else if (y > dstY) nextY--;
}
// Move to next tile
const nextTile = this.mg.ref(nextX, nextY);
if (nextTile !== undefined) {
this.nuke.move(nextTile);
} else {
consolex.warn(`invalid tile position ${nextX},${nextY}`);
this.active = false;
return;
}
}
}
private detonate() {
const magnitude =
this.type == UnitType.AtomBomb
? { inner: 15, outer: 40 }
: { inner: 140, outer: 160 };
let magnitude;
switch (this.type) {
case UnitType.MIRVWarhead:
magnitude = { inner: 10, outer: 14 };
break;
case UnitType.AtomBomb:
magnitude = { inner: 15, outer: 40 };
break;
case UnitType.HydrogenBomb:
magnitude = { inner: 140, outer: 160 };
break;
}
const rand = new PseudoRandom(this.mg.ticks());
const toDestroy = this.mg.bfs(this.dst, (_, n: TileRef) => {
const d = this.mg.euclideanDist(this.dst, n);
@@ -88,7 +115,7 @@ export class NukeExecution implements Execution {
const ratio = Object.fromEntries(
this.mg
.players()
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()])
);
const attacked = new Map<Player, number>();
for (const tile of toDestroy) {
@@ -108,7 +135,8 @@ export class NukeExecution implements Execution {
}
}
for (const [other, tilesDestroyed] of attacked) {
if (tilesDestroyed > 100) {
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);
@@ -122,7 +150,9 @@ export class NukeExecution implements Execution {
for (const unit of this.mg.units()) {
if (
unit.type() != UnitType.AtomBomb &&
unit.type() != UnitType.HydrogenBomb
unit.type() != UnitType.HydrogenBomb &&
unit.type() != UnitType.MIRVWarhead &&
unit.type() != UnitType.MIRV
) {
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
unit.delete();
+3 -1
View File
@@ -59,7 +59,9 @@ export class PlayerExecution implements Execution {
this.player.units().forEach((u) => {
if (
u.type() != UnitType.AtomBomb &&
u.type() != UnitType.HydrogenBomb
u.type() != UnitType.HydrogenBomb &&
u.type() != UnitType.MIRVWarhead &&
u.type() != UnitType.MIRV
) {
u.delete();
}
+2
View File
@@ -71,6 +71,8 @@ export enum UnitType {
MissileSilo = "Missile Silo",
DefensePost = "Defense Post",
City = "City",
MIRV = "MIRV",
MIRVWarhead = "MIRV Warhead",
}
export enum Relation {
+3
View File
@@ -562,9 +562,12 @@ export class PlayerImpl implements Player {
return false;
}
switch (unitType) {
case UnitType.MIRV:
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return this.nukeSpawn(targetTile);
case UnitType.MIRVWarhead:
return targetTile;
case UnitType.Port:
return this.portSpawn(targetTile);
case UnitType.Warship: