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

forked from benjivers/val

workbenchthis machine
Files edbb432768e43870332e96f00764183b016fcfde
265 lines · 8.9 KB · ~2.6k tok Raw History
1
/**
2
 * The map: one grid, and the geometry everything else asks questions of.
3
 *
4
 * Two sites (A left, B right), a mid corridor between the spawns, and a back
5
 * corridor along the bottom that links A main, spawn and B main — the flank.
6
 * Tiles are 80 units; a player is a circle of radius 16, so a one-tile
7
 * corridor is comfortably wide and a wall is never thin enough for a ray to
8
 * step over.
9
 *
10
 * Everything here is a pure function of the grid: collision, line of sight
11
 * and the walk distance a bot uses to path. No state.
12
 */
13
14
export const TILE = 80;
15
16
// '#' wall · '.' floor · 'A'/'B' plant site · 'a' attacker spawn · 'd' defender spawn
17
const ROWS_SRC = [
18
  "##############################",
19
  "#........###.dddd.###........#",
20
  "#.AAAAA..###.dddd.###..BBBBB.#",
21
  "#.AAAAA..##........##..BBBBB.#",
22
  "#.AAAAA..##.#....#.##..BBBBB.#",
23
  "#.AAAAA..##........##..BBBBB.#",
24
  "#...##......#....#......##...#",
25
  "#..####....#......#....####..#",
26
  "#..####..###......###..####..#",
27
  "#..####..####....####..####..#",
28
  "#..####..####.##.####..####..#",
29
  "#........####.##.####........#",
30
  "#..####..####....####..####..#",
31
  "#..####..#####..#####..####..#",
32
  "#..####..#####..#####..####..#",
33
  "#..####..###......###..####..#",
34
  "#........##..aaaa..##........#",
35
  "#............aaaa............#",
36
  "##############################",
37
];
38
39
export const COLS = ROWS_SRC[0]!.length;
40
export const ROWS = ROWS_SRC.length;
41
export const WIDTH = COLS * TILE;
42
export const HEIGHT = ROWS * TILE;
43
44
export const grid: string[] = ROWS_SRC;
45
export const gridRows = (): string[] => ROWS_SRC.slice();
46
47
export type Site = "A" | "B";
48
export type Point = { x: number; y: number };
49
50
const at = (col: number, row: number): string =>
51
  row < 0 || row >= ROWS || col < 0 || col >= COLS ? "#" : ROWS_SRC[row]![col]!;
52
53
export const solidTile = (col: number, row: number): boolean => at(col, row) === "#";
54
55
export const tileOf = (x: number, y: number): { col: number; row: number } => ({
56
  col: Math.floor(x / TILE),
57
  row: Math.floor(y / TILE),
58
});
59
60
export const centreOf = (col: number, row: number): Point => ({
61
  x: col * TILE + TILE / 2,
62
  y: row * TILE + TILE / 2,
63
});
64
65
export const solidAt = (x: number, y: number): boolean => {
66
  const t = tileOf(x, y);
67
  return solidTile(t.col, t.row);
68
};
69
70
/** The site a point plants on, or null: the spike only arms inside a site. */
71
export const siteAt = (x: number, y: number): Site | null => {
72
  const t = tileOf(x, y);
73
  const c = at(t.col, t.row);
74
  return c === "A" || c === "B" ? c : null;
75
};
76
77
const cellsOf = (mark: string): Point[] => {
78
  const out: Point[] = [];
79
  for (let row = 0; row < ROWS; row++)
80
    for (let col = 0; col < COLS; col++) if (at(col, row) === mark) out.push(centreOf(col, row));
81
  return out;
82
};
83
84
export const spawnCells = (side: "atk" | "def"): Point[] => cellsOf(side === "atk" ? "a" : "d");
85
export const siteCells = (site: Site): Point[] => cellsOf(site);
86
87
export const siteCentre = (site: Site): Point => {
88
  const cells = siteCells(site);
89
  const sum = cells.reduce((a, p) => ({ x: a.x + p.x, y: a.y + p.y }), { x: 0, y: 0 });
90
  return { x: sum.x / cells.length, y: sum.y / cells.length };
91
};
92
93
/** Ult orbs: one on each half of mid, free for whoever walks over them. */
94
export const orbSpots = (): Point[] => [centreOf(4, 11), centreOf(25, 11)];
95
96
/**
97
 * Slide a circle through the grid: each axis is moved and resolved on its
98
 * own, so running along a wall slides instead of sticking.
99
 */
100
export function moveCircle(x: number, y: number, dx: number, dy: number, r: number): Point {
101
  let nx = x;
102
  let ny = y;
103
  if (dx !== 0) {
104
    const test = nx + dx;
105
    if (!blockedCircle(test, ny, r)) nx = test;
106
    else {
107
      const col = Math.floor(test / TILE);
108
      nx = dx > 0 ? col * TILE - r - 0.01 : (col + 1) * TILE + r + 0.01;
109
      if (blockedCircle(nx, ny, r)) nx = x;
110
    }
111
  }
112
  if (dy !== 0) {
113
    const test = ny + dy;
114
    if (!blockedCircle(nx, test, r)) ny = test;
115
    else {
116
      const row = Math.floor(test / TILE);
117
      ny = dy > 0 ? row * TILE - r - 0.01 : (row + 1) * TILE + r + 0.01;
118
      if (blockedCircle(nx, ny, r)) ny = y;
119
    }
120
  }
121
  return { x: nx, y: ny };
122
}
123
124
/** True when a circle at (x,y) overlaps any solid tile. */
125
export function blockedCircle(x: number, y: number, r: number): boolean {
126
  const c0 = Math.floor((x - r) / TILE);
127
  const c1 = Math.floor((x + r) / TILE);
128
  const r0 = Math.floor((y - r) / TILE);
129
  const r1 = Math.floor((y + r) / TILE);
130
  for (let row = r0; row <= r1; row++)
131
    for (let col = c0; col <= c1; col++) {
132
      if (!solidTile(col, row)) continue;
133
      const nearX = Math.max(col * TILE, Math.min(x, col * TILE + TILE));
134
      const nearY = Math.max(row * TILE, Math.min(y, row * TILE + TILE));
135
      const dx = x - nearX;
136
      const dy = y - nearY;
137
      if (dx * dx + dy * dy < r * r) return true;
138
    }
139
  return false;
140
}
141
142
/**
143
 * Does a wall stand between two points? Sampled every third of a tile: a
144
 * wall is a whole tile thick, so nothing is ever stepped over.
145
 */
