Initial Commit
This commit is contained in:
184
packages/foundation/bitpacker/bitpacker-game/src/game.test.ts
Normal file
184
packages/foundation/bitpacker/bitpacker-game/src/game.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user