import { expect, test } from "bun:test"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Database } from "bun:sqlite"; import * as store from "./db.ts"; import { act, breedOf, cacheCells, delverFor, populate, viewFor } from "./game.ts"; import { distance, floorAt, xy, at } from "./dungeon.ts"; const fresh = (): Database => store.open(mkdtempSync(join(tmpdir(), "lanternfall-"))); test("a hand gets one delver, and the same one back", () => { const db = fresh(); const first = delverFor(db, "hand-a"); expect(first.hp).toBe(12); expect(first.depth).toBe(1); expect(delverFor(db, "hand-a").id).toBe(first.id); expect(delverFor(db, "hand-b").id).not.toBe(first.id); db.close(); }); test("nothing is waiting on the stair you arrive on", () => { const db = fresh(); for (let depth = 1; depth <= 5; depth += 1) { populate(db, depth); const floor = floorAt(depth); for (const monster of store.monstersOn(db, depth, 0)) expect(distance(monster.cell, floor.entrance)).toBeGreaterThan(8); } db.close(); }); test("walking into a monster hits it, and killing it pays", () => { const db = fresh(); const delver = delverFor(db, "hand-a"); const east = delver.cell + 1; db.query( "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)", ).run(east); const hit = act(db, "hand-a", "east"); expect(hit.log.join(" ")).toMatch(/put the rat down|hit the rat/); if (hit.log.join(" ").includes("put the rat down")) { expect(hit.delver.gold).toBe(breedOf("rat").gold); // It stays dead for a while, then the dungeon has rats again. expect(store.monstersOn(db, 1, store.turn(db)).some((m) => m.cell === east)).toBe(false); expect(store.monstersOn(db, 1, store.turn(db) + 200).length).toBeGreaterThan(0); } db.close(); }); test("dying leaves a grave where it happened, with what you were carrying", () => { const db = fresh(); const delver = delverFor(db, "hand-a"); db.query("UPDATE delver SET hp = 1, gold = 30 WHERE id = ?1").run(delver.id); // A wight next door finishes anyone in one bite. db.query( "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'wight', 12)", ).run(delver.cell + 1); const turn = act(db, "hand-a", "wait"); expect(turn.died).toBe(true); const graves = store.recentGraves(db); expect(graves.length).toBe(1); expect(graves[0]?.gold).toBe(30); expect(graves[0]?.epitaph).toContain(delver.name); expect(graves[0]?.epitaph).toMatch(/Traveller:/); // And the hand gets a new delver, with a new name, at full health. const next = delverFor(db, "hand-a"); expect(next.id).not.toBe(delver.id); expect(next.hp).toBe(12); expect(next.gold).toBe(0); db.close(); }); test("a grave pays the next delver half of what it holds, once", () => { const db = fresh(); const delver = delverFor(db, "hand-a"); const east = delver.cell + 1; db.query( "INSERT INTO grave (depth, cell, name, epitaph, gold) VALUES (1, ?1, 'Wren Tallow', 'Wren Tallow died. Traveller: beware the corridor.', 20)", ).run(east); const first = act(db, "hand-a", "east"); expect(first.delver.gold).toBe(10); expect(first.log.join(" ")).toContain("Traveller:"); const again = act(db, "hand-a", "west"); const back = act(db, "hand-a", "east"); expect(back.delver.gold).toBe(10); expect(again.delver.gold).toBe(10); db.close(); }); test("the stairs are the only way down, and climbing out banks the gold", () => { const db = fresh(); const delver = delverFor(db, "hand-a"); expect(act(db, "hand-a", "descend").log.join(" ")).toContain("no stair"); const floor = floorAt(1); db.query("UPDATE delver SET cell = ?2, gold = 41 WHERE id = ?1").run( delver.id, floor.descent as number, ); const down = act(db, "hand-a", "descend"); expect(down.delver.depth).toBe(2); expect(down.delver.cell).toBe(floorAt(2).entrance); // Back up, then out through the first floor's entrance. const up = act(db, "hand-a", "ascend"); expect(up.delver.depth).toBe(1); db.query("UPDATE delver SET cell = ?2 WHERE id = ?1").run(delver.id, floor.entrance); const out = act(db, "hand-a", "ascend"); expect(out.climbedOut).toBe(true); const scores = store.topScores(db); expect(scores[0]?.gold).toBe(41); expect(scores[0]?.name).toBe(delver.name); db.close(); }); test("resting heals, but only when nothing can see you", () => { const db = fresh(); const delver = delverFor(db, "hand-a"); db.query("UPDATE delver SET hp = 5 WHERE id = ?1").run(delver.id); const rest = act(db, "hand-a", "wait"); expect(rest.delver.hp).toBe(6); expect(rest.log.join(" ")).toContain("catch your breath"); db.query( "INSERT INTO monster (depth, spawn, cell, kind, hp) VALUES (1, ?1, ?1, 'rat', 2)", ).run(rest.delver.cell + 2); const watched = act(db, "hand-a", "wait"); expect(watched.log.join(" ")).toContain("still moving"); db.close(); }); test("the map is shared: one lantern lights it for everyone", () => { const db = fresh(); delverFor(db, "hand-a"); const explored = store.seenOn(db, 1).size; expect(explored).toBeGreaterThan(10); for (let i = 0; i < 6; i += 1) act(db, "hand-a", "west"); const after = store.seenOn(db, 1); expect(after.size).toBeGreaterThan(explored); // A hand that has never moved sees everything the other one lit. delverFor(db, "hand-b"); const view = viewFor(db, delverFor(db, "hand-b")); for (const cell of after) expect(view.seen.has(cell)).toBe(true); // But only its own lantern is *visible* — memory is not sight. expect(view.visible.size).toBeLessThan(after.size); db.close(); }); test("caches pay once and refill later, so a stripped floor is worth returning to", () => { const db = fresh(); const floor = floorAt(1); const cache = cacheCells(floor)[0] as number; const delver = delverFor(db, "hand-a"); db.query("UPDATE delver SET cell = ?2 WHERE id = ?1").run(delver.id, cache - 1); const took = act(db, "hand-a", "east"); expect(took.delver.gold).toBeGreaterThan(0); const carried = took.delver.gold; act(db, "hand-a", "west"); const again = act(db, "hand-a", "east"); expect(again.delver.gold).toBe(carried); db.close(); });