format codebase with prettier

This commit is contained in:
Evan
2025-01-30 19:46:36 -08:00
parent cd121a5cd4
commit 4ee37323f9
98 changed files with 12191 additions and 10234 deletions
+261 -206
View File
@@ -1,243 +1,298 @@
import { PriorityQueue } from "@datastructures-js/priority-queue";
import { Cell, Execution, Game, Player, PlayerID, PlayerType, TerrainType, TerraNullius } from "../game/Game";
import {
Cell,
Execution,
Game,
Player,
PlayerID,
PlayerType,
TerrainType,
TerraNullius,
} from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { MessageType } from '../game/Game';
import { MessageType } from "../game/Game";
import { renderNumber } from "../../client/Utils";
import { TileRef } from "../game/GameMap";
export class AttackExecution implements Execution {
private breakAlliance = false
private active: boolean = true;
private toConquer: PriorityQueue<TileContainer> = new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
if (a.priority == b.priority) {
if (a.tick == b.tick) {
return 0
// return this.random.nextInt(-1, 1)
}
return a.tick - b.tick
private breakAlliance = false;
private active: boolean = true;
private toConquer: PriorityQueue<TileContainer> =
new PriorityQueue<TileContainer>((a: TileContainer, b: TileContainer) => {
if (a.priority == b.priority) {
if (a.tick == b.tick) {
return 0;
// return this.random.nextInt(-1, 1)
}
return a.priority - b.priority
return a.tick - b.tick;
}
return a.priority - b.priority;
});
private random = new PseudoRandom(123)
private random = new PseudoRandom(123);
private _owner: Player
private target: Player | TerraNullius
private _owner: Player;
private target: Player | TerraNullius;
private mg: Game
private mg: Game;
private border = new Set<TileRef>()
private border = new Set<TileRef>();
constructor(
private troops: number | null,
private _ownerID: PlayerID,
private _targetID: PlayerID | null,
private sourceTile: TileRef | null,
private removeTroops: boolean = true,
) { }
constructor(
private troops: number | null,
private _ownerID: PlayerID,
private _targetID: PlayerID | null,
private sourceTile: TileRef | null,
private removeTroops: boolean = true,
) {}
public targetID(): PlayerID {
return this._targetID
public targetID(): PlayerID {
return this._targetID;
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
if (!this.active) {
return;
}
this.mg = mg;
this._owner = mg.player(this._ownerID);
this.target =
this._targetID == this.mg.terraNullius().id()
? mg.terraNullius()
: mg.player(this._targetID);
if (this._owner == this.target) {
throw new Error(`Player ${this._owner} cannot attack itself`);
}
activeDuringSpawnPhase(): boolean {
return false
if (this.troops == null) {
this.troops = this.mg.config().attackAmount(this._owner, this.target);
}
this.troops = Math.min(this._owner.troops(), this.troops);
if (this.removeTroops) {
this._owner.removeTroops(this.troops);
}
init(mg: Game, ticks: number) {
if (!this.active) {
return
for (const exec of mg.executions()) {
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
const otherAttack = exec as AttackExecution;
// Target has opposing attack, cancel them out
if (
this.target.isPlayer() &&
otherAttack._targetID == this._ownerID &&
this._targetID == otherAttack._ownerID
) {
if (otherAttack.troops > this.troops) {
otherAttack.troops -= this.troops;
// otherAttack.calculateToConquer()
this.active = false;
return;
} else {
this.troops -= otherAttack.troops;
otherAttack.active = false;
}
}
this.mg = mg
this._owner = mg.player(this._ownerID)
this.target = this._targetID == this.mg.terraNullius().id() ? mg.terraNullius() : mg.player(this._targetID)
if (this._owner == this.target) {
throw new Error(`Player ${this._owner} cannot attack itself`)
// Existing attack on same target, add troops
if (
otherAttack._owner == this._owner &&
otherAttack._targetID == this._targetID &&
this.sourceTile == otherAttack.sourceTile
) {
otherAttack.troops += this.troops;
otherAttack.refreshToConquer();
this.active = false;
return;
}
if (this.troops == null) {
this.troops = this.mg.config().attackAmount(this._owner, this.target)
}
this.troops = Math.min(this._owner.troops(), this.troops)
if (this.removeTroops) {
this._owner.removeTroops(this.troops)
}
for (const exec of mg.executions()) {
if (exec.isActive() && exec instanceof AttackExecution && exec != this) {
const otherAttack = exec as AttackExecution
// Target has opposing attack, cancel them out
if (this.target.isPlayer() && otherAttack._targetID == this._ownerID && this._targetID == otherAttack._ownerID) {
if (otherAttack.troops > this.troops) {
otherAttack.troops -= this.troops
// otherAttack.calculateToConquer()
this.active = false
return
} else {
this.troops -= otherAttack.troops
otherAttack.active = false
}
}
// Existing attack on same target, add troops
if (otherAttack._owner == this._owner && otherAttack._targetID == this._targetID && this.sourceTile == otherAttack.sourceTile) {
otherAttack.troops += this.troops
otherAttack.refreshToConquer()
this.active = false
return
}
}
}
if (this._owner.type() != PlayerType.Bot && this.target.isPlayer() && this.target.type() == PlayerType.Human) {
mg.displayMessage(`You are being attacked by ${this._owner.displayName()}`, MessageType.ERROR, this._targetID)
}
if (this.sourceTile != null) {
this.addNeighbors(this.sourceTile)
} else {
this.refreshToConquer()
}
if (this.target.isPlayer()) {
if (this._owner.isAlliedWith(this.target)) {
// No updates should happen in init.
this.breakAlliance = true
}
this.target.updateRelation(this._owner, -80)
}
}
}
if (
this._owner.type() != PlayerType.Bot &&
this.target.isPlayer() &&
this.target.type() == PlayerType.Human
) {
mg.displayMessage(
`You are being attacked by ${this._owner.displayName()}`,
MessageType.ERROR,
this._targetID,
);
}
if (this.sourceTile != null) {
this.addNeighbors(this.sourceTile);
} else {
this.refreshToConquer();
}
private refreshToConquer() {
this.toConquer.clear()
this.border.clear()
for (const tile of this._owner.borderTiles()) {
this.addNeighbors(tile)
}
if (this.target.isPlayer()) {
if (this._owner.isAlliedWith(this.target)) {
// No updates should happen in init.
this.breakAlliance = true;
}
this.target.updateRelation(this._owner, -80);
}
}
private refreshToConquer() {
this.toConquer.clear();
this.border.clear();
for (const tile of this._owner.borderTiles()) {
this.addNeighbors(tile);
}
}
tick(ticks: number) {
if (!this.active) {
return;
}
const alliance = this._owner.allianceWith(this.target as Player);
if (this.breakAlliance && alliance != null) {
this.breakAlliance = false;
this._owner.breakAlliance(alliance);
}
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
// In this case a new alliance was created AFTER the attack started.
this._owner.addTroops(this.troops);
this.active = false;
return;
}
tick(ticks: number) {
if (!this.active) {
return
}
const alliance = this._owner.allianceWith(this.target as Player)
if (this.breakAlliance && alliance != null) {
this.breakAlliance = false
this._owner.breakAlliance(alliance)
}
if (this.target.isPlayer() && this._owner.isAlliedWith(this.target)) {
// In this case a new alliance was created AFTER the attack started.
this._owner.addTroops(this.troops)
this.active = false
return
}
let numTilesPerTick = this.mg
.config()
.attackTilesPerTick(
this.troops,
this._owner,
this.target,
this.border.size + this.random.nextInt(0, 5),
);
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
// consolex.log(`num execs: ${this.mg.executions().length}`)
let numTilesPerTick = this.mg.config().attackTilesPerTick(this.troops, this._owner, this.target, this.border.size + this.random.nextInt(0, 5))
// consolex.log(`num tiles per tick: ${numTilesPerTick}`)
// consolex.log(`num execs: ${this.mg.executions().length}`)
while (numTilesPerTick > 0) {
if (this.troops < 1) {
this.active = false;
return;
}
if (this.toConquer.size() == 0) {
this.refreshToConquer();
this.active = false;
this._owner.addTroops(this.troops);
return;
}
while (numTilesPerTick > 0) {
if (this.troops < 1) {
this.active = false
return
}
const tileToConquer = this.toConquer.dequeue().tile;
this.border.delete(tileToConquer);
if (this.toConquer.size() == 0) {
this.refreshToConquer()
this.active = false
this._owner.addTroops(this.troops)
return
}
const tileToConquer = this.toConquer.dequeue().tile
this.border.delete(tileToConquer)
const onBorder = this.mg.neighbors(tileToConquer).filter(t => this.mg.owner(t) == this._owner).length > 0
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
continue
}
this.addNeighbors(tileToConquer)
const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg.config().attackLogic(this.mg, this.troops, this._owner, this.target, tileToConquer)
numTilesPerTick -= tilesPerTickUsed
this.troops -= attackerTroopLoss
if (this.target.isPlayer()) {
this.target.removeTroops(defenderTroopLoss)
}
this._owner.conquer(tileToConquer)
this.handleDeadDefender()
}
const onBorder =
this.mg
.neighbors(tileToConquer)
.filter((t) => this.mg.owner(t) == this._owner).length > 0;
if (this.mg.owner(tileToConquer) != this.target || !onBorder) {
continue;
}
this.addNeighbors(tileToConquer);
const { attackerTroopLoss, defenderTroopLoss, tilesPerTickUsed } = this.mg
.config()
.attackLogic(
this.mg,
this.troops,
this._owner,
this.target,
tileToConquer,
);
numTilesPerTick -= tilesPerTickUsed;
this.troops -= attackerTroopLoss;
if (this.target.isPlayer()) {
this.target.removeTroops(defenderTroopLoss);
}
this._owner.conquer(tileToConquer);
this.handleDeadDefender();
}
}
private addNeighbors(tile: TileRef) {
for (const neighbor of this.mg.neighbors(tile)) {
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
continue
private addNeighbors(tile: TileRef) {
for (const neighbor of this.mg.neighbors(tile)) {
if (this.mg.isWater(neighbor) || this.mg.owner(neighbor) != this.target) {
continue;
}
this.border.add(neighbor);
let numOwnedByMe = this.mg
.neighbors(neighbor)
.filter((t) => this.mg.owner(t) == this._owner).length;
let dist = 0;
if (numOwnedByMe > 2) {
numOwnedByMe = 10;
}
let mag = 0;
switch (this.mg.terrainType(tile)) {
case TerrainType.Plains:
mag = 1;
break;
case TerrainType.Highland:
mag = 1.5;
break;
case TerrainType.Mountain:
mag = 2;
break;
}
this.toConquer.enqueue(
new TileContainer(
neighbor,
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
this.mg.ticks(),
),
);
}
}
private handleDeadDefender() {
if (this.target.isPlayer() && this.target.numTilesOwned() < 100) {
const gold = this.target.gold();
this.mg.displayMessage(
`Conquered ${this.target.displayName()} received ${renderNumber(gold)} gold`,
MessageType.SUCCESS,
this._owner.id(),
);
this.target.removeGold(gold);
this._owner.addGold(gold);
for (let i = 0; i < 10; i++) {
for (const tile of this.target.tiles()) {
const borders = this.mg
.neighbors(tile)
.some((t) => this.mg.owner(t) == this._owner);
if (borders) {
this._owner.conquer(tile);
} else {
for (const neighbor of this.mg.neighbors(tile)) {
const no = this.mg.owner(neighbor);
if (no.isPlayer() && no != this.target) {
this.mg.player(no.id()).conquer(tile);
break;
}
}
this.border.add(neighbor)
let numOwnedByMe = this.mg.neighbors(neighbor)
.filter(t => this.mg.owner(t) == this._owner)
.length
let dist = 0
if (numOwnedByMe > 2) {
numOwnedByMe = 10
}
let mag = 0
switch (this.mg.terrainType(tile)) {
case TerrainType.Plains:
mag = 1
break
case TerrainType.Highland:
mag = 1.5
break
case TerrainType.Mountain:
mag = 2
break
}
this.toConquer.enqueue(new TileContainer(
neighbor,
dist / 100 + this.random.nextInt(0, 2) - numOwnedByMe + mag,
this.mg.ticks()
))
}
}
}
}
}
private handleDeadDefender() {
if (this.target.isPlayer() && this.target.numTilesOwned() < 100) {
const gold = this.target.gold()
this.mg.displayMessage(`Conquered ${this.target.displayName()} received ${renderNumber(gold)} gold`, MessageType.SUCCESS, this._owner.id())
this.target.removeGold(gold)
this._owner.addGold(gold)
for (let i = 0; i < 10; i++) {
for (const tile of this.target.tiles()) {
const borders = this.mg.neighbors(tile).some(t => this.mg.owner(t) == this._owner)
if (borders) {
this._owner.conquer(tile)
} else {
for (const neighbor of this.mg.neighbors(tile)) {
const no = this.mg.owner(neighbor)
if (no.isPlayer() && no != this.target) {
this.mg.player(no.id()).conquer(tile)
break
}
}
}
}
}
}
}
owner(): Player {
return this._owner
}
isActive(): boolean {
return this.active
}
owner(): Player {
return this._owner;
}
isActive(): boolean {
return this.active;
}
}
class TileContainer {
constructor(public readonly tile: TileRef, public readonly priority: number, public readonly tick: number) { }
}
constructor(
public readonly tile: TileRef,
public readonly priority: number,
public readonly tick: number,
) {}
}
+145 -112
View File
@@ -1,4 +1,13 @@
import { Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
import {
Cell,
Execution,
Game,
Player,
Unit,
PlayerID,
TerrainType,
UnitType,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { PseudoRandom } from "../PseudoRandom";
@@ -8,133 +17,157 @@ import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class BattleshipExecution implements Execution {
private random: PseudoRandom
private random: PseudoRandom;
private _owner: Player
private active = true
private battleship: Unit = null
private mg: Game = null
private _owner: Player;
private active = true;
private battleship: Unit = null;
private mg: Game = null;
private pathfinder: PathFinder
private pathfinder: PathFinder;
private patrolTile: TileRef;
private patrolTile: TileRef;
// TODO: put in config
private searchRange = 100
private attackRate = 5
private lastAttack = 0
// TODO: put in config
private searchRange = 100;
private attackRate = 5;
private lastAttack = 0;
private alreadyTargeted = new Set<Unit>()
private alreadyTargeted = new Set<Unit>();
constructor(
private playerID: PlayerID,
private patrolCenterTile: TileRef,
) { }
constructor(
private playerID: PlayerID,
private patrolCenterTile: TileRef,
) {}
init(mg: Game, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, false);
this._owner = mg.player(this.playerID);
this.mg = mg;
this.patrolTile = this.patrolCenterTile;
this.random = new PseudoRandom(mg.ticks());
}
init(mg: Game, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, false)
this._owner = mg.player(this.playerID)
this.mg = mg
this.patrolTile = this.patrolCenterTile
this.random = new PseudoRandom(mg.ticks())
tick(ticks: number): void {
this.alreadyTargeted.forEach((u) => {
if (!u.isActive()) {
this.alreadyTargeted.delete(u);
}
});
if (this.battleship == null) {
const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile);
if (spawn == false) {
this.active = false;
return;
}
this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn);
return;
}
if (!this.battleship.isActive()) {
this.active = false;
return;
}
tick(ticks: number): void {
this.alreadyTargeted.forEach(u => {
if (!u.isActive()) {
this.alreadyTargeted.delete(u)
}
})
if (this.battleship == null) {
const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile)
if (spawn == false) {
this.active = false
return
}
this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn)
return
}
if (!this.battleship.isActive()) {
this.active = false
return
}
if (this.mg.ticks() % 2 == 0) {
const result = this.pathfinder.nextTile(this.battleship.tile(), this.patrolTile)
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = this.randomTile()
break
case PathFindResultType.NextTile:
this.battleship.move(result.tile)
break
case PathFindResultType.Pending:
return
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`)
this.patrolTile = this.randomTile()
break
}
}
if (this.mg.ticks() - this.lastAttack < this.attackRate) {
return
}
let ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship)
.filter(u => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100)
.filter(u => u.owner() != this.battleship.owner())
.filter(u => u != this.battleship)
.filter(u => !u.owner().isAlliedWith(this.battleship.owner()))
.filter(u => !this.alreadyTargeted.has(u))
.sort(distSortUnit(this.mg, this.battleship));
const friendlyDestroyerNearby = this.battleship.owner().units(UnitType.Destroyer)
.filter(d => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120)
.length > 0
if (friendlyDestroyerNearby) {
// Don't attack trade ships to allow friendly destroyer to capture them
ships = ships.filter(s => s.type() != UnitType.TradeShip)
}
if (ships.length > 0) {
const toAttack = ships[0]
if (!toAttack.hasHealth()) {
// Don't send multiple shells to target if it can be one-shotted.
this.alreadyTargeted.add(toAttack)
}
this.lastAttack = this.mg.ticks()
this.mg.addExecution(new ShellExecution(this.battleship.tile(), this.battleship.owner(), this.battleship, toAttack))
}
if (this.mg.ticks() % 2 == 0) {
const result = this.pathfinder.nextTile(
this.battleship.tile(),
this.patrolTile,
);
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = this.randomTile();
break;
case PathFindResultType.NextTile:
this.battleship.move(result.tile);
break;
case PathFindResultType.Pending:
return;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`);
this.patrolTile = this.randomTile();
break;
}
}
owner(): Player {
return this._owner
if (this.mg.ticks() - this.lastAttack < this.attackRate) {
return;
}
isActive(): boolean {
return this.active
let ships = this.mg
.units(
UnitType.TransportShip,
UnitType.Destroyer,
UnitType.TradeShip,
UnitType.Battleship,
)
.filter(
(u) => this.mg.manhattanDist(u.tile(), this.battleship.tile()) < 100,
)
.filter((u) => u.owner() != this.battleship.owner())
.filter((u) => u != this.battleship)
.filter((u) => !u.owner().isAlliedWith(this.battleship.owner()))
.filter((u) => !this.alreadyTargeted.has(u))
.sort(distSortUnit(this.mg, this.battleship));
const friendlyDestroyerNearby =
this.battleship
.owner()
.units(UnitType.Destroyer)
.filter(
(d) => this.mg.manhattanDist(d.tile(), this.battleship.tile()) < 120,
).length > 0;
if (friendlyDestroyerNearby) {
// Don't attack trade ships to allow friendly destroyer to capture them
ships = ships.filter((s) => s.type() != UnitType.TradeShip);
}
activeDuringSpawnPhase(): boolean {
return false
if (ships.length > 0) {
const toAttack = ships[0];
if (!toAttack.hasHealth()) {
// Don't send multiple shells to target if it can be one-shotted.
this.alreadyTargeted.add(toAttack);
}
this.lastAttack = this.mg.ticks();
this.mg.addExecution(
new ShellExecution(
this.battleship.tile(),
this.battleship.owner(),
this.battleship,
toAttack,
),
);
}
}
randomTile(): TileRef {
while (true) {
const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
if (!this.mg.isValidCoord(x, y)) {
continue
}
const tile = this.mg.ref(x, y)
if (!this.mg.isOcean(tile)) {
continue
}
return tile
}
owner(): Player {
return this._owner;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
randomTile(): TileRef {
while (true) {
const x =
this.mg.x(this.patrolCenterTile) +
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
const y =
this.mg.y(this.patrolCenterTile) +
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
if (!this.mg.isValidCoord(x, y)) {
continue;
}
const tile = this.mg.ref(x, y);
if (!this.mg.isOcean(tile)) {
continue;
}
return tile;
}
}
}
}
+100 -91
View File
@@ -1,109 +1,118 @@
import { Cell, Execution, Game, Player, PlayerType, TerraNullius } from "../game/Game"
import { PseudoRandom } from "../PseudoRandom"
import {
Cell,
Execution,
Game,
Player,
PlayerType,
TerraNullius,
} from "../game/Game";
import { PseudoRandom } from "../PseudoRandom";
import { simpleHash } from "../Util";
import { AttackExecution } from "./AttackExecution";
export class BotExecution implements Execution {
private active = true;
private random: PseudoRandom;
private attackRate: number;
private mg: Game;
private neighborsTerraNullius = true;
private active = true
private random: PseudoRandom;
private attackRate: number
private mg: Game
private neighborsTerraNullius = true
constructor(private bot: Player) {
this.random = new PseudoRandom(simpleHash(bot.id()));
this.attackRate = this.random.nextInt(10, 50);
}
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
this.mg = mg;
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
}
constructor(private bot: Player) {
this.random = new PseudoRandom(simpleHash(bot.id()))
this.attackRate = this.random.nextInt(10, 50)
}
activeDuringSpawnPhase(): boolean {
return false
tick(ticks: number) {
if (!this.bot.isAlive()) {
this.active = false;
return;
}
init(mg: Game, ticks: number) {
this.mg = mg
// this.neighborsTerra = this.bot.neighbors().filter(n => n == this.gs.terraNullius()).length > 0
if (ticks % this.attackRate != 0) {
return;
}
tick(ticks: number) {
if (!this.bot.isAlive()) {
this.active = false
return
this.bot.incomingAllianceRequests().forEach((ar) => {
if (ar.requestor().isTraitor()) {
ar.reject();
} else {
ar.accept();
}
});
const traitors = this.bot
.neighbors()
.filter((n) => n.isPlayer() && n.isTraitor()) as Player[];
if (traitors.length > 0) {
const toAttack = this.random.randElement(traitors);
const odds = this.bot.isAlliedWith(toAttack) ? 6 : 3;
if (this.random.chance(odds)) {
this.sendAttack(toAttack);
return;
}
}
if (this.neighborsTerraNullius) {
for (const b of this.bot.borderTiles()) {
for (const n of this.mg.neighbors(b)) {
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
this.sendAttack(this.mg.terraNullius());
return;
}
}
if (ticks % this.attackRate != 0) {
return
}
this.bot.incomingAllianceRequests().forEach(ar => {
if (ar.requestor().isTraitor()) {
ar.reject()
} else {
ar.accept()
}
})
const traitors = this.bot.neighbors().filter(n => n.isPlayer() && n.isTraitor()) as Player[]
if (traitors.length > 0) {
const toAttack = this.random.randElement(traitors)
const odds = this.bot.isAlliedWith(toAttack) ? 6 : 3
if (this.random.chance(odds)) {
this.sendAttack(toAttack)
return
}
}
if (this.neighborsTerraNullius) {
for (const b of this.bot.borderTiles()) {
for (const n of this.mg.neighbors(b)) {
if (!this.mg.hasOwner(n) && this.mg.isLand(n)) {
this.sendAttack(this.mg.terraNullius())
return
}
}
}
this.neighborsTerraNullius = false
}
const border = Array.from(this.bot.borderTiles())
.flatMap(t => this.mg.neighbors(t))
.filter(t => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot)
if (border.length == 0) {
return
}
const toAttack = border[this.random.nextInt(0, border.length)]
const owner = this.mg.owner(toAttack)
if (owner.isPlayer()) {
if (this.bot.isAlliedWith(owner)) {
return
}
if (owner.type() == PlayerType.FakeHuman) {
if (!this.random.chance(2)) {
return
}
}
}
this.sendAttack(owner)
}
this.neighborsTerraNullius = false;
}
sendAttack(toAttack: Player | TerraNullius) {
this.mg.addExecution(new AttackExecution(
this.bot.troops() / 20,
this.bot.id(),
toAttack.isPlayer() ? toAttack.id() : null,
null,
null
))
const border = Array.from(this.bot.borderTiles())
.flatMap((t) => this.mg.neighbors(t))
.filter((t) => this.mg.hasOwner(t) && this.mg.owner(t) != this.bot);
if (border.length == 0) {
return;
}
owner(): Player {
return this.bot
}
const toAttack = border[this.random.nextInt(0, border.length)];
const owner = this.mg.owner(toAttack);
isActive(): boolean {
return this.active
if (owner.isPlayer()) {
if (this.bot.isAlliedWith(owner)) {
return;
}
if (owner.type() == PlayerType.FakeHuman) {
if (!this.random.chance(2)) {
return;
}
}
}
}
this.sendAttack(owner);
}
sendAttack(toAttack: Player | TerraNullius) {
this.mg.addExecution(
new AttackExecution(
this.bot.troops() / 20,
this.bot.id(),
toAttack.isPlayer() ? toAttack.id() : null,
null,
null,
),
);
}
owner(): Player {
return this.bot;
}
isActive(): boolean {
return this.active;
}
}
+52 -50
View File
@@ -6,63 +6,65 @@ import { GameID, SpawnIntent } from "../Schemas";
import { simpleHash } from "../Util";
import { BOT_NAME_PREFIXES, BOT_NAME_SUFFIXES } from "./utils/BotNames";
export class BotSpawner {
private random: PseudoRandom
private bots: SpawnIntent[] = [];
private random: PseudoRandom;
private bots: SpawnIntent[] = [];
constructor(private gs: Game, gameID: GameID) {
this.random = new PseudoRandom(simpleHash(gameID))
}
constructor(
private gs: Game,
gameID: GameID,
) {
this.random = new PseudoRandom(simpleHash(gameID));
}
spawnBots(numBots: number): SpawnIntent[] {
let tries = 0
while (this.bots.length < numBots) {
if (tries > 10000) {
consolex.log('too many retries while spawning bots, giving up')
return this.bots
}
const botName = this.randomBotName();
const spawn = this.spawnBot(botName);
if (spawn != null) {
this.bots.push(spawn);
} else {
tries++
}
}
spawnBots(numBots: number): SpawnIntent[] {
let tries = 0;
while (this.bots.length < numBots) {
if (tries > 10000) {
consolex.log("too many retries while spawning bots, giving up");
return this.bots;
}
const botName = this.randomBotName();
const spawn = this.spawnBot(botName);
if (spawn != null) {
this.bots.push(spawn);
} else {
tries++;
}
}
return this.bots;
}
spawnBot(botName: string): SpawnIntent | null {
const tile = this.randTile()
if (!this.gs.isLand(tile)) {
return null
}
for (const spawn of this.bots) {
if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) {
return null
}
}
return {
type: 'spawn',
playerID: this.random.nextID(),
name: botName,
playerType: PlayerType.Bot,
x: this.gs.x(tile),
y: this.gs.y(tile)
};
spawnBot(botName: string): SpawnIntent | null {
const tile = this.randTile();
if (!this.gs.isLand(tile)) {
return null;
}
for (const spawn of this.bots) {
if (this.gs.manhattanDist(this.gs.ref(spawn.x, spawn.y), tile) < 30) {
return null;
}
}
return {
type: "spawn",
playerID: this.random.nextID(),
name: botName,
playerType: PlayerType.Bot,
x: this.gs.x(tile),
y: this.gs.y(tile),
};
}
private randomBotName(): string {
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
}
private randomBotName(): string {
const prefixIndex = this.random.nextInt(0, BOT_NAME_PREFIXES.length);
const suffixIndex = this.random.nextInt(0, BOT_NAME_SUFFIXES.length);
return `${BOT_NAME_PREFIXES[prefixIndex]} ${BOT_NAME_SUFFIXES[suffixIndex]}`;
}
private randTile(): TileRef {
return this.gs.ref(
this.random.nextInt(0, this.gs.width()),
this.random.nextInt(0, this.gs.height())
)
}
private randTile(): TileRef {
return this.gs.ref(
this.random.nextInt(0, this.gs.width()),
this.random.nextInt(0, this.gs.height()),
);
}
}
+43 -35
View File
@@ -1,47 +1,55 @@
import { consolex } from "../Consolex";
import { Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
import {
Execution,
Game,
Player,
Unit,
PlayerID,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
export class CityExecution implements Execution {
private player: Player;
private mg: Game;
private city: Unit;
private active: boolean = true;
private player: Player
private mg: Game
private city: Unit
private active: boolean = true
constructor(
private ownerId: PlayerID,
private tile: TileRef,
) {}
constructor(private ownerId: PlayerID, private tile: TileRef) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.player = mg.player(this.ownerId);
}
init(mg: Game, ticks: number): void {
this.mg = mg
this.player = mg.player(this.ownerId)
tick(ticks: number): void {
if (this.city == null) {
const spawnTile = this.player.canBuild(UnitType.City, this.tile);
if (spawnTile == false) {
consolex.warn("cannot build city");
this.active = false;
return;
}
this.city = this.player.buildUnit(UnitType.City, 0, spawnTile);
}
tick(ticks: number): void {
if (this.city == null) {
const spawnTile = this.player.canBuild(UnitType.City, this.tile)
if (spawnTile == false) {
consolex.warn('cannot build city')
this.active = false
return
}
this.city = this.player.buildUnit(UnitType.City, 0, spawnTile)
}
if (!this.city.isActive()) {
this.active = false
return
}
if (!this.city.isActive()) {
this.active = false;
return;
}
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+63 -44
View File
@@ -1,57 +1,76 @@
import { consolex } from "../Consolex";
import { Cell, DefenseBonus, Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
import {
Cell,
DefenseBonus,
Execution,
Game,
Player,
Unit,
PlayerID,
UnitType,
} from "../game/Game";
import { manhattanDistFN, TileRef } from "../game/GameMap";
export class DefensePostExecution implements Execution {
private player: Player;
private mg: Game;
private post: Unit;
private active: boolean = true;
private player: Player
private mg: Game
private post: Unit
private active: boolean = true
private defenseBonuses: DefenseBonus[] = [];
private defenseBonuses: DefenseBonus[] = []
constructor(
private ownerId: PlayerID,
private tile: TileRef,
) {}
constructor(private ownerId: PlayerID, private tile: TileRef) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.player = mg.player(this.ownerId);
}
init(mg: Game, ticks: number): void {
this.mg = mg
this.player = mg.player(this.ownerId)
tick(ticks: number): void {
if (this.post == null) {
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile);
if (spawnTile == false) {
consolex.warn("cannot build Defense Post");
this.active = false;
return;
}
this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile);
this.mg
.bfs(
spawnTile,
manhattanDistFN(spawnTile, this.mg.config().defensePostRange()),
)
.forEach((t) => {
if (this.mg.isLake(t)) {
this.defenseBonuses.push(
this.mg.addTileDefenseBonus(
t,
this.post,
this.mg.config().defensePostDefenseBonus(),
),
);
}
});
}
tick(ticks: number): void {
if (this.post == null) {
const spawnTile = this.player.canBuild(UnitType.DefensePost, this.tile)
if (spawnTile == false) {
consolex.warn('cannot build Defense Post')
this.active = false
return
}
this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile)
this.mg.bfs(spawnTile, manhattanDistFN(spawnTile, this.mg.config().defensePostRange())).forEach(t => {
if (this.mg.isLake(t)) {
this.defenseBonuses.push(
this.mg.addTileDefenseBonus(t, this.post, this.mg.config().defensePostDefenseBonus())
)
}
})
}
if (!this.post.isActive()) {
this.defenseBonuses.forEach(df => this.mg.removeTileDefenseBonus(df))
this.active = false
return
}
if (!this.post.isActive()) {
this.defenseBonuses.forEach((df) => this.mg.removeTileDefenseBonus(df));
this.active = false;
return;
}
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+158 -128
View File
@@ -1,4 +1,13 @@
import { Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
import {
Cell,
Execution,
Game,
Player,
Unit,
PlayerID,
TerrainType,
UnitType,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { PseudoRandom } from "../PseudoRandom";
@@ -7,142 +16,163 @@ import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class DestroyerExecution implements Execution {
private random: PseudoRandom
private random: PseudoRandom;
private _owner: Player
private active = true
private destroyer: Unit = null
private mg: Game = null
private _owner: Player;
private active = true;
private destroyer: Unit = null;
private mg: Game = null;
private target: Unit = null
private pathfinder: PathFinder
private target: Unit = null;
private pathfinder: PathFinder;
private patrolTile: TileRef;
private patrolTile: TileRef;
// TODO: put in config
private searchRange = 100
// TODO: put in config
private searchRange = 100;
constructor(
private playerID: PlayerID,
private patrolCenterTile: TileRef,
) { }
constructor(
private playerID: PlayerID,
private patrolCenterTile: TileRef,
) {}
init(mg: Game, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, false);
this._owner = mg.player(this.playerID);
this.mg = mg;
this.patrolTile = this.patrolCenterTile;
this.random = new PseudoRandom(mg.ticks());
}
init(mg: Game, ticks: number): void {
this.pathfinder = PathFinder.Mini(mg, 5000, false)
this._owner = mg.player(this.playerID)
this.mg = mg
this.patrolTile = this.patrolCenterTile
this.random = new PseudoRandom(mg.ticks())
tick(ticks: number): void {
if (this.destroyer == null) {
const spawn = this._owner.canBuild(UnitType.Destroyer, this.patrolTile);
if (spawn == false) {
this.active = false;
return;
}
this.destroyer = this._owner.buildUnit(UnitType.Destroyer, 0, spawn);
return;
}
if (!this.destroyer.isActive()) {
this.active = false;
return;
}
if (this.target != null && !this.target.isActive()) {
this.target = null;
}
if (this.target == null) {
const ships = this.mg
.units(
UnitType.TransportShip,
UnitType.Destroyer,
UnitType.TradeShip,
UnitType.Battleship,
)
.filter(
(u) => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100,
)
.filter(
(u) =>
u.type() != UnitType.Destroyer ||
u.health() < this.destroyer.health(),
) // only attack Destroyers weaker than it.
.filter((u) => u.owner() != this.destroyer.owner())
.filter((u) => u != this.destroyer)
.filter((u) => !u.owner().isAlliedWith(this.destroyer.owner()));
if (ships.length == 0) {
const result = this.pathfinder.nextTile(
this.destroyer.tile(),
this.patrolTile,
);
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = this.randomTile();
break;
case PathFindResultType.NextTile:
this.destroyer.move(result.tile);
break;
case PathFindResultType.Pending:
return;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`);
this.patrolTile = this.randomTile();
break;
}
return;
}
this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0];
}
if (!this.target.isActive() || this.target.owner() == this._owner) {
// Incase another destroyer captured or destroyed target
this.target = null;
return;
}
tick(ticks: number): void {
if (this.destroyer == null) {
const spawn = this._owner.canBuild(UnitType.Destroyer, this.patrolTile)
if (spawn == false) {
this.active = false
return
}
this.destroyer = this._owner.buildUnit(UnitType.Destroyer, 0, spawn)
return
}
if (!this.destroyer.isActive()) {
this.active = false
return
}
if (this.target != null && !this.target.isActive()) {
this.target = null
}
if (this.target == null) {
const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship)
.filter(u => this.mg.manhattanDist(u.tile(), this.destroyer.tile()) < 100)
.filter(u => u.type() != UnitType.Destroyer || u.health() < this.destroyer.health()) // only attack Destroyers weaker than it.
.filter(u => u.owner() != this.destroyer.owner())
.filter(u => u != this.destroyer)
.filter(u => !u.owner().isAlliedWith(this.destroyer.owner()))
if (ships.length == 0) {
const result = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile)
switch (result.type) {
case PathFindResultType.Completed:
this.patrolTile = this.randomTile()
break
case PathFindResultType.NextTile:
this.destroyer.move(result.tile)
break
case PathFindResultType.Pending:
return
case PathFindResultType.PathNotFound:
consolex.log(`path not found to patrol tile`)
this.patrolTile = this.randomTile()
break
}
return
}
this.target = ships.sort(distSortUnit(this.mg, this.destroyer))[0]
}
if (!this.target.isActive() || this.target.owner() == this._owner) {
// Incase another destroyer captured or destroyed target
this.target = null
return
}
for (let i = 0; i < 2; i++) {
const result = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile(), 5)
switch (result.type) {
case PathFindResultType.Completed:
switch (this.target.type()) {
case UnitType.TransportShip:
case UnitType.Battleship:
this.target.delete()
break
case UnitType.TradeShip:
this.owner().captureUnit(this.target)
break
case UnitType.Destroyer:
const health = this.target.health()
this.target.modifyHealth(-this.destroyer.health())
this.destroyer.modifyHealth(-health)
break
}
this.target = null
return
case PathFindResultType.NextTile:
this.destroyer.move(result.tile)
break
case PathFindResultType.Pending:
break
case PathFindResultType.PathNotFound:
consolex.log(`path not found to target`)
break
}
}
for (let i = 0; i < 2; i++) {
const result = this.pathfinder.nextTile(
this.destroyer.tile(),
this.target.tile(),
5,
);
switch (result.type) {
case PathFindResultType.Completed:
switch (this.target.type()) {
case UnitType.TransportShip:
case UnitType.Battleship:
this.target.delete();
break;
case UnitType.TradeShip:
this.owner().captureUnit(this.target);
break;
case UnitType.Destroyer:
const health = this.target.health();
this.target.modifyHealth(-this.destroyer.health());
this.destroyer.modifyHealth(-health);
break;
}
this.target = null;
return;
case PathFindResultType.NextTile:
this.destroyer.move(result.tile);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
consolex.log(`path not found to target`);
break;
}
}
}
owner(): Player {
return this._owner
owner(): Player {
return this._owner;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
randomTile(): TileRef {
while (true) {
const x =
this.mg.x(this.patrolCenterTile) +
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
const y =
this.mg.y(this.patrolCenterTile) +
this.random.nextInt(-this.searchRange / 2, this.searchRange / 2);
if (!this.mg.isValidCoord(x, y)) {
continue;
}
const tile = this.mg.ref(x, y);
if (!this.mg.isOcean(tile)) {
continue;
}
return tile;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
randomTile(): TileRef {
while (true) {
const x = this.mg.x(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
const y = this.mg.y(this.patrolCenterTile) + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2)
if (!this.mg.isValidCoord(x, y)) {
continue
}
const tile = this.mg.ref(x, y)
if (!this.mg.isOcean(tile)) {
continue
}
return tile
}
}
}
}
}
+34 -35
View File
@@ -2,47 +2,46 @@ import { consolex } from "../Consolex";
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class DonateExecution implements Execution {
private sender: Player;
private recipient: Player;
private sender: Player
private recipient: Player
private active = true;
private active = true
constructor(
private senderID: PlayerID,
private recipientID: PlayerID,
private troops: number | null,
) {}
constructor(
private senderID: PlayerID,
private recipientID: PlayerID,
private troops: number | null
) { }
init(mg: Game, ticks: number): void {
this.sender = mg.player(this.senderID)
this.recipient = mg.player(this.recipientID)
if (this.troops == null) {
this.troops = mg.config().defaultDonationAmount(this.sender)
}
init(mg: Game, ticks: number): void {
this.sender = mg.player(this.senderID);
this.recipient = mg.player(this.recipientID);
if (this.troops == null) {
this.troops = mg.config().defaultDonationAmount(this.sender);
}
}
tick(ticks: number): void {
if (this.sender.canDonate(this.recipient)) {
this.sender.donate(this.recipient, this.troops)
this.recipient.updateRelation(this.sender, 50)
} else {
consolex.warn(`cannot send tropps from ${this.sender} to ${this.recipient}`)
}
this.active = false
tick(ticks: number): void {
if (this.sender.canDonate(this.recipient)) {
this.sender.donate(this.recipient, this.troops);
this.recipient.updateRelation(this.sender, 50);
} else {
consolex.warn(
`cannot send tropps from ${this.sender} to ${this.recipient}`,
);
}
this.active = false;
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+48 -36
View File
@@ -1,47 +1,59 @@
import { consolex } from "../Consolex";
import { AllPlayers, Execution, Game, Player, PlayerID, PlayerType, UnitType } from "../game/Game";
import {
AllPlayers,
Execution,
Game,
Player,
PlayerID,
PlayerType,
UnitType,
} from "../game/Game";
export class EmojiExecution implements Execution {
private requestor: Player;
private recipient: Player | typeof AllPlayers;
private requestor: Player
private recipient: Player | typeof AllPlayers
private active = true;
private active = true
constructor(
private senderID: PlayerID,
private recipientID: PlayerID | typeof AllPlayers,
private emoji: string,
) {}
constructor(
private senderID: PlayerID,
private recipientID: PlayerID | typeof AllPlayers,
private emoji: string
) { }
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.senderID);
this.recipient =
this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID);
}
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.senderID)
this.recipient = this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID)
tick(ticks: number): void {
if (this.requestor.canSendEmoji(this.recipient)) {
this.requestor.sendEmoji(this.recipient, this.emoji);
if (
this.emoji == "🖕" &&
this.recipient != AllPlayers &&
this.recipient.type() == PlayerType.FakeHuman
) {
this.recipient.updateRelation(this.requestor, -100);
}
} else {
consolex.warn(
`cannot send emoji from ${this.requestor} to ${this.recipient}`,
);
}
this.active = false;
}
tick(ticks: number): void {
if (this.requestor.canSendEmoji(this.recipient)) {
this.requestor.sendEmoji(this.recipient, this.emoji)
if (this.emoji == "🖕" && this.recipient != AllPlayers && this.recipient.type() == PlayerType.FakeHuman) {
this.recipient.updateRelation(this.requestor, -100)
}
} else {
consolex.warn(`cannot send emoji from ${this.requestor} to ${this.recipient}`)
}
this.active = false
}
owner(): Player {
return null;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+155 -95
View File
@@ -1,5 +1,21 @@
import { Cell, Execution, Game, Player, PlayerInfo, TerraNullius, PlayerType, Alliance, UnitType } from "../game/Game";
import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas";
import {
Cell,
Execution,
Game,
Player,
PlayerInfo,
TerraNullius,
PlayerType,
Alliance,
UnitType,
} from "../game/Game";
import {
AttackIntent,
BoatAttackIntentSchema,
GameID,
Intent,
Turn,
} from "../Schemas";
import { AttackExecution } from "./AttackExecution";
import { SpawnExecution } from "./SpawnExecution";
import { BotSpawner } from "./BotSpawner";
@@ -23,104 +39,148 @@ import { DefensePostExecution } from "./DefensePostExecution";
import { CityExecution } from "./CityExecution";
import { TileRef } from "../game/GameMap";
export class Executor {
// private random = new PseudoRandom(999)
private random: PseudoRandom = null;
constructor(
private mg: Game,
private gameID: GameID,
) {
// Add one to avoid id collisions with bots.
this.random = new PseudoRandom(simpleHash(gameID) + 1);
}
// private random = new PseudoRandom(999)
private random: PseudoRandom = null
createExecs(turn: Turn): Execution[] {
return turn.intents.map((i) => this.createExec(i));
}
constructor(private mg: Game, private gameID: GameID) {
// Add one to avoid id collisions with bots.
this.random = new PseudoRandom(simpleHash(gameID) + 1)
}
createExecs(turn: Turn): Execution[] {
return turn.intents.map(i => this.createExec(i))
}
createExec(intent: Intent): Execution {
switch (intent.type) {
case "attack": {
return new AttackExecution(
intent.troops,
intent.attackerID,
intent.targetID,
null
);
}
case "spawn":
return new SpawnExecution(
new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID),
this.mg.ref(intent.x, intent.y)
);
case "boat":
return new TransportShipExecution(
intent.attackerID,
intent.targetID,
this.mg.ref(intent.x, intent.y),
intent.troops
);
case "allianceRequest":
return new AllianceRequestExecution(intent.requestor, intent.recipient);
case "allianceRequestReply":
return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept);
case "breakAlliance":
return new BreakAllianceExecution(intent.requestor, intent.recipient);
case "targetPlayer":
return new TargetPlayerExecution(intent.requestor, intent.target);
case "emoji":
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji);
case "donate":
return new DonateExecution(intent.sender, intent.recipient, intent.troops);
case "troop_ratio":
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
case "build_unit":
switch (intent.unit) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return new NukeExecution(intent.unit, intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.Destroyer:
return new DestroyerExecution(intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.Battleship:
return new BattleshipExecution(intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.Port:
return new PortExecution(intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.MissileSilo:
return new MissileSiloExecution(intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.DefensePost:
return new DefensePostExecution(intent.player, this.mg.ref(intent.x, intent.y))
case UnitType.City:
return new CityExecution(intent.player, this.mg.ref(intent.x, intent.y))
default:
throw Error(`unit type ${intent.unit} not supported`)
}
default:
throw new Error(`intent type ${intent} not found`);
createExec(intent: Intent): Execution {
switch (intent.type) {
case "attack": {
return new AttackExecution(
intent.troops,
intent.attackerID,
intent.targetID,
null,
);
}
case "spawn":
return new SpawnExecution(
new PlayerInfo(
sanitize(intent.name),
intent.playerType,
intent.clientID,
intent.playerID,
),
this.mg.ref(intent.x, intent.y),
);
case "boat":
return new TransportShipExecution(
intent.attackerID,
intent.targetID,
this.mg.ref(intent.x, intent.y),
intent.troops,
);
case "allianceRequest":
return new AllianceRequestExecution(intent.requestor, intent.recipient);
case "allianceRequestReply":
return new AllianceRequestReplyExecution(
intent.requestor,
intent.recipient,
intent.accept,
);
case "breakAlliance":
return new BreakAllianceExecution(intent.requestor, intent.recipient);
case "targetPlayer":
return new TargetPlayerExecution(intent.requestor, intent.target);
case "emoji":
return new EmojiExecution(
intent.sender,
intent.recipient,
intent.emoji,
);
case "donate":
return new DonateExecution(
intent.sender,
intent.recipient,
intent.troops,
);
case "troop_ratio":
return new SetTargetTroopRatioExecution(intent.player, intent.ratio);
case "build_unit":
switch (intent.unit) {
case UnitType.AtomBomb:
case UnitType.HydrogenBomb:
return new NukeExecution(
intent.unit,
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.Destroyer:
return new DestroyerExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.Battleship:
return new BattleshipExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.Port:
return new PortExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.MissileSilo:
return new MissileSiloExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.DefensePost:
return new DefensePostExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
case UnitType.City:
return new CityExecution(
intent.player,
this.mg.ref(intent.x, intent.y),
);
default:
throw Error(`unit type ${intent.unit} not supported`);
}
default:
throw new Error(`intent type ${intent} not found`);
}
}
spawnBots(numBots: number): Execution[] {
return new BotSpawner(this.mg, this.gameID).spawnBots(numBots).map(i => this.createExec(i))
spawnBots(numBots: number): Execution[] {
return new BotSpawner(this.mg, this.gameID)
.spawnBots(numBots)
.map((i) => this.createExec(i));
}
fakeHumanExecutions(): Execution[] {
const execs = [];
for (const nation of this.mg.nations()) {
execs.push(
new FakeHumanExecution(
this.gameID,
new PlayerInfo(
nation.name,
PlayerType.FakeHuman,
null,
this.random.nextID(),
),
nation.cell,
nation.strength *
this.mg
.config()
.difficultyModifier(this.mg.config().gameConfig().difficulty),
),
);
}
fakeHumanExecutions(): Execution[] {
const execs = []
for (const nation of this.mg.nations()) {
execs.push(new FakeHumanExecution(
this.gameID,
new PlayerInfo(
nation.name,
PlayerType.FakeHuman,
null,
this.random.nextID()
),
nation.cell,
nation.strength * this.mg.config().difficultyModifier(this.mg.config().gameConfig().difficulty)
))
}
return execs
}
}
return execs;
}
}
File diff suppressed because it is too large Load Diff
+42 -35
View File
@@ -1,46 +1,53 @@
import { consolex } from "../Consolex";
import { Cell, Execution, Game, Player, Unit, PlayerID, UnitType } from "../game/Game";
import {
Cell,
Execution,
Game,
Player,
Unit,
PlayerID,
UnitType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
export class MissileSiloExecution implements Execution {
private active = true;
private mg: Game;
private player: Player;
private silo: Unit;
private active = true
private mg: Game
private player: Player
private silo: Unit
constructor(
private _owner: PlayerID,
private tile: TileRef,
) {}
constructor(
private _owner: PlayerID,
private tile: TileRef
) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.player = mg.player(this._owner);
}
init(mg: Game, ticks: number): void {
this.mg = mg
this.player = mg.player(this._owner)
tick(ticks: number): void {
if (this.silo == null) {
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
consolex.warn(
`player ${this.player} cannot build port at ${this.tile}`,
);
this.active = false;
return;
}
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile);
}
}
tick(ticks: number): void {
if (this.silo == null) {
if (!this.player.canBuild(UnitType.MissileSilo, this.tile)) {
consolex.warn(`player ${this.player} cannot build port at ${this.tile}`)
this.active = false
return
}
this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, this.tile)
}
}
owner(): Player {
return null;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+120 -104
View File
@@ -1,5 +1,14 @@
import { nextTick } from "process";
import { Cell, Execution, Game, Player, PlayerID, Unit, UnitType, TerraNullius } from "../game/Game";
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";
@@ -7,125 +16,132 @@ import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class NukeExecution implements Execution {
private player: Player;
private player: Player
private active = true;
private active = true
private mg: Game;
private mg: Game
private nuke: Unit;
private nuke: Unit
private pathFinder: PathFinder;
constructor(
private type: UnitType.AtomBomb | UnitType.HydrogenBomb,
private senderID: PlayerID,
private dst: TileRef,
) {}
private pathFinder: PathFinder
constructor(
private type: UnitType.AtomBomb | UnitType.HydrogenBomb,
private senderID: PlayerID,
private dst: TileRef,
) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, true);
this.player = mg.player(this.senderID);
}
public target(): Player | TerraNullius {
return this.mg.owner(this.dst);
}
init(mg: Game, ticks: number): void {
this.mg = mg
this.pathFinder = PathFinder.Mini(mg, 10_000, true)
this.player = mg.player(this.senderID)
tick(ticks: number): void {
if (this.nuke == null) {
const spawn = this.player.canBuild(this.type, this.dst);
if (spawn == false) {
consolex.warn(`cannot build Nuke`);
this.active = false;
return;
}
this.nuke = this.player.buildUnit(this.type, 0, spawn);
}
public target(): Player | TerraNullius {
return this.mg.owner(this.dst)
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;
}
}
}
tick(ticks: number): void {
if (this.nuke == null) {
const spawn = this.player.canBuild(this.type, this.dst)
if (spawn == false) {
consolex.warn(`cannot build Nuke`)
this.active = false
return
}
this.nuke = this.player.buildUnit(this.type, 0, spawn)
private detonate() {
const magnitude =
this.type == UnitType.AtomBomb
? { inner: 15, outer: 40 }
: { inner: 140, outer: 160 };
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 ratio = Object.fromEntries(
this.mg
.players()
.map((p) => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()]),
);
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(2 * ratio[mp.id()]);
if (!attacked.has(mp)) {
attacked.set(mp, 0);
}
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
}
const prev = attacked.get(mp);
attacked.set(mp, prev + 1);
}
if (this.mg.isLand(tile)) {
this.mg.setFallout(tile, true);
}
}
for (const [other, tilesDestroyed] of attacked) {
if (tilesDestroyed > 100) {
const alliance = this.player.allianceWith(other);
if (alliance != null) {
this.player.breakAlliance(alliance);
}
}
private detonate() {
const magnitude = this.type == UnitType.AtomBomb ? { inner: 15, outer: 40 } : { inner: 140, outer: 160 }
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 ratio = Object.fromEntries(
this.mg.players().map(p => [p.id(), (p.troops() + p.workers()) / p.numTilesOwned()])
)
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(2 * ratio[mp.id()])
if (!attacked.has(mp)) {
attacked.set(mp, 0)
}
const prev = attacked.get(mp)
attacked.set(mp, prev + 1)
}
if (this.mg.isLand(tile)) {
this.mg.setFallout(tile, true)
}
if (other != this.player) {
other.updateRelation(this.player, -100);
}
for (const [other, tilesDestroyed] of attacked) {
if (tilesDestroyed > 100) {
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 (
unit.type() != UnitType.AtomBomb &&
unit.type() != UnitType.HydrogenBomb
) {
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
unit.delete();
}
for (const unit of this.mg.units()) {
if (unit.type() != UnitType.AtomBomb && unit.type() != UnitType.HydrogenBomb) {
if (this.mg.euclideanDist(this.dst, unit.tile()) < magnitude.outer) {
unit.delete()
}
}
}
this.active = false
this.nuke.delete(false)
}
}
this.active = false;
this.nuke.delete(false);
}
owner(): Player {
return this.player
}
owner(): Player {
return this.player;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+201 -186
View File
@@ -1,211 +1,226 @@
import { Config } from "../configuration/Config"
import { Execution, Game, Player, PlayerID, TerraNullius, UnitType } from "../game/Game"
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"
import { GameImpl } from "../game/GameImpl"
import { consolex } from "../Consolex"
import { GameMap, TileRef } from "../game/GameMap"
import { Config } from "../configuration/Config";
import {
Execution,
Game,
Player,
PlayerID,
TerraNullius,
UnitType,
} from "../game/Game";
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
import { GameImpl } from "../game/GameImpl";
import { consolex } from "../Consolex";
import { GameMap, TileRef } from "../game/GameMap";
export class PlayerExecution implements Execution {
private readonly ticksPerClusterCalc = 20;
private readonly ticksPerClusterCalc = 20
private player: Player;
private config: Config;
private lastCalc = 0;
private mg: Game;
private active = true;
private player: Player
private config: Config
private lastCalc = 0
private mg: Game
private active = true
constructor(private playerID: PlayerID) {}
constructor(private playerID: PlayerID) {
activeDuringSpawnPhase(): boolean {
return false;
}
init(mg: Game, ticks: number) {
this.mg = mg;
this.config = mg.config();
this.player = mg.player(this.playerID);
this.lastCalc =
ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc);
}
tick(ticks: number) {
this.player.decayRelations();
this.player.units().forEach((u) => {
if (u.health() <= 0) {
u.delete();
return;
}
u.modifyHealth(1);
const tileOwner = this.mg.owner(u.tile());
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner != this.player) {
this.mg.player(tileOwner.id()).captureUnit(u);
}
} else {
u.delete();
}
}
});
if (!this.player.isAlive()) {
this.player.units().forEach((u) => {
if (
u.type() != UnitType.AtomBomb &&
u.type() != UnitType.HydrogenBomb
) {
u.delete();
}
});
this.active = false;
return;
}
activeDuringSpawnPhase(): boolean {
return false
const popInc = this.config.populationIncreaseRate(this.player);
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio())); // (1 - this.player.targetTroopRatio()))
this.player.addTroops(popInc * this.player.targetTroopRatio());
this.player.addGold(this.config.goldAdditionRate(this.player));
const adjustRate = this.config.troopAdjustmentRate(this.player);
this.player.addTroops(adjustRate);
this.player.removeWorkers(adjustRate);
const alliances = Array.from(this.player.alliances());
for (const alliance of alliances) {
if (
this.mg.ticks() - alliance.createdAt() >
this.mg.config().allianceDuration()
) {
alliance.expire();
}
}
init(mg: Game, ticks: number) {
this.mg = mg
this.config = mg.config()
this.player = mg.player(this.playerID)
this.lastCalc = ticks + (simpleHash(this.player.name()) % this.ticksPerClusterCalc)
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
if (this.player.lastTileChange() > this.lastCalc) {
this.lastCalc = ticks;
const start = performance.now();
this.removeClusters();
const end = performance.now();
if (end - start > 1000) {
consolex.log(`player ${this.player.name()}, took ${end - start}ms`);
}
}
}
}
private removeClusters() {
const clusters = this.calculateClusters();
clusters.sort((a, b) => b.size - a.size);
const main = clusters.shift();
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main);
const surroundedBy = this.surroundedBySamePlayer(main);
if (surroundedBy && !this.player.isAlliedWith(surroundedBy)) {
this.removeCluster(main);
}
tick(ticks: number) {
this.player.decayRelations()
this.player.units().forEach(u => {
if (u.health() <= 0) {
u.delete()
return
}
u.modifyHealth(1)
const tileOwner = this.mg.owner(u.tile())
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner != this.player) {
this.mg.player(tileOwner.id()).captureUnit(u)
}
} else {
u.delete()
}
}
})
if (!this.player.isAlive()) {
this.player.units().forEach(u => {
if (u.type() != UnitType.AtomBomb && u.type() != UnitType.HydrogenBomb) {
u.delete()
}
})
this.active = false
return
}
const popInc = this.config.populationIncreaseRate(this.player)
this.player.addWorkers(popInc * (1 - this.player.targetTroopRatio()))// (1 - this.player.targetTroopRatio()))
this.player.addTroops(popInc * this.player.targetTroopRatio())
this.player.addGold(this.config.goldAdditionRate(this.player))
const adjustRate = this.config.troopAdjustmentRate(this.player)
this.player.addTroops(adjustRate)
this.player.removeWorkers(adjustRate)
const alliances = Array.from(this.player.alliances())
for (const alliance of alliances) {
if (this.mg.ticks() - alliance.createdAt() > this.mg.config().allianceDuration()) {
alliance.expire()
}
}
if (ticks - this.lastCalc > this.ticksPerClusterCalc) {
if (this.player.lastTileChange() > this.lastCalc) {
this.lastCalc = ticks
const start = performance.now()
this.removeClusters()
const end = performance.now()
if (end - start > 1000) {
consolex.log(`player ${this.player.name()}, took ${end - start}ms`)
}
}
}
for (const cluster of clusters) {
if (this.isSurrounded(cluster)) {
this.removeCluster(cluster);
}
}
}
private removeClusters() {
const clusters = this.calculateClusters()
clusters.sort((a, b) => b.size - a.size);
const main = clusters.shift()
this.player.largestClusterBoundingBox = calculateBoundingBox(this.mg, main)
const surroundedBy = this.surroundedBySamePlayer(main)
if (surroundedBy && !this.player.isAlliedWith(surroundedBy)) {
this.removeCluster(main)
}
for (const cluster of clusters) {
if (this.isSurrounded(cluster)) {
this.removeCluster(cluster)
}
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
const enemies = new Set<number>();
for (const ref of cluster) {
if (
this.mg.isOceanShore(ref) ||
this.mg.neighbors(ref).some((n) => !this.mg.hasOwner(n))
) {
return false;
}
this.mg
.neighbors(ref)
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
.forEach((p) => enemies.add(this.mg.ownerID(p)));
if (enemies.size != 1) {
return false;
}
}
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
const enemies = new Set<number>()
for (const ref of cluster) {
if (this.mg.isOceanShore(ref) || this.mg.neighbors(ref).some(n => !this.mg.hasOwner(n))) {
return false
}
this.mg.neighbors(ref)
.filter(n => this.mg.ownerID(n) != this.player.smallID())
.forEach(p => enemies.add(this.mg.ownerID(p)))
if (enemies.size != 1) {
return false
}
}
if (enemies.size != 1) {
return false
}
return this.mg.playerBySmallID(Array.from(enemies)[0]) as Player
if (enemies.size != 1) {
return false;
}
return this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
}
private isSurrounded(cluster: Set<TileRef>): boolean {
let enemyTiles = new Set<TileRef>()
for (const tr of cluster) {
if (this.mg.isOceanShore(tr)) {
return false
}
this.mg.neighbors(tr)
.filter(n => this.mg.ownerID(n) != this.player.smallID())
.forEach(n => enemyTiles.add(n))
}
if (enemyTiles.size == 0) {
return false
}
const enemyBox = calculateBoundingBox(this.mg, enemyTiles)
const clusterBox = calculateBoundingBox(this.mg, cluster)
return inscribed(enemyBox, clusterBox)
private isSurrounded(cluster: Set<TileRef>): boolean {
let enemyTiles = new Set<TileRef>();
for (const tr of cluster) {
if (this.mg.isOceanShore(tr)) {
return false;
}
this.mg
.neighbors(tr)
.filter((n) => this.mg.ownerID(n) != this.player.smallID())
.forEach((n) => enemyTiles.add(n));
}
private removeCluster(cluster: Set<TileRef>) {
const result = new Set<number>(); // Use Set to automatically deduplicate ownerIDs
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
result.add(this.mg.ownerID(neighbor));
}
}
}
const mode = getMode(result)
if (!this.mg.playerBySmallID(mode).isPlayer()) {
return
}
const firstTile = cluster.values().next().value
const filter = (_, t: TileRef): boolean => this.mg.ownerID(t) == this.mg.ownerID(firstTile)
const tiles = this.mg.bfs(firstTile, filter)
const modePlayer = this.mg.playerBySmallID(mode)
if (!modePlayer.isPlayer()) {
consolex.warn('mode player is null')
return
}
for (const tile of tiles) {
(modePlayer as Player).conquer(tile)
}
if (enemyTiles.size == 0) {
return false;
}
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
const clusterBox = calculateBoundingBox(this.mg, cluster);
return inscribed(enemyBox, clusterBox);
}
private calculateClusters(): Set<TileRef>[] {
const seen = new Set<TileRef>()
const border = this.player.borderTiles()
const clusters: Set<TileRef>[] = []
for (const tile of border) {
if (seen.has(tile)) {
continue
}
const cluster = new Set<TileRef>()
const queue: TileRef[] = [tile]
seen.add(tile)
while (queue.length > 0) {
const curr = queue.shift()
cluster.add(curr)
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr)
for (const neighbor of neighbors) {
if (border.has(neighbor) && !seen.has(neighbor)) {
queue.push(neighbor)
seen.add(neighbor)
}
}
}
clusters.push(cluster)
private removeCluster(cluster: Set<TileRef>) {
const result = new Set<number>(); // Use Set to automatically deduplicate ownerIDs
for (const t of cluster) {
for (const neighbor of this.mg.neighbors(t)) {
if (this.mg.ownerID(neighbor) != this.player.smallID()) {
result.add(this.mg.ownerID(neighbor));
}
return clusters
}
}
const mode = getMode(result);
if (!this.mg.playerBySmallID(mode).isPlayer()) {
return;
}
const firstTile = cluster.values().next().value;
const filter = (_, t: TileRef): boolean =>
this.mg.ownerID(t) == this.mg.ownerID(firstTile);
const tiles = this.mg.bfs(firstTile, filter);
owner(): Player {
return this.player
const modePlayer = this.mg.playerBySmallID(mode);
if (!modePlayer.isPlayer()) {
consolex.warn("mode player is null");
return;
}
for (const tile of tiles) {
(modePlayer as Player).conquer(tile);
}
}
isActive(): boolean {
return this.active
private calculateClusters(): Set<TileRef>[] {
const seen = new Set<TileRef>();
const border = this.player.borderTiles();
const clusters: Set<TileRef>[] = [];
for (const tile of border) {
if (seen.has(tile)) {
continue;
}
const cluster = new Set<TileRef>();
const queue: TileRef[] = [tile];
seen.add(tile);
while (queue.length > 0) {
const curr = queue.shift();
cluster.add(curr);
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
for (const neighbor of neighbors) {
if (border.has(neighbor) && !seen.has(neighbor)) {
queue.push(neighbor);
seen.add(neighbor);
}
}
}
clusters.push(cluster);
}
}
return clusters;
}
owner(): Player {
return this.player;
}
isActive(): boolean {
return this.active;
}
}
+123 -106
View File
@@ -1,4 +1,14 @@
import { AllPlayers, Cell, Execution, Game, Player, Unit, PlayerID, TerrainType, UnitType } from "../game/Game";
import {
AllPlayers,
Cell,
Execution,
Game,
Player,
Unit,
PlayerID,
TerrainType,
UnitType,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { PseudoRandom } from "../PseudoRandom";
@@ -8,126 +18,133 @@ import { MiniAStar } from "../pathfinding/MiniAStar";
import { manhattanDistFN, TileRef } from "../game/GameMap";
export class PortExecution implements Execution {
private active = true;
private mg: Game;
private port: Unit;
private random: PseudoRandom;
private portPaths = new Map<Unit, TileRef[]>();
private computingPaths = new Map<Unit, MiniAStar>();
private active = true
private mg: Game
private port: Unit
private random: PseudoRandom
private portPaths = new Map<Unit, TileRef[]>()
private computingPaths = new Map<Unit, MiniAStar>()
constructor(
private _owner: PlayerID,
private tile: TileRef,
) {}
constructor(
private _owner: PlayerID,
private tile: TileRef,
) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.random = new PseudoRandom(mg.ticks());
}
tick(ticks: number): void {
if (this.port == null) {
// TODO: use canBuild
const tile = this.tile;
const player = this.mg.player(this._owner);
if (!player.canBuild(UnitType.Port, tile)) {
consolex.warn(`player ${player} cannot build port at ${this.tile}`);
this.active = false;
return;
}
const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20)))
.filter((t) => this.mg.isOceanShore(t) && this.mg.owner(t) == player)
.sort(
(a, b) =>
this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile),
);
init(mg: Game, ticks: number): void {
this.mg = mg
this.random = new PseudoRandom(mg.ticks())
if (spawns.length == 0) {
consolex.warn(`cannot find spawn for port`);
this.active = false;
return;
}
this.port = player.buildUnit(UnitType.Port, 0, spawns[0]);
}
if (!this.port.isActive()) {
this.active = false;
return;
}
tick(ticks: number): void {
const alliedPorts = this.player()
.alliances()
.map((a) => a.other(this.player()))
.flatMap((p) => p.units(UnitType.Port));
const alliedPortsSet = new Set(alliedPorts);
if (this.port == null) {
// TODO: use canBuild
const tile = this.tile
const player = this.mg.player(this._owner)
if (!player.canBuild(UnitType.Port, tile)) {
consolex.warn(`player ${player} cannot build port at ${this.tile}`)
this.active = false
return
}
const spawns = Array.from(this.mg.bfs(tile, manhattanDistFN(tile, 20)))
.filter(t => this.mg.isOceanShore(t) && this.mg.owner(t) == player)
.sort((a, b) => this.mg.manhattanDist(a, tile) - this.mg.manhattanDist(b, tile))
const allyConnections = new Set(
Array.from(this.portPaths.keys()).map((p) => p.owner()),
);
allyConnections;
if (spawns.length == 0) {
consolex.warn(`cannot find spawn for port`)
this.active = false
return
}
this.port = player.buildUnit(UnitType.Port, 0, spawns[0])
}
if (!this.port.isActive()) {
this.active = false
return
for (const port of alliedPorts) {
if (allyConnections.has(port.owner())) {
continue;
}
allyConnections.add(port.owner());
if (this.computingPaths.has(port)) {
const aStar = this.computingPaths.get(port);
switch (aStar.compute()) {
case PathFindResultType.Completed:
this.portPaths.set(port, aStar.reconstructPath());
this.computingPaths.delete(port);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
consolex.warn(`path not found to port`);
break;
}
continue;
}
const alliedPorts = this.player().alliances().map(a => a.other(this.player())).flatMap(p => p.units(UnitType.Port))
const alliedPortsSet = new Set(alliedPorts)
const allyConnections = new Set(Array.from(this.portPaths.keys()).map(p => p.owner()))
allyConnections
for (const port of alliedPorts) {
if (allyConnections.has(port.owner())) {
continue
}
allyConnections.add(port.owner())
if (this.computingPaths.has(port)) {
const aStar = this.computingPaths.get(port)
switch (aStar.compute()) {
case PathFindResultType.Completed:
this.portPaths.set(port, aStar.reconstructPath())
this.computingPaths.delete(port)
break
case PathFindResultType.Pending:
break
case PathFindResultType.PathNotFound:
consolex.warn(`path not found to port`)
break
}
continue
}
const pf = new MiniAStar(
this.mg.map(),
this.mg.miniMap(),
this.port.tile(),
port.tile(),
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
10_000,
25
)
this.computingPaths.set(port, pf)
}
for (const port of this.portPaths.keys()) {
if (!port.isActive() || !alliedPortsSet.has(port)) {
this.portPaths.delete(port)
this.computingPaths.delete(port)
}
}
const portConnections = Array.from(this.portPaths.keys())
if (portConnections.length > 0 && this.random.chance(this.mg.config().tradeShipSpawnRate())) {
const port = this.random.randElement(portConnections)
const path = this.portPaths.get(port)
if (path != null) {
const pf = PathFinder.Mini(this.mg, 10, false)
this.mg.addExecution(new TradeShipExecution(this.player().id(), this.port, port, pf, path))
}
}
const pf = new MiniAStar(
this.mg.map(),
this.mg.miniMap(),
this.port.tile(),
port.tile(),
(tr: TileRef) => this.mg.miniMap().isOcean(tr),
10_000,
25,
);
this.computingPaths.set(port, pf);
}
owner(): Player {
return null
for (const port of this.portPaths.keys()) {
if (!port.isActive() || !alliedPortsSet.has(port)) {
this.portPaths.delete(port);
this.computingPaths.delete(port);
}
}
isActive(): boolean {
return this.active
}
const portConnections = Array.from(this.portPaths.keys());
activeDuringSpawnPhase(): boolean {
return false
if (
portConnections.length > 0 &&
this.random.chance(this.mg.config().tradeShipSpawnRate())
) {
const port = this.random.randElement(portConnections);
const path = this.portPaths.get(port);
if (path != null) {
const pf = PathFinder.Mini(this.mg, 10, false);
this.mg.addExecution(
new TradeShipExecution(this.player().id(), this.port, port, pf, path),
);
}
}
}
player(): Player {
return this.port.owner()
}
owner(): Player {
return null;
}
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
player(): Player {
return this.port.owner();
}
}
@@ -2,37 +2,39 @@ import { consolex } from "../Consolex";
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class SetTargetTroopRatioExecution implements Execution {
private player: Player;
private player: Player
private active = true;
private active = true
constructor(
private playerID: PlayerID,
private targetTroopsRatio: number,
) {}
constructor(private playerID: PlayerID, private targetTroopsRatio: number) { }
init(mg: Game, ticks: number): void {
this.player = mg.player(this.playerID);
}
init(mg: Game, ticks: number): void {
this.player = mg.player(this.playerID)
tick(ticks: number): void {
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
consolex.warn(
`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`,
);
} else {
this.player.setTargetTroopRatio(this.targetTroopsRatio);
}
this.active = false;
}
tick(ticks: number): void {
if (this.targetTroopsRatio < 0 || this.targetTroopsRatio > 1) {
consolex.warn(`target troop ratio of ${this.targetTroopsRatio} for player ${this.player} invalid`)
} else {
this.player.setTargetTroopRatio(this.targetTroopsRatio)
}
this.active = false
}
owner(): Player {
return null;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+63 -54
View File
@@ -5,62 +5,71 @@ import { consolex } from "../Consolex";
import { TileRef } from "../game/GameMap";
export class ShellExecution implements Execution {
private active = true;
private pathFinder: PathFinder;
private shell: Unit;
private active = true
private pathFinder: PathFinder
private shell: Unit
constructor(
private spawn: TileRef,
private _owner: Player,
private ownerUnit: Unit,
private target: Unit,
) {}
constructor(private spawn: TileRef, private _owner: Player, private ownerUnit: Unit, private target: Unit) {
init(mg: Game, ticks: number): void {
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10);
}
tick(ticks: number): void {
if (this.shell == null) {
this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn);
}
if (!this.shell.isActive()) {
this.active = false;
return;
}
if (
!this.target.isActive() ||
!this.ownerUnit.isActive() ||
this.target.owner() == this.shell.owner()
) {
this.shell.delete(false);
this.active = false;
return;
}
for (let i = 0; i < 3; i++) {
const result = this.pathFinder.nextTile(
this.shell.tile(),
this.target.tile(),
3,
);
switch (result.type) {
case PathFindResultType.Completed:
this.active = false;
this.target.modifyHealth(-this.shell.info().damage);
this.shell.delete(false);
return;
case PathFindResultType.NextTile:
this.shell.move(result.tile);
break;
case PathFindResultType.Pending:
return;
case PathFindResultType.PathNotFound:
consolex.log(`Shell ${this.shell} could not find target`);
this.active = false;
this.shell.delete(false);
return;
}
}
}
init(mg: Game, ticks: number): void {
this.pathFinder = PathFinder.Mini(mg, 2000, true, 10)
}
tick(ticks: number): void {
if (this.shell == null) {
this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn)
}
if (!this.shell.isActive()) {
this.active = false
return
}
if (!this.target.isActive() || !this.ownerUnit.isActive() || this.target.owner() == this.shell.owner()) {
this.shell.delete(false)
this.active = false
return
}
for (let i = 0; i < 3; i++) {
const result = this.pathFinder.nextTile(this.shell.tile(), this.target.tile(), 3)
switch (result.type) {
case PathFindResultType.Completed:
this.active = false
this.target.modifyHealth(-this.shell.info().damage)
this.shell.delete(false)
return
case PathFindResultType.NextTile:
this.shell.move(result.tile)
break
case PathFindResultType.Pending:
return
case PathFindResultType.PathNotFound:
consolex.log(`Shell ${this.shell} could not find target`)
this.active = false
this.shell.delete(false)
return
}
}
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+56 -46
View File
@@ -1,58 +1,68 @@
import { Cell, Execution, Game, Player, PlayerInfo, PlayerType } from "../game/Game"
import { TileRef } from "../game/GameMap"
import { BotExecution } from "./BotExecution"
import { PlayerExecution } from "./PlayerExecution"
import { getSpawnTiles } from "./Util"
import {
Cell,
Execution,
Game,
Player,
PlayerInfo,
PlayerType,
} from "../game/Game";
import { TileRef } from "../game/GameMap";
import { BotExecution } from "./BotExecution";
import { PlayerExecution } from "./PlayerExecution";
import { getSpawnTiles } from "./Util";
export class SpawnExecution implements Execution {
active: boolean = true;
private mg: Game;
active: boolean = true
private mg: Game
constructor(
private playerInfo: PlayerInfo,
private tile: TileRef,
) {}
constructor(
private playerInfo: PlayerInfo,
private tile: TileRef
) { }
init(mg: Game, ticks: number) {
this.mg = mg;
}
init(mg: Game, ticks: number) {
this.mg = mg
tick(ticks: number) {
this.active = false;
if (!this.mg.inSpawnPhase()) {
return;
}
tick(ticks: number) {
this.active = false
if (!this.mg.inSpawnPhase()) {
return
}
const existing = this.mg.players().find(p => p.id() == this.playerInfo.id)
if (existing) {
existing.tiles().forEach(t => existing.relinquish(t))
getSpawnTiles(this.mg, this.tile).forEach(t => {
existing.conquer(t)
})
return
}
const player = this.mg.addPlayer(this.playerInfo, this.mg.config().startManpower(this.playerInfo))
getSpawnTiles(this.mg, this.tile).forEach(t => {
player.conquer(t)
})
this.mg.addExecution(new PlayerExecution(player.id()))
if (player.type() == PlayerType.Bot) {
this.mg.addExecution(new BotExecution(player))
}
const existing = this.mg
.players()
.find((p) => p.id() == this.playerInfo.id);
if (existing) {
existing.tiles().forEach((t) => existing.relinquish(t));
getSpawnTiles(this.mg, this.tile).forEach((t) => {
existing.conquer(t);
});
return;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active
const player = this.mg.addPlayer(
this.playerInfo,
this.mg.config().startManpower(this.playerInfo),
);
getSpawnTiles(this.mg, this.tile).forEach((t) => {
player.conquer(t);
});
this.mg.addExecution(new PlayerExecution(player.id()));
if (player.type() == PlayerType.Bot) {
this.mg.addExecution(new BotExecution(player));
}
}
activeDuringSpawnPhase(): boolean {
return true
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
}
activeDuringSpawnPhase(): boolean {
return true;
}
}
+27 -27
View File
@@ -1,38 +1,38 @@
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class TargetPlayerExecution implements Execution {
private requestor: Player;
private target: Player;
private requestor: Player
private target: Player
private active = true;
private active = true
constructor(
private requestorID: PlayerID,
private targetID: PlayerID,
) {}
constructor(private requestorID: PlayerID, private targetID: PlayerID) { }
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.requestorID);
this.target = mg.player(this.targetID);
}
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.requestorID)
this.target = mg.player(this.targetID)
tick(ticks: number): void {
if (this.requestor.canTarget(this.target)) {
this.requestor.target(this.target);
this.target.updateRelation(this.requestor, -40);
}
this.active = false;
}
tick(ticks: number): void {
if (this.requestor.canTarget(this.target)) {
this.requestor.target(this.target)
this.target.updateRelation(this.requestor, -40)
}
this.active = false
}
owner(): Player {
return null;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+137 -111
View File
@@ -1,131 +1,157 @@
import { MessageType } from '../game/Game';
import { MessageType } from "../game/Game";
import { renderNumber } from "../../client/Utils";
import { AllPlayers, Cell, Execution, Game, Unit, Player, PlayerID, UnitType } from "../game/Game";
import {
AllPlayers,
Cell,
Execution,
Game,
Unit,
Player,
PlayerID,
UnitType,
} from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { distSortUnit } from "../Util";
import { consolex } from "../Consolex";
import { TileRef } from '../game/GameMap';
import { TileRef } from "../game/GameMap";
export class TradeShipExecution implements Execution {
private active = true;
private mg: Game;
private origOwner: Player;
private tradeShip: Unit;
private index = 0;
private wasCaptured = false;
private active = true
private mg: Game
private origOwner: Player
private tradeShip: Unit
private index = 0
private wasCaptured = false
constructor(
private _owner: PlayerID,
private srcPort: Unit,
private dstPort: Unit,
private pathFinder: PathFinder,
// don't modify
private path: TileRef[],
) {}
constructor(
private _owner: PlayerID,
private srcPort: Unit,
private dstPort: Unit,
private pathFinder: PathFinder,
// don't modify
private path: TileRef[]
) { }
init(mg: Game, ticks: number): void {
this.mg = mg;
this.origOwner = mg.player(this._owner);
}
init(mg: Game, ticks: number): void {
this.mg = mg
this.origOwner = mg.player(this._owner)
tick(ticks: number): void {
if (this.tradeShip == null) {
const spawn = this.origOwner.canBuild(
UnitType.TradeShip,
this.srcPort.tile(),
);
if (spawn == false) {
consolex.warn(`cannot build trade ship`);
this.active = false;
return;
}
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn);
}
tick(ticks: number): void {
if (this.tradeShip == null) {
const spawn = this.origOwner.canBuild(UnitType.TradeShip, this.srcPort.tile())
if (spawn == false) {
consolex.warn(`cannot build trade ship`)
this.active = false
return
}
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn)
}
if (!this.tradeShip.isActive()) {
this.active = false
return
}
if (this.origOwner != this.tradeShip.owner()) {
// Store as vairable in case ship is recaptured by previous owner
this.wasCaptured = true
}
if (!this.wasCaptured && (!this.dstPort.isActive() || !this.tradeShip.owner().isAlliedWith(this.dstPort.owner()))) {
this.tradeShip.delete(false)
this.active = false
return
}
if (this.wasCaptured) {
const ports = this.tradeShip.owner().units(UnitType.Port).sort(distSortUnit(this.mg, this.tradeShip))
if (ports.length == 0) {
this.tradeShip.delete(false)
this.active = false
return
}
const dstPort = ports[0]
const result = this.pathFinder.nextTile(this.tradeShip.tile(), dstPort.tile())
switch (result.type) {
case PathFindResultType.Completed:
const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), dstPort.tile()))
this.tradeShip.owner().addGold(gold)
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
MessageType.SUCCESS,
this.tradeShip.owner().id()
)
this.tradeShip.delete(false)
break
case PathFindResultType.Pending:
// Fire unit event to rerender.
this.tradeShip.move(this.tradeShip.tile())
break
case PathFindResultType.NextTile:
this.tradeShip.move(result.tile)
break
case PathFindResultType.PathNotFound:
consolex.warn('captured trade ship cannot find route')
this.active = false
break
}
return
}
if (this.index >= this.path.length) {
this.active = false
const gold = this.mg.config().tradeShipGold(this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile()))
this.srcPort.owner().addGold(gold)
this.dstPort.owner().addGold(gold)
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
MessageType.SUCCESS,
this.dstPort.owner().id()
)
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.dstPort.owner().displayName()}`,
MessageType.SUCCESS,
this.srcPort.owner().id()
)
this.tradeShip.delete(false)
return
}
this.tradeShip.move(this.path[this.index])
this.index++
if (!this.tradeShip.isActive()) {
this.active = false;
return;
}
owner(): Player {
return null
if (this.origOwner != this.tradeShip.owner()) {
// Store as vairable in case ship is recaptured by previous owner
this.wasCaptured = true;
}
isActive(): boolean {
return this.active
if (
!this.wasCaptured &&
(!this.dstPort.isActive() ||
!this.tradeShip.owner().isAlliedWith(this.dstPort.owner()))
) {
this.tradeShip.delete(false);
this.active = false;
return;
}
activeDuringSpawnPhase(): boolean {
return false
if (this.wasCaptured) {
const ports = this.tradeShip
.owner()
.units(UnitType.Port)
.sort(distSortUnit(this.mg, this.tradeShip));
if (ports.length == 0) {
this.tradeShip.delete(false);
this.active = false;
return;
}
const dstPort = ports[0];
const result = this.pathFinder.nextTile(
this.tradeShip.tile(),
dstPort.tile(),
);
switch (result.type) {
case PathFindResultType.Completed:
const gold = this.mg
.config()
.tradeShipGold(
this.mg.manhattanDist(this.srcPort.tile(), dstPort.tile()),
);
this.tradeShip.owner().addGold(gold);
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from ship captured from ${this.origOwner.displayName()}`,
MessageType.SUCCESS,
this.tradeShip.owner().id(),
);
this.tradeShip.delete(false);
break;
case PathFindResultType.Pending:
// Fire unit event to rerender.
this.tradeShip.move(this.tradeShip.tile());
break;
case PathFindResultType.NextTile:
this.tradeShip.move(result.tile);
break;
case PathFindResultType.PathNotFound:
consolex.warn("captured trade ship cannot find route");
this.active = false;
break;
}
return;
}
}
if (this.index >= this.path.length) {
this.active = false;
const gold = this.mg
.config()
.tradeShipGold(
this.mg.manhattanDist(this.srcPort.tile(), this.dstPort.tile()),
);
this.srcPort.owner().addGold(gold);
this.dstPort.owner().addGold(gold);
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.srcPort.owner().displayName()}`,
MessageType.SUCCESS,
this.dstPort.owner().id(),
);
this.mg.displayMessage(
`Received ${renderNumber(gold)} gold from trade with ${this.dstPort.owner().displayName()}`,
MessageType.SUCCESS,
this.srcPort.owner().id(),
);
this.tradeShip.delete(false);
return;
}
this.tradeShip.move(this.path[this.index]);
this.index++;
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+142 -116
View File
@@ -1,6 +1,16 @@
import { Unit, Cell, Execution, Game, Player, PlayerID, TerraNullius, UnitType, TerrainType } from "../game/Game";
import {
Unit,
Cell,
Execution,
Game,
Player,
PlayerID,
TerraNullius,
UnitType,
TerrainType,
} from "../game/Game";
import { AttackExecution } from "./AttackExecution";
import { MessageType } from '../game/Game';
import { MessageType } from "../game/Game";
import { PathFinder } from "../pathfinding/PathFinding";
import { PathFindResultType } from "../pathfinding/AStar";
import { consolex } from "../Consolex";
@@ -8,138 +18,154 @@ import { TileRef } from "../game/GameMap";
import { targetTransportTile } from "../Util";
export class TransportShipExecution implements Execution {
private lastMove: number;
private lastMove: number
// TODO: make this configurable
private ticksPerMove = 1;
// TODO: make this configurable
private ticksPerMove = 1
private active = true;
private active = true
private mg: Game;
private attacker: Player;
private target: Player | TerraNullius;
private mg: Game
private attacker: Player
private target: Player | TerraNullius
// TODO make private
public path: TileRef[];
private src: TileRef | null;
private dst: TileRef | null;
// TODO make private
public path: TileRef[]
private src: TileRef | null
private dst: TileRef | null
private boat: Unit;
private pathFinder: PathFinder;
private boat: Unit
constructor(
private attackerID: PlayerID,
private targetID: PlayerID | null,
private ref: TileRef,
private troops: number | null,
) {}
private pathFinder: PathFinder
activeDuringSpawnPhase(): boolean {
return false;
}
constructor(
private attackerID: PlayerID,
private targetID: PlayerID | null,
private ref: TileRef,
private troops: number | null,
) { }
init(mg: Game, ticks: number) {
this.lastMove = ticks;
this.mg = mg;
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2);
activeDuringSpawnPhase(): boolean {
return false
this.attacker = mg.player(this.attackerID);
if (
this.attacker.units(UnitType.TransportShip).length >=
mg.config().boatMaxNumber()
) {
mg.displayMessage(
`No boats available, max ${mg.config().boatMaxNumber()}`,
MessageType.WARN,
this.attackerID,
);
this.active = false;
this.attacker.addTroops(this.troops);
return;
}
init(mg: Game, ticks: number) {
this.lastMove = ticks
this.mg = mg
this.pathFinder = PathFinder.Mini(mg, 10_000, false, 2)
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
this.target = mg.terraNullius();
} else {
this.target = mg.player(this.targetID);
}
this.attacker = mg.player(this.attackerID)
if (this.troops == null) {
this.troops = this.mg
.config()
.boatAttackAmount(this.attacker, this.target);
}
if (this.attacker.units(UnitType.TransportShip).length >= mg.config().boatMaxNumber()) {
mg.displayMessage(`No boats available, max ${mg.config().boatMaxNumber()}`, MessageType.WARN, this.attackerID)
this.active = false
this.attacker.addTroops(this.troops)
return
this.troops = Math.min(this.troops, this.attacker.troops());
this.dst = targetTransportTile(this.mg, this.ref);
if (this.dst == null) {
consolex.warn(
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
);
this.active = false;
return;
}
const src = this.attacker.canBuild(UnitType.TransportShip, this.dst);
if (src == false) {
consolex.warn(`can't build transport ship`);
this.active = false;
return;
}
this.src = src;
this.boat = this.attacker.buildUnit(
UnitType.TransportShip,
this.troops,
this.src,
);
}
tick(ticks: number) {
if (!this.active) {
return;
}
if (!this.boat.isActive()) {
this.active = false;
return;
}
if (ticks - this.lastMove < this.ticksPerMove) {
return;
}
this.lastMove = ticks;
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst);
switch (result.type) {
case PathFindResultType.Completed:
if (this.mg.owner(this.dst) == this.attacker) {
this.attacker.addTroops(this.troops);
this.boat.delete(false);
this.active = false;
return;
}
if (this.targetID == null || this.targetID == this.mg.terraNullius().id()) {
this.target = mg.terraNullius()
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
this.target.addTroops(this.troops);
} else {
this.target = mg.player(this.targetID)
this.attacker.conquer(this.dst);
this.mg.addExecution(
new AttackExecution(
this.troops,
this.attacker.id(),
this.targetID,
this.dst,
false,
),
);
}
if (this.troops == null) {
this.troops = this.mg.config().boatAttackAmount(this.attacker, this.target)
}
this.troops = Math.min(this.troops, this.attacker.troops())
this.dst = targetTransportTile(this.mg, this.ref)
if (this.dst == null) {
consolex.warn(`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`)
this.active = false
return
}
const src = this.attacker.canBuild(UnitType.TransportShip, this.dst)
if (src == false) {
consolex.warn(`can't build transport ship`)
this.active = false
return
}
this.src = src
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.troops, this.src)
this.boat.delete(false);
this.active = false;
return;
case PathFindResultType.NextTile:
this.boat.move(result.tile);
break;
case PathFindResultType.Pending:
break;
case PathFindResultType.PathNotFound:
// TODO: add to poisoned port list
consolex.warn(`path not found tot dst`);
this.boat.delete(false);
this.active = false;
return;
}
}
tick(ticks: number) {
if (!this.active) {
return
}
if (!this.boat.isActive()) {
this.active = false
return
}
if (ticks - this.lastMove < this.ticksPerMove) {
return
}
this.lastMove = ticks
const result = this.pathFinder.nextTile(this.boat.tile(), this.dst)
switch (result.type) {
case PathFindResultType.Completed:
if (this.mg.owner(this.dst) == this.attacker) {
this.attacker.addTroops(this.troops)
this.boat.delete(false)
this.active = false
return
}
if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) {
this.target.addTroops(this.troops)
} else {
this.attacker.conquer(this.dst)
this.mg.addExecution(
new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst, false)
)
}
this.boat.delete(false)
this.active = false
return
case PathFindResultType.NextTile:
this.boat.move(result.tile)
break
case PathFindResultType.Pending:
break
case PathFindResultType.PathNotFound:
// TODO: add to poisoned port list
consolex.warn(`path not found tot dst`)
this.boat.delete(false)
this.active = false
return
}
}
owner(): Player {
return this.attacker
}
isActive(): boolean {
return this.active
}
owner(): Player {
return this.attacker;
}
isActive(): boolean {
return this.active;
}
}
+46 -42
View File
@@ -1,52 +1,56 @@
import { euclDistFN, GameMap, TileRef } from "../game/GameMap";
export function getSpawnTiles(gm: GameMap, tile: TileRef): TileRef[] {
return Array.from(gm.bfs(tile, euclDistFN(tile, 4)))
.filter(t => !gm.hasOwner(t) && gm.isLand(t))
return Array.from(gm.bfs(tile, euclDistFN(tile, 4))).filter(
(t) => !gm.hasOwner(t) && gm.isLand(t),
);
}
export function closestTwoTiles(gm: GameMap, x: Iterable<TileRef>, y: Iterable<TileRef>): { x: TileRef, y: TileRef } {
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
export function closestTwoTiles(
gm: GameMap,
x: Iterable<TileRef>,
y: Iterable<TileRef>,
): { x: TileRef; y: TileRef } {
const xSorted = Array.from(x).sort((a, b) => gm.x(a) - gm.x(b));
const ySorted = Array.from(y).sort((a, b) => gm.x(a) - gm.x(b));
if (xSorted.length == 0 || ySorted.length == 0) {
return null;
if (xSorted.length == 0 || ySorted.length == 0) {
return null;
}
let i = 0;
let j = 0;
let minDistance = Infinity;
let result = { x: xSorted[0], y: ySorted[0] };
while (i < xSorted.length && j < ySorted.length) {
const currentX = xSorted[i];
const currentY = ySorted[j];
const distance =
Math.abs(gm.x(currentX) - gm.x(currentY)) +
Math.abs(gm.y(currentX) - gm.y(currentY));
if (distance < minDistance) {
minDistance = distance;
result = { x: currentX, y: currentY };
}
let i = 0;
let j = 0;
let minDistance = Infinity;
let result = { x: xSorted[0], y: ySorted[0] };
while (i < xSorted.length && j < ySorted.length) {
const currentX = xSorted[i];
const currentY = ySorted[j];
const distance =
Math.abs(gm.x(currentX) - gm.x(currentY)) +
Math.abs(gm.y(currentX) - gm.y(currentY));
if (distance < minDistance) {
minDistance = distance;
result = { x: currentX, y: currentY };
}
// If we're at the end of X, must move Y forward
if (i === xSorted.length - 1) {
j++;
}
// If we're at the end of Y, must move X forward
else if (j === ySorted.length - 1) {
i++;
}
// Otherwise, move whichever pointer has smaller x value
else if (gm.x(currentX) < gm.x(currentY)) {
i++;
} else {
j++;
}
// If we're at the end of X, must move Y forward
if (i === xSorted.length - 1) {
j++;
}
// If we're at the end of Y, must move X forward
else if (j === ySorted.length - 1) {
i++;
}
// Otherwise, move whichever pointer has smaller x value
else if (gm.x(currentX) < gm.x(currentY)) {
i++;
} else {
j++;
}
}
return result;
}
return result;
}
+39 -35
View File
@@ -1,49 +1,53 @@
import { EventBus, GameEvent } from "../EventBus"
import { Execution, Game, Player, PlayerID } from "../game/Game"
import { EventBus, GameEvent } from "../EventBus";
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class WinEvent implements GameEvent {
constructor(public readonly winner: Player) { }
constructor(public readonly winner: Player) {}
}
export class WinCheckExecution implements Execution {
private active = true;
private active = true
private mg: Game;
private mg: Game
constructor() {}
constructor() {
init(mg: Game, ticks: number) {
this.mg = mg;
}
tick(ticks: number) {
if (ticks % 10 != 0) {
return;
}
init(mg: Game, ticks: number) {
this.mg = mg
const sorted = this.mg
.players()
.sort((a, b) => b.numTilesOwned() - a.numTilesOwned());
if (sorted.length == 0) {
return;
}
tick(ticks: number) {
if (ticks % 10 != 0) {
return
}
const sorted = this.mg.players().sort((a, b) => b.numTilesOwned() - a.numTilesOwned())
if (sorted.length == 0) {
return
}
const max = sorted[0]
const numTilesWithoutFallout = this.mg.numLandTiles() - this.mg.numTilesWithFallout()
if (max.numTilesOwned() / numTilesWithoutFallout * 100 > this.mg.config().percentageTilesOwnedToWin()) {
this.mg.setWinner(max)
console.log(`${max.name()} has won the game`)
this.active = false
}
const max = sorted[0];
const numTilesWithoutFallout =
this.mg.numLandTiles() - this.mg.numTilesWithFallout();
if (
(max.numTilesOwned() / numTilesWithoutFallout) * 100 >
this.mg.config().percentageTilesOwnedToWin()
) {
this.mg.setWinner(max);
console.log(`${max.name()} has won the game`);
this.active = false;
}
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
@@ -1,40 +1,51 @@
import { consolex } from "../../Consolex";
import {AllianceRequest, Execution, Game, Player, PlayerID} from "../../game/Game";
import {
AllianceRequest,
Execution,
Game,
Player,
PlayerID,
} from "../../game/Game";
export class AllianceRequestExecution implements Execution {
private active = true
private mg: Game = null
private requestor: Player;
private recipient: Player
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
constructor(private requestorID: PlayerID, private recipientID: PlayerID) { }
constructor(
private requestorID: PlayerID,
private recipientID: PlayerID,
) {}
init(mg: Game, ticks: number): void {
this.mg = mg
this.requestor = mg.player(this.requestorID)
this.recipient = mg.player(this.recipientID)
init(mg: Game, ticks: number): void {
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
tick(ticks: number): void {
if (this.requestor.isAlliedWith(this.recipient)) {
consolex.warn("already allied");
} else if (
this.requestor.recentOrPendingAllianceRequestWith(this.recipient)
) {
consolex.warn("recent or pending alliance request");
} else {
this.requestor.createAllianceRequest(this.recipient);
}
this.active = false;
}
tick(ticks: number): void {
if (this.requestor.isAlliedWith(this.recipient)) {
consolex.warn('already allied')
} else if (this.requestor.recentOrPendingAllianceRequestWith(this.recipient)) {
consolex.warn('recent or pending alliance request')
} else {
this.requestor.createAllianceRequest(this.recipient)
}
this.active = false
}
owner(): Player {
return null;
}
owner(): Player {
return null
}
isActive(): boolean {
return this.active;
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
@@ -1,49 +1,61 @@
import { consolex } from "../../Consolex";
import { AllianceRequest, Execution, Game, Player, PlayerID } from "../../game/Game";
import {
AllianceRequest,
Execution,
Game,
Player,
PlayerID,
} from "../../game/Game";
export class AllianceRequestReplyExecution implements Execution {
private active = true
private mg: Game = null
private requestor: Player;
private recipient: Player
private active = true;
private mg: Game = null;
private requestor: Player;
private recipient: Player;
constructor(private requestorID: PlayerID, private recipientID: PlayerID, private accept: boolean) { }
constructor(
private requestorID: PlayerID,
private recipientID: PlayerID,
private accept: boolean,
) {}
init(mg: Game, ticks: number): void {
this.mg = mg
this.requestor = mg.player(this.requestorID)
this.recipient = mg.player(this.recipientID)
}
init(mg: Game, ticks: number): void {
this.mg = mg;
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
}
tick(ticks: number): void {
if (this.requestor.isAlliedWith(this.recipient)) {
consolex.warn('already allied')
tick(ticks: number): void {
if (this.requestor.isAlliedWith(this.recipient)) {
consolex.warn("already allied");
} else {
const request = this.requestor
.outgoingAllianceRequests()
.find((ar) => ar.recipient() == this.recipient);
if (request == null) {
consolex.warn("no alliance request found");
} else {
if (this.accept) {
request.accept();
this.requestor.updateRelation(this.recipient, 100);
this.recipient.updateRelation(this.requestor, 100);
} else {
const request = this.requestor.outgoingAllianceRequests().find(ar => ar.recipient() == this.recipient)
if (request == null) {
consolex.warn('no alliance request found')
} else {
if (this.accept) {
request.accept()
this.requestor.updateRelation(this.recipient, 100)
this.recipient.updateRelation(this.requestor, 100)
} else {
request.reject()
}
}
request.reject();
}
this.active = false
}
}
this.active = false;
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
@@ -1,45 +1,54 @@
import { consolex } from "../../Consolex";
import { AllianceRequest, Execution, Game, Player, PlayerID } from "../../game/Game";
import {
AllianceRequest,
Execution,
Game,
Player,
PlayerID,
} from "../../game/Game";
export class BreakAllianceExecution implements Execution {
private active = true
private requestor: Player;
private recipient: Player
private mg: Game
private active = true;
private requestor: Player;
private recipient: Player;
private mg: Game;
constructor(private requestorID: PlayerID, private recipientID: PlayerID) { }
constructor(
private requestorID: PlayerID,
private recipientID: PlayerID,
) {}
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.requestorID)
this.recipient = mg.player(this.recipientID)
this.mg = mg
}
init(mg: Game, ticks: number): void {
this.requestor = mg.player(this.requestorID);
this.recipient = mg.player(this.recipientID);
this.mg = mg;
}
tick(ticks: number): void {
const alliance = this.requestor.allianceWith(this.recipient)
if (alliance == null) {
consolex.warn('cant break alliance, not allied')
} else {
this.requestor.breakAlliance(alliance)
this.recipient.updateRelation(this.requestor, -200)
for (const player of this.mg.players()) {
if (player != this.requestor) {
player.updateRelation(this.requestor, -40)
}
}
tick(ticks: number): void {
const alliance = this.requestor.allianceWith(this.recipient);
if (alliance == null) {
consolex.warn("cant break alliance, not allied");
} else {
this.requestor.breakAlliance(alliance);
this.recipient.updateRelation(this.requestor, -200);
for (const player of this.mg.players()) {
if (player != this.requestor) {
player.updateRelation(this.requestor, -40);
}
this.active = false
}
}
this.active = false;
}
owner(): Player {
return null
}
owner(): Player {
return null;
}
isActive(): boolean {
return this.active
}
isActive(): boolean {
return this.active;
}
activeDuringSpawnPhase(): boolean {
return false
}
}
activeDuringSpawnPhase(): boolean {
return false;
}
}
+231 -41
View File
@@ -1,43 +1,233 @@
export const BOT_NAME_PREFIXES = [
"Akkadian", "Babylonian", "Assyrian", "Sumerian", "Hittite", "Phoenician",
"Canaanite", "Minoan", "Mycenaean", "Etruscan", "Scythian", "Thracian",
"Dacian", "Illyrian", "Median", "Chaldean",
"Roman", "Greek", "Byzantine", "Persian", "Parthian", "Seleucid",
"Ptolemaic", "Palmyrene", "Macedonian", "Carthaginian",
"Ming", "Tang", "Song", "Yuan",
"Mauryan", "Kushan", "Rajput", "Mughal", "Satavahana", "Vijayanagara",
"Egyptian", "Nubian", "Kushite", "Aksumite", "Ethiopian", "Songhai",
"Malian", "Ghanaian", "Benin", "Ashanti", "Zulu", "Tuareg", "Berber",
"Kanem-Bornu", "Buganda", "Mossi", "Swahili", "Somali", "Wolof",
"Umayyad", "Abbasid", "Ayyubid", "Fatimid", "Mamluk", "Seljuk",
"Safavid", "Ottoman", "Almoravid", "Almohad", "Rashidun", "Ziyarid",
"Frankish", "Visigothic", "Ostrogothic", "Viking", "Norman", "Saxon",
"Anglo-Saxon", "Celtic", "Gaulish", "Carolingian", "Merovingian",
"Capetian", "Plantagenet", "Tudor", "Stuart", "Habsburg", "Romanov",
"Lancaster", "York", "Bourbon", "Napoleonic",
"British", "French", "Spanish", "Portuguese", "Dutch", "Russian",
"German", "Italian", "Swedish", "Norwegian", "Danish", "Polish",
"Hungarian", "Austrian", "Swiss", "Czech", "Slovak", "Serbian",
"Croatian", "Bosnian", "Montenegrin", "Bulgarian", "Romanian",
"Apache", "Sioux", "Cherokee", "Navajo", "Iroquois", "Inuit", "Arawak",
"Carib", "Taino", "Aztec", "Mayan", "Incan", "Mapuche", "Guarani",
"Tupi", "Yanomami", "Zuni", "Hopi", "Kiowa", "Comanche", "Shoshone",
"Japanese", "Ryukyu", "Ainu", "Cham", "Khmer", "Thai", "Vietnamese",
"Burmese", "Balinese", "Malay", "Filipino", "Mongolian",
"Korean", "Tibetan", "Manchu", "Uyghur", "Hmong", "Karen", "Pyu",
"Hawaiian", "Fijian", "Tongan", "Samoan", "Maori", "Micronesian",
"Hebrew", "Armenian", "Georgian", "Phoenician", "Assyrian", "Chaldean",
"Kurdish", "Turkic", "Kazakh", "Uzbek", "Kyrgyz", "Tajik", "Uighur",
"Pashtun", "Baloch", "Afghan", "Persian",
]
export const BOT_NAME_SUFFIXES = [
"Empire", "Dynasty", "Kingdom", "Sultanate", "Confederation", "Union",
"Republic", "Caliphate", "Dominion", "Realm", "State",
"Federation", "Territory", "Commonwealth", "League", "Duchy", "Province",
"Protectorate", "Colony", "Mandate", "Free State","Canton", "Region", "Nation",
"Assembly", "Hierarchy", "Archduchy", "Grand Duchy","Metropolis", "Cluster",
"Alliance", "Tribunal", "Council", "Confederacy", "Order", "Regime",
"Dominion", "Syndicate","Guild", "Corporation", "Patriarchy",
"Matriarchy","Legion", "Horde", "Clan", "Brotherhood", "Sisterhood","Ascendancy", "Supremacy",
"Province","Kingdoms", "Tribes", "Dominion", "Assembly", "Republics"
"Akkadian",
"Babylonian",
"Assyrian",
"Sumerian",
"Hittite",
"Phoenician",
"Canaanite",
"Minoan",
"Mycenaean",
"Etruscan",
"Scythian",
"Thracian",
"Dacian",
"Illyrian",
"Median",
"Chaldean",
"Roman",
"Greek",
"Byzantine",
"Persian",
"Parthian",
"Seleucid",
"Ptolemaic",
"Palmyrene",
"Macedonian",
"Carthaginian",
"Ming",
"Tang",
"Song",
"Yuan",
"Mauryan",
"Kushan",
"Rajput",
"Mughal",
"Satavahana",
"Vijayanagara",
"Egyptian",
"Nubian",
"Kushite",
"Aksumite",
"Ethiopian",
"Songhai",
"Malian",
"Ghanaian",
"Benin",
"Ashanti",
"Zulu",
"Tuareg",
"Berber",
"Kanem-Bornu",
"Buganda",
"Mossi",
"Swahili",
"Somali",
"Wolof",
"Umayyad",
"Abbasid",
"Ayyubid",
"Fatimid",
"Mamluk",
"Seljuk",
"Safavid",
"Ottoman",
"Almoravid",
"Almohad",
"Rashidun",
"Ziyarid",
"Frankish",
"Visigothic",
"Ostrogothic",
"Viking",
"Norman",
"Saxon",
"Anglo-Saxon",
"Celtic",
"Gaulish",
"Carolingian",
"Merovingian",
"Capetian",
"Plantagenet",
"Tudor",
"Stuart",
"Habsburg",
"Romanov",
"Lancaster",
"York",
"Bourbon",
"Napoleonic",
"British",
"French",
"Spanish",
"Portuguese",
"Dutch",
"Russian",
"German",
"Italian",
"Swedish",
"Norwegian",
"Danish",
"Polish",
"Hungarian",
"Austrian",
"Swiss",
"Czech",
"Slovak",
"Serbian",
"Croatian",
"Bosnian",
"Montenegrin",
"Bulgarian",
"Romanian",
"Apache",
"Sioux",
"Cherokee",
"Navajo",
"Iroquois",
"Inuit",
"Arawak",
"Carib",
"Taino",
"Aztec",
"Mayan",
"Incan",
"Mapuche",
"Guarani",
"Tupi",
"Yanomami",
"Zuni",
"Hopi",
"Kiowa",
"Comanche",
"Shoshone",
"Japanese",
"Ryukyu",
"Ainu",
"Cham",
"Khmer",
"Thai",
"Vietnamese",
"Burmese",
"Balinese",
"Malay",
"Filipino",
"Mongolian",
"Korean",
"Tibetan",
"Manchu",
"Uyghur",
"Hmong",
"Karen",
"Pyu",
"Hawaiian",
"Fijian",
"Tongan",
"Samoan",
"Maori",
"Micronesian",
"Hebrew",
"Armenian",
"Georgian",
"Phoenician",
"Assyrian",
"Chaldean",
"Kurdish",
"Turkic",
"Kazakh",
"Uzbek",
"Kyrgyz",
"Tajik",
"Uighur",
"Pashtun",
"Baloch",
"Afghan",
"Persian",
];
export const BOT_NAME_SUFFIXES = [
"Empire",
"Dynasty",
"Kingdom",
"Sultanate",
"Confederation",
"Union",
"Republic",
"Caliphate",
"Dominion",
"Realm",
"State",
"Federation",
"Territory",
"Commonwealth",
"League",
"Duchy",
"Province",
"Protectorate",
"Colony",
"Mandate",
"Free State",
"Canton",
"Region",
"Nation",
"Assembly",
"Hierarchy",
"Archduchy",
"Grand Duchy",
"Metropolis",
"Cluster",
"Alliance",
"Tribunal",
"Council",
"Confederacy",
"Order",
"Regime",
"Dominion",
"Syndicate",
"Guild",
"Corporation",
"Patriarchy",
"Matriarchy",
"Legion",
"Horde",
"Clan",
"Brotherhood",
"Sisterhood",
"Ascendancy",
"Supremacy",
"Province",
"Kingdoms",
"Tribes",
"Dominion",
"Assembly",
"Republics",
];