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

workbenchthis machine
Files 8c843521bbc603b656573701a58a86c57edd8cbc
245 lines · 9.2 KB · ~2.7k tok Raw History
1
import { expect, test } from "bun:test";
2
import { mkdtempSync } from "node:fs";
3
import { tmpdir } from "node:os";
4
import { join } from "node:path";
5
import { Database } from "bun:sqlite";
6
import * as store from "./db.ts";
7
import { act, breedOf, cacheCells, delverFor, populate, viewFor } from "./game.ts";
8
import { distance, floorAt, visibleFrom, xy, at } from "./dungeon.ts";
9
10
const fresh = (): Database => store.open(mkdtempSync(join(tmpdir(), "lanternfall-")));
11
12
test("a hand gets one delver, and the same one back", () => {
13
  const db = fresh();
14
  const first = delverFor(db, "hand-a");
15
  expect(first.hp).toBe(12);
16
  expect(first.depth).toBe(1);
17
  expect(delverFor(db, "hand-a").id).toBe(first.id);
18
  expect(delverFor(db, "hand-b").id).not.toBe(first.id);
19
  db.close();
20
});
21
22
test("nothing is waiting on the stair you arrive on", () => {
23
  const db = fresh();
24
  for (let depth = 1; depth <= 5; depth += 1) {
25
    populate(db, depth);
26
    const floor = floorAt(depth);
27
    for (const monster of store.monstersOn(db, depth, 0))
28
      expect(distance(monster.cell, floor.entrance)).toBeGreaterThan(8);
29
  }
30
  db.close();
31
});
32
33
test("walking into a monster hits it, and killing it pays", () => {
34
  const db = fresh();
35
  const delver = delverFor(db, "hand-a");
36
  const east = delver.cell + 1;
37
  db.query(
38
    "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)",
39
  ).run(east);
40
  const hit = act(db, "hand-a", "east");
41
  expect(hit.log.join(" ")).toMatch(/put the rat down|hit the rat/);
42
  if (hit.log.join(" ").includes("put the rat down")) {
43
    expect(hit.delver.gold).toBe(breedOf("rat").gold);
44
    // It stays dead for a while, then the dungeon has rats again.
45
    expect(store.monstersOn(db, 1, store.turn(db)).some((m) => m.cell === east)).toBe(false);
46
    expect(store.monstersOn(db, 1, store.turn(db) + 200).length).toBeGreaterThan(0);
47
  }
48
  db.close();
49
});
50
51
test("dying leaves a grave where it happened, with what you were carrying", () => {
52
  const db = fresh();
53
  const delver = delverFor(db, "hand-a");
54
  db.query("UPDATE delver SET hp = 1, gold = 30 WHERE id = ?1").run(delver.id);
55
  // A wight next door finishes anyone in one bite.
56
  db.query(
57
    "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'wight', 12)",
58
  ).run(delver.cell + 1);
59
  const turn = act(db, "hand-a", "wait");
60
  expect(turn.died).toBe(true);
61
  const graves = store.recentGraves(db);
62
  expect(graves.length).toBe(1);
63
  expect(graves[0]?.gold).toBe(30);
64
  expect(graves[0]?.epitaph).toContain(delver.name);
65
  expect(graves[0]?.epitaph).toMatch(/Traveller:/);
66
  // And the hand gets a new delver, with a new name, at full health.
67
  const next = delverFor(db, "hand-a");
68
  expect(next.id).not.toBe(delver.id);
69
  expect(next.hp).toBe(12);
70
  expect(next.gold).toBe(0);
71
  db.close();
72
});
73
74
test("a grave pays the next delver half of what it holds, once", () => {
75
  const db = fresh();
76
  const delver = delverFor(db, "hand-a");
77
  const east = delver.cell + 1;
78
  db.query(
79
    "INSERT INTO grave (depth, cell, name, epitaph, gold) VALUES (1, ?1, 'Wren Tallow', 'Wren Tallow died. Traveller: beware the corridor.', 20)",
80
  ).run(east);
81
  const first = act(db, "hand-a", "east");
82
  expect(first.delver.gold).toBe(10);
83
  expect(first.log.join(" ")).toContain("Traveller:");
84
  const again = act(db, "hand-a", "west");
85
  const back = act(db, "hand-a", "east");
86
  expect(back.delver.gold).toBe(10);
87
  expect(again.delver.gold).toBe(10);
88
  db.close();
89
});
90
91
test("the stairs are the only way down, and climbing out banks the gold", () => {
92
  const db = fresh();
93
  const delver = delverFor(db, "hand-a");
94
  expect(act(db, "hand-a", "descend").log.join(" ")).toContain("no stair");
95
  const floor = floorAt(1);
96
  db.query("UPDATE delver SET cell = ?2, gold = 41 WHERE id = ?1").run(
97
    delver.id,
98
    floor.descent as number,
99
  );
100
  const down = act(db, "hand-a", "descend");
101
  expect(down.delver.depth).toBe(2);
102
  expect(down.delver.cell).toBe(floorAt(2).entrance);
103
  // Back up, then out through the first floor's entrance.
104
  const up = act(db, "hand-a", "ascend");
105
  expect(up.delver.depth).toBe(1);
106
  db.query("UPDATE delver SET cell = ?2 WHERE id = ?1").run(delver.id, floor.entrance);
107
  const out = act(db, "hand-a", "ascend");
108
  expect(out.climbedOut).toBe(true);
109
  const scores = store.topScores(db);
110
  expect(scores[0]?.gold).toBe(41);
111
  expect(scores[0]?.name).toBe(delver.name);
112
  db.close();
113
});
114
115
test("the floor says what you are standing on", () => {
116
  const db = fresh();
117
  const delver = delverFor(db, "hand-a");
118
  const floor = floorAt(1);
119
  db.query("UPDATE delver SET cell = ?2 WHERE id = ?1").run(delver.id, (floor.descent as number) - 1);
120
  const onto = act(db, "hand-a", "east");
121
  expect(onto.log.join(" ")).toContain("A stair down, here.");
122
  const back = act(db, "hand-a", "west");
123
  expect(back.log.join(" ")).not.toContain("stair");
124
  db.close();
125
});
126
127
test("something coming into the light is worth saying once", () => {
128
  const db = fresh();
129
  const delver = delverFor(db, "hand-a");
130
  db.query(
131
    "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)",
132
  ).run(delver.cell + 2);
133
  const first = act(db, "hand-a", "wait");
134
  expect(first.log.join(" ")).toContain("still moving");
135
  db.close();
136
});
137
138
test("resting heals, but only when nothing can see you", () => {
139
  const db = fresh();
140
  const delver = delverFor(db, "hand-a");
141
  db.query("UPDATE delver SET hp = 5 WHERE id = ?1").run(delver.id);
142
  const rest = act(db, "hand-a", "wait");
143
  expect(rest.delver.hp).toBe(6);
144
  expect(rest.log.join(" ")).toContain("catch your breath");
145
  db.query(
146
    "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)",
147
  ).run(rest.delver.cell + 2);
148
  const watched = act(db, "hand-a", "wait");
149
  expect(watched.log.join(" ")).toContain("still moving");
150
  db.close();
151
});
152
153
test("the map is shared: one lantern lights it for everyone", () => {
154
  const db = fresh();
155
  delverFor(db, "hand-a");
156
  const explored = store.seenOn(db, 1).size;
157
  expect(explored).toBeGreaterThan(10);
158
  for (let i = 0; i < 6; i += 1) act(db, "hand-a", "west");
159
  const after = store.seenOn(db, 1);
160
  expect(after.size).toBeGreaterThan(explored);
161
  // A hand that has never moved sees everything the other one lit.
162
  delverFor(db, "hand-b");
163
  const view = viewFor(db, delverFor(db, "hand-b"));
164
  for (const cell of after) expect(view.seen.has(cell)).toBe(true);
165
  // But only its own lantern is *visible* — memory is not sight.
166
  expect(view.visible.size).toBeLessThan(after.size);
167
  db.close();
168
});
169
170
test("caches pay once and refill later, so a stripped floor is worth returning to", () => {
171
  const db = fresh();
172
  const floor = floorAt(1);
173
  const cache = cacheCells(floor)[0] as number;
174
  const delver = delverFor(db, "hand-a");
175
  db.query("UPDATE delver SET cell = ?2 WHERE id = ?1").run(delver.id, cache - 1);
176
  const took = act(db, "hand-a", "east");
177
  expect(took.delver.gold).toBeGreaterThan(0);
178
  const carried = took.delver.gold;
179
  act(db, "hand-a", "west");
180
  const again = act(db, "hand-a", "east");
181
  expect(again.delver.gold).toBe(carried);
182
  db.close();
183
});
184
185
test("a delver can break away: chasers move at two thirds pace", () => {
186
  const db = fresh();
187
  const delver = delverFor(db, "hand-a");
188
  const floor = floorAt(1);
189
  // A rat three tiles off, on real floor, in plain sight of the delver.
190
  const spot = visibleFrom(floor, delver.cell)
191
    .find((cell) => floor.tiles[cell] === "." && distance(cell, delver.cell) === 3);
192
  expect(spot).toBeDefined();
193
  db.query(
194
    "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)",
195
  ).run(spot as number);
196
197
  // Three tiles away is two steps from biting distance. At equal pace it is
198
  // adjacent on the delver's second turn; skipping one turn in three, it
199
  // needs at least a third.
200
  let turnsToReach = 0;
201
  for (let i = 1; i <= 12; i += 1) {
202
    act(db, "hand-a", "wait");
203
    const rat = store.monstersOn(db, 1, store.turn(db))[0];
204
    if (rat !== undefined && distance(rat.cell, delver.cell) <= 1) {
205
      turnsToReach = i;
206
      break;
207
    }
208
  }
209
  expect(turnsToReach).toBeGreaterThan(2);
210
  db.close();
211
});
212
213
test("the log does not repeat its own last line", () => {
214
  const db = fresh();
215
  const delver = delverFor(db, "hand-a");
216
  store.say(db, delver.id, 1, ["Stone.", "Stone.", "Stone."]);
217
  store.say(db, delver.id, 2, ["Stone."]);
218
  const heard = store.heard(db, delver.id, 10);
219
  expect(heard.filter((l) => l.text === "Stone.").length).toBe(1);
220
  db.close();
221
});
222
223
test("a line said again is re-dated to the turn it was said on", () => {
224
  // Pressing "descend" twice off a stair used to answer once and then say
225
  // nothing at all; the one line now belongs to the latest press.
226
  const db = fresh();
227
  const delver = delverFor(db, "hand-a");
228
  store.say(db, delver.id, 1, ["There is no stair here."]);
229
  store.say(db, delver.id, 2, ["There is no stair here."]);
230
  const heard = store.heard(db, delver.id, 10);
231
  expect(heard).toEqual([{ text: "There is no stair here.", turn: 2 }]);
232
  db.close();
233
});
234
235
test("the log is dated by the delver's own turns", () => {
236
  const db = fresh();
237
  act(db, "hand-a", "descend");
238
  act(db, "hand-a", "wait");
239
  const delver = delverFor(db, "hand-a");
240
  expect(delver.turns).toBe(2);
241
  const heard = store.heard(db, delver.id, 10);
242
  expect(heard.map((l) => l.turn)).toEqual([1, 2]);
243
  expect(heard[1]?.text).toContain("You wait");
244
  db.close();
245
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts