/** * State management functionality for GameBitPacker */ import type { BitPackerSchema } from "@suaveplan/types"; import type { StateDelta } from "../types.js"; import type { GameStateManagerInterface } from "./state-manager.types.js"; export class GameStateManager implements GameStateManagerInterface { private currentState: unknown | undefined; private lastSentState: unknown | undefined; private sequence = 0; private lastReceivedSequence = -1; public static readonly MAX_SEQUENCE = 15; // 4 bits for sequence /** * Update the current state with new data */ public update(data: unknown, isInitialState = false): void { this.currentState = this.deepClone(data); if (isInitialState) { this.lastSentState = this.deepClone(data); this.sequence = 0; this.lastReceivedSequence = 0; // Start at 0 to receive sequence 1 } else { this.sequence = (this.sequence + 1) % GameStateManager.MAX_SEQUENCE; } } /** * Get the current state */ public getState(): unknown | undefined { return this.currentState ? this.deepClone(this.currentState) : undefined; } /** * Get the last sent state */ public getLastSentState(): unknown | undefined { return this.lastSentState ? this.deepClone(this.lastSentState) : undefined; } /** * Update last sent state */ public updateLastSentState(state: unknown): void { this.lastSentState = this.deepClone(state); } /** * Get current sequence number */ public getSequence(): number { return this.sequence; } /** * Get last received sequence number */ public getLastReceivedSequence(): number { return this.lastReceivedSequence; } /** * Update last received sequence */ public updateLastReceivedSequence(sequence: number): void { this.lastReceivedSequence = sequence; } /** * Reset state */ public reset(): void { this.currentState = undefined; this.lastSentState = undefined; this.sequence = 0; this.lastReceivedSequence = -1; } /** * Check if initialized */ public isInitialized(): boolean { return this.currentState !== undefined; } /** * Compute delta between two states */ public computeDelta( current: unknown, previous: unknown ): StateDelta { const delta: Record = {}; const changedPaths: string[] = []; let hasChanges = false; const compareAndAdd = (curr: unknown, prev: unknown, path: string) => { if (curr !== prev) { hasChanges = true; changedPaths.push(path); this.setNestedValue(delta, path, curr); } }; this.traverseObject(current, previous, "", compareAndAdd); return { hasChanges, delta, changedPaths }; } /** * Deep clone an object */ private deepClone(obj: U): U { if (obj === null || typeof obj !== "object") return obj; if (obj instanceof Date) return new Date(obj.getTime()) as unknown as U; if (Array.isArray(obj)) { const clonedArr: unknown[] = []; for (const item of obj) { clonedArr.push(this.deepClone(item)); } return clonedArr as unknown as U; } if (obj instanceof Object) { const clonedObj: Record = {}; for (const key in obj) { if (Object.hasOwn(obj, key)) { clonedObj[key] = this.deepClone(obj[key]); } } return clonedObj as U; } return obj; } /** * Traverse object recursively */ private traverseObject( current: unknown, previous: unknown, path: string, callback: (curr: unknown, prev: unknown, path: string) => void ): void { if (typeof current !== "object" || current === null) { callback(current, previous, path); return; } if (Array.isArray(current)) { const prevArray = Array.isArray(previous) ? previous : []; for (let i = 0; i < current.length; i++) { this.traverseObject( current[i], prevArray[i], path ? `${path}[${i}]` : `[${i}]`, callback ); } } else { const prevObj = typeof previous === "object" && previous !== null ? previous : {}; for (const key in current) { if (Object.hasOwn(current, key)) { this.traverseObject( (current as Record)[key], (prevObj as Record)[key], path ? `${path}.${key}` : key, callback ); } } } } /** * Set nested value in object using path */ private setNestedValue(obj: unknown, path: string, value: unknown): void { const keys = path.split(/\.|\[|\]/).filter((k) => k); let current = obj as Record; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key) continue; if (!(key in current)) { const nextKey = keys[i + 1]; current[key] = nextKey && /^\d+$/.test(nextKey) ? [] : {}; } current = current[key] as Record; } const lastKey = keys[keys.length - 1]; if (lastKey) { current[lastKey] = value; } } }