import { describe, expect, test } from "@suaveplan/testing/runner"; import { calculateVelocity, dequantizeVector, GameInterpolator, GameStateManager, PositionPredictor, PredictionAlgorithm, quantizeVector, RotationPredictor, StateReconciliation, } from "./index.js"; describe("GameStateManager", () => { test("pushes and retrieves states", () => { const manager = new GameStateManager(); manager.pushState({ version: 1, entities: {} }); expect(manager.getStateCount()).toBe(1); expect(manager.getLatestState()?.version).toBe(1); expect(manager.getStateAt(0)?.version).toBe(1); }); test("getStateAt returns undefined for out of bounds", () => { const manager = new GameStateManager(); expect(manager.getStateAt(0)).toBeUndefined(); }); test("getLatestState returns undefined when empty", () => { const manager = new GameStateManager(); expect(manager.getLatestState()).toBeUndefined(); }); }); describe("quantize/dequantize", () => { test("roundtrips a vector", () => { const v = { x: 0.5, y: -0.25, z: 0.75 }; const q = quantizeVector(v, 16); const d = dequantizeVector(q, 16); expect(d.x).toBeCloseTo(v.x, 1); expect(d.y).toBeCloseTo(v.y, 1); expect(d.z).toBeCloseTo(v.z, 1); }); test("handles vector without z component", () => { const v = { x: 0.5, y: -0.25 }; const q = quantizeVector(v, 8); expect(q.z).toBeUndefined(); const d = dequantizeVector(q, 8); expect(d.x).toBeCloseTo(v.x, 1); expect(d.y).toBeCloseTo(v.y, 1); expect(d.z).toBeUndefined(); }); }); describe("PositionPredictor", () => { test("needs at least 2 points to predict", () => { const predictor = new PositionPredictor(); predictor.addPosition({ x: 0, y: 0, z: 0 }, 0); expect(predictor.predictPosition(100)).toBeNull(); }); test("linear prediction works", () => { const predictor = new PositionPredictor({ algorithm: PredictionAlgorithm.LINEAR, }); predictor.addPosition({ x: 0, y: 0, z: 0 }, 0); predictor.addPosition({ x: 10, y: 0, z: 0 }, 1000); const pred = predictor.predictPosition(100); expect(pred).not.toBeNull(); expect(pred!.x).toBeGreaterThan(10); }); test("clear history resets predictor", () => { const predictor = new PositionPredictor(); predictor.addPosition({ x: 0, y: 0, z: 0 }, 0); predictor.addPosition({ x: 10, y: 0, z: 0 }, 1000); predictor.clearHistory(); expect(predictor.predictPosition(100)).toBeNull(); }); }); describe("RotationPredictor", () => { test("predicts rotation", () => { const predictor = new RotationPredictor(); predictor.addRotation({ yaw: 0, pitch: 0 }, 0); predictor.addRotation({ yaw: 10, pitch: 5 }, 1000); const pred = predictor.predictRotation(100); expect(pred).not.toBeNull(); expect(pred!.yaw).toBeGreaterThan(0); }); }); describe("GameInterpolator", () => { test("linear position interpolation", () => { const interp = new GameInterpolator(); const result = interp.interpolatePosition( { x: 0, y: 0, z: 0 }, { x: 10, y: 20, z: 30 }, 0.5 ); expect(result.x).toBe(5); expect(result.y).toBe(10); expect(result.z).toBe(15); }); test("rotation interpolation handles angle wrapping", () => { const interp = new GameInterpolator(); const result = interp.interpolateRotation( { yaw: 170, pitch: 0 }, { yaw: -170, pitch: 0 }, 0.5 ); expect(Math.abs(result.yaw)).toBeLessThanOrEqual(180); }); test("player state interpolation", () => { const interp = new GameInterpolator(); const start = { position: { x: 0, y: 0, z: 0 }, rotation: { yaw: 0, pitch: 0 }, stats: { health: 100, shield: 50, speed: 5, isMoving: false, isSprinting: false, }, }; const end = { position: { x: 10, y: 0, z: 0 }, rotation: { yaw: 90, pitch: 0 }, stats: { health: 80, shield: 40, speed: 10, isMoving: true, isSprinting: true, }, }; const result = interp.interpolatePlayerState(start, end, 0.5); expect(result.position.x).toBe(5); expect(result.stats.health).toBe(90); }); }); describe("StateReconciliation", () => { test("processes inputs", () => { const sr = new StateReconciliation(); const seq = sr.processInput( { moveX: 1, moveY: 0, rotateYaw: 0, rotatePitch: 0, actions: [] }, 0 ); expect(seq).toBe(0); expect(sr.getClientSequence()).toBe(0); }); test("ping/pong tracking", () => { const sr = new StateReconciliation(); sr.sendPing(); sr.receivePong(); expect(sr.getAveragePing()).toBeGreaterThanOrEqual(0); }); test("reset clears state", () => { const sr = new StateReconciliation(); sr.processInput( { moveX: 1, moveY: 0, rotateYaw: 0, rotatePitch: 0, actions: [] }, 0 ); sr.reset(); expect(sr.getClientSequence()).toBe(-1); }); }); describe("calculateVelocity", () => { test("calculates velocity from history", () => { const history = [ { value: { x: 0, y: 0, z: 0 }, timestamp: 0 }, { value: { x: 10, y: 0, z: 0 }, timestamp: 1000 }, ]; const vel = calculateVelocity(history); expect(vel.x).toBe(10); expect(vel.y).toBe(0); }); test("returns zero for single point", () => { const history = [{ value: { x: 5, y: 0, z: 0 }, timestamp: 0 }]; const vel = calculateVelocity(history); expect(vel.x).toBe(0); }); });