ai
This commit is contained in:
parent
b6db7e8dc7
commit
d9b80b92ca
22 changed files with 563 additions and 62 deletions
96
test/prediction-reconciliation.test.ts
Normal file
96
test/prediction-reconciliation.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Reconciliation guard for the REAL client predictor. It exercises
|
||||
// LocalCharacterPredictor end-to-end with an injected clock, verifying the two
|
||||
// properties reconciliation depends on:
|
||||
// 1. a fully-acknowledged snapshot reproduces the authoritative pose exactly
|
||||
// (no spurious drift on top of server truth), and
|
||||
// 2. un-acknowledged input is replayed forward deterministically.
|
||||
// This is the net for prediction-touching changes (the death-while-dead fix,
|
||||
// future netcode work) — a regression shows up as drift or non-determinism.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
LocalCharacterPredictor,
|
||||
setPredictorClockForTesting,
|
||||
} from '../frontend/src/scripts/helper/prediction/local-character-predictor';
|
||||
|
||||
const HEAD_RADIUS = 50;
|
||||
const FEET_RADIUS = 20;
|
||||
|
||||
// A Circle-shaped pose; the predictor only reads .center / .radius, so plain
|
||||
// objects with array centres are sufficient (and avoid importing gl-matrix).
|
||||
const poseAt = (cx: number, cy: number) => ({
|
||||
head: { center: [cx, cy + 37], radius: HEAD_RADIUS },
|
||||
leftFoot: { center: [cx - 33, cy - 18], radius: FEET_RADIUS },
|
||||
rightFoot: { center: [cx + 33, cy - 18], radius: FEET_RADIUS },
|
||||
});
|
||||
|
||||
describe('local prediction reconciliation', () => {
|
||||
it('reproduces the authoritative pose exactly when all input is acknowledged', () => {
|
||||
let clock = 1000;
|
||||
setPredictorClockForTesting(() => clock);
|
||||
|
||||
const predictor = new LocalCharacterPredictor();
|
||||
const auth = poseAt(500, 500);
|
||||
|
||||
const t = predictor.recordInput([1, 0]);
|
||||
predictor.acknowledge(t, [0, 0], -Infinity); // server has consumed this input
|
||||
predictor.setStrength(80);
|
||||
predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never);
|
||||
|
||||
// No clock advance → zero replay window → predicted pose == authoritative.
|
||||
const used = predictor.update([], 1 / 60);
|
||||
|
||||
expect(used).toBe(true);
|
||||
expect(predictor.head.center[0]).toBeCloseTo(auth.head.center[0], 5);
|
||||
expect(predictor.head.center[1]).toBeCloseTo(auth.head.center[1], 5);
|
||||
expect(predictor.leftFoot.center[0]).toBeCloseTo(auth.leftFoot.center[0], 5);
|
||||
expect(predictor.rightFoot.center[0]).toBeCloseTo(auth.rightFoot.center[0], 5);
|
||||
});
|
||||
|
||||
it('replays un-acknowledged input deterministically', () => {
|
||||
const drive = () => {
|
||||
let clock = 0;
|
||||
setPredictorClockForTesting(() => clock);
|
||||
|
||||
const predictor = new LocalCharacterPredictor();
|
||||
clock = 1000;
|
||||
predictor.acknowledge(900, [0, 0], -Infinity); // baseline ack (older than the input below)
|
||||
predictor.setStrength(80);
|
||||
const auth = poseAt(0, 0);
|
||||
predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never);
|
||||
predictor.recordInput([1, 0]); // unacked rightward input at t=1000
|
||||
|
||||
clock = 1100; // replay ~100 ms forward
|
||||
predictor.update([], 1 / 60);
|
||||
return [
|
||||
predictor.head.center[0],
|
||||
predictor.head.center[1],
|
||||
predictor.leftFoot.center[0],
|
||||
predictor.rightFoot.center[0],
|
||||
];
|
||||
};
|
||||
|
||||
const a = drive();
|
||||
const b = drive();
|
||||
expect(a).toEqual(b); // deterministic replay
|
||||
expect(a.every((n) => Number.isFinite(n))).toBe(true);
|
||||
expect(a[0]).toBeGreaterThan(0.5); // rightward input actually moved the body
|
||||
});
|
||||
|
||||
it('suppresses prediction while the local player is dead', () => {
|
||||
let clock = 5000;
|
||||
setPredictorClockForTesting(() => clock);
|
||||
|
||||
const predictor = new LocalCharacterPredictor();
|
||||
const auth = poseAt(200, 200);
|
||||
const t = predictor.recordInput([1, 0]);
|
||||
predictor.acknowledge(t, [0, 0], -Infinity);
|
||||
predictor.setStrength(80);
|
||||
predictor.setAuthoritative(auth.head as never, auth.leftFoot as never, auth.rightFoot as never);
|
||||
predictor.setAlive(false); // dead, awaiting respawn
|
||||
|
||||
clock = 5200; // input + elapsed time that would otherwise be replayed forward
|
||||
// Even with a valid authoritative pose and pending input, a dead body must
|
||||
// not be predicted/moved — that was the "move while dead" bug.
|
||||
expect(predictor.update([], 1 / 60)).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue