Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
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);
});
});