lose disconnected territory

This commit is contained in:
evanpelle
2024-08-30 19:41:40 -07:00
parent 9cfe983824
commit f01949f007
9 changed files with 199 additions and 64 deletions
+7 -3
View File
@@ -60,15 +60,19 @@
* boats can go around the world DONE 8/29/2024
* max boats (3) DONE 8/30/2024
* PERF: more efficient spawns DONE 8/30/2024
* PERF: load terrain map async
* PERF: load terrain map async DONE 8/30/2024
* if completely surrended, lose piece of land DONE 8/30/2024
* Add terrain elevation to map
* PERF: enable CDN
* enable load balancing metrics
* end game when no players left (or after 1 hour or so?)
* if completely surrended, lose piece of land
* use better favicon
* BUG: tiles get left behind during conquer
* Add terrain elevation to map
* REFACTOR: give terranullius an ID, game.player() returns terranullius
* REFACTOR: ocean is considered TerraNullius ?
* Create exit to menu button
* Make fake humans
* Load terrain dataImage in background
* BUG: shore tiles left behind during conquer
* BUG: when sending boat to TerraNullius, only takes one tile
* directed expansion
+3 -3
View File
@@ -224,7 +224,7 @@ export class ClientGame {
// Attack Terra Nullius
if (tile.isLand()) {
const neighbors = Array.from(bfs(tile, and((r, t) => t.isLand(), dist(100))));
const neighbors = Array.from(bfs(tile, and((t) => t.isLand(), dist(tile, 100))));
for (const n of neighbors) {
if (this.myPlayer.borderTiles().has(n)) {
this.sendAttackIntent(targetID, cell, this.gs.config().attackAmount(this.myPlayer, owner))
@@ -232,7 +232,7 @@ export class ClientGame {
}
}
const tn = Array.from(bfs(tile, dist(30)))
const tn = Array.from(bfs(tile, dist(tile, 30)))
.filter(t => t.isOceanShore())
.filter(t => !t.hasOwner())
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
@@ -248,7 +248,7 @@ export class ClientGame {
if (!bordersOcean) {
return
}
const tn = Array.from(bfs(tile, dist(3)))
const tn = Array.from(bfs(tile, dist(tile, 3)))
.filter(t => t.isOceanShore())
.filter(t => !t.hasOwner())
.sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell()))
+6 -3
View File
@@ -117,6 +117,9 @@ export class GameRenderer {
renderTerritory() {
let numToRender = Math.floor(this.tileToRenderQueue.size() / 10)
if (numToRender == 0) {
numToRender = this.tileToRenderQueue.size()
}
while (numToRender > 0) {
numToRender--
@@ -157,10 +160,10 @@ export class GameRenderer {
}
boatEvent(event: BoatEvent) {
bfs(event.oldTile, dist(2)).forEach(t => this.paintTerritory(t))
bfs(event.oldTile, dist(event.oldTile, 2)).forEach(t => this.paintTerritory(t))
if (event.boat.isActive()) {
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())))
bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id())))
bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id())))
}
}
+2 -16
View File
@@ -1,5 +1,5 @@
import {Game, Player, Tile, Cell} from '../../core/Game';
import {within} from '../../core/Util';
import {calculateBoundingBox, within} from '../../core/Util';
export interface Point {
x: number;
@@ -15,7 +15,7 @@ export interface Rectangle {
export function placeName(game: Game, player: Player): [position: Cell, fontSize: number] {
const boundingBox = calculateBoundingBox(player);
const boundingBox = calculateBoundingBox(player.borderTiles());
const rawScalingFactor = (boundingBox.max.x - boundingBox.min.x) / 50
const scalingFactor = within(Math.floor(rawScalingFactor), 1, 100)
@@ -37,20 +37,6 @@ export function placeName(game: Game, player: Player): [position: Cell, fontSize
return [center, fontSize]
}
export function calculateBoundingBox(player: Player): {min: Cell; max: Cell} {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
player.borderTiles().forEach((tile: Tile) => {
const cell = tile.cell();
minX = Math.min(minX, cell.x);
minY = Math.min(minY, cell.y);
maxX = Math.max(maxX, cell.x);
maxY = Math.max(maxY, cell.y);
});
return {min: new Cell(minX, minY), max: new Cell(maxX, maxY)}
}
export function createGrid(game: Game, player: Player, boundingBox: {min: Point; max: Point}, scalingFactor: number): boolean[][] {
const scaledBoundingBox: {min: Point; max: Point} = {
min: {
+3 -2
View File
@@ -1,7 +1,8 @@
import {Cell, Game, Player} from "../../core/Game"
import {PseudoRandom} from "../../core/PseudoRandom"
import {calculateBoundingBox} from "../../core/Util"
import {Theme} from "../../core/configuration/Config"
import {calculateBoundingBox, placeName} from "./NameBoxCalculator"
import {placeName} from "./NameBoxCalculator"
class RenderInfo {
public isVisible = true
@@ -62,7 +63,7 @@ export class NameRenderer {
for (const render of this.renders) {
const now = Date.now()
if (now - render.lastBoundingCalculated > this.refreshRate) {
render.boundingBox = calculateBoundingBox(render.player);
render.boundingBox = calculateBoundingBox(render.player.borderTiles());
render.lastBoundingCalculated = now
}
if (render.isVisible && now - render.lastRenderCalc > this.refreshRate) {
+22 -26
View File
@@ -156,8 +156,7 @@ export class BoatImpl implements MutableBoat {
}
export class PlayerImpl implements MutablePlayer {
public _borderTiles: Map<CellString, Tile> = new Map()
public _borderTileSet: Set<Tile> = new Set()
public _borderTiles: Set<Tile> = new Set()
public _boats: BoatImpl[] = []
public _tiles: Map<CellString, Tile> = new Map<CellString, Tile>()
@@ -200,7 +199,7 @@ export class PlayerImpl implements MutablePlayer {
}
sharesBorderWith(other: Player | TerraNullius): boolean {
for (const border of this._borderTileSet) {
for (const border of this._borderTiles) {
for (const neighbor of border.neighbors()) {
if (neighbor.owner() == other) {
return true
@@ -218,7 +217,7 @@ export class PlayerImpl implements MutablePlayer {
}
borderTiles(): ReadonlySet<Tile> {
return this._borderTileSet
return this._borderTiles
}
neighbors(): (MutablePlayer | TerraNullius)[] {
@@ -488,21 +487,20 @@ export class GameImpl implements MutableGame {
}
conquer(owner: PlayerImpl, tile: Tile): void {
if (tile.owner() == owner) {
throw new Error(`Player ${owner} already owns cell ${tile.cell().toString()}`)
}
if (!owner.isPlayer()) {
throw new Error("Must be a player")
}
if (tile.isWater()) {
throw new Error("Cannot conquer water")
}
// if (tile.owner() == owner) {
// throw new Error(`Player ${owner} already owns cell ${tile.cell().toString()}`)
// }
// if (!owner.isPlayer()) {
// throw new Error("Must be a player")
// }
// if (tile.isWater()) {
// throw new Error("Cannot conquer water")
// }
const tileImpl = tile as TileImpl
let previousOwner = tileImpl._owner
if (previousOwner.isPlayer()) {
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tile.cell().toString())
previousOwner._borderTileSet.delete(tile)
previousOwner._borderTiles.delete(tile)
tileImpl._isBorder = false
}
tileImpl._owner = owner
@@ -522,8 +520,7 @@ export class GameImpl implements MutableGame {
const tileImpl = tile as TileImpl
let previousOwner = tileImpl._owner as PlayerImpl
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tile.cell().toString())
previousOwner._borderTileSet.delete(tile)
previousOwner._borderTiles.delete(tile)
tileImpl._isBorder = false
tileImpl._owner = this._terraNullius
@@ -532,22 +529,21 @@ export class GameImpl implements MutableGame {
}
private updateBorders(tile: Tile) {
const tiles: Tile[] = []
tiles.push(tile)
tile.neighbors().forEach(t => tiles.push(t))
const tiles: TileImpl[] = []
tiles.push(tile as TileImpl)
tile.neighbors().forEach(t => tiles.push(t as TileImpl))
for (const t of tiles) {
if (!t.hasOwner()) {
t._isBorder = false
continue
}
if (this.isBorder(t)) {
(t.owner() as PlayerImpl)._borderTiles.set(t.cell().toString(), t);
(t.owner() as PlayerImpl)._borderTileSet.add(t);
(t as TileImpl)._isBorder = true
(t.owner() as PlayerImpl)._borderTiles.add(t);
t._isBorder = true
} else {
(t.owner() as PlayerImpl)._borderTiles.delete(t.cell().toString());
(t.owner() as PlayerImpl)._borderTileSet.delete(t);
(t as TileImpl)._isBorder = false
(t.owner() as PlayerImpl)._borderTiles.delete(t);
t._isBorder = false
}
}
}
+51 -7
View File
@@ -1,5 +1,5 @@
import {functional} from "typia";
import {Cell, Tile} from "./Game";
import {Cell, Player, Tile} from "./Game";
export function manhattanDist(c1: Cell, c2: Cell): number {
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
@@ -22,15 +22,15 @@ export function within(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
export function dist(dist: number): (root: Tile, tile: Tile) => boolean {
return (root: Tile, n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist;
export function dist(root: Tile, dist: number): (tile: Tile) => boolean {
return (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 and(x: (tile: Tile) => boolean, y: (tile: Tile) => boolean): (tile: Tile) => boolean {
return (tile: Tile) => x(tile) && y(tile)
}
export function bfs(tile: Tile, filter: (root: Tile, tile: Tile) => boolean): Set<Tile> {
export function bfs(tile: Tile, filter: (tile: Tile) => boolean): Set<Tile> {
const seen = new Set<Tile>
const q: Tile[] = []
q.push(tile)
@@ -38,7 +38,7 @@ export function bfs(tile: Tile, filter: (root: Tile, tile: Tile) => boolean): Se
const curr = q.pop()
seen.add(curr)
for (const n of curr.neighbors()) {
if (!seen.has(n) && filter(tile, n)) {
if (!seen.has(n) && filter(n)) {
q.push(n)
}
}
@@ -54,4 +54,48 @@ export function simpleHash(str: string): number {
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
export function calculateBoundingBox(borderTiles: ReadonlySet<Tile>): {min: Cell; max: Cell} {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
borderTiles.forEach((tile: Tile) => {
const cell = tile.cell();
minX = Math.min(minX, cell.x);
minY = Math.min(minY, cell.y);
maxX = Math.max(maxX, cell.x);
maxY = Math.max(maxY, cell.y);
});
return {min: new Cell(minX, minY), max: new Cell(maxX, maxY)}
}
export function inscribed(outer: { min: Cell; max: Cell }, inner: { min: Cell; max: Cell }): boolean {
return (
outer.min.x <= inner.min.x &&
outer.min.y <= inner.min.y &&
outer.max.x >= inner.max.x &&
outer.max.y >= inner.max.y
);
}
export function getMode(list: string[]): string {
// Count occurrences
const counts: {[key: string]: number} = {};
for (const item of list) {
counts[item] = (counts[item] || 0) + 1;
}
// Find the item with the highest count
let mode = '';
let maxCount = 0;
for (const item in counts) {
if (counts[item] > maxCount) {
maxCount = counts[item];
mode = item;
}
}
return mode;
}
+2 -2
View File
@@ -12,7 +12,7 @@ export class BotSpawner {
spawnBots(numBots: number): SpawnIntent[] {
let tries = 0
while (this.bots.length < numBots - 1) {
while (this.bots.length < numBots) {
if (tries > 10000) {
console.log('too many retries while spawning bots, giving up')
return this.bots
@@ -33,7 +33,7 @@ export class BotSpawner {
return null
}
for (const spawn of this.bots) {
if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 50) {
if (manhattanDist(new Cell(spawn.x, spawn.y), tile.cell()) < 70) {
return null
}
}
+103 -2
View File
@@ -1,10 +1,19 @@
import cluster from "cluster"
import {Config} from "../configuration/Config"
import {Execution, MutableGame, MutablePlayer, PlayerID} from "../Game"
import {Execution, MutableGame, MutablePlayer, PlayerID, Tile} from "../Game"
import {bfs, calculateBoundingBox, getMode, inscribed, simpleHash} from "../Util"
import {GameImpl} from "../GameImpl"
import {gr} from "../../client/ClientGame"
import {AttackExecution} from "./AttackExecution"
export class PlayerExecution implements Execution {
private readonly ticksPerIslandCalc = 50
private player: MutablePlayer
private config: Config
private lastCalc = 0
private mg: MutableGame
constructor(private playerID: PlayerID) {
}
@@ -14,8 +23,10 @@ export class PlayerExecution implements Execution {
}
init(mg: MutableGame, ticks: number) {
this.mg = mg
this.config = mg.config()
this.player = mg.player(this.playerID)
this.lastCalc = ticks + (simpleHash(this.player.name()) % this.ticksPerIslandCalc)
}
tick(ticks: number) {
@@ -23,13 +34,103 @@ export class PlayerExecution implements Execution {
return
}
this.player.setTroops(this.config.troopAdditionRate(this.player))
if (ticks - this.lastCalc > this.ticksPerIslandCalc) {
this.lastCalc = ticks
const start = performance.now()
this.removeIslands()
const end = performance.now()
if (end - start > 1000) {
console.log(`player ${this.player.name()}, took ${end - start}ms`)
}
}
}
private removeIslands() {
const clusters = this.calculateClusters()
if (clusters.length <= 1) {
return
}
clusters.sort((a, b) => b.size - a.size);
const main = clusters.shift()
const mainBox = calculateBoundingBox(main)
for (const toRemove of clusters) {
const toRemoveBox = calculateBoundingBox(toRemove)
if (inscribed(mainBox, toRemoveBox)) {
return
}
for (const tile of toRemove) {
if (tile.isOceanShore()) {
return
}
}
this.removeIsland(toRemove)
}
}
private removeIsland(cluster: Set<Tile>) {
console.log('removing island!')
const arr = Array.from(cluster)
const mode = getMode(arr.flatMap(t => t.neighbors()).filter(t => t.hasOwner() && t.owner() != this.player).map(t => t.owner().id()))
if (mode == null) {
console.warn('mode is null')
return
}
const firstTile = arr[0]
const filter = (n: Tile): boolean => n.owner() == firstTile.owner()
const tiles = bfs(firstTile, filter)
const modePlayer = this.mg.player(mode)
for (const tile of tiles) {
modePlayer.conquer(tile)
}
}
private calculateClusters(): Set<Tile>[] {
const seen = new Set<Tile>()
const border = this.player.borderTiles()
const clusters: Set<Tile>[] = []
for (const tile of border) {
if (seen.has(tile)) {
continue
}
const cluster = new Set<Tile>()
const queue: Tile[] = [tile]
seen.add(tile)
let loops = 0;
while (queue.length > 0) {
loops += 1
const curr = queue.shift()
cluster.add(curr)
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr)
for (const neighbor of neighbors) {
// if (this.mg.ticks() == 736 && loops > 580000) {
// // console.log(`got neighbor ${neighbor.cell().toString()}`)
// gr.paintBlack(neighbor)
// }
if (neighbor.isBorder() && border.has(neighbor)) {
if (!seen.has(neighbor)) {
queue.push(neighbor)
seen.add(neighbor)
}
}
}
}
clusters.push(cluster)
}
return clusters
}
owner(): MutablePlayer {
return this.player
}
private active = true
isActive(): boolean {
return this.player.isAlive()
// return this.player.isAlive()
return this.active
}
}