From 3da6008e9fd9e8bcbe0c184624f1e1d46e6134a1 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 12 Jan 2025 11:51:49 -0800 Subject: [PATCH] thread split: get units working --- src/client/ClientGameRunner.ts | 6 +++ src/client/graphics/layers/UnitLayer.ts | 7 ++- src/core/GameRunner.ts | 24 +++++----- src/core/GameView.ts | 64 ++++++++++++++++--------- src/core/game/Game.ts | 12 ++++- src/core/game/GameImpl.ts | 61 +++++++++++++++-------- src/core/worker/Worker.worker.ts | 2 + src/core/worker/WorkerClient.ts | 6 +++ src/core/worker/WorkerMessages.ts | 7 ++- 9 files changed, 127 insertions(+), 62 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 28a245cee..13e204b62 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -128,6 +128,12 @@ export class ClientGameRunner { this.gameView.update(gu) this.renderer.tick() }) + const worker = this.worker + const keepWorkerAlive = () => { + worker.sendHeartbeat + requestAnimationFrame(keepWorkerAlive) + } + requestAnimationFrame(keepWorkerAlive) const onconnect = () => { consolex.log('Connected to game server!'); diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 94cfd6fba..cf97d1555 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -28,12 +28,10 @@ export class UnitLayer implements Layer { private oldShellTile = new Map() - constructor(private game: GameView, private eventBus: EventBus, private clientID: ClientID) { this.theme = game.config().theme(); } - shouldTransform(): boolean { return true; } @@ -42,8 +40,9 @@ export class UnitLayer implements Layer { if (this.myPlayer == null) { this.myPlayer = this.game.playerByClientID(this.clientID) } - for (const unit of this.game.recentlyUpdatedUnits()) { - this.onUnitEvent(unit) + for (const unit of this.game.units()) { + if (unit.wasUpdated()) + this.onUnitEvent(unit) } } diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 4f0085df3..c34372056 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -4,7 +4,7 @@ import { getConfig } from "./configuration/Config"; import { EventBus } from "./EventBus"; import { Executor } from "./execution/ExecutionManager"; import { WinCheckExecution } from "./execution/WinCheckExecution"; -import { Cell, DisplayMessageUpdate, Game, GameUpdateType, MessageType, MutableGame, MutableTile, NameViewData, Player, PlayerActions, PlayerID, Tile, UnitType } from "./game/Game"; +import { Cell, DisplayMessageUpdate, Game, GameUpdateType, MessageType, MutableGame, MutableTile, NameViewData, Player, PlayerActions, PlayerID, Tile, TileUpdate, UnitType, UnitUpdate } from "./game/Game"; import { createGame } from "./game/GameImpl"; import { loadTerrainMap } from "./game/TerrainMapLoader"; import { GameConfig, Turn } from "./Schemas"; @@ -26,7 +26,7 @@ export class GameRunner { private currTurn = 0 private isExecuting = false - private playerToName = new Map() + private playerViewData: Record = {} constructor( public game: MutableGame, @@ -63,22 +63,20 @@ export class GameRunner { const updates = this.game.executeNextTick() if (this.game.inSpawnPhase() || this.game.ticks() % 20 == 0) { - this.game.players() - .forEach(p => this.playerToName.set(p.id(), placeName(this.game, p))) + this.game.players().forEach(p => { + this.playerViewData[p.id()] = placeName(this.game, p) + }) } - const playerViewData = {} - for (const player of this.game.allPlayers()) { - const viewData = player.toUpdate() - viewData.nameViewData = this.playerToName.get(player.id()) - playerViewData[player.id()] = viewData - } + // Many tiles are updated to pack it into an array + const packedTileUpdates = updates[GameUpdateType.Tile].map(u => packTileData(u as TileUpdate)) + updates[GameUpdateType.Tile] = [] this.callBack({ tick: this.game.ticks(), - units: updates.filter(u => u.type == GameUpdateType.Unit), - packedTileUpdates: updates.filter(u => u.type == GameUpdateType.Tile).map(u => packTileData(u)), - players: playerViewData + packedTileUpdates: packedTileUpdates, + updates: updates, + playerNameViewData: this.playerViewData }) this.isExecuting = false } diff --git a/src/core/GameView.ts b/src/core/GameView.ts index c14bb86ac..64ac806c5 100644 --- a/src/core/GameView.ts +++ b/src/core/GameView.ts @@ -1,4 +1,4 @@ -import { GameUpdateType, MapPos, MessageType, NameViewData, Player, PlayerActions, PlayerUpdate, Tile, TileUpdate, Unit, UnitUpdate } from './game/Game'; +import { GameUpdates, GameUpdateType, MapPos, MessageType, NameViewData, Player, PlayerActions, PlayerUpdate, Tile, TileUpdate, Unit, UnitUpdate } from './game/Game'; import { Config } from "./configuration/Config"; import { Alliance, AllianceRequest, AllPlayers, Cell, DefenseBonus, EmojiMessage, Execution, ExecutionView, Game, Gold, MutableTile, Nation, PlayerID, PlayerInfo, PlayerType, Relation, TerrainMap, TerrainTile, TerrainType, TerraNullius, Tick, UnitInfo, UnitType } from "./game/Game"; import { ClientID } from "./Schemas"; @@ -58,13 +58,31 @@ export class TileView { } export class UnitView implements Unit { - constructor(private gameView: GameView, private data: UnitUpdate) { } + public _wasUpdated = true + public lastPos: MapPos[] = [] + + constructor(private gameView: GameView, private data: UnitUpdate) { + this.lastPos.push(data.pos) + } + + wasUpdated(): boolean { + return this._wasUpdated + } + + lastTiles(): Tile[] { + return this.lastPos.map(pos => this.gameView.tile(new Cell(pos.x, pos.y))) + } lastTile(): Tile { - return this.gameView.tile(new Cell(this.data.lastPos.x, this.data.lastPos.y)) + if (this.lastPos.length == 0) { + return this.gameView.tile(new Cell(this.data.pos.x, this.data.pos.y)) + } + return this.gameView.tile(new Cell(this.lastPos[0].x, this.lastPos[0].y)) } update(data: UnitUpdate) { + this.lastPos.push(data.pos) + this._wasUpdated = true this.data = data } @@ -96,14 +114,15 @@ export class UnitView implements Unit { } export class PlayerView implements Player { - constructor(private game: GameView, public data: PlayerUpdate) { } + + constructor(private game: GameView, public data: PlayerUpdate, public nameData: NameViewData) { } async actions(tile: Tile): Promise { return this.game.worker.playerInteraction(this.id(), tile) } nameLocation(): NameViewData { - return this.data.nameViewData + return this.nameData } smallID(): number { @@ -220,9 +239,9 @@ export class PlayerView implements Player { export interface GameUpdateViewData { tick: number - units: UnitUpdate[] - players: Record + updates: GameUpdates packedTileUpdates: Uint16Array[] + playerNameViewData: Record } export class GameView { @@ -232,7 +251,6 @@ export class GameView { private _players = new Map() private _units = new Map() private updatedTiles: TileView[] = [] - private updatedUnits: UnitView[] = [] constructor(public worker: WorkerClient, private _config: Config, private _terrainMap: TerrainMap) { // Initialize the 2D array @@ -246,9 +264,10 @@ export class GameView { } this.lastUpdate = { tick: 0, - units: [], packedTileUpdates: [], - players: {} + // TODO: make this empty map instead of null? + updates: null, + playerNameViewData: {}, } } @@ -262,30 +281,31 @@ export class GameView { }) this.updatedTiles = Array.from(updated).map(pos => this.tiles[pos.x][pos.y]) - Object.entries(gu.players).forEach(([key, value]) => { - this.smallIDToID.set(value.smallID, key); - if (this._players.has(key)) { - this._players.get(key).data = value + gu.updates[GameUpdateType.Player].forEach((pu) => { + this.smallIDToID.set(pu.smallID, pu.id); + if (this._players.has(pu.id)) { + this._players.get(pu.id).data = pu + this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id] } else { - this._players.set(key, new PlayerView(this, value)) + this._players.set(pu.id, new PlayerView(this, pu, gu.playerNameViewData[pu.id])) } }); - gu.units.forEach(unit => { + for (const unit of this._units.values()) { + unit._wasUpdated = false + unit.lastPos = unit.lastPos.slice(-1) + } + gu.updates[GameUpdateType.Unit].forEach(unit => { if (this._units.has(unit.id)) { this._units.get(unit.id).update(unit) } else { this._units.set(unit.id, new UnitView(this, unit)) } }) - this.updatedUnits = gu.units.map(u => this._units.get(u.id)) } recentlyUpdatedTiles(): TileView[] { return this.updatedTiles } - recentlyUpdatedUnits(): UnitView[] { - return this.updatedUnits - } player(id: PlayerID): PlayerView { if (this._players.has(id)) { @@ -347,7 +367,7 @@ export class GameView { config(): Config { return this._config } - units(...types: UnitType[]): Unit[] { + units(...types: UnitType[]): UnitView[] { return Array.from(this._units.values()) } unitInfo(type: UnitType): UnitInfo { @@ -384,4 +404,4 @@ export function unpackTileData(packed: Uint16Array): TileUpdate { hasDefenseBonus: !!(packed[3] & 2), isBorder: !!(packed[3] & 4), }; -} \ No newline at end of file +} diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a0dbadf15..17405f42a 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -9,6 +9,15 @@ export type Gold = number export const AllPlayers = "AllPlayers" as const; +// export type GameUpdates = Record; +// Create a type that maps GameUpdateType to its corresponding update type +type UpdateTypeMap = Extract; + +// Then use it to create the record type +export type GameUpdates = { + [K in GameUpdateType]: UpdateTypeMap[]; +} + export interface MapPos { x: number y: number @@ -83,6 +92,7 @@ export class EmojiMessage { } export class Cell { + public index: number private strRepr: string @@ -347,7 +357,7 @@ export interface Game { forEachTile(fn: (tile: Tile) => void): void executions(): ExecutionView[] terraNullius(): TerraNullius - executeNextTick(): GameUpdate[] + executeNextTick(): GameUpdates ticks(): Tick inSpawnPhase(): boolean addExecution(...exec: Execution[]): void diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index d5540d4d2..dca2fcd60 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,5 +1,5 @@ import { Config } from "../configuration/Config"; -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerID, PlayerInfo, Player, TerraNullius, Tile, Unit, MutableAllianceRequest, Alliance, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus, MutableTile, GameUpdate, GameUpdateType, AllPlayers } from "./Game"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerID, PlayerInfo, Player, TerraNullius, Tile, Unit, MutableAllianceRequest, Alliance, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus, MutableTile, GameUpdate, GameUpdateType, AllPlayers, GameUpdates } from "./Game"; import { TerrainMapImpl } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; @@ -23,7 +23,6 @@ export class GameImpl implements MutableGame { private unInitExecs: Execution[] = [] - // idCounter: PlayerID = 1; // Zero reserved for TerraNullius map: TileImpl[][] private nations_: Nation[] = [] @@ -41,7 +40,7 @@ export class GameImpl implements MutableGame { private nextPlayerID = 1 private _nextUnitID = 1 - private updates: GameUpdate[] = [] + private updates: GameUpdates = createGameUpdatesMap() constructor( private _terrainMap: TerrainMapImpl, @@ -67,6 +66,11 @@ export class GameImpl implements MutableGame { )) } + addUpdate(update: GameUpdate) { + (this.updates[update.type] as any[]).push(update); + } + + nextUnitID(): number { const old = this._nextUnitID this._nextUnitID++ @@ -79,20 +83,20 @@ export class GameImpl implements MutableGame { throw Error(`cannot set fallout, tile ${tile} has owner`) } ti._hasFallout = true - this.updates.push(ti.toUpdate()) + this.addUpdate(ti.toUpdate()) } addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { const df = { unit: unit, tile: tile, amount: amount }; (tile as TileImpl)._defenseBonuses.push(df) - this.updates.push((tile as TileImpl).toUpdate()) + this.addUpdate((tile as TileImpl).toUpdate()) return df } removeTileDefenseBonus(bonus: DefenseBonus): void { const t = bonus.tile as TileImpl t._defenseBonuses = t._defenseBonuses.filter(db => db != bonus) - this.updates.push(t.toUpdate()) + this.addUpdate(t.toUpdate()) } units(...types: UnitType[]): UnitImpl[] { @@ -122,7 +126,7 @@ export class GameImpl implements MutableGame { } const ar = new AllianceRequestImpl(requestor, recipient, this._ticks, this) this.allianceRequests.push(ar) - this.updates.push(ar.toUpdate()) + this.addUpdate(ar.toUpdate()) return ar } @@ -131,7 +135,7 @@ export class GameImpl implements MutableGame { const alliance = new AllianceImpl(this, request.requestor() as PlayerImpl, request.recipient() as PlayerImpl, this._ticks) this.alliances_.push(alliance); (request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(request) - this.updates.push({ + this.addUpdate({ type: GameUpdateType.AllianceRequestReply, request: request.toUpdate(), accepted: true, @@ -142,7 +146,7 @@ export class GameImpl implements MutableGame { rejectAllianceRequest(request: AllianceRequestImpl) { this.allianceRequests = this.allianceRequests.filter(ar => ar != request); (request.requestor() as PlayerImpl).pastOutgoingAllianceRequests.push(request) - this.updates.push({ + this.addUpdate({ type: GameUpdateType.AllianceRequestReply, request: request.toUpdate(), accepted: true @@ -164,8 +168,8 @@ export class GameImpl implements MutableGame { return this._ticks } - executeNextTick(): GameUpdate[] { - this.updates = [] + executeNextTick(): GameUpdates { + this.updates = createGameUpdatesMap() this.execs.forEach(e => { if (e.isActive() && (!this.inSpawnPhase() || e.activeDuringSpawnPhase())) { e.tick(this._ticks) @@ -194,6 +198,10 @@ export class GameImpl implements MutableGame { }) consolex.log(`tick ${this._ticks}: hash ${hash}`) } + for (const player of this._players.values()) { + // Players change each to so always add them + this.addUpdate(player.toUpdate()) + } return this.updates } @@ -357,7 +365,7 @@ export class GameImpl implements MutableGame { owner._lastTileChange = this._ticks this.updateBorders(tile) tileImpl._hasFallout = false - this.updates.push((tile as TileImpl).toUpdate()) + this.addUpdate((tile as TileImpl).toUpdate()) } relinquish(tile: Tile) { @@ -377,7 +385,7 @@ export class GameImpl implements MutableGame { tileImpl._owner = this._terraNullius this.updateBorders(tile) - this.updates.push( + this.addUpdate( (tile as TileImpl).toUpdate() ) } @@ -417,11 +425,11 @@ export class GameImpl implements MutableGame { } public fireUnitUpdateEvent(unit: Unit) { - this.updates.push((unit as UnitImpl).toUpdate()) + this.addUpdate((unit as UnitImpl).toUpdate()) } target(targeter: Player, target: Player) { - this.updates.push({ + this.addUpdate({ type: GameUpdateType.TargetPlayer, playerID: targeter.smallID(), targetID: target.smallID(), @@ -448,7 +456,7 @@ export class GameImpl implements MutableGame { throw new Error(`must have exactly one alliance, have ${alliances.length}`) } this.alliances_ = this.alliances_.filter(a => a != alliances[0]) - this.updates.push({ + this.addUpdate({ type: GameUpdateType.BrokeAlliance, traitorID: breaker.smallID(), betrayedID: other.smallID() @@ -463,7 +471,7 @@ export class GameImpl implements MutableGame { throw new Error(`cannot expire alliance: must have exactly one alliance, have ${alliances.length}`) } this.alliances_ = this.alliances_.filter(a => a != alliances[0]) - this.updates.push({ + this.addUpdate({ type: GameUpdateType.AllianceExpired, player1: alliance.requestor().smallID(), player2: alliance.recipient().smallID() @@ -473,7 +481,7 @@ export class GameImpl implements MutableGame { sendEmojiUpdate(sender: Player, recipient: Player | typeof AllPlayers, emoji: string): void { const recipientID = recipient === AllPlayers ? recipient : recipient.smallID(); - this.updates.push({ + this.addUpdate({ type: GameUpdateType.EmojiUpdate, message: emoji, senderID: sender.smallID(), @@ -483,7 +491,7 @@ export class GameImpl implements MutableGame { } setWinner(winner: Player): void { - this.updates.push({ + this.addUpdate({ type: GameUpdateType.WinUpdate, winnerID: winner.smallID() }) @@ -502,11 +510,22 @@ export class GameImpl implements MutableGame { if (playerID != null) { id = this.player(playerID).smallID() } - this.updates.push({ + this.addUpdate({ type: GameUpdateType.DisplayEvent, messageType: type, message: message, playerID: id }) } -} \ No newline at end of file +} + +// Or a more dynamic approach that will catch new enum values: +const createGameUpdatesMap = (): GameUpdates => { + const map = {} as GameUpdates; + Object.values(GameUpdateType) + .filter(key => !isNaN(Number(key))) // Filter out reverse mappings + .forEach(key => { + map[key as GameUpdateType] = []; + }); + return map; +}; \ No newline at end of file diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index bee7f2892..66cfb0728 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -25,6 +25,8 @@ ctx.addEventListener('message', async (e: MessageEvent) => { const message = e.data; switch (message.type) { + case 'heartbeat': + break case 'init': try { gameRunner = createGameRunner( diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index bec500bfa..ebb511eed 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -85,6 +85,12 @@ export class WorkerClient { }); } + sendHeartbeat() { + this.worker.postMessage({ + type: 'heartbeat' + }); + } + playerInteraction(playerID: PlayerID, tile: Tile): Promise { return new Promise((resolve, reject) => { if (!this.isInitialized) { diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index 605365c4e..2359f32bd 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -3,6 +3,7 @@ import { GameConfig, GameID, Turn } from "../Schemas"; import { PlayerActions, PlayerID } from "../game/Game"; export type WorkerMessageType = + | 'heartbeat' | 'init' | 'initialized' | 'turn' @@ -16,6 +17,10 @@ interface BaseWorkerMessage { id?: string; } +export interface HeartbeatMessage extends BaseWorkerMessage { + type: 'heartbeat' +} + // Messages from main thread to worker export interface InitMessage extends BaseWorkerMessage { type: 'init'; @@ -51,7 +56,7 @@ export interface PlayerActionsResultMessage extends BaseWorkerMessage { } // Union types for type safety -export type MainThreadMessage = InitMessage | TurnMessage | PlayerActionsMessage +export type MainThreadMessage = HeartbeatMessage | InitMessage | TurnMessage | PlayerActionsMessage // Message send from worker export type WorkerMessage = InitializedMessage | GameUpdateMessage | PlayerActionsResultMessage; \ No newline at end of file