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

forked from benjivers/val

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

Keyboard shortcuts