have Game delegate to GameMap for cleaner API

This commit is contained in:
evanpelle
2025-01-16 16:44:44 -08:00
committed by Evan
parent a17ae48cd3
commit c42cc2a9b4
10 changed files with 121 additions and 83 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage
import { ClientID } from "./Schemas";
import { TerraNulliusImpl } from './game/TerraNulliusImpl';
import { WorkerClient } from './worker/WorkerClient';
import { GameMap, TileRef } from './game/GameMap';
import { GameMapImpl, TileRef } from './game/GameMap';
export class TileView {
+1 -1
View File
@@ -400,7 +400,7 @@ export class FakeHumanExecution implements Execution {
}
if (oceanShore == null) {
oceanShore = Array.from(this.player.borderTileRefs()).filter(t => this.mg.M.isOceanShore(t)).map(tr => this.mg.fromRef(tr))
oceanShore = Array.from(this.player.borderTileRefs()).filter(t => this.mg.isOceanShore(t)).map(tr => this.mg.fromRef(tr))
}
if (oceanShore.length == 0) {
return
+11 -11
View File
@@ -114,12 +114,12 @@ export class PlayerExecution implements Execution {
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))) {
if (this.mg.isOceanShore(ref) || this.mg.neighbors(ref).some(n => !this.mg.hasOwner(n))) {
return false
}
this.mg.M.neighbors(ref)
.filter(n => this.mg.M.ownerID(n) != this.player.smallID())
.forEach(p => enemies.add(this.mg.M.ownerID(p)))
this.mg.neighbors(ref)
.filter(n => this.mg.ownerID(n) != this.player.smallID())
.forEach(p => enemies.add(this.mg.ownerID(p)))
if (enemies.size != 1) {
return false
}
@@ -133,11 +133,11 @@ export class PlayerExecution implements Execution {
private isSurrounded(cluster: Set<TileRef>): boolean {
let enemyTiles = new Set<TileRef>()
for (const tr of cluster) {
if (this.mg.M.isOceanShore(tr)) {
if (this.mg.isOceanShore(tr)) {
return false
}
this.mg.M.neighbors(tr)
.filter(n => this.mg.M.ownerID(n) != this.player.smallID())
this.mg.neighbors(tr)
.filter(n => this.mg.ownerID(n) != this.player.smallID())
.forEach(n => enemyTiles.add(n))
}
if (enemyTiles.size == 0) {
@@ -152,16 +152,16 @@ export class PlayerExecution implements Execution {
const arr = Array.from(cluster)
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))
flatMap(t => this.mg.neighbors(t))
.filter(t => this.mg.ownerID(t) != this.player.smallID())
.map(t => this.mg.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().smallID() == this.mg.M.ownerID(firstTile)
const filter = (n: Tile): boolean => n.owner().smallID() == this.mg.ownerID(firstTile)
const tiles = bfs(this.mg.fromRef(firstTile), filter)
const modePlayer = this.mg.playerBySmallID(mode)
+4 -6
View File
@@ -1,7 +1,7 @@
import { Config } from "../configuration/Config"
import { GameEvent } from "../EventBus"
import { ClientID, GameConfig, GameID } from "../Schemas"
import { GameMap, TileRef } from "./GameMap"
import { GameMap, GameMapImpl, TileRef } from "./GameMap"
export type PlayerID = string
export type Tick = number
@@ -354,8 +354,7 @@ export interface MutablePlayer extends Player {
toUpdate(): PlayerUpdate
}
export interface Game {
M: GameMap
export interface Game extends GameMap {
// Throws exception is player not found
player(id: PlayerID): Player
playerByClientID(id: ClientID): Player | null
@@ -363,7 +362,6 @@ export interface Game {
players(): Player[]
tile(cell: Cell): Tile
isOnMap(cell: Cell): boolean
neighbors(cell: Cell | Tile): Tile[]
width(): number
height(): number
forEachTile(fn: (tile: Tile) => void): void
@@ -380,8 +378,8 @@ export interface Game {
unitInfo(type: UnitType): UnitInfo
playerBySmallID(id: number): Player | TerraNullius
fromRef(ref: TileRef): Tile
map(): GameMap
miniMap(): GameMap
map(): GameMapImpl
miniMap(): GameMapImpl
}
export interface MutableGame extends Game {
+44 -33
View File
@@ -10,9 +10,9 @@ import { ClientID, GameConfig } from "../Schemas";
import { MessageType } from './Game';
import { UnitImpl } from "./UnitImpl";
import { consolex } from "../Consolex";
import { GameMap, TileRef } from "./GameMap";
import { GameMapImpl, TileRef } from "./GameMap";
export function createGame(gameMap: GameMap, miniGameMap: GameMap, nationMap: NationMap, config: Config): Game {
export function createGame(gameMap: GameMapImpl, miniGameMap: GameMapImpl, nationMap: NationMap, config: Config): Game {
return new GameImpl(gameMap, miniGameMap, nationMap, config)
}
@@ -43,14 +43,14 @@ export class GameImpl implements MutableGame {
private updates: GameUpdates = createGameUpdatesMap()
constructor(
public readonly M: GameMap,
private miniGameMap: GameMap,
private _map: GameMapImpl,
private miniGameMap: GameMapImpl,
nationMap: NationMap,
private _config: Config,
) {
this._terraNullius = new TerraNulliusImpl()
this._width = M.width();
this._height = M.height();
this._width = _map.width();
this._height = _map.height();
this.nations_ = nationMap.nations
.map(n => new Nation(
n.name,
@@ -64,10 +64,10 @@ export class GameImpl implements MutableGame {
}
return this._playersBySmallID[id - 1]
}
map(): GameMap {
return this.M
map(): GameMapImpl {
return this._map
}
miniMap(): GameMap {
miniMap(): GameMapImpl {
return this.miniGameMap
}
@@ -87,7 +87,7 @@ export class GameImpl implements MutableGame {
if (tile.hasOwner()) {
throw Error(`cannot set fallout, tile ${tile} has owner`)
}
this.M.setFallout(tile.ref(), true)
this._map.setFallout(tile.ref(), true)
this.addUpdate(ti.toUpdate())
}
@@ -257,14 +257,6 @@ export class GameImpl implements MutableGame {
this.unInitExecs = this.unInitExecs.filter(execution => execution !== exec)
}
width(): number {
return this._width
}
height(): number {
return this._height
}
forEachTile(fn: (tile: Tile) => void): void {
for (let x = 0; x < this._width; x++) {
for (let y = 0; y < this._height; y++) {
@@ -304,7 +296,7 @@ export class GameImpl implements MutableGame {
tile(cell: Cell): MutableTile {
this.assertIsOnMap(cell)
return new TileImpl(this, this.M.ref(cell.x, cell.y))
return new TileImpl(this, this._map.ref(cell.x, cell.y))
}
isOnMap(cell: Cell): boolean {
@@ -318,9 +310,6 @@ export class GameImpl implements MutableGame {
return new TileImpl(this, ref)
}
neighbors(tile: Tile): Tile[] {
return this.M.neighbors(tile.ref()).map(tr => new TileImpl(this, tr))
}
neighborsWithDiag(tile: Tile): Tile[] {
const x = tile.cell().x
@@ -332,7 +321,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.M.ref(newX, newY)))
ns.push(this.fromRef(this._map.ref(newX, newY)))
}
}
}
@@ -355,13 +344,13 @@ export class GameImpl implements MutableGame {
previousOwner._lastTileChange = this._ticks
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tileImpl.ref())
this.M.setBorder(tileImpl.ref(), false)
this._map.setBorder(tileImpl.ref(), false)
}
this.M.setOwnerID(tileImpl.ref(), owner.smallID())
this._map.setOwnerID(tileImpl.ref(), owner.smallID())
owner._tiles.set(tile.cell().toString(), tile)
owner._lastTileChange = this._ticks
this.updateBorders(tile)
this.M.setFallout(tileImpl.ref(), false)
this._map.setFallout(tileImpl.ref(), false)
this.addUpdate((tile as TileImpl).toUpdate())
}
@@ -378,9 +367,9 @@ export class GameImpl implements MutableGame {
previousOwner._lastTileChange = this._ticks
previousOwner._tiles.delete(tile.cell().toString())
previousOwner._borderTiles.delete(tileImpl.ref())
this.M.setBorder(tileImpl.ref(), false)
this._map.setBorder(tileImpl.ref(), false)
this.M.setOwnerID(tileImpl.ref(), 0)
this._map.setOwnerID(tileImpl.ref(), 0)
this.updateBorders(tile)
this.addUpdate(
(tile as TileImpl).toUpdate()
@@ -394,21 +383,21 @@ export class GameImpl implements MutableGame {
for (const t of tiles) {
if (!t.hasOwner()) {
this.M.setBorder(t.ref(), false)
this._map.setBorder(t.ref(), false)
continue
}
if (this.isBorder(t)) {
if (this.calcIsBorder(t)) {
(t.owner() as PlayerImpl)._borderTiles.add(t.ref());
this.M.setBorder(t.ref(), true)
this._map.setBorder(t.ref(), true)
} else {
(t.owner() as PlayerImpl)._borderTiles.delete(t.ref());
this.M.setBorder(t.ref(), false)
this._map.setBorder(t.ref(), false)
}
// this.updates.push(t.toUpdate())
}
}
isBorder(tile: Tile): boolean {
private calcIsBorder(tile: Tile): boolean {
if (!tile.hasOwner()) {
return false
}
@@ -506,6 +495,28 @@ export class GameImpl implements MutableGame {
playerID: id
})
}
ref(x: number, y: number): TileRef { return this._map.ref(x, y) }
x(ref: TileRef): number { return this._map.x(ref) }
y(ref: TileRef): number { return this._map.y(ref) }
cell(ref: TileRef): Cell { return this._map.cell(ref) }
width(): number { return this._map.width() }
height(): number { return this._map.height() }
numLandTiles(): number { return this._map.numLandTiles() }
isValidCoord(x: number, y: number): boolean { return this._map.isValidCoord(x, y) }
isLand(ref: TileRef): boolean { return this._map.isLake(ref) }
isOceanShore(ref: TileRef): boolean { return this._map.isOceanShore(ref) }
isOcean(ref: TileRef): boolean { return this._map.isOcean(ref) }
isShoreline(ref: TileRef): boolean { return this._map.isShoreline(ref) }
magnitude(ref: TileRef): number { return this._map.magnitude(ref) }
ownerID(ref: TileRef): number { return this._map.ownerID(ref) }
hasOwner(ref: TileRef): boolean { return this._map.hasOwner(ref) }
setOwnerID(ref: TileRef, playerId: number): void { return this._map.setOwnerID(ref, playerId) }
hasFallout(ref: TileRef): boolean { return this._map.hasFallout(ref) }
setFallout(ref: TileRef, value: boolean): void { return this._map.setFallout(ref, value) }
isBorder(ref: TileRef): boolean { return this._map.isBorder(ref) }
setBorder(ref: TileRef, value: boolean): void { return this._map.setBorder(ref, value) }
neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref) }
}
// Or a more dynamic approach that will catch new enum values:
+47 -18
View File
@@ -2,7 +2,36 @@ import { Cell, TerrainType } from "./Game";
export type TileRef = number;
export class GameMap {
export interface GameMap {
ref(x: number, y: number): TileRef
x(ref: TileRef): number
y(ref: TileRef): number
cell(ref: TileRef): Cell
width(): number
height(): number
numLandTiles(): number
isValidCoord(x: number, y: number): boolean
// Terrain getters (immutable)
isLand(ref: TileRef): boolean
isOceanShore(ref: TileRef): boolean
isOcean(ref: TileRef): boolean
isShoreline(ref: TileRef): boolean
magnitude(ref: TileRef): number
// State getters and setters (mutable)
ownerID(ref: TileRef): number
hasOwner(ref: TileRef): boolean
setOwnerID(ref: TileRef, playerId: number): void
hasFallout(ref: TileRef): boolean
setFallout(ref: TileRef, value: boolean): void
isBorder(ref: TileRef): boolean
setBorder(ref: TileRef, value: boolean): void
neighbors(ref: TileRef): TileRef[]
}
export class GameMapImpl implements GameMap {
private readonly terrain: Uint8Array; // Immutable terrain data
private readonly state: Uint16Array; // Mutable game state
private readonly width_: number;
@@ -62,7 +91,7 @@ export class GameMap {
// Terrain getters (immutable)
isLand(ref: TileRef): boolean {
return Boolean(this.terrain[ref] & (1 << GameMap.IS_LAND_BIT));
return Boolean(this.terrain[ref] & (1 << GameMapImpl.IS_LAND_BIT));
}
isOceanShore(ref: TileRef): boolean {
@@ -70,20 +99,20 @@ export class GameMap {
}
isOcean(ref: TileRef): boolean {
return Boolean(this.terrain[ref] & (1 << GameMap.OCEAN_BIT));
return Boolean(this.terrain[ref] & (1 << GameMapImpl.OCEAN_BIT));
}
isShoreline(ref: TileRef): boolean {
return Boolean(this.terrain[ref] & (1 << GameMap.SHORELINE_BIT));
return Boolean(this.terrain[ref] & (1 << GameMapImpl.SHORELINE_BIT));
}
magnitude(ref: TileRef): number {
return this.terrain[ref] & GameMap.MAGNITUDE_MASK;
return this.terrain[ref] & GameMapImpl.MAGNITUDE_MASK;
}
// State getters and setters (mutable)
ownerID(ref: TileRef): number {
return this.state[ref] & GameMap.PLAYER_ID_MASK;
return this.state[ref] & GameMapImpl.PLAYER_ID_MASK;
}
hasOwner(ref: TileRef): boolean {
@@ -92,45 +121,45 @@ export class GameMap {
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}`);
if (playerId > GameMapImpl.PLAYER_ID_MASK) {
throw new Error(`Player ID ${playerId} exceeds maximum value ${GameMapImpl.PLAYER_ID_MASK}`);
}
this.state[ref] = (this.state[ref] & ~GameMap.PLAYER_ID_MASK) | playerId;
this.state[ref] = (this.state[ref] & ~GameMapImpl.PLAYER_ID_MASK) | playerId;
}
hasFallout(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMap.FALLOUT_BIT));
return Boolean(this.state[ref] & (1 << GameMapImpl.FALLOUT_BIT));
}
setFallout(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMap.FALLOUT_BIT;
this.state[ref] |= 1 << GameMapImpl.FALLOUT_BIT;
} else {
this.state[ref] &= ~(1 << GameMap.FALLOUT_BIT);
this.state[ref] &= ~(1 << GameMapImpl.FALLOUT_BIT);
}
}
isBorder(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMap.BORDER_BIT));
return Boolean(this.state[ref] & (1 << GameMapImpl.BORDER_BIT));
}
setBorder(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMap.BORDER_BIT;
this.state[ref] |= 1 << GameMapImpl.BORDER_BIT;
} else {
this.state[ref] &= ~(1 << GameMap.BORDER_BIT);
this.state[ref] &= ~(1 << GameMapImpl.BORDER_BIT);
}
}
hasDefenseBonus(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMap.DEFENSE_BONUS_BIT));
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENSE_BONUS_BIT));
}
setDefenseBonus(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMap.DEFENSE_BONUS_BIT;
this.state[ref] |= 1 << GameMapImpl.DEFENSE_BONUS_BIT;
} else {
this.state[ref] &= ~(1 << GameMap.DEFENSE_BONUS_BIT);
this.state[ref] &= ~(1 << GameMapImpl.DEFENSE_BONUS_BIT);
}
}
+5 -5
View File
@@ -1,9 +1,9 @@
import { consolex } from '../Consolex';
import { Cell, GameMapType, TerrainMap, TerrainTile, TerrainType } from './Game';
import { GameMap } from './GameMap';
import { GameMapImpl } from './GameMap';
import { terrainMapFileLoader } from './TerrainMapFileLoader';
const loadedMaps = new Map<GameMapType, { nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap, terrain: TerrainMap }>()
const loadedMaps = new Map<GameMapType, { nationMap: NationMap, gameMap: GameMapImpl, miniGameMap: GameMapImpl, terrain: TerrainMap }>()
export interface NationMap {
name: string;
@@ -149,7 +149,7 @@ export class TerrainMapImpl implements TerrainMap {
}
export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: NationMap, gameMap: GameMap, miniGameMap: GameMap, terrain: TerrainMap }> {
export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: NationMap, gameMap: GameMapImpl, miniGameMap: GameMapImpl, terrain: TerrainMap }> {
if (loadedMaps.has(map)) {
return loadedMaps.get(map)
}
@@ -162,7 +162,7 @@ export async function loadTerrainMap(map: GameMapType): Promise<{ nationMap: Nat
return result
}
export async function loadTerrainFromFile(fileData: string): Promise<{ map: GameMap, terrain: TerrainMap }> {
export async function loadTerrainFromFile(fileData: string): Promise<{ map: GameMapImpl, terrain: TerrainMap }> {
const width = (fileData.charCodeAt(1) << 8) | fileData.charCodeAt(0);
const height = (fileData.charCodeAt(3) << 8) | fileData.charCodeAt(2);
@@ -184,7 +184,7 @@ export async function loadTerrainFromFile(fileData: string): Promise<{ map: Game
m.rawData[i] = packedByte;
if (packedByte & 0b10000000) numLand++;
}
const gm = new GameMap(width, height, m.rawData, numLand)
const gm = new GameMapImpl(width, height, m.rawData, numLand)
m._numLandTiles = numLand;
return { map: gm, terrain: m }
+3 -3
View File
@@ -3,7 +3,7 @@ import { TerrainMapImpl, TerrainTileImpl } from "./TerrainMapLoader";
import { GameImpl } from "./GameImpl";
import { PlayerImpl } from "./PlayerImpl";
import { TerraNulliusImpl } from "./TerraNulliusImpl";
import { GameMap, TileRef } from "./GameMap";
import { GameMapImpl, TileRef } from "./GameMap";
export class TileImpl implements MutableTile {
@@ -102,14 +102,14 @@ export class TileImpl implements MutableTile {
}
neighbors(): Tile[] {
return this.gs.neighbors(this)
return this.gs.neighbors(this.ref()).map(n => this.gs.fromRef(n))
}
}
export class TerrainRef implements TerrainTile {
constructor(private map: GameMap, private ref: TileRef) { }
constructor(private map: GameMapImpl, private ref: TileRef) { }
isLand(): boolean {
return this.map.isLand(this.ref)
+3 -3
View File
@@ -1,6 +1,6 @@
import { GameManager } from "../../server/GameManager";
import { Cell, Game, TerrainMap, TerrainType } from "../game/Game";
import { GameMap, TileRef } from "../game/GameMap";
import { GameMapImpl, TileRef } from "../game/GameMap";
import { AStar, PathFindResultType, } from "./AStar";
import { SerialAStar } from "./SerialAStar";
@@ -10,8 +10,8 @@ export class MiniAStar implements AStar {
private aStar: SerialAStar
constructor(
private gameMap: GameMap,
private miniMap: GameMap,
private gameMap: GameMapImpl,
private miniMap: GameMapImpl,
private src: TileRef,
private dst: TileRef,
private canMove: (t: TileRef) => boolean,
+2 -2
View File
@@ -3,7 +3,7 @@ import { AStar } from "./AStar";
import { PathFindResultType } from "./AStar";
import { Cell } from "../game/Game";
import { consolex } from "../Consolex";
import { GameMap, TileRef } from "../game/GameMap";
import { GameMapImpl, TileRef } from "../game/GameMap";
export class SerialAStar implements AStar {
@@ -22,7 +22,7 @@ export class SerialAStar implements AStar {
private canMove: (t: TileRef) => boolean,
private iterations: number,
private maxTries: number,
private gameMap: GameMap
private gameMap: GameMapImpl
) {
this.fwdOpenSet = new PriorityQueue<{ tile: TileRef; fScore: number; }>(
(a, b) => a.fScore - b.fScore