use TileRef instead of tile

This commit is contained in:
evanpelle
2025-01-15 12:57:09 -08:00
committed by Evan
parent 2068e42982
commit a17ae48cd3
13 changed files with 149 additions and 98 deletions
+7
View File
@@ -15,9 +15,16 @@ export interface Rectangle {
}
export function placeName(game: Game, player: Player): NameViewData {
return {
x: 0,
y: 0,
size: 0
}
const boundingBox = calculateBoundingBox(player.borderTiles());
const rawScalingFactor = (boundingBox.max.x - boundingBox.min.x) / 100
const scalingFactor = within(Math.floor(rawScalingFactor), 1, 1000)
+7 -2
View File
@@ -14,7 +14,9 @@ export class TileView {
constructor(private game: GameView, public data: TileUpdate, private _terrain: TerrainTile) { }
ref(): TileRef {
throw new Error('uh oh')
if (!this.data) { return 0 }
return this.data.pos.x * this.game.width() + this.data.pos.y
}
type(): TerrainType {
return this._terrain.type()
@@ -123,6 +125,9 @@ export class UnitView implements Unit {
export class PlayerView implements Player {
constructor(private game: GameView, public data: PlayerUpdate, public nameData: NameViewData) { }
borderTiles(): ReadonlySet<Tile> {
throw new Error('Method not implemented.');
}
async actions(tile: Tile): Promise<PlayerActions> {
return this.game.worker.playerInteraction(this.id(), tile)
@@ -187,7 +192,7 @@ export class PlayerView implements Player {
allianceWith(other: Player): Alliance | null {
return null
}
borderTiles(): ReadonlySet<Tile> {
borderTileRefs(): ReadonlySet<TileRef> {
return new Set()
}
units(...types: UnitType[]): Unit[] {
+12 -11
View File
@@ -8,6 +8,7 @@ import { number } from 'zod';
import { GameConfig, GameID, GameRecord, PlayerRecord, Turn } from './Schemas';
import { customAlphabet, nanoid } from 'nanoid';
import { GameView } from './GameView';
import { TileRef } from './game/GameMap';
@@ -110,19 +111,19 @@ function closestOceanShoreTN(tile: Tile, searchDist: number): Tile {
}
export function bfs(tile: Tile, filter: (tile: Tile) => boolean): Set<Tile> {
const seen = new Set<Tile>
const seen = new Map<TileRef, Tile>()
const q: Tile[] = []
q.push(tile)
while (q.length > 0) {
const curr = q.pop()
seen.add(curr)
seen.set(curr.ref(), curr)
for (const n of curr.neighbors()) {
if (!seen.has(n) && filter(n)) {
if (!seen.has(n.ref()) && filter(n)) {
q.push(n)
}
}
}
return seen
return new Set(seen.values())
}
export function simpleHash(str: string): number {
@@ -166,20 +167,20 @@ export function inscribed(outer: { min: Cell; max: Cell }, inner: { min: Cell; m
);
}
export function getMode(list: string[]): string {
export function getMode(list: number[]): number {
// Count occurrences
const counts: { [key: string]: number } = {};
const counts = new Map<number, number>()
for (const item of list) {
counts[item] = (counts[item] || 0) + 1;
counts.set(item, (counts.get(item) || 0) + 1);
}
// Find the item with the highest count
let mode = '';
let mode = 0;
let maxCount = 0;
for (const item in counts) {
if (counts[item] > maxCount) {
maxCount = counts[item];
for (const [item, count] of counts) {
if (count > maxCount) {
maxCount = count
mode = item;
}
}
+7 -3
View File
@@ -189,7 +189,8 @@ export class DefaultConfig implements Config {
attackLogic(attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: MutableTile): { attackerTroopLoss: number; defenderTroopLoss: number; tilesPerTickUsed: number } {
let mag = 0
let speed = 0
switch (tileToConquer.terrain().type()) {
const type = tileToConquer.terrain().type()
switch (type) {
case TerrainType.Plains:
mag = 80
speed = 15
@@ -202,9 +203,12 @@ export class DefaultConfig implements Config {
mag = 120
speed = 25
break
default:
throw new Error(`terrain type ${type} not supported`)
}
mag *= tileToConquer.defenseBonus(attacker)
speed *= tileToConquer.defenseBonus(attacker)
// TODO
// mag *= tileToConquer.defenseBonus(attacker)
// speed *= tileToConquer.defenseBonus(attacker)
if (tileToConquer.hasFallout()) {
mag *= this.falloutDefenseModifier()
speed *= this.falloutDefenseModifier()
+4 -3
View File
@@ -4,6 +4,7 @@ import { PseudoRandom } from "../PseudoRandom";
import { manhattanDist } from "../Util";
import { MessageType } from '../game/Game';
import { renderNumber } from "../../client/Utils";
import { TileRef } from "../game/GameMap";
export class AttackExecution implements Execution {
private breakAlliance = false
@@ -25,7 +26,7 @@ export class AttackExecution implements Execution {
private mg: MutableGame
private border = new Set<Tile>()
private border = new Set<TileRef>()
constructor(
private troops: number | null,
@@ -153,7 +154,7 @@ export class AttackExecution implements Execution {
}
const tileToConquer = this.toConquer.dequeue().tile
this.border.delete(tileToConquer)
this.border.delete(tileToConquer.ref())
const onBorder = tileToConquer.neighbors().filter(t => t.owner() == this._owner).length > 0
if (tileToConquer.owner() != this.target || !onBorder) {
@@ -176,7 +177,7 @@ export class AttackExecution implements Execution {
if (neighbor.terrain().isWater() || neighbor.owner() != this.target) {
continue
}
this.border.add(neighbor)
this.border.add(neighbor.ref())
let numOwnedByMe = neighbor.neighbors()
.filter(t => t.terrain().isLand())
.filter(t => t.owner() == this._owner)
+4 -4
View File
@@ -1,7 +1,7 @@
import {Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerInfo, PlayerType, TerraNullius} from "../game/Game"
import {PseudoRandom} from "../PseudoRandom"
import {simpleHash} from "../Util";
import {AttackExecution} from "./AttackExecution";
import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerID, PlayerInfo, PlayerType, TerraNullius } from "../game/Game"
import { PseudoRandom } from "../PseudoRandom"
import { simpleHash } from "../Util";
import { AttackExecution } from "./AttackExecution";
export class BotExecution implements Execution {
+1 -1
View File
@@ -400,7 +400,7 @@ export class FakeHumanExecution implements Execution {
}
if (oceanShore == null) {
oceanShore = Array.from(this.player.borderTiles()).filter(t => t.terrain().isOceanShore())
oceanShore = Array.from(this.player.borderTileRefs()).filter(t => this.mg.M.isOceanShore(t)).map(tr => this.mg.fromRef(tr))
}
if (oceanShore.length == 0) {
return
+43 -41
View File
@@ -3,6 +3,7 @@ import { Execution, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius,
import { bfs, calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"
import { GameImpl } from "../game/GameImpl"
import { consolex } from "../Consolex"
import { TileRef } from "../game/GameMap"
export class PlayerExecution implements Execution {
@@ -79,7 +80,8 @@ export class PlayerExecution implements Execution {
if (this.player.lastTileChange() > this.lastCalc) {
this.lastCalc = ticks
const start = performance.now()
this.removeClusters()
// TODO
// this.removeClusters()
const end = performance.now()
if (end - start > 1000) {
consolex.log(`player ${this.player.name()}, took ${end - start}ms`)
@@ -109,15 +111,15 @@ export class PlayerExecution implements Execution {
}
}
private surroundedBySamePlayer(cluster: Set<Tile>): false | Player {
const enemies = new Set<Player>()
for (const tile of cluster) {
if (tile.terrain().isOceanShore() || tile.neighbors().find(n => !n.hasOwner())) {
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
const enemies = new Set<number>()
for (const ref of cluster) {
if (this.mg.M.isOceanShore(ref) || this.mg.M.neighbors(ref).some(n => !this.mg.M.hasOwner(n))) {
return false
}
tile.neighbors()
.filter(n => n.hasOwner() && n.owner() != this.player)
.forEach(p => enemies.add(p.owner() as Player))
this.mg.M.neighbors(ref)
.filter(n => this.mg.M.ownerID(n) != this.player.smallID())
.forEach(p => enemies.add(this.mg.M.ownerID(p)))
if (enemies.size != 1) {
return false
}
@@ -125,63 +127,63 @@ export class PlayerExecution implements Execution {
if (enemies.size != 1) {
return false
}
return Array.from(enemies)[0]
return this.mg.playerBySmallID(Array.from(enemies)[0]) as Player
}
private isSurrounded(cluster: Set<Tile>): boolean {
let enemyTiles = new Set<Tile>()
for (const tile of cluster) {
if (tile.terrain().isOceanShore()) {
private isSurrounded(cluster: Set<TileRef>): boolean {
let enemyTiles = new Set<TileRef>()
for (const tr of cluster) {
if (this.mg.M.isOceanShore(tr)) {
return false
}
tile.neighbors()
.filter(n => n.hasOwner() && n.owner() != this.player)
this.mg.M.neighbors(tr)
.filter(n => this.mg.M.ownerID(n) != this.player.smallID())
.forEach(n => enemyTiles.add(n))
}
if (enemyTiles.size == 0) {
return false
}
const enemyBox = calculateBoundingBox(enemyTiles)
const clusterBox = calculateBoundingBox(cluster)
const enemyBox = calculateBoundingBox(new Set(Array.from(enemyTiles).map(tr => this.mg.fromRef(tr))))
const clusterBox = calculateBoundingBox(new Set(Array.from(cluster).map(tr => this.mg.fromRef(tr))))
return inscribed(enemyBox, clusterBox)
}
private removeCluster(cluster: Set<Tile>) {
private removeCluster(cluster: Set<TileRef>) {
const arr = Array.from(cluster)
if (arr.some(t => t.owner() != this.player)) {
// Other removeCluster operations could change tile owners,
// so double check.
return
}
const mode = getMode(arr.flatMap(t => t.neighbors()).filter(t => t.hasOwner() && t.owner() != this.player).map(t => t.owner().id()))
if (!this.mg.hasPlayer(mode)) {
const mode = getMode(
arr.
flatMap(t => this.mg.M.neighbors(t))
.filter(t => this.mg.M.ownerID(t) != this.player.smallID())
.map(t => this.mg.M.ownerID(t))
)
if (!this.mg.playerBySmallID(mode).isPlayer()) {
consolex.warn('mode is not found')
return
}
const firstTile = arr[0]
const filter = (n: Tile): boolean => n.owner() == firstTile.owner()
const tiles = bfs(firstTile, filter)
const filter = (n: Tile): boolean => n.owner().smallID() == this.mg.M.ownerID(firstTile)
const tiles = bfs(this.mg.fromRef(firstTile), filter)
const modePlayer = this.mg.player(mode)
if (modePlayer == null) {
const modePlayer = this.mg.playerBySmallID(mode)
if (!modePlayer.isPlayer()) {
consolex.warn('mode player is null')
}
for (const tile of tiles) {
modePlayer.conquer(tile)
(modePlayer as MutablePlayer).conquer(tile)
}
}
private calculateClusters(): Set<Tile>[] {
const seen = new Set<Tile>()
const border = this.player.borderTiles()
const clusters: Set<Tile>[] = []
private calculateClusters(): Set<TileRef>[] {
const seen = new Set<TileRef>()
const border = this.player.borderTileRefs()
const clusters: Set<TileRef>[] = []
for (const tile of border) {
if (seen.has(tile)) {
continue
}
const cluster = new Set<Tile>()
const queue: Tile[] = [tile]
const cluster = new Set<TileRef>()
const queue: TileRef[] = [tile]
seen.add(tile)
let loops = 0;
while (queue.length > 0) {
@@ -189,12 +191,12 @@ export class PlayerExecution implements Execution {
const curr = queue.shift()
cluster.add(curr)
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr)
const neighbors = (this.mg as GameImpl).neighborsWithDiag(this.mg.fromRef(curr))
for (const neighbor of neighbors) {
if (neighbor.isBorder() && border.has(neighbor)) {
if (!seen.has(neighbor)) {
queue.push(neighbor)
seen.add(neighbor)
if (neighbor.isBorder() && border.has(neighbor.ref())) {
if (!seen.has(neighbor.ref())) {
queue.push(neighbor.ref())
seen.add(neighbor.ref())
}
}
}
+5 -2
View File
@@ -272,6 +272,7 @@ export interface Player {
type(): PlayerType
units(...types: UnitType[]): Unit[]
isAlive(): boolean
borderTileRefs(): ReadonlySet<TileRef>
borderTiles(): ReadonlySet<Tile>
isPlayer(): this is Player
numTilesOwned(): number
@@ -316,7 +317,7 @@ export interface MutablePlayer extends Player {
neighbors(): (Player | TerraNullius)[]
tiles(): ReadonlySet<MutableTile>
ownsTile(cell: Cell): boolean
tiles(): ReadonlySet<MutableTile>
tiles(): ReadonlySet<Tile>
conquer(tile: Tile): void
relinquish(tile: Tile): void
executions(): Execution[]
@@ -354,6 +355,7 @@ export interface MutablePlayer extends Player {
}
export interface Game {
M: GameMap
// Throws exception is player not found
player(id: PlayerID): Player
playerByClientID(id: ClientID): Player | null
@@ -376,7 +378,8 @@ export interface Game {
displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void
units(...types: UnitType[]): Unit[]
unitInfo(type: UnitType): UnitInfo
playerBySmallID(id: number): Player | TerraNullius
fromRef(ref: TileRef): Tile
map(): GameMap
miniMap(): GameMap
}
+29 -20
View File
@@ -27,6 +27,8 @@ export class GameImpl implements MutableGame {
private nations_: Nation[] = []
_players: Map<PlayerID, PlayerImpl> = new Map<PlayerID, PlayerImpl>
_playersBySmallID = []
private execs: Execution[] = []
private _width: number
private _height: number
@@ -41,14 +43,14 @@ export class GameImpl implements MutableGame {
private updates: GameUpdates = createGameUpdatesMap()
constructor(
private gameMap: GameMap,
public readonly M: GameMap,
private miniGameMap: GameMap,
nationMap: NationMap,
private _config: Config,
) {
this._terraNullius = new TerraNulliusImpl()
this._width = gameMap.width();
this._height = gameMap.height();
this._width = M.width();
this._height = M.height();
this.nations_ = nationMap.nations
.map(n => new Nation(
n.name,
@@ -56,8 +58,14 @@ export class GameImpl implements MutableGame {
n.strength
))
}
playerBySmallID(id: number): Player | TerraNullius {
if (id == 0) {
return this.terraNullius()
}
return this._playersBySmallID[id - 1]
}
map(): GameMap {
return this.gameMap
return this.M
}
miniMap(): GameMap {
return this.miniGameMap
@@ -79,7 +87,7 @@ export class GameImpl implements MutableGame {
if (tile.hasOwner()) {
throw Error(`cannot set fallout, tile ${tile} has owner`)
}
this.gameMap.setFallout(tile.ref(), true)
this.M.setFallout(tile.ref(), true)
this.addUpdate(ti.toUpdate())
}
@@ -271,6 +279,7 @@ export class GameImpl implements MutableGame {
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer {
let player = new PlayerImpl(this, this.nextPlayerID, playerInfo, manpower)
this._playersBySmallID.push(player)
this.nextPlayerID++
this._players.set(playerInfo.id, player)
return player
@@ -295,7 +304,7 @@ export class GameImpl implements MutableGame {
tile(cell: Cell): MutableTile {
this.assertIsOnMap(cell)
return new TileImpl(this, this.gameMap.ref(cell.x, cell.y))
return new TileImpl(this, this.M.ref(cell.x, cell.y))
}
isOnMap(cell: Cell): boolean {
@@ -310,7 +319,7 @@ export class GameImpl implements MutableGame {
}
neighbors(tile: Tile): Tile[] {
return this.gameMap.neighbors(tile.ref()).map(tr => new TileImpl(this, tr))
return this.M.neighbors(tile.ref()).map(tr => new TileImpl(this, tr))
}
neighborsWithDiag(tile: Tile): Tile[] {
@@ -323,7 +332,7 @@ export class GameImpl implements MutableGame {
const newX = x + dx
const newY = y + dy
if (newX >= 0 && newX < this._width && newY >= 0 && newY < this._height) {
ns.push(this.fromRef(this.gameMap.ref(newX, newY)))
ns.push(this.fromRef(this.M.ref(newX, newY)))
}
}
}
@@ -345,14 +354,14 @@ export class GameImpl implements MutableGame {
if (previousOwner.isPlayer()) {
previousOwner._lastTileChange = this._ticks
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tileImpl)
this.gameMap.setBorder(tileImpl.ref(), false)
previousOwner._borderTiles.delete(tileImpl.ref())
this.M.setBorder(tileImpl.ref(), false)
}
this.gameMap.setPlayerId(tileImpl.ref(), owner.smallID())
this.M.setOwnerID(tileImpl.ref(), owner.smallID())
owner._tiles.set(tile.cell().toString(), tile)
owner._lastTileChange = this._ticks
this.updateBorders(tile)
this.gameMap.setFallout(tileImpl.ref(), false)
this.M.setFallout(tileImpl.ref(), false)
this.addUpdate((tile as TileImpl).toUpdate())
}
@@ -368,10 +377,10 @@ export class GameImpl implements MutableGame {
let previousOwner = tileImpl.owner() as PlayerImpl
previousOwner._lastTileChange = this._ticks
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tileImpl)
this.gameMap.setBorder(tileImpl.ref(), false)
previousOwner._borderTiles.delete(tileImpl.ref())
this.M.setBorder(tileImpl.ref(), false)
this.gameMap.setPlayerId(tileImpl.ref(), 0)
this.M.setOwnerID(tileImpl.ref(), 0)
this.updateBorders(tile)
this.addUpdate(
(tile as TileImpl).toUpdate()
@@ -385,15 +394,15 @@ export class GameImpl implements MutableGame {
for (const t of tiles) {
if (!t.hasOwner()) {
this.gameMap.setBorder(t.ref(), false)
this.M.setBorder(t.ref(), false)
continue
}
if (this.isBorder(t)) {
(t.owner() as PlayerImpl)._borderTiles.add(t);
this.gameMap.setBorder(t.ref(), true)
(t.owner() as PlayerImpl)._borderTiles.add(t.ref());
this.M.setBorder(t.ref(), true)
} else {
(t.owner() as PlayerImpl)._borderTiles.delete(t);
this.gameMap.setBorder(t.ref(), false)
(t.owner() as PlayerImpl)._borderTiles.delete(t.ref());
this.M.setBorder(t.ref(), false)
}
// this.updates.push(t.toUpdate())
}
+11 -2
View File
@@ -65,6 +65,10 @@ export class GameMap {
return Boolean(this.terrain[ref] & (1 << GameMap.IS_LAND_BIT));
}
isOceanShore(ref: TileRef): boolean {
return this.isLand(ref) && this.neighbors(ref).some(tr => this.isOcean(tr))
}
isOcean(ref: TileRef): boolean {
return Boolean(this.terrain[ref] & (1 << GameMap.OCEAN_BIT));
}
@@ -78,11 +82,16 @@ export class GameMap {
}
// State getters and setters (mutable)
playerId(ref: TileRef): number {
ownerID(ref: TileRef): number {
return this.state[ref] & GameMap.PLAYER_ID_MASK;
}
setPlayerId(ref: TileRef, playerId: number): void {
hasOwner(ref: TileRef): boolean {
return this.ownerID(ref) != 0
}
setOwnerID(ref: TileRef, playerId: number): void {
if (playerId > GameMap.PLAYER_ID_MASK) {
throw new Error(`Player ID ${playerId} exceeds maximum value ${GameMap.PLAYER_ID_MASK}`);
}
+16 -8
View File
@@ -7,6 +7,7 @@ import { TileImpl } from "./TileImpl";
import { MessageType } from './Game';
import { renderTroops } from "../../client/Utils";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { TileRef } from "./GameMap";
interface Target {
tick: Tick
@@ -28,7 +29,7 @@ export class PlayerImpl implements MutablePlayer {
isTraitor_ = false
public _borderTiles: Set<TileImpl> = new Set();
public _borderTiles: Set<TileRef> = new Set();
public _units: UnitImpl[] = [];
public _tiles: Map<CellString, Tile> = new Map<CellString, TileImpl>();
@@ -111,8 +112,8 @@ export class PlayerImpl implements MutablePlayer {
sharesBorderWith(other: Player | TerraNullius): boolean {
for (const border of this._borderTiles) {
for (const neighbor of border.neighbors()) {
if (neighbor.owner() == other) {
for (const neighbor of this.gs.map().neighbors(border)) {
if (this.gs.map().ownerID(neighbor) == other.smallID()) {
return true;
}
}
@@ -127,16 +128,23 @@ export class PlayerImpl implements MutablePlayer {
return new Set(this._tiles.values()) as Set<MutableTile>;
}
borderTiles(): ReadonlySet<MutableTile> {
borderTileRefs(): ReadonlySet<TileRef> {
return this._borderTiles;
}
borderTiles(): ReadonlySet<Tile> {
return new Set(Array.from(this._borderTiles).map(tr => this.gs.fromRef(tr)))
}
neighbors(): (MutablePlayer | TerraNullius)[] {
const ns: Set<(MutablePlayer | TerraNullius)> = new Set();
for (const border of this.borderTiles()) {
for (const neighbor of border.neighbors()) {
if (neighbor.terrain().isLand() && neighbor.owner() != this) {
ns.add(neighbor.owner() as PlayerImpl | TerraNulliusImpl);
for (const border of this.borderTileRefs()) {
for (const neighbor of this.gs.map().neighbors(border)) {
if (this.gs.map().isLake(neighbor)) {
const owner = this.gs.map().ownerID(neighbor)
if (owner != this.smallID()) {
ns.add(this.gs.playerBySmallID(owner) as PlayerImpl | TerraNulliusImpl);
}
}
}
}
+3 -1
View File
@@ -82,11 +82,13 @@ export class TileImpl implements MutableTile {
}
hasOwner(): boolean { return this.owner().smallID() != 0 }
owner(): MutablePlayer | TerraNullius {
const ownerID = this.gs.map().playerId(this.ref_)
const ownerID = this.gs.map().ownerID(this.ref_)
if (ownerID == 0) {
return this.gs.terraNullius()
}
return this.gs.playerBySmallID(ownerID) as MutablePlayer
}
isBorder(): boolean { return this.gs.map().isBorder(this.ref_); }