packing tiles for more efficient transfer

This commit is contained in:
Evan
2025-01-03 13:54:01 -08:00
parent 2441abd7c8
commit 1bbb5c9ac3
8 changed files with 126 additions and 26 deletions
+2
View File
@@ -124,6 +124,8 @@ export class ClientGameRunner {
this.renderer.initialize()
this.input.initialize()
this.worker.start((gu: GameUpdateViewData) => {
const size = gu.packedTileUpdates.length * 4 / 1000
console.log(`game update size: ${size}kb`)
this.gameView.update(gu)
this.renderer.tick()
})
@@ -36,6 +36,10 @@ export class TerritoryLayer implements Layer {
}
tick() {
this.game.recentlyUpdatedTiles()
.forEach(t => this.enqueue(t))
if (!this.game.inSpawnPhase()) {
return
}
+29 -6
View File
@@ -5,7 +5,7 @@ import { WinCheckExecution } from "./execution/WinCheckExecution";
import { Game, MutableGame, MutableTile, PlayerID, Tile, TileEvent } from "./game/Game";
import { createGame } from "./game/GameImpl";
import { loadTerrainMap } from "./game/TerrainMapLoader";
import { GameUpdateViewData, PlayerViewData } from "./GameView";
import { GameUpdateViewData, packTileData, PlayerViewData } from "./GameView";
import { GameConfig, Turn } from "./Schemas";
export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise<GameRunner> {
@@ -19,7 +19,8 @@ export async function createGameRunner(gameID: string, gameConfig: GameConfig, c
}
export class GameRunner {
private updatedTiles: MutableTile[]
private updatedTiles: Set<MutableTile> = new Set()
private borderOnlyUpdated: Set<MutableTile> = new Set()
private tickInterval = null
private turns: Turn[] = []
private currTurn = 0
@@ -34,7 +35,14 @@ export class GameRunner {
}
init() {
this.eventBus.on(TileEvent, (e) => { this.updatedTiles.push(e.tile as MutableTile) })
this.eventBus.on(TileEvent, (e) => {
this.updatedTiles.add(e.tile as MutableTile)
if (e.borderOnlyChange) {
this.borderOnlyUpdated.add(e.tile as MutableTile)
} else {
this.updatedTiles.add(e.tile as MutableTile)
}
})
this.game.addExecution(...this.execManager.spawnBots(this.game.config().numBots()))
if (this.game.config().spawnNPCs()) {
this.game.addExecution(...this.execManager.fakeHumanExecutions())
@@ -55,19 +63,34 @@ export class GameRunner {
return
}
this.isExecuting = true
this.updatedTiles = []
this.updatedTiles.clear()
this.borderOnlyUpdated.clear()
this.game.addExecution(...this.execManager.createExecs(this.turns[this.currTurn]))
this.currTurn++
this.game.executeNextTick()
this.updatedTiles.forEach(t => this.borderOnlyUpdated.delete(t))
const updatedData = Array.from(this.updatedTiles).map(t => t.toViewData())
updatedData.forEach(t => t.borderOnlyChange = false)
const borderOnlyData = Array.from(this.borderOnlyUpdated).map(t => t.toViewData())
borderOnlyData.forEach(t => t.borderOnlyChange = true)
updatedData.concat(borderOnlyData)
this.callBack({
tick: this.game.ticks(),
units: this.game.units().map(u => u.toViewData()),
tileUpdates: this.updatedTiles.map(t => t.toViewData()),
packedTileUpdates: updatedData.map(d => packTileData(d)),
players: Object.fromEntries(
this.game.players().map(p => [p.id(), p.toViewData()])
this.game.allPlayers().map(p => [p.id(), p.toViewData()])
) as Record<PlayerID, PlayerViewData>
})
+69 -15
View File
@@ -15,15 +15,16 @@ export interface ViewData<T> {
export interface TileViewData extends ViewData<TileViewData> {
x: number
y: number
owner: PlayerID,
smallID: number,
hasFallout: boolean
hasDefenseBonus: boolean
isBorder: boolean
borderOnlyChange: boolean
}
export class TileView {
constructor(private game: GameView, private data: TileViewData, private _terrain: TerrainTile) { }
constructor(private game: GameView, public data: TileViewData, private _terrain: TerrainTile) { }
type(): TerrainType {
return this._terrain.type()
@@ -32,10 +33,10 @@ export class TileView {
if (!this.hasOwner()) {
return new TerraNulliusImpl()
}
return this.game.player(this.data?.owner)
return this.game.playerBySmallID(this.data?.smallID)
}
hasOwner(): boolean {
return this.data?.owner != undefined
return this.data?.smallID !== undefined && this.data.smallID !== 0;
}
isBorder(): boolean {
return this.data?.isBorder
@@ -50,16 +51,15 @@ export class TileView {
return this._terrain
}
neighbors(): TileView[] {
throw new Error("Method not implemented.");
neighbors(): Tile[] {
return this._terrain.neighbors().map(t => this.game.tile(t.cell()))
}
hasDefenseBonus(): boolean {
throw new Error("Method not implemented.");
return this.data?.hasDefenseBonus ?? false
}
cost(): number {
throw new Error("Method not implemented.");
return this._terrain.cost()
}
}
@@ -104,6 +104,7 @@ export interface PlayerViewData extends ViewData<PlayerViewData> {
name: string,
displayName: string,
id: PlayerID,
smallID: number,
type: PlayerType,
isAlive: boolean,
tilesOwned: number,
@@ -117,6 +118,9 @@ export interface PlayerViewData extends ViewData<PlayerViewData> {
export class PlayerView implements Player {
constructor(private game: GameView, private data: PlayerViewData) { }
smallID(): number {
return this.data.smallID
}
lastTileChange(): Tick {
return 0
}
@@ -221,7 +225,7 @@ export class PlayerView implements Player {
return false
}
info(): PlayerInfo {
return null
return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id())
}
}
@@ -229,32 +233,47 @@ export interface GameUpdateViewData extends ViewData<GameUpdateViewData> {
tick: number
units: UnitViewData[]
players: Record<PlayerID, PlayerViewData>
tileUpdates: TileViewData[]
tileUpdates?: TileViewData[]
packedTileUpdates: Uint16Array[]
}
export class GameView {
private data: GameUpdateViewData
private tiles: TileViewData[][] = []
private tiles: TileView[][] = []
private smallIDToID = new Map<number, PlayerID>()
constructor(private _config: Config, private _terrainMap: TerrainMap) {
// Initialize the 2D array
this.tiles = Array(_terrainMap.width()).fill(null).map(() => Array(_terrainMap.height()).fill(null));
// Fill the array with new TileView objects
for (let x = 0; x < _terrainMap.width(); x++) {
for (let y = 0; y < _terrainMap.height(); y++) {
this.tiles[x][y] = new TileView(this, null, _terrainMap.terrain(new Cell(x, y)));
}
}
this.data = {
tick: 0,
units: [],
tileUpdates: [],
packedTileUpdates: [],
players: {}
}
}
public update(gu: GameUpdateViewData) {
this.data = gu
this.data.tileUpdates = this.data.packedTileUpdates.map(tu => unpackTileData(tu))
Object.entries(gu.players).forEach(([key, value]) => {
this.smallIDToID.set(value.smallID, key);
});
gu.tileUpdates.forEach(tu => {
this.tiles[tu.x][tu.y] = tu
this.tiles[tu.x][tu.y].data = tu
})
}
recentlyUpdatedTiles(): TileView[] {
return this.data.tileUpdates.map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y))))
return this.data.tileUpdates.filter(d => true).map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y))))
}
player(id: PlayerID): Player {
@@ -263,6 +282,14 @@ export class GameView {
}
throw Error(`player id ${id} not found`)
}
playerBySmallID(id: number): Player {
if (!this.smallIDToID.has(id)) {
throw new Error(`small id ${id} not found`)
}
return this.player(this.smallIDToID.get(id))
}
playerByClientID(id: ClientID): Player | null {
return null
}
@@ -273,7 +300,7 @@ export class GameView {
return []
}
tile(cell: Cell): Tile {
return new TileView(this, this.tiles[cell.x][cell.y], this._terrainMap.terrain(cell))
return this.tiles[cell.x][cell.y]
}
isOnMap(cell: Cell): boolean {
return this._terrainMap.isOnMap(cell)
@@ -312,4 +339,31 @@ export class GameView {
terrainMap(): TerrainMap {
return this._terrainMap
}
}
export function packTileData(tile: TileViewData): Uint16Array {
const packed = new Uint16Array(4);
packed[0] = tile.x;
packed[1] = tile.y;
packed[2] = tile.smallID;
// Pack booleans into bits
packed[3] = (tile.hasFallout ? 1 : 0) |
(tile.hasDefenseBonus ? 2 : 0) |
(tile.isBorder ? 4 : 0) |
(tile.borderOnlyChange ? 8 : 0)
return packed;
}
export function unpackTileData(packed: Uint16Array): TileViewData {
return {
x: packed[0],
y: packed[1],
smallID: packed[2],
hasFallout: !!(packed[3] & 1),
hasDefenseBonus: !!(packed[3] & 2),
isBorder: !!(packed[3] & 4),
borderOnlyChange: !!(packed[4] & 8)
};
}
+3 -1
View File
@@ -227,6 +227,7 @@ export interface TerraNullius {
}
export interface Player {
smallID(): number
info(): PlayerInfo
name(): string
displayName(): string
@@ -347,6 +348,7 @@ export interface MutableGame extends Game {
player(id: PlayerID): MutablePlayer
playerByClientID(id: ClientID): MutablePlayer | null
players(): MutablePlayer[]
allPlayers(): MutablePlayer[]
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer
executions(): Execution[]
units(...types: UnitType[]): MutableUnit[]
@@ -356,7 +358,7 @@ export interface MutableGame extends Game {
}
export class TileEvent implements GameEvent {
constructor(public readonly tile: Tile) { }
constructor(public readonly tile: Tile, public readonly borderOnlyChange: boolean = false) { }
}
export class PlayerEvent implements GameEvent {
+9 -1
View File
@@ -40,6 +40,8 @@ export class GameImpl implements MutableGame {
allianceRequests: AllianceRequestImpl[] = []
alliances_: AllianceImpl[] = []
private nextID = 1
constructor(
private _terrainMap: TerrainMapImpl,
@@ -211,6 +213,10 @@ export class GameImpl implements MutableGame {
return Array.from(this._players.values()).filter(p => p.isAlive())
}
allPlayers(): MutablePlayer[] {
return Array.from(this._players.values())
}
executions(): Execution[] {
return [...this.execs, ...this.unInitExecs]
}
@@ -245,7 +251,8 @@ export class GameImpl implements MutableGame {
}
addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer {
let player = new PlayerImpl(this, playerInfo, manpower)
let player = new PlayerImpl(this, this.nextID, playerInfo, manpower)
this.nextID++
this._players.set(playerInfo.id, player)
this.eventBus.emit(new PlayerEvent(player))
return player
@@ -365,6 +372,7 @@ export class GameImpl implements MutableGame {
tile.neighbors().forEach(t => tiles.push(t as TileImpl))
for (const t of tiles) {
this.eventBus.emit(new TileEvent(t, true))
if (!t.hasOwner()) {
t._isBorder = false
continue
+7 -1
View File
@@ -46,7 +46,8 @@ export class PlayerImpl implements MutablePlayer {
private relations = new Map<Player, number>()
constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, startPopulation: number) {
constructor(private gs: GameImpl, private _smallID: number, private readonly playerInfo: PlayerInfo, startPopulation: number) {
this._name = playerInfo.name;
this._targetTroopRatio = 1
this._troops = startPopulation * this._targetTroopRatio;
@@ -61,6 +62,7 @@ export class PlayerImpl implements MutablePlayer {
name: this.name(),
displayName: this.displayName(),
id: this.id(),
smallID: this.smallID(),
type: this.type(),
isAlive: this.isAlive(),
tilesOwned: this.numTilesOwned(),
@@ -73,6 +75,10 @@ export class PlayerImpl implements MutablePlayer {
}
}
smallID(): number {
return this._smallID
}
name(): string {
return this._name;
}
+3 -2
View File
@@ -27,10 +27,11 @@ export class TileImpl implements MutableTile {
return {
x: this._cell.x,
y: this._cell.y,
owner: this._owner?.id(),
smallID: this._owner.isPlayer() ? this._owner.smallID() : 0,
hasFallout: this._hasFallout,
hasDefenseBonus: this.hasDefenseBonus(),
isBorder: this.isBorder()
isBorder: this.isBorder(),
borderOnlyChange: false,
}
}