146
export function wallBetween(a: Point, b: Point): boolean {
147
  const dx = b.x - a.x;
148
  const dy = b.y - a.y;
149
  const dist = Math.hypot(dx, dy);
150
  const steps = Math.ceil(dist / (TILE / 3));
151
  for (let i = 1; i < steps; i++) {
152
    const t = i / steps;
153
    if (solidAt(a.x + dx * t, a.y + dy * t)) return true;
154
  }
155
  return false;
156
}
157
158
/** Where a shot stops: the first wall along the ray, or its full range. */
159
export function rayHitWall(from: Point, angle: number, range: number): Point {
160
  const dx = Math.cos(angle);
161
  const dy = Math.sin(angle);
162
  const step = TILE / 4;
163
  for (let d = step; d <= range; d += step) {
164
    const x = from.x + dx * d;
165
    const y = from.y + dy * d;
166
    if (solidAt(x, y)) return { x: from.x + dx * (d - step), y: from.y + dy * (d - step) };
167
  }
168
  return { x: from.x + dx * range, y: from.y + dy * range };
169
}
170
171
/** Closest approach of a segment to a point, as distance and travel along it. */
172
export function segmentPoint(a: Point, b: Point, p: Point): { dist: number; travel: number } {
173
  const dx = b.x - a.x;
174
  const dy = b.y - a.y;
175
  const len2 = dx * dx + dy * dy;
176
  const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
177
  const cx = a.x + dx * t;
178
  const cy = a.y + dy * t;
179
  return { dist: Math.hypot(p.x - cx, p.y - cy), travel: t * Math.sqrt(len2) };
180
}
181
182
export const segmentHitsCircle = (a: Point, b: Point, c: Point, r: number): boolean =>
183
  segmentPoint(a, b, c).dist <= r;
184
185
// Walk distance from a goal tile, breadth-first over floor. Bots ask for it
186
// every few ticks; the field for a goal is built once and kept for the round.
187
const fields = new Map<number, Int32Array>();
188
189
export const clearPaths = (): void => void fields.clear();
190
191
function field(goalCol: number, goalRow: number): Int32Array {
192
  const key = goalRow * COLS + goalCol;
193
  const cached = fields.get(key);
194
  if (cached) return cached;
195
  const dist = new Int32Array(COLS * ROWS).fill(-1);
196
  if (!solidTile(goalCol, goalRow)) {
197
    dist[key] = 0;
198
    const queue: number[] = [key];
199
    for (let head = 0; head < queue.length; head++) {
200
      const here = queue[head]!;
201
      const col = here % COLS;
202
      const row = (here - col) / COLS;
203
      const d = dist[here]!;
204
      for (const [dc, dr] of [
205
        [1, 0],
206
        [-1, 0],
207
        [0, 1],
208
        [0, -1],
209
      ] as const) {
210
        const nc = col + dc;
211
        const nr = row + dr;
212
        if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
213
        const nk = nr * COLS + nc;
214
        if (dist[nk] !== -1 || solidTile(nc, nr)) continue;
215
        dist[nk] = d + 1;
216
        queue.push(nk);
217
      }
218
    }
219
  }
220
  if (fields.size > 64) fields.clear();
221
  fields.set(key, dist);
222
  return dist;
223
}
224
225
/** Walk distance in tiles between two points, or -1 when unreachable. */
226
export function walkDistance(from: Point, to: Point): number {
227
  const f = tileOf(from.x, from.y);
228
  const g = tileOf(to.x, to.y);
229
  return field(g.col, g.row)[f.row * COLS + f.col] ?? -1;
230
}
231
232
/**
233
 * The next point to walk toward on the way to a goal: the centre of the
234
 * neighbouring tile that is one step closer, so bots round corners instead
235
 * of grinding into them.
236
 */
237
export function pathStep(from: Point, to: Point): Point {
238
  const f = tileOf(from.x, from.y);
239
  const g = tileOf(to.x, to.y);
240
  if (f.col === g.col && f.row === g.row) return to;
241
  const dist = field(g.col, g.row);
242
  const here = dist[f.row * COLS + f.col] ?? -1;
243
  if (here <= 0) return to;
244
  let best: Point | null = null;
245
  let bestD = here;
246
  for (const [dc, dr] of [
247
    [1, 0],
248
    [-1, 0],
249
    [0, 1],
250
    [0, -1],
251
  ] as const) {
252
    const nc = f.col + dc;
253
    const nr = f.row + dr;
254
    if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
255
    const d = dist[nr * COLS + nc] ?? -1;
256
    if (d !== -1 && d < bestD) {
257
      bestD = d;
258
      best = centreOf(nc, nr);
259
    }
260
  }
261
  // Straight to the goal when nothing stands in the way — smoother than
262
  // walking tile centre to tile centre down an open corridor.
263
  if (best && !wallBetween(from, to) && (walkDistance(from, to) ?? 99) < 6) return to;
264
  return best ?? to;
265
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts