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

forked from benjivers/val

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

Keyboard shortcuts