First Commit

This commit is contained in:
evanpelle
2024-08-04 19:51:23 -07:00
commit 05f55c490f
53 changed files with 15862 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
import PriorityQueue from "priority-queue-typescript";
import {Cell, Execution, MutableGame, MutablePlayer, PlayerID, Player, TerrainTypes, TerraNullius, Tile} from "../Game";
import {PseudoRandom} from "../PseudoRandom";
import {manhattanDist} from "../Util";
export class AttackExecution implements Execution {
private active: boolean = true;
private toConquer: PriorityQueue<TileContainer> = new PriorityQueue<TileContainer>(11, (a: TileContainer, b: TileContainer) => a.priority - b.priority);
private random = new PseudoRandom(123)
private _owner: MutablePlayer
private target: MutablePlayer | TerraNullius
constructor(
private troops: number,
private _ownerID: PlayerID,
private targetID: PlayerID | null,
private targetCell: Cell | null
) { }
init(gs: MutableGame, ticks: number) {
this._owner = gs.player(this._ownerID)
this.target = this.targetID == null ? gs.terraNullius() : gs.player(this.targetID)
this.troops = Math.min(this._owner.troops(), this.troops)
this._owner.setTroops(this._owner.troops() - this.troops)
}
tick(ticks: number) {
if (!this.active) {
return
}
let numTilesPerTick = this._owner.borderTilesWith(this.target).size / 2
while (numTilesPerTick > 0) {
if (this.troops < 1) {
this.active = false
return
}
if (this.toConquer.size() == 0) {
this.calculateToConquer()
}
if (this.toConquer.size() == 0) {
this.active = false
this._owner.addTroops(this.troops)
return
}
const tileToConquer: Tile = this.toConquer.poll().tile
const onBorder = tileToConquer.neighbors().filter(t => t.owner() == this._owner).length > 0
if (tileToConquer.owner() != this.target || !onBorder) {
continue
}
this._owner.conquer(tileToConquer.cell())
this.troops -= 1
numTilesPerTick -= 1
}
}
private calculateToConquer() {
const border = this.owner().borderTilesWith(this.target)
const enemyBorder: Set<Tile> = new Set()
for (const b of border) {
b.neighbors()
.filter(t => t.terrain() == TerrainTypes.Land)
.filter(t => t.owner() == this.target)
.forEach(t => enemyBorder.add(t))
}
// let closestTile: Tile;
// let closestDist: number = Number.POSITIVE_INFINITY;
// for (const enemyTile of enemyBorder) {
// const dist = manhattanDist(enemyTile.cell(), this.targetCell)
// if (dist < closestDist) {
// closestTile = enemyTile
// }
// }
// tileByDist.forEach(t => console.log(`tile dist: ${manhattanDist(t.cell(), closestTile.cell())}`))
let tileByDist = []
if (this.targetCell == null) {
tileByDist = Array.from(enemyBorder).slice().sort((a, b) => this.random.next() - .5)
} else {
tileByDist = Array.from(enemyBorder).slice().sort((a, b) => manhattanDist(a.cell(), this.targetCell) - manhattanDist(b.cell(), this.targetCell))
}
for (let i = 0; i < Math.min(enemyBorder.size / 2, tileByDist.length); i++) {
const enemyTile = tileByDist[i]
const numOwnedByMe = enemyTile.neighbors()
.filter(t => t.terrain() == TerrainTypes.Land)
.filter(t => t.owner() == this._owner)
.length
// this.toConquer.add(new TileContainer(enemyTile, numOwnedByMe + (this.random.next() % 5) + (-5 * i / tileByDist.length)))
const r = this.random.next() % 4
this.toConquer.add(new TileContainer(enemyTile, r + numOwnedByMe * 1000))
}
}
owner(): MutablePlayer {
return this._owner
}
isActive(): boolean {
return this.active
}
}
class TileContainer {
constructor(public readonly tile: Tile, public readonly priority: number) { }
}
+158
View File
@@ -0,0 +1,158 @@
import PriorityQueue from "priority-queue-typescript";
import {Boat, Cell, Execution, MutableBoat, MutableGame, MutablePlayer, Player, PlayerID, Tile} from "../Game";
import {manhattanDist} from "../Util";
import {AttackExecution} from "./AttackExecution";
export class BoatAttackExecution implements Execution {
private lastMove: number
// TODO: make this configurable
private ticksPerMove = 1
private active = true
private mg: MutableGame
private attacker: MutablePlayer
private target: MutablePlayer
// TODO make private
public path: Tile[]
private src: Tile
private dst: Tile
private currTileIndex: number = 0
private boat: MutableBoat
constructor(
private attackerID: PlayerID,
private targetID: PlayerID | null,
private cell: Cell,
private troops: number
) { }
init(mg: MutableGame, ticks: number) {
if (this.targetID == null) {
throw new Error("attacking terranullius not supported")
}
this.lastMove = ticks
this.mg = mg
this.attacker = mg.player(this.attackerID)
this.target = mg.player(this.targetID)
this.troops = Math.min(this.troops, this.attacker.troops())
this.attacker.removeTroops(this.troops)
this.src = this.closestShoreTileToTarget(this.attacker, this.cell)
this.dst = this.closestShoreTileToTarget(this.target, this.cell)
this.path = this.computePath(this.src, this.dst)
if (this.path != null) {
console.log(`got path ${this.path.map(t => t.cell().toString())}`)
this.boat = this.attacker.addBoat(1000, this.src.cell(), this.target)
} else {
console.log('got null path')
this.active = false
}
}
tick(ticks: number) {
if (!this.active) {
return
}
if (ticks - this.lastMove < this.ticksPerMove) {
return
}
this.lastMove = ticks
this.currTileIndex++
if (this.currTileIndex >= this.path.length) {
if (this.dst.owner() == this.attacker) {
this.attacker.addTroops(this.troops)
this.active = false
return
}
this.attacker.conquer(this.dst.cell())
this.mg.addExecution(new AttackExecution(this.troops, this.attacker.id(), this.targetID, null))
this.active = false
return
}
const nextTile = this.path[this.currTileIndex]
this.boat.move(nextTile.cell())
}
owner(): MutablePlayer {
return this.attacker
}
isActive(): boolean {
return this.active
}
private closestShoreTileToTarget(player: Player, target: Cell): Tile {
const shoreTiles = Array.from(player.borderTiles()).filter(t => t.onShore())
return shoreTiles.reduce((closest, current) => {
const closestDistance = manhattanDist(target, closest.cell());
const currentDistance = manhattanDist(target, current.cell());
return currentDistance < closestDistance ? current : closest;
});
}
private computePath(src: Tile, dst: Tile): Tile[] {
if (!src.onShore() || !dst.onShore()) {
return null; // Both source and destination must be on water
}
const openSet = new PriorityQueue<{tile: Tile, fScore: number}>(
11,
(a, b) => a.fScore - b.fScore
);
const cameFrom = new Map<Tile, Tile>();
const gScore = new Map<Tile, number>();
gScore.set(src, 0);
openSet.add({tile: src, fScore: this.heuristic(src, dst)});
while (!openSet.empty()) {
const current = openSet.poll()!.tile;
if (current === dst) {
return this.reconstructPath(cameFrom, current);
}
for (const neighbor of current.neighbors()) {
if (!neighbor.onShore()) continue; // Skip non-water tiles
const tentativeGScore = gScore.get(current)! + 1; // Assuming uniform cost
if (!gScore.has(neighbor) || tentativeGScore < gScore.get(neighbor)!) {
cameFrom.set(neighbor, current);
gScore.set(neighbor, tentativeGScore);
const fScore = tentativeGScore + this.heuristic(neighbor, dst);
openSet.add({tile: neighbor, fScore: fScore});
}
}
}
return null; // No path found
}
private heuristic(a: Tile, b: Tile): number {
// Manhattan distance
return Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y);
}
private reconstructPath(cameFrom: Map<Tile, Tile>, current: Tile): Tile[] {
const path = [current];
while (cameFrom.has(current)) {
current = cameFrom.get(current)!;
path.unshift(current);
}
return path;
}
}
+55
View File
@@ -0,0 +1,55 @@
import {Cell, Execution, MutableGame, MutablePlayer, PlayerID, PlayerInfo} from "../Game"
import {PseudoRandom} from "../PseudoRandom"
import {AttackExecution} from "./AttackExecution";
export class BotExecution implements Execution {
private ticks = 0
private active = true
private random: PseudoRandom;
private attackRate: number
private gs: MutableGame
constructor(private bot: MutablePlayer) {
this.random = new PseudoRandom(bot.id())
this.attackRate = this.random.nextInt(100, 500)
}
init(gs: MutableGame, ticks: number) {
this.gs = gs
}
tick(ticks: number) {
if (!this.bot.isAlive()) {
this.active = false
return
}
this.ticks++
if (this.ticks % this.attackRate == 0) {
const ns = this.bot.neighbors()
if (ns.length == 0) {
return
}
const toAttack = ns[this.random.nextInt(0, ns.length)]
this.gs.addExecution(new AttackExecution(
this.bot.troops() / 5,
this.bot.id(),
toAttack.isPlayer() ? toAttack.id() : null,
null
))
}
}
owner(): MutablePlayer {
return this.bot
}
isActive(): boolean {
return this.active
}
}
+60
View File
@@ -0,0 +1,60 @@
import {Cell, Game, TerrainTypes} from "../Game";
import {PseudoRandom} from "../PseudoRandom";
import {SpawnIntent} from "../Schemas";
import {getSpawnCells} from "./Util";
export class BotSpawner {
private cellToIndex;
private freeTiles: Cell[];
private numFreeTiles;
private random = new PseudoRandom(123);
constructor(private gs: Game) { }
spawnBots(numBots: number): SpawnIntent[] {
const bots: SpawnIntent[] = [];
this.cellToIndex = new Map<string, number>();
this.freeTiles = new Array();
this.numFreeTiles = 0;
this.gs.forEachTile(tile => {
if (tile.terrain() == TerrainTypes.Water) {
return;
}
if (tile.hasOwner()) {
return;
}
this.freeTiles.push(tile.cell());
this.cellToIndex.set(tile.cell().toString(), this.numFreeTiles);
this.numFreeTiles++;
});
for (let i = 0; i < numBots; i++) {
bots.push(this.spawnBot("Bot" + i));
}
return bots;
}
spawnBot(botName: string): SpawnIntent {
const rand = this.random.nextInt(0, this.numFreeTiles);
const spawn = this.freeTiles[rand];
const spawnCells = getSpawnCells(this.gs, spawn);
spawnCells.forEach(c => this.removeCell(c));
const spawnIntent: SpawnIntent = {
type: 'spawn',
name: botName,
isBot: true,
x: spawn.x,
y: spawn.y
};
return spawnIntent;
}
private removeCell(cell: Cell) {
const index = this.cellToIndex[cell.toString()];
this.freeTiles[index] = this.freeTiles[this.numFreeTiles - 1];
this.cellToIndex[this.freeTiles[index].toString()] = index;
this.numFreeTiles--;
}
}
+55
View File
@@ -0,0 +1,55 @@
import PriorityQueue from "priority-queue-typescript";
import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile} from "../Game";
import {AttackIntent, BoatAttackIntentSchema, Intent, Turn} from "../Schemas";
import {AttackExecution} from "./AttackExecution";
import {SpawnExecution} from "./SpawnExecution";
import {BotSpawner} from "./BotSpawner";
import {BoatAttackExecution} from "./BoatAttackExecution";
export class Executor {
constructor(private gs: Game) {
}
addTurn(turn: Turn) {
turn.intents.forEach(i => this.addIntent(i))
}
addIntent(intent: Intent) {
if (intent.type == "attack") {
this.gs.addExecution(
new AttackExecution(
intent.troops,
intent.attackerID,
intent.targetID,
new Cell(intent.targetX, intent.targetY)
)
)
} else if (intent.type == "spawn") {
this.gs.addExecution(
new SpawnExecution(
new PlayerInfo(intent.name, intent.isBot),
new Cell(intent.x, intent.y),
)
)
} else if (intent.type == "boat") {
this.gs.addExecution(
new BoatAttackExecution(
intent.attackerID,
intent.targetID,
new Cell(intent.x, intent.y),
intent.troops,
)
)
} else {
throw new Error(`intent type ${intent} not found`)
}
}
spawnBots(numBots: number): void {
new BotSpawner(this.gs).spawnBots(numBots).forEach(i => this.addIntent(i))
}
}
+25
View File
@@ -0,0 +1,25 @@
import {Execution, MutableGame, MutablePlayer, PlayerID} from "../Game"
export class PlayerExecution implements Execution {
private player: MutablePlayer
constructor(private playerID: PlayerID) {
}
init(gs: MutableGame, ticks: number) {
this.player = gs.player(this.playerID)
}
tick(ticks: number) {
this.player.addTroops(Math.sqrt(this.player.numTilesOwned() * this.player.troops() + 1000) / 1000)
}
owner(): MutablePlayer {
return this.player
}
isActive(): boolean {
return this.player.isAlive()
}
}
+42
View File
@@ -0,0 +1,42 @@
import {Cell, Execution, MutableGame, MutablePlayer, PlayerInfo} from "../Game"
import {BotExecution} from "./BotExecution"
import {PlayerExecution} from "./PlayerExecution"
import {getSpawnCells} from "./Util"
export class SpawnExecution implements Execution {
active: boolean = true
private gs: MutableGame
constructor(
private playerInfo: PlayerInfo,
private cell: Cell,
) { }
init(gs: MutableGame, ticks: number) {
this.gs = gs
}
tick(ticks: number) {
if (!this.isActive()) {
return
}
const player = this.gs.addPlayer(this.playerInfo)
getSpawnCells(this.gs, this.cell).forEach(c => {
console.log('conquering cell')
player.conquer(c)
})
this.gs.addExecution(new PlayerExecution(player.id()))
if (player.info().isBot) {
this.gs.addExecution(new BotExecution(player))
}
this.active = false
}
owner(): MutablePlayer {
return null
}
isActive(): boolean {
return this.active
}
}
+25
View File
@@ -0,0 +1,25 @@
import {Game, Cell, TerrainTypes} from "../Game";
export function getSpawnCells(gs: Game, cell: Cell): Cell[] {
let result: Cell[] = [];
for (let dx = -2; dx <= 2; dx++) {
for (let dy = -2; dy <= 2; dy++) {
let c = new Cell(cell.x + dx, cell.y + dy);
if (!gs.isOnMap(c)) {
continue;
}
if (Math.abs(dx) === 2 && Math.abs(dy) === 2) {
continue;
}
if (gs.tile(c).terrain() != TerrainTypes.Land) {
continue;
}
if (gs.tile(c).hasOwner()) {
continue;
}
result.push(c);
}
}
return result;
}