fable/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine

Check the drawing against the simulation's own motion

A body drawn at the server's speed takes even steps; the chase this replaced
took a long one just after each snapshot and shorter ones after it. The test
runs a real match at 30Hz, samples it the way a snapshot does, draws it at
60Hz and measures the spread of the per-frame steps: under 0.15 now, 0.22
with the old exponential smooth, which is the choppiness with a number on it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Wren committed 3c63a82 Browse files
1 file changed +65 −0
src/interp.test.ts +65
@@ -163,3 +163,68 @@ test("the moment being drawn is always bracketed by samples on hand", () => {
163163 expect(last.x - drawn.x).toBeGreaterThan(0);
164164 expect(last.x - drawn.x).toBeLessThan(20);
165165 });
166
167 // --------------------------------------------- against the real simulation
168
169 /**
170 * The match's own motion, sampled at the rate the server broadcasts it and
171 * drawn at the rate a browser draws: what a bot walking across the map
172 * actually looks like. The samples carry exactly what a snapshot carries —
173 * a position rounded to a tenth, thirty times a second.
174 *
175 * The measure is the spread of the per-frame steps. A body drawn at the
176 * server's speed takes even steps; the exponential chase this replaced took
177 * a long step just after each snapshot and shorter ones after it, which is
178 * choppiness with a number on it — twice this spread, measured the same way.
179 */
180 test("the simulation's own motion is drawn at an even speed", async () => {
181 const { runBots } = await import("./bots.ts");
182 const { newMatch, tick } = await import("./match.ts");
183 const round2 = (n: number) => Math.round(n * 10) / 10;
184
185 // A match of its own: nothing here touches the arena everybody shares.
186 const state = newMatch();
187 const step = () => {
188 runBots(state, 1 / 30);
189 tick(state, 1 / 30);
190 };
191 for (let i = 0; i < 30 * 40 && state.phase !== "live"; i++) step();
192 const stream: { t: number; bodies: { id: string; x: number; y: number; angle: number }[] }[] = [];
193 for (let i = 0; i < 60; i++) {
194 step();
195 stream.push({
196 t: i / 30,
197 bodies: state.players.map((p) => ({ id: p.id, x: round2(p.x), y: round2(p.y), angle: round2(p.angle) })),
198 });
199 }
200
201 const timeline = makeTimeline();
202 const paths = new Map<string, { x: number; y: number }[]>();
203 let next = 0;
204 for (let f = 0; f / 60 < 1.8; f++) {
205 const now = f / 60;
206 while (next < stream.length && stream[next]!.t <= now) {
207 arrive(timeline, stream[next]!.t);
208 for (const p of stream[next]!.bodies) feed(timeline, p.id, p, stream[next]!.t);
209 next += 1;
210 }
211 for (const p of stream[Math.max(0, next - 1)]!.bodies) {
212 const at = bodyAt(timeline, p, now - delay(timeline));
213 paths.set(p.id, [...(paths.get(p.id) ?? []), { x: at.x, y: at.y }]);
214 }
215 }
216
217 // Whoever walked furthest in that second and a half.
218 const walked = [...paths.values()]
219 .filter((path) => path.length > 80)
220 .map((path) => path.slice(1).map((at, i) => Math.hypot(at.x - path[i]!.x, at.y - path[i]!.y)))
221 .map((steps) => steps.filter((step) => step > 0.001))
222 .sort((a, b) => b.reduce((s, x) => s + x, 0) - a.reduce((s, x) => s + x, 0))[0]!;
223 expect(walked.length).toBeGreaterThan(30);
224 const mean = walked.reduce((s, x) => s + x, 0) / walked.length;
225 const spread = Math.sqrt(walked.reduce((s, x) => s + (x - mean) ** 2, 0) / walked.length) / mean;
226 // A body moving at the server's own speed: about four units every sixtieth
227 // of a second, which is the run speed.
228 expect(mean * 60).toBeGreaterThan(150);
229 expect(spread).toBeLessThan(0.15);
230 });
↑↓ navigate ↵ open esc close

Keyboard shortcuts