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

forked from benjivers/val

workbenchthis machine
Files d7211248ad6d576d3c93536129a0d746e3c90526
1
/**
2
 * A tile grid and the questions everything else asks of it: which tiles are
3
 * solid, where a circle slides to, whether a wall stands between two points,
4
 * where a ray stops, and how far a walk is.
5
 *
6
 * The grid is rows of letters. `#` is a wall; every other letter is floor,
7
 * and what a letter means beyond that is the game's business — this module
8
 * hands back the cells of a letter and never asks what they are for.
9
 *
10
 * `makeGrid` binds the maths to one set of rows and one tile size. The
11
 * collision itself is `glide.ts`, which the browser runs too; the walk
12
 * distances are cached per goal for the round and cleared by the caller.
13
 */
14
15
import { circleBlocked, slideCircle } from "./glide.ts";
16
17
export type Point = { x: number; y: number };
18
19
export type Grid = {
20
  lines: string[];
21
  cols: number;
22
  rows: number;
23
  tile: number;
24
  width: number;
25
  height: number;
26
  /** The letter at a tile; anything off the grid is a wall. */
27
  at: (col: number, row: number) => string;
28
  solidTile: (col: number, row: number) => boolean;
29
  tileOf: (x: number, y: number) => { col: number; row: number };
30
  centreOf: (col: number, row: number) => Point;
31
  solidAt: (x: number, y: number) => boolean;
32
  /** The letter under a point. */
33
  cellAt: (x: number, y: number) => string;
34
  /** The centres of every tile carrying this letter, row by row. */
35
  cellsOf: (mark: string) => Point[];
36
  /** Slide a circle through this grid. */
37
  moveCircle: (x: number, y: number, dx: number, dy: number, r: number) => Point;
38
  /** True when a circle at (x,y) overlaps any solid tile. */
39
  blockedCircle: (x: number, y: number, r: number) => boolean;
40
  wallBetween: (a: Point, b: Point) => boolean;
41
  rayHitWall: (from: Point, angle: number, range: number) => Point;
42
  walkDistance: (from: Point, to: Point) => number;
43
  pathStep: (from: Point, to: Point) => Point;
44
  clearPaths: () => void;
45
};
46
47
/** Closest approach of a segment to a point, as distance and travel along it. */
48
export function segmentPoint(a: Point, b: Point, p: Point): { dist: number; travel: number } {
49
  const dx = b.x - a.x;
50
  const dy = b.y - a.y;
51
  const len2 = dx * dx + dy * dy;
52
  const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2));
53
  const cx = a.x + dx * t;
54
  const cy = a.y + dy * t;
55
  return { dist: Math.hypot(p.x - cx, p.y - cy), travel: t * Math.sqrt(len2) };
56
}
57
58
export const segmentHitsCircle = (a: Point, b: Point, c: Point, r: number): boolean =>
59
  segmentPoint(a, b, c).dist <= r;
60
61
const STEPS = [
62
  [1, 0],
63
  [-1, 0],
64
  [0, 1],
65
  [0, -1],
66
] as const;
67
68
export function makeGrid(lines: string[], tile: number): Grid {
69
  const cols = lines[0]!.length;
70
  const rows = lines.length;
71
72
  const at = (col: number, row: number): string =>
73
    row < 0 || row >= rows || col < 0 || col >= cols ? "#" : lines[row]![col]!;
74
  const solidTile = (col: number, row: number): boolean => at(col, row) === "#";
75
  const tileOf = (x: number, y: number): { col: number; row: number } => ({
76
    col: Math.floor(x / tile),
77
    row: Math.floor(y / tile),
78
  });
79
  const centreOf = (col: number, row: number): Point => ({
80
    x: col * tile + tile / 2,
81
    y: row * tile + tile / 2,
82
  });
83
  const solidAt = (x: number, y: number): boolean => {
84
    const t = tileOf(x, y);
85
    return solidTile(t.col, t.row);
86
  };
87
  const cellAt = (x: number, y: number): string => {
88
    const t = tileOf(x, y);
89
    return at(t.col, t.row);
90
  };
91
  const cellsOf = (mark: string): Point[] => {
92
    const out: Point[] = [];
93
    for (let row = 0; row < rows; row++)
94
      for (let col = 0; col < cols; col++) if (at(col, row) === mark) out.push(centreOf(col, row));
95
    return out;
96
  };
97
98
  const moveCircle = (x: number, y: number, dx: number, dy: number, r: number): Point =>
99
    slideCircle(x, y, dx, dy, r, solidTile, tile);
100
  const blockedCircle = (x: number, y: number, r: number): boolean => circleBlocked(x, y, r, solidTile, tile);
101
102
  /**
103
   * Does a wall stand between two points? Sampled every third of a tile: a
104
   * wall is a whole tile thick, so nothing is ever stepped over.
105
   */
106
  function wallBetween(a: Point, b: Point): boolean {
107
    const dx = b.x - a.x;
108
    const dy = b.y - a.y;
109
    const dist = Math.hypot(dx, dy);
110
    const steps = Math.ceil(dist / (tile / 3));
111
    for (let i = 1; i < steps; i++) {
112
      const t = i / steps;
113
      if (solidAt(a.x + dx * t, a.y + dy * t)) return true;
114
    }
115
    return false;
116
  }
117
118
  /** Where a shot stops: the first wall along the ray, or its full range. */
119
  function rayHitWall(from: Point, angle: number, range: number): Point {
120
    const dx = Math.cos(angle);
121
    const dy = Math.sin(angle);
122
    const step = tile / 4;
123
    for (let d = step; d <= range; d += step) {
124
      const x = from.x + dx * d;
125
      const y = from.y + dy * d;
126
      if (solidAt(x, y)) return { x: from.x + dx * (d - step), y: from.y + dy * (d - step) };
127
    }
128
    return { x: from.x + dx * range, y: from.y + dy * range };
129
  }
130
131
  // Walk distance from a goal tile, breadth-first over floor. Bots ask for it
132
  // every few ticks; the field for a goal is built once and kept for the round.
133
  const fields = new Map<number, Int32Array>();
134
  const clearPaths = (): void => void fields.clear();
135
136
  function field(goalCol: number, goalRow: number): Int32Array {
137
    const key = goalRow * cols + goalCol;
138
    const cached = fields.get(key);
139
    if (cached) return cached;
140
    const dist = new Int32Array(cols * rows).fill(-1);
141
    if (!solidTile(goalCol, goalRow)) {
142
      dist[key] = 0;
143
      const queue: number[] = [key];
144
      for (let head = 0; head < queue.length; head++) {
145
        const here = queue[head]!;
146
        const col = here % cols;
147
        const row = (here - col) / cols;
148
        const d = dist[here]!;
149
        for (const [dc, dr] of STEPS) {
150
          const nc = col + dc;
151
          const nr = row + dr;
152
          if (nc < 0 || nc >= cols || nr < 0 || nr >= rows) continue;
153
          const nk = nr * cols + nc;
154
          if (dist[nk] !== -1 || solidTile(nc, nr)) continue;
155
          dist[nk] = d + 1;
156
          queue.push(nk);
157
        }
158
      }
159
    }
160
    if (fields.size > 64) fields.clear();
161
    fields.set(key, dist);
162
    return dist;
163
  }
164
165
  /** Walk distance in tiles between two points, or -1 when unreachable. */
166
  function walkDistance(from: Point, to: Point): number {
167
    const f = tileOf(from.x, from.y);
168
    const g = tileOf(to.x, to.y);
169
    return field(g.col, g.row)[f.row * cols + f.col] ?? -1;
170
  }
171
172
  /**
173
   * The next point to walk toward on the way to a goal: the centre of the
174
   * neighbouring tile that is one step closer, so a walker rounds corners
175
   * instead of grinding into them.
176
   */
177
  function pathStep(from: Point, to: Point): Point {
178
    const f = tileOf(from.x, from.y);
179
    const g = tileOf(to.x, to.y);
180
    if (f.col === g.col && f.row === g.row) return to;
181
    const dist = field(g.col, g.row);
182
    const here = dist[f.row * cols + f.col] ?? -1;
183
    if (here <= 0) return to;
184
    let best: Point | null = null;
185
    let bestD = here;
186
    for (const [dc, dr] of STEPS) {
187
      const nc = f.col + dc;
188
      const nr = f.row + dr;
189
      if (nc < 0 || nc >= cols || nr < 0 || nr >= rows) continue;
190
      const d = dist[nr * cols + nc] ?? -1;
191
      if (d !== -1 && d < bestD) {
192
        bestD = d;
193
        best = centreOf(nc, nr);
194
      }
195
    }
196
    // Straight to the goal when nothing stands in the way — smoother than
197
    // walking tile centre to tile centre down an open corridor.
198
    if (best && !wallBetween(from, to) && (walkDistance(from, to) ?? 99) < 6) return to;
199
    return best ?? to;
200
  }
201
202
  return {
203
    lines,
204
    cols,
205
    rows,
206
    tile,
207
    width: cols * tile,
208
    height: rows * tile,
209
    at,
210
    solidTile,
211
    tileOf,
212
    centreOf,
213
    solidAt,
214
    cellAt,
215
    cellsOf,
216
    moveCircle,
217
    blockedCircle,
218
    wallBetween,
219
    rayHitWall,
220
    walkDistance,
221
    pathStep,
222
    clearPaths,
223
  };
224
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts