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

forked from benjivers/val

workbenchthis machine
Files d9114b2743b72a3c21d37c918b85c793a2cf7574
1
/**
2
 * The browser's guess about where you are, checked against the server's
3
 * answer: the same maths, the same wall, and a disagreement that closes.
4
 */
5
6
import { expect, test } from "bun:test";
7
import { BODY_R, moveSpeed } from "./glide.ts";
8
import { moveCircle, solidTile, TILE } from "./map.ts";
9
import { advance, anchor, holdBack, insideWall, LEAD_MAX, makeLocal, project, reconcile, SNAP } from "../static/predict.js";
10
11
// The world the browser builds out of map.json, which is the map the server
12
// plays on: the same rows, the same tile, the same body.
13
const world = { solid: solidTile, tile: TILE, r: BODY_R };
14
15
test("nothing is drawn until the server has said where you are", () => {
16
  const local = makeLocal();
17
  expect(local.ready).toBe(false);
18
  advance(local, 1, 0, 250, 1 / 60, world);
19
  expect(local).toMatchObject({ x: 0, y: 0 });
20
  reconcile(local, { x: 1200, y: 640 }, 1 / 60);
21
  expect(local).toMatchObject({ x: 1200, y: 640, ready: true });
22
});
23
24
test("a frame of movement is the speed it was given, in the direction it was given", () => {
25
  const local = anchor(makeLocal(), { x: 1200, y: 640 });
26
  advance(local, 1, 0, 250, 1 / 50, world);
27
  expect(local.x).toBeCloseTo(1205, 6);
28
  expect(local.y).toBeCloseTo(640, 6);
29
  // A diagonal is a direction, not a speed boost.
30
  const diagonal = anchor(makeLocal(), { x: 1200, y: 640 });
31
  advance(diagonal, 1, 1, 250, 1 / 50, world);
32
  expect(Math.hypot(diagonal.x - 1200, diagonal.y - 640)).toBeCloseTo(5, 6);
33
  // Standing still, planted, or stopped to defuse: nothing moves.
34
  const still = anchor(makeLocal(), { x: 1200, y: 640 });
35
  advance(still, 0, 0, 250, 1 / 50, world);
36
  advance(still, 1, 0, moveSpeed({ stopped: true }), 1 / 50, world);
37
  expect(still).toMatchObject({ x: 1200, y: 640 });
38
});
39
40
test("the guess resolves a wall exactly the way the server does", () => {
41
  // Every open tile of the real map, walked into from eight directions: the
42
  // browser's step and the server's land in the same place, or the camera
43
  // spends the round being dragged off whatever it guessed.
44
  const rows = 19;
45
  const cols = 30;
46
  let tried = 0;
47
  for (let row = 1; row < rows - 1; row++)
48
    for (let col = 1; col < cols - 1; col++) {
49
      if (solidTile(col, row)) continue;
50
      const x = col * TILE + TILE / 2;
51
      const y = row * TILE + TILE / 2;
52
      for (const [mx, my] of [
53
        [1, 0],
54
        [-1, 0],
55
        [0, 1],
56
        [0, -1],
57
        [0.7, 0.7],
58
        [-0.7, 0.7],
59
        [0.7, -0.7],
60
        [-0.7, -0.7],
61
      ]) {
62
        const local = anchor(makeLocal(), { x, y });
63
        advance(local, mx, my, 250, 1 / 15, world);
64
        const server = moveCircle(x, y, mx * 250 * (1 / 15), my * 250 * (1 / 15), BODY_R);
65
        expect(local.x).toBeCloseTo(server.x, 9);
66
        expect(local.y).toBeCloseTo(server.y, 9);
67
        tried += 1;
68
      }
69
    }
70
  expect(tried).toBeGreaterThan(1000);
71
});
72
73
test("a disagreement is leaned off, not jumped", () => {
74
  const local = anchor(makeLocal(), { x: 1200, y: 640 });
75
  reconcile(local, { x: 1210, y: 640 }, 1 / 60);
76
  expect(local.gap).toBeCloseTo(10, 6);
77
  // A frame moves a fraction of the way, never the whole way.
78
  expect(local.x).toBeGreaterThan(1200);
79
  expect(local.x).toBeLessThan(1203);
80
  // Held there, it closes.
81
  for (let i = 0; i < 60; i++) reconcile(local, { x: 1210, y: 640 }, 1 / 60);
82
  expect(local.x).toBeCloseTo(1210, 2);
83
});
84
85
test("a real disagreement is taken outright: the server is the answer", () => {
86
  const local = anchor(makeLocal(), { x: 1200, y: 640 });
87
  // A spawn, a teleport, a seat handed to somebody else mid-round.
88
  reconcile(local, { x: 1200 + SNAP + 1, y: 640 }, 1 / 60);
89
  expect(local.x).toBe(1200 + SNAP + 1);
90
  expect(local.gap).toBe(0);
91
});
92
93
test("a snapshot is carried forward by the input held since it was sent", () => {
94
  const server = { x: 1200, y: 640 };
95
  // Holding nothing, the answer is the answer: no lead, nothing invented.
96
  expect(project(server, 0, 0, 250, 0.1, world)).toEqual({ x: 1200, y: 640 });
97
  expect(project(server, 1, 0, 0, 0.1, world)).toEqual({ x: 1200, y: 640 });
98
  expect(project(server, 1, 0, 250, 0, world)).toEqual({ x: 1200, y: 640 });
99
  // Holding east for 66ms, it is 66ms of running east ahead.
100
  const ahead = project(server, 1, 0, 250, 1 / 15, world);
101
  expect(ahead.x - 1200).toBeCloseTo(250 / 15, 6);
102
  // A connection that stopped talking is not a licence to run off the map.
103
  const far = project(server, 1, 0, 250, 30, world);
104
  expect(far.x - 1200).toBeLessThanOrEqual(250 * LEAD_MAX);
105
  // And the carry is the server's own step-and-slide: never through a wall.
106
  for (let y = 100; y < 1400; y += 37) {
107
    const from = { x: 300, y };
108
    if (insideWall(from, world)) continue; // not a place anybody can stand
109
    expect(insideWall(project(from, -1, 0, 250, LEAD_MAX, world), world)).toBe(false);
110
  }
111
});
112
113
test("the guess is never drawn past the answer it is running toward", () => {
114
  const target = { x: 1300, y: 640 };
115
  // Ahead of the target along the way it is going: pulled back onto it.
116
  const past = anchor(makeLocal(), { x: 1320, y: 640 });
117
  holdBack(past, target, 1, 0);
118
  expect(past.x).toBeCloseTo(1300, 9);
119
  // Short of it, or across it, or standing still: left alone.
120
  const short = anchor(makeLocal(), { x: 1280, y: 648 });
121
  holdBack(short, target, 1, 0);
122
  expect(short).toMatchObject({ x: 1280, y: 648 });
123
  const still = anchor(makeLocal(), { x: 1320, y: 640 });
124
  holdBack(still, target, 0, 0);
125
  expect(still.x).toBe(1320);
126
});
127
128
test("a guess running with the server stays within a stride of it, and never inside a wall", () => {
129
  // Sixty frames a second guessing, fifteen a second corrected, walking east
130
  // into the wall that bounds mid. This is the case that used to skip.
131
  const speed = 250;
132
  const start = { x: 1200, y: 640 };
133
  let server = { ...start };
134
  const local = anchor(makeLocal(), start);
135
  let worst = 0;
136
  let back = 0;
137
  let previous = local.x;
138
  for (let frame = 0; frame < 240; frame++) {
139
    advance(local, 1, 0, speed, 1 / 60, world);
140
    // The server ticks at 30Hz and is heard from at 15Hz.
141
    if (frame % 2 === 0) server = moveCircle(server.x, server.y, speed / 30, 0, BODY_R);
142
    if (frame % 4 === 0) reconcile(local, server, 1 / 15);
143
    worst = Math.max(worst, Math.hypot(local.x - server.x, local.y - server.y));
144
    // The drawn body never walks backwards: that is what a skip looks like.
145
    back = Math.min(back, local.x - previous);
146
    previous = local.x;
147
    expect(insideWall(local, world)).toBe(false);
148
  }
149
  expect(worst).toBeLessThan(BODY_R);
150
  expect(back).toBeGreaterThan(-0.01);
151
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts