diff --git a/TODO.txt b/TODO.txt index 13b021602..1b81a98dd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -53,12 +53,10 @@ * REFACTOR: use new priority queue DONE 8/27/2024 * BUG: players attack each other same time creates islands DONE 8/28/2024 * make bot spawn better DONE 8/28/2024 -* make UX for attacking TerraNullius by boat better -* make bots more likely to attack weaker players +* make UX for attacking TerraNullius by boat better DONE 8/29/2024 * make bot territory less funky (more likely attack neighbors with larger border) - - maybe use bounding box, bots want to be circular? * PERF: use hierarchical a* search for boats -* precompute spawns +* PERF: precompute spawns * end game when no players left (or after 1 hour or so?) * boats can go around the world * Add terrain elevation to map @@ -66,4 +64,4 @@ * REFACTOR: give terranullius an ID, game.player() returns terranullius * REFACTOR: ocean is considered TerraNullius ? * REFACTOR: remove player config? -* PERF: render tiles more efficiently +* Make fake humans diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index ed2fc059a..d53ae0baf 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -7,7 +7,7 @@ import {GameRenderer} from "./graphics/GameRenderer"; import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas"; import {TerrainMap} from "../core/TerrainMapLoader"; -import {bfs, manhattanDist} from "../core/Util"; +import {and, bfs, dist, manhattanDist} from "../core/Util"; import {TerrainRenderer} from "./graphics/TerrainRenderer"; @@ -203,28 +203,12 @@ export class ClientGame { const owner = tile.owner() const targetID = owner.isPlayer() ? owner.id() : null; - let tn: Tile[] = [] - if (tile.owner() != this.myPlayer) { - // Boat Attack Terra Nullius - if (tile.isLand()) { - tn = Array.from(bfs(tile, 2)) - .filter(t => t.isOcean()) - .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) - .flatMap(t => t.neighbors()) - .filter(n => n.isShore()) - .filter(n => !n.hasOwner()) - } else if (tile.isOcean()) { - tn = Array.from(bfs(tile, 3)) - .filter(t => t.isShore()) - .filter(t => !t.hasOwner()) - .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) - } - if (tn.length > 0) { - this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner)) - return - } + if (tile.owner() == this.myPlayer) { + return + } + if (tile.hasOwner()) { // Attack Player if (tile.isLand()) { if (this.myPlayer.sharesBorderWith(tile.owner())) { @@ -234,6 +218,45 @@ export class ClientGame { this.sendBoatAttackIntent(targetID, cell, this.gs.config().player().boatAttackAmount(this.myPlayer, owner)) } } + return + } + + + + // Attack Terra Nullius + if (tile.isLand()) { + + const neighbors = Array.from(bfs(tile, and((r, t) => t.isLand(), dist(100)))); + for (const n of neighbors) { + if (this.myPlayer.borderTiles().has(n)) { + this.sendAttackIntent(targetID, cell, this.gs.config().player().attackAmount(this.myPlayer, owner)) + return + } + } + + const tn = Array.from(bfs(tile, dist(30))) + .filter(t => t.isOceanShore()) + .filter(t => !t.hasOwner()) + .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) + if (tn.length > 0) { + this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner)) + } else { + this.sendAttackIntent(targetID, cell, this.gs.config().player().attackAmount(this.myPlayer, owner)) + } + } + + if (tile.isOcean()) { + const bordersOcean = Array.from(this.myPlayer.borderTiles()).filter(t => t.isOceanShore()).length > 0 + if (!bordersOcean) { + return + } + const tn = Array.from(bfs(tile, dist(3))) + .filter(t => t.isOceanShore()) + .filter(t => !t.hasOwner()) + .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) + if (tn.length > 0) { + this.sendBoatAttackIntent(targetID, tn[0].cell(), this.gs.config().player().boatAttackAmount(this.myPlayer, owner)) + } } } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 34e0f47f5..fe889d62c 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -3,7 +3,7 @@ import {Cell, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent} import {Theme} from "../../core/configuration/Config"; import {DragEvent, ZoomEvent} from "../InputHandler"; import {NameRenderer} from "./NameRenderer"; -import {bfs, manhattanDist} from "../../core/Util"; +import {bfs, dist, manhattanDist} from "../../core/Util"; import {PseudoRandom} from "../../core/PseudoRandom"; import {TerrainRenderer} from "./TerrainRenderer"; import {PriorityQueue} from "@datastructures-js/priority-queue"; @@ -157,11 +157,10 @@ export class GameRenderer { } boatEvent(event: BoatEvent) { - bfs(event.oldTile, 2).forEach(t => this.paintTerritory(t)) + bfs(event.oldTile, dist(2)).forEach(t => this.paintTerritory(t)) if (event.boat.isActive()) { - bfs(event.boat.tile(), 2).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id()))) - bfs(event.boat.tile(), 1).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id()))) - + bfs(event.boat.tile(), dist(2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id()))) + bfs(event.boat.tile(), dist(1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id()))) } } diff --git a/src/core/Game.ts b/src/core/Game.ts index ca3131c72..93d0024fe 100644 --- a/src/core/Game.ts +++ b/src/core/Game.ts @@ -43,6 +43,7 @@ export class PlayerInfo { export interface Tile { isLand(): boolean isShore(): boolean + isOceanShore(): boolean isWater(): boolean isShorelineWater(): boolean isOcean(): boolean diff --git a/src/core/GameImpl.ts b/src/core/GameImpl.ts index 24d24aec0..c18d35ea8 100644 --- a/src/core/GameImpl.ts +++ b/src/core/GameImpl.ts @@ -35,6 +35,10 @@ class TileImpl implements Tile { isShore(): boolean { return this.isLand() && this._terrain.shoreline } + isOceanShore(): boolean { + return this.isShore() && this.neighbors().find(t => t.isOcean()) != null + } + isShorelineWater(): boolean { return this.isWater() && this._terrain.shoreline } diff --git a/src/core/Util.ts b/src/core/Util.ts index 13f401840..6ba9d4b6c 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -1,3 +1,4 @@ +import {functional} from "typia"; import {Cell, Tile} from "./Game"; export function manhattanDist(c1: Cell, c2: Cell): number { @@ -8,7 +9,15 @@ export function within(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -export function bfs(tile: Tile, dist: number): Set { +export function dist(dist: number): (root: Tile, tile: Tile) => boolean { + return (root: Tile, n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist; +} + +export function and(x: (root: Tile, tile: Tile) => boolean, y: (root: Tile, tile: Tile) => boolean): (root: Tile, tile: Tile) => boolean { + return (root: Tile, tile: Tile) => x(root, tile) && y(root, tile) +} + +export function bfs(tile: Tile, filter: (root: Tile, tile: Tile) => boolean): Set { const seen = new Set const q: Tile[] = [] q.push(tile) @@ -16,7 +25,7 @@ export function bfs(tile: Tile, dist: number): Set { const curr = q.pop() seen.add(curr) for (const n of curr.neighbors()) { - if (!seen.has(n) && manhattanDist(tile.cell(), n.cell()) <= dist) { + if (!seen.has(n) && filter(tile, n)) { q.push(n) } } diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 7cca37118..40d50ccca 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -4,7 +4,7 @@ import {DefaultConfig, DefaultPlayerConfig, defaultPlayerConfig} from "./Default export const devConfig = new class extends DefaultConfig { numSpawnPhaseTurns(): number { - return 40 + return 60 } gameCreationRate(): number { return 3 * 1000 @@ -19,7 +19,7 @@ export const devConfig = new class extends DefaultConfig { return devPlayerConfig } numBots(): number { - return 250 + return 50 } } diff --git a/src/core/execution/BoatAttackExecution.ts b/src/core/execution/BoatAttackExecution.ts index 3464d3bad..9f748522a 100644 --- a/src/core/execution/BoatAttackExecution.ts +++ b/src/core/execution/BoatAttackExecution.ts @@ -160,7 +160,7 @@ export class AStar { compute(iterations: number): boolean { if (this.completed) return true; - while (!this.openSet.size()) { + while (!this.openSet.isEmpty()) { iterations-- this.current = this.openSet.dequeue()!.tile; if (iterations <= 0) { diff --git a/src/core/execution/BotSpawner.ts b/src/core/execution/BotSpawner.ts index 3a32ff82b..08b83c3c5 100644 --- a/src/core/execution/BotSpawner.ts +++ b/src/core/execution/BotSpawner.ts @@ -1,7 +1,7 @@ import {Cell, Game} from "../Game"; import {PseudoRandom} from "../PseudoRandom"; import {SpawnIntent} from "../Schemas"; -import {bfs} from "../Util"; +import {bfs, dist as dist} from "../Util"; import {getSpawnCells} from "./Util"; @@ -40,7 +40,7 @@ export class BotSpawner { spawnBot(botName: string): SpawnIntent { const rand = this.random.nextInt(0, this.numFreeTiles); const spawn = this.freeTiles[rand]; - bfs(this.gs.tile(spawn), 50).forEach(t => this.removeCell(t.cell())) + bfs(this.gs.tile(spawn), dist(50)).forEach(t => this.removeCell(t.cell())) const spawnIntent: SpawnIntent = { type: 'spawn', name: botName,