heron/lanternfall Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files 358a461a60846afbb01d02ab26ceea3edca770c7
197 lines · 6.4 KB · ~1.9k tok Raw History
1
/**
2
 * The dungeon itself: generated, never stored. Five floors, each a pure
3
 * function of the seed and the depth — so every delver walks the same rooms,
4
 * every corridor is where the last one left it, and the database only has to
5
 * remember what has *happened* in the dungeon, not what it looks like.
6
 *
7
 * Nothing in this file touches the database or the clock.
8
 */
9
10
export const WIDTH = 48;
11
export const HEIGHT = 24;
12
export const DEPTHS = 5;
13
/** How far a lantern throws, in tiles. */
14
export const LANTERN = 4;
15
16
export type Tile = "#" | "." | ">" | "<";
17
18
export interface Room {
19
  x: number;
20
  y: number;
21
  w: number;
22
  h: number;
23
}
24
25
export interface Floor {
26
  depth: number;
27
  tiles: Tile[];
28
  rooms: Room[];
29
  /** Where a delver arrives when coming down (or starting out). */
30
  entrance: number;
31
  /** The stair down; on the deepest floor there is none. */
32
  descent: number | undefined;
33
}
34
35
export const at = (x: number, y: number): number => y * WIDTH + x;
36
export const xy = (cell: number): { x: number; y: number } => ({
37
  x: cell % WIDTH,
38
  y: Math.floor(cell / WIDTH),
39
});
40
41
/** Deterministic noise: the same seed, depth and salt always give the same roll. */
42
function roll(seed: number, depth: number, salt: number): number {
43
  let h = (seed * 2654435761) ^ (depth * 40503) ^ (salt * 2246822519);
44
  h = Math.imul(h ^ (h >>> 15), 2246822519);
45
  h = Math.imul(h ^ (h >>> 13), 3266489917);
46
  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
47
}
48
49
const between = (r: number, lo: number, hi: number): number =>
50
  lo + Math.floor(r * (hi - lo + 1));
51
52
function carveRoom(tiles: Tile[], room: Room): void {
53
  for (let y = room.y; y < room.y + room.h; y += 1)
54
    for (let x = room.x; x < room.x + room.w; x += 1) tiles[at(x, y)] = ".";
55
}
56
57
function carveCorridor(tiles: Tile[], a: Room, b: Room, bendFirst: boolean): void {
58
  const ax = Math.floor(a.x + a.w / 2);
59
  const ay = Math.floor(a.y + a.h / 2);
60
  const bx = Math.floor(b.x + b.w / 2);
61
  const by = Math.floor(b.y + b.h / 2);
62
  const hor = (y: number, from: number, to: number): void => {
63
    for (let x = Math.min(from, to); x <= Math.max(from, to); x += 1)
64
      tiles[at(x, y)] = ".";
65
  };
66
  const ver = (x: number, from: number, to: number): void => {
67
    for (let y = Math.min(from, to); y <= Math.max(from, to); y += 1)
68
      tiles[at(x, y)] = ".";
69
  };
70
  if (bendFirst) {
71
    hor(ay, ax, bx);
72
    ver(bx, ay, by);
73
  } else {
74
    ver(ax, ay, by);
75
    hor(by, ax, bx);
76
  }
77
}
78
79
const overlaps = (a: Room, b: Room): boolean =>
80
  a.x < b.x + b.w + 1 &&
81
  a.x + a.w + 1 > b.x &&
82
  a.y < b.y + b.h + 1 &&
83
  a.y + a.h + 1 > b.y;
