diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index f4b1ddc68..95561e35f 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -6,11 +6,16 @@ import { ClientID, GameConfig, GameID, ServerMessage } from "../core/Schemas"; import { loadTerrainMap } from "../core/game/TerrainMapLoader"; import { SendAttackIntentEvent, + SendHashEvent, SendSpawnIntentEvent, Transport, } from "./Transport"; import { createCanvas } from "./Utils"; -import { ErrorUpdate } from "../core/game/GameUpdates"; +import { + ErrorUpdate, + GameUpdateType, + HashUpdate, +} from "../core/game/GameUpdates"; import { WorkerClient } from "../core/worker/WorkerClient"; import { consolex, initRemoteSender } from "../core/Consolex"; import { getConfig, getServerConfig } from "../core/configuration/Config"; @@ -171,6 +176,9 @@ export class ClientGameRunner { showErrorModal(gu.errMsg, gu.stack, this.clientID); return; } + gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { + this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); + }); this.gameView.update(gu); this.renderer.tick(); }); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index c4fd5f700..5cdbeb2fe 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -9,6 +9,7 @@ import { Player, PlayerID, PlayerType, + Tick, UnitType, } from "../core/game/Game"; import { @@ -23,6 +24,7 @@ import { GameConfig, ClientLogMessageSchema, ClientSendWinnerSchema, + ClientMessageSchema, } from "../core/Schemas"; import { LobbyConfig } from "./ClientGameRunner"; import { LocalServer } from "./LocalServer"; @@ -107,6 +109,12 @@ export class SendSetTargetTroopRatioEvent implements GameEvent { export class SendWinnerEvent implements GameEvent { constructor(public readonly winner: ClientID) {} } +export class SendHashEvent implements GameEvent { + constructor( + public readonly tick: Tick, + public readonly hash: number, + ) {} +} export class Transport { private socket: WebSocket; @@ -159,6 +167,7 @@ export class Transport { this.eventBus.on(SendLogEvent, (e) => this.onSendLogEvent(e)); this.eventBus.on(PauseGameEvent, (e) => this.onPauseGameEvent(e)); this.eventBus.on(SendWinnerEvent, (e) => this.onSendWinnerEvent(e)); + this.eventBus.on(SendHashEvent, (e) => this.onSendHashEvent(e)); } private startPing() { @@ -448,6 +457,26 @@ export class Transport { } } + private onSendHashEvent(event: SendHashEvent) { + if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { + const msg = ClientMessageSchema.parse({ + type: "hash", + clientID: this.lobbyConfig.clientID, + persistentID: this.lobbyConfig.persistentID, + gameID: this.lobbyConfig.gameID, + tick: event.tick, + hash: event.hash, + }); + this.sendMsg(JSON.stringify(msg)); + } else { + console.log( + "WebSocket is not open. Current state:", + this.socket.readyState, + ); + console.log("attempting reconnect"); + } + } + private sendIntent(intent: Intent) { if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index 223219210..c2b2e15b2 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -63,7 +63,7 @@ export class EventsDisplay extends LitElement implements Layer { ], [GameUpdateType.BrokeAlliance, (u) => this.onBrokeAllianceEvent(u)], [GameUpdateType.TargetPlayer, (u) => this.onTargetPlayerEvent(u)], - [GameUpdateType.EmojiUpdate, (u) => this.onEmojiMessageEvent(u)], + [GameUpdateType.Emoji, (u) => this.onEmojiMessageEvent(u)], ]); constructor() { diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index af105ce08..80b375f28 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -107,7 +107,7 @@ export class OptionsMenu extends LitElement implements Layer { tick() { this.hasWinner = this.hasWinner || - this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].length > 0; + this.game.updatesSinceLastTick()[GameUpdateType.Win].length > 0; if (this.game.inSpawnPhase()) { this.timer = 0; } else if (!this.hasWinner && this.game.ticks() % 10 == 0) { diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 6991d8075..7a122bf87 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -208,7 +208,7 @@ export class WinModal extends LitElement implements Layer { } this.show(); } - this.game.updatesSinceLastTick()[GameUpdateType.WinUpdate].forEach((wu) => { + this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => { const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView; this.eventBus.emit(new SendWinnerEvent(winner.clientID())); if (winner == this.game.myPlayer()) { diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 8ea615139..95d0833dc 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -48,7 +48,8 @@ export type ClientMessage = | ClientPingMessage | ClientIntentMessage | ClientJoinMessage - | ClientLogMessage; + | ClientLogMessage + | ClientHashMessage; export type ServerMessage = | ServerSyncMessage | ServerStartGameMessage @@ -65,6 +66,7 @@ export type ClientPingMessage = z.infer; export type ClientIntentMessage = z.infer; export type ClientJoinMessage = z.infer; export type ClientLogMessage = z.infer; +export type ClientHashMessage = z.infer; export type PlayerRecord = z.infer; export type GameRecord = z.infer; @@ -268,7 +270,7 @@ export const ServerMessageSchema = z.union([ // Client const ClientBaseMessageSchema = z.object({ - type: z.enum(["winner", "join", "intent", "ping", "log"]), + type: z.enum(["winner", "join", "intent", "ping", "log", "hash"]), clientID: ID, persistentID: SafeString.nullable(), // WARNING: persistent id is private. gameID: ID, @@ -279,6 +281,12 @@ export const ClientSendWinnerSchema = ClientBaseMessageSchema.extend({ winner: ID.nullable(), }); +export const ClientHashSchema = ClientBaseMessageSchema.extend({ + type: z.literal("hash"), + hash: z.number(), + tick: z.number(), +}); + export const ClientLogMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal("log"), severity: z.nativeEnum(LogSeverity), @@ -308,6 +316,7 @@ export const ClientMessageSchema = z.union([ ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLogMessageSchema, + ClientHashSchema, ]); export const PlayerRecordSchema = z.object({ diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 27c721111..a83ad78cd 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -241,21 +241,29 @@ export class GameImpl implements Game { this.execs.push(...inited); this.unInitExecs = unInited; - this._ticks++; - if (this._ticks % 100 == 0) { - let hash = 1; - this._players.forEach((p) => { - hash += p.hash(); - }); - 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()); } + if (this.ticks() % 10 == 0) { + this.addUpdate({ + type: GameUpdateType.Hash, + tick: this.ticks(), + hash: this.hash(), + }); + } + this._ticks++; return this.updates; } + private hash(): number { + let hash = 1; + this._players.forEach((p) => { + hash += p.hash(); + }); + return hash; + } + terraNullius(): TerraNullius { return this._terraNullius; } @@ -494,14 +502,14 @@ export class GameImpl implements Game { sendEmojiUpdate(msg: EmojiMessage): void { this.addUpdate({ - type: GameUpdateType.EmojiUpdate, + type: GameUpdateType.Emoji, emoji: msg, }); } setWinner(winner: Player): void { this.addUpdate({ - type: GameUpdateType.WinUpdate, + type: GameUpdateType.Win, winnerID: winner.smallID(), }); } diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 815136884..db2ac04d2 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -34,8 +34,9 @@ export enum GameUpdateType { BrokeAlliance, AllianceExpired, TargetPlayer, - EmojiUpdate, - WinUpdate, + Emoji, + Win, + Hash, } export type GameUpdate = @@ -49,7 +50,8 @@ export type GameUpdate = | DisplayMessageUpdate | TargetPlayerUpdate | EmojiUpdate - | WinUpdate; + | WinUpdate + | HashUpdate; export interface TileUpdateWrapper { type: GameUpdateType.Tile; @@ -133,7 +135,7 @@ export interface TargetPlayerUpdate { } export interface EmojiUpdate { - type: GameUpdateType.EmojiUpdate; + type: GameUpdateType.Emoji; emoji: EmojiMessage; } @@ -145,6 +147,12 @@ export interface DisplayMessageUpdate { } export interface WinUpdate { - type: GameUpdateType.WinUpdate; + type: GameUpdateType.Win; winnerID: number; } + +export interface HashUpdate { + type: GameUpdateType.Hash; + tick: Tick; + hash: number; +} diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 7c3de09a0..8129d9d73 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -135,7 +135,7 @@ export class UnitImpl implements Unit { } hash(): number { - return this.tile() + simpleHash(this.type()); + return this.tile() + simpleHash(this.type()) * this._id; } toString(): string { diff --git a/src/server/Client.ts b/src/server/Client.ts index 4ce9345ac..1765693f2 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -1,9 +1,12 @@ import WebSocket from "ws"; import { ClientID } from "../core/Schemas"; +import { Tick } from "../core/game/Game"; export class Client { public lastPing: number; + public hashes: Map = new Map(); + constructor( public readonly clientID: ClientID, public readonly persistentID: string, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 46434563a..f0b134cd7 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -3,11 +3,8 @@ import { ClientMessage, ClientMessageSchema, GameConfig, - GameRecordSchema, Intent, PlayerRecord, - ServerPingMessageSchema, - ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn, @@ -162,6 +159,12 @@ export class GameServer { this.lastPingUpdate = Date.now(); client.lastPing = Date.now(); } + if (clientMsg.type == "hash") { + console.log( + `client ${clientMsg.clientID} got hash ${clientMsg.hash} on tick ${clientMsg.tick}`, + ); + client.hashes.set(clientMsg.tick, clientMsg.hash); + } if (clientMsg.type == "winner") { this.winner = clientMsg.winner; }