Inital commit
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
// Validates the client-side dead-reckoning: applies real server snapshots,
|
||||
// simulates at 60 fps like the browser, and measures prediction error
|
||||
// between the locally simulated head and the server's authoritative head.
|
||||
import WebSocket from 'ws';
|
||||
import { applySnakeSnaps, simulateSnakes, State } from '../public/js/client/src/state.js';
|
||||
|
||||
const state = new State();
|
||||
const ws = new WebSocket('ws://localhost:8080/ws');
|
||||
let myAngle = 0.7;
|
||||
let myBoost = false;
|
||||
let samples = 0, sumErr = 0, maxErr = 0, resets = 0;
|
||||
|
||||
ws.on('open', () => ws.send(JSON.stringify({ t: 'join', name: 'SimTest' })));
|
||||
ws.on('message', (d) => {
|
||||
const m = JSON.parse(d);
|
||||
if (m.t === 'init') { state.myId = m.id; state.arena = m.arena; return; }
|
||||
if (m.t !== 'snap') return;
|
||||
const before = state.snakes.get(state.myId);
|
||||
const prevHead = before ? [before.pts[0], before.pts[1]] : null;
|
||||
applySnakeSnaps(state, m.snakes);
|
||||
if (m.you) state.you = m.you;
|
||||
for (const ev of m.ev) if (ev.t === 'die' && ev.id === state.myId) ws.send(JSON.stringify({ t: 'respawn' }));
|
||||
const me = state.snakes.get(state.myId);
|
||||
const snap = m.snakes.find((s) => s.id === state.myId);
|
||||
if (me && snap) {
|
||||
if (prevHead && Math.hypot(prevHead[0] - snap.p[0], prevHead[1] - snap.p[1]) > 140) resets++;
|
||||
const err = Math.hypot(me.pts[0] - snap.p[0], me.pts[1] - snap.p[1]);
|
||||
sumErr += err; samples++;
|
||||
if (err > maxErr) maxErr = err;
|
||||
}
|
||||
});
|
||||
|
||||
// mimic the browser: input at ~33 Hz, simulation at 60 fps
|
||||
setInterval(() => {
|
||||
myAngle += (Math.random() - 0.5) * 0.3; // wander like a mouse would
|
||||
myBoost = Math.random() < 0.1; // sometimes boost
|
||||
const me = state.snakes.get(state.myId);
|
||||
if (me && Math.hypot(me.pts[0], me.pts[1]) > 3000) {
|
||||
myAngle = Math.atan2(-me.pts[1], -me.pts[0]); // steer back to safety
|
||||
myBoost = false;
|
||||
}
|
||||
ws.send(JSON.stringify({ t: 'in', a: myAngle, b: myBoost }));
|
||||
}, 30);
|
||||
setInterval(() => simulateSnakes(state, 1 / 60, myAngle, myBoost && state.you.score > 20), 1000 / 60);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`samples=${samples} avgErr=${(sumErr / Math.max(1, samples)).toFixed(2)}px maxErr=${maxErr.toFixed(2)}px hardResets=${resets}`);
|
||||
if (samples > 100 && sumErr / samples < 25 && resets === 0) {
|
||||
console.log('SIM TEST PASSED (prediction tracks server closely)');
|
||||
process.exit(0);
|
||||
}
|
||||
console.log('SIM TEST FAILED');
|
||||
process.exit(1);
|
||||
}, 20000);
|
||||
@@ -0,0 +1,67 @@
|
||||
// Smoke test: join the game, verify snapshots flow, movement works,
|
||||
// boosting drains score, and steering toward the border kills us.
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const ws = new WebSocket('ws://localhost:8080/ws');
|
||||
let init = null;
|
||||
let snaps = 0;
|
||||
let firstHead = null;
|
||||
let lastHead = null;
|
||||
let maxScore = 0;
|
||||
let died = false;
|
||||
let respawned = false;
|
||||
let phase = 0;
|
||||
|
||||
ws.on('open', () => ws.send(JSON.stringify({ t: 'join', name: 'SmokeTest' })));
|
||||
|
||||
ws.on('message', (d) => {
|
||||
const m = JSON.parse(d);
|
||||
if (m.t === 'init') {
|
||||
init = m;
|
||||
console.log(`[init] id=${m.id} arena=${m.arena} pellets=${m.pellets.length}`);
|
||||
ws.send(JSON.stringify({ t: 'in', a: 0, b: false }));
|
||||
return;
|
||||
}
|
||||
if (m.t !== 'snap') return;
|
||||
snaps++;
|
||||
|
||||
const me = m.snakes.find((s) => s.id === init.id);
|
||||
if (me) {
|
||||
lastHead = [me.p[0], me.p[1]];
|
||||
if (!firstHead) firstHead = [...lastHead];
|
||||
maxScore = Math.max(maxScore, m.you.score);
|
||||
}
|
||||
|
||||
if (snaps === 30) {
|
||||
const moved = Math.hypot(lastHead[0] - firstHead[0], lastHead[1] - firstHead[1]);
|
||||
console.log(`[move] head moved ${moved.toFixed(1)}px over ~2s (${m.snakes.length} snakes, lb=${m.lb.length}, total=${m.total})`);
|
||||
if (moved < 50) fail('snake did not move');
|
||||
// start boosting toward the border to test boost drain + border death
|
||||
phase = 1;
|
||||
ws.send(JSON.stringify({ t: 'in', a: Math.atan2(lastHead[1], lastHead[0]), b: true }));
|
||||
}
|
||||
|
||||
for (const ev of m.ev) {
|
||||
if (ev.t === 'die' && ev.id === init.id && !died) {
|
||||
died = true;
|
||||
console.log(`[death] died as expected (score=${ev.s}, pellets sample pts=${ev.p.length / 2})`);
|
||||
ws.send(JSON.stringify({ t: 'respawn' }));
|
||||
}
|
||||
}
|
||||
if (died && me && m.you.alive && !respawned) {
|
||||
respawned = true;
|
||||
console.log(`[respawn] instant respawn OK, alive with score=${m.you.score}`);
|
||||
console.log('SMOKE TEST PASSED');
|
||||
process.exit(0);
|
||||
}
|
||||
if (snaps === 150 && phase === 1) {
|
||||
console.log(`[boost] max score observed while boosting: ${maxScore}`);
|
||||
}
|
||||
});
|
||||
|
||||
function fail(msg) {
|
||||
console.error('SMOKE TEST FAILED: ' + msg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
setTimeout(() => fail(`timeout (snaps=${snaps}, died=${died}, respawned=${respawned})`), 90000);
|
||||
Reference in New Issue
Block a user