mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
lose disconnected territory
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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())))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user