thread split: get units working

This commit is contained in:
Evan
2025-01-12 11:51:49 -08:00
parent e395619abc
commit 3da6008e9f
9 changed files with 127 additions and 62 deletions
+6
View File
@@ -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!');
+3 -4
View File
@@ -28,12 +28,10 @@ export class UnitLayer implements Layer {
private oldShellTile = new Map<Unit, Tile>()
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)
}
}
+11 -13
View File
@@ -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<PlayerID, NameViewData>()
private playerViewData: Record<PlayerID, NameViewData> = {}
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
}
+42 -22
View File
@@ -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<PlayerActions> {
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<PlayerID, PlayerUpdate>
updates: GameUpdates
packedTileUpdates: Uint16Array[]
playerNameViewData: Record<number, NameViewData>
}
export class GameView {
@@ -232,7 +251,6 @@ export class GameView {
private _players = new Map<PlayerID, PlayerView>()
private _units = new Map<number, UnitView>()
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),
};
}
}
+11 -1
View File
@@ -9,6 +9,15 @@ export type Gold = number
export const AllPlayers = "AllPlayers" as const;
// export type GameUpdates = Record<GameUpdateType, GameUpdate[]>;
// Create a type that maps GameUpdateType to its corresponding update type
type UpdateTypeMap<T extends GameUpdateType> = Extract<GameUpdate, { type: T }>;
// Then use it to create the record type
export type GameUpdates = {
[K in GameUpdateType]: UpdateTypeMap<K>[];
}
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
+40 -21
View File
@@ -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
})
}
}
}
// 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;
};
+2
View File
@@ -25,6 +25,8 @@ ctx.addEventListener('message', async (e: MessageEvent<MainThreadMessage>) => {
const message = e.data;
switch (message.type) {
case 'heartbeat':
break
case 'init':
try {
gameRunner = createGameRunner(
+6
View File
@@ -85,6 +85,12 @@ export class WorkerClient {
});
}
sendHeartbeat() {
this.worker.postMessage({
type: 'heartbeat'
});
}
playerInteraction(playerID: PlayerID, tile: Tile): Promise<PlayerActions> {
return new Promise((resolve, reject) => {
if (!this.isInitialized) {
+6 -1
View File
@@ -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;