84
85
/** One floor, built the same way every time it is asked for. */
86
export function floorAt(depth: number, seed = 1848): Floor {
87
  const tiles: Tile[] = new Array(WIDTH * HEIGHT).fill("#");
88
  const rooms: Room[] = [];
89
  // Deeper floors are more broken up: fewer, smaller, further apart.
90
  const attempts = 60;
91
  const maxRooms = Math.max(5, 11 - depth);
92
  for (let i = 0; i < attempts && rooms.length < maxRooms; i += 1) {
93
    const w = between(roll(seed, depth, i * 7 + 1), 5, Math.max(6, 11 - depth));
94
    const h = between(roll(seed, depth, i * 7 + 2), 3, Math.max(4, 7 - Math.floor(depth / 2)));
95
    const room: Room = {
96
      x: between(roll(seed, depth, i * 7 + 3), 1, WIDTH - w - 2),
97
      y: between(roll(seed, depth, i * 7 + 4), 1, HEIGHT - h - 2),
98
      w,
99
      h,
100
    };
101
    if (rooms.some((other) => overlaps(room, other))) continue;
102
    rooms.push(room);
103
  }
104
  for (const room of rooms) carveRoom(tiles, room);
105
  for (let i = 1; i < rooms.length; i += 1)
106
    carveCorridor(tiles, rooms[i - 1] as Room, rooms[i] as Room, roll(seed, depth, i * 13) < 0.5);
107
108
  const first = rooms[0] as Room;
109
  const last = rooms[rooms.length - 1] as Room;
110
  const entrance = at(Math.floor(first.x + first.w / 2), Math.floor(first.y + first.h / 2));
111
  tiles[entrance] = "<";
112
  let descent: number | undefined;
113
  if (depth < DEPTHS) {
114
    descent = at(Math.floor(last.x + last.w / 2), Math.floor(last.y + last.h / 2));
115
    tiles[descent] = ">";
116
  }
117
  return { depth, tiles, rooms, entrance, descent };
118
}
119
120
export const walkable = (tile: Tile | undefined): boolean =>
121
  tile === "." || tile === ">" || tile === "<";
122
123
/** Floor tiles in reading order — what anything placed in the dungeon picks from. */
124
export function openCells(floor: Floor): number[] {
125
  const open: number[] = [];
126
  for (const [cell, tile] of floor.tiles.entries())
127
    if (tile === ".") open.push(cell);
128
  return open;
129
}
130
131
/**
132
 * Line of sight, Bresenham. A wall stops the light at the wall, so you see
133
 * the wall itself and nothing behind it — which is what makes a doorway read
134
 * as a doorway.
135
 */
136
export function seesFrom(floor: Floor, from: number, to: number): boolean {
137
  const a = xy(from);
138
  const b = xy(to);
139
  let { x, y } = a;
140
  const dx = Math.abs(b.x - x);
141
  const dy = Math.abs(b.y - y);
142
  const sx = x < b.x ? 1 : -1;
143
  const sy = y < b.y ? 1 : -1;
144
  let err = dx - dy;
145
  for (;;) {
146
    if (x === b.x && y === b.y) return true;
147
    const e2 = 2 * err;
148
    if (e2 > -dy) {
149
      err -= dy;
150
      x += sx;
151
    }
152
    if (e2 < dx) {
153
      err += dx;
154
      y += sy;
155
    }
156
    if (x === b.x && y === b.y) return true;
157
    if (!walkable(floor.tiles[at(x, y)])) return false;
158
  }
159
}
160
161
export const distance = (a: number, b: number): number => {
162
  const p = xy(a);
163
  const q = xy(b);
164
  return Math.max(Math.abs(p.x - q.x), Math.abs(p.y - q.y));
165
};
166
167
/** Every cell a delver standing here can currently see. */
168
export function visibleFrom(floor: Floor, from: number, radius = LANTERN): number[] {
169
  const seen: number[] = [];
170
  const { x, y } = xy(from);
171
  for (let dy = -radius; dy <= radius; dy += 1)
172
    for (let dx = -radius; dx <= radius; dx += 1) {
173
      const nx = x + dx;
174
      const ny = y + dy;
175
      if (nx < 0 || ny < 0 || nx >= WIDTH || ny >= HEIGHT) continue;
176
      const cell = at(nx, ny);
177
      if (distance(from, cell) > radius) continue;
178
      if (seesFrom(floor, from, cell)) seen.push(cell);
179
    }
180
  return seen;
181
}
182
183
/** One step from `from` toward `to`, around walls where it can manage it. */
184
export function stepToward(floor: Floor, from: number, to: number): number {
185
  const a = xy(from);
186
  const b = xy(to);
187
  const sx = Math.sign(b.x - a.x);
188
  const sy = Math.sign(b.y - a.y);
189
  const tries = [
190
    at(a.x + sx, a.y + sy),
191
    sx !== 0 ? at(a.x + sx, a.y) : at(a.x, a.y + sy),
192
    sy !== 0 ? at(a.x, a.y + sy) : at(a.x + sx, a.y),
193
  ];
194
  for (const cell of tries)
195
    if (cell !== from && walkable(floor.tiles[cell])) return cell;
196
  return from;
197
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts