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

workbenchthis machine

lanternfall: make being chased survivable, and stop the log stuttering

Two things a live delve showed, neither visible from the tests that existed:

- A chaser crossing the edge of the lantern wrote 'Something moves at the
  edge of your lantern.' thirteen times in a row. It was not a bug in the
  first-sight check: a monster at distance 5 closing to 4 and back is
  genuinely entering and leaving the light. The log now never repeats its own
  last line, which is the right rule regardless of who is chasing whom.

- Monsters moved at exactly a delver's pace, so nothing pursued could ever
  reach a stair or rest: two goblins took a fresh delver from 12 hp to 2 with
  no legal way to disengage, and the delver before it died the same way.
  Chasers now skip one turn in three - deterministic in the turn and the
  monster id, so disengaging is a decision rather than a dice roll. They
  still close; you can now get to the stair first if you turn early enough.

30 tests. The new one puts a rat three tiles off in plain sight and asserts
it needs more than the minimum two turns to reach biting distance.
benjivers committed 358a461 Browse files
3 files changed +60 −2
src/db.ts +15 −1
@@ -239,7 +239,21 @@ export const countSeen = (db: Database): number =>
239239
240240 export function say(db: Database, delverId: number, turn: number, lines: readonly string[]): void {
241241 const insert = db.query("INSERT INTO message (delver_id, turn, text) VALUES (?1, ?2, ?3)");
242 for (const line of lines) insert.run(delverId, turn, line);
242 // A chaser crossing the edge of the lantern every other step used to write
243 // "Something moves at the edge of your lantern." thirteen times in a row,
244 // which reads as a broken game rather than a tense one. The log never
245 // repeats its own last line.
246 let previous =
247 db
248 .query<{ text: string }, [number]>(
249 "SELECT text FROM message WHERE delver_id = ?1 ORDER BY id DESC LIMIT 1",
250 )
251 .get(delverId)?.text ?? "";
252 for (const line of lines) {
253 if (line === previous) continue;
254 insert.run(delverId, turn, line);
255 previous = line;
256 }
243257 // Keep the tail only: nobody scrolls a dungeon log.
244258 db.query(
245259 `DELETE FROM message WHERE delver_id = ?1 AND id NOT IN
src/game.test.ts +39 −1
@@ -5,7 +5,7 @@ import { join } from "node:path";
55 import { Database } from "bun:sqlite";
66 import * as store from "./db.ts";
77 import { act, breedOf, cacheCells, delverFor, populate, viewFor } from "./game.ts";
8 import { distance, floorAt, xy, at } from "./dungeon.ts";
8 import { distance, floorAt, visibleFrom, xy, at } from "./dungeon.ts";
99
1010 const fresh = (): Database => store.open(mkdtempSync(join(tmpdir(), "lanternfall-")));
1111
@@ -181,3 +181,41 @@ test("caches pay once and refill later, so a stripped floor is worth returning t
181181 expect(again.delver.gold).toBe(carried);
182182 db.close();
183183 });
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 === "Stone.").length).toBe(1);
220 db.close();
221 });
src/game.ts +6
@@ -148,6 +148,12 @@ function monsterTurn(db: Database, delver: Delver, now: number, log: string[]):
148148 }
149149 continue;
150150 }
151 // Chasers move at two thirds of a delver's pace. At equal speed nothing
152 // pursued can ever reach a stair or rest, so every fight on a floor with
153 // more than one monster was a death sentence — two goblins took a fresh
154 // delver from 12 to 2 with no legal way to disengage. Deterministic in
155 // the turn and the monster, so it is not luck either.
156 if ((now + monster.id) % 3 === 0) continue;
151157 const next = stepToward(floor, monster.cell, target.cell);
152158 const occupied = monsters.some((m) => m.id !== monster.id && m.cell === next);
153159 if (!occupied && next !== monster.cell) {
↑↓ navigate ↵ open esc close

Keyboard shortcuts