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

workbenchthis machine
Files 1088a14b6a321ebc5ef08f91e2687da00670cf11
133 lines · 4.5 KB · ~1.3k tok Raw History
1
/**
2
 * lanternfall in a terminal — the same dungeon, the same database, the same
3
 * shared map. `ssh` in and you are another delver on the floor; the web
4
 * players can see you standing there, and you can see them.
5
 *
6
 * hjkl or the arrows move, `.` waits and rests, `>` and `<` take stairs,
7
 * `q` leaves. Your name comes from the account at the keyboard, so the
8
 * gravestone will have it on.
9
 */
10
11
import * as store from "./db.ts";
12
import { HEIGHT, WIDTH, floorAt } from "./dungeon.ts";
13
import { act, breedOf, cacheCells, delverFor, isAction, viewFor } from "./game.ts";
14
import { ordinal } from "./names.ts";
15
16
const db = store.open();
17
const hand = `tty:${process.env.FORGE_APP_USER || "guest"}`;
18
const esc = "\x1b[";
19
const out = (s: string): void => {
20
  process.stdout.write(s);
21
};
22
const paint = (code: number, text: string, bold = false): string =>
23
  `${esc}${bold ? "1;" : ""}38;5;${code}m${text}${esc}0m`;
24
25
const DARK = 234;
26
const MEMORY = 239;
27
const LIT_FLOOR = 137;
28
const LIT_WALL = 95;
29
const STAIR = 71;
30
const GOLD = 178;
31
const BONE = 183;
32
const BEAST = 167;
33
const OTHER = 74;
34
const YOU = 230;
35
36
function draw(): void {
37
  const delver = delverFor(db, hand);
38
  const view = viewFor(db, delver);
39
  const caches = store.cacheState(db, delver.depth, store.turn(db));
40
  out(`${esc}2J${esc}H`);
41
  out(
42
    `${paint(GOLD, "lanternfall", true)} ${paint(MEMORY, `· ${ordinal(delver.depth)} floor · turn ${delver.turns}`)}\r\n\r\n`,
43
  );
44
  for (let y = 0; y < HEIGHT; y += 1) {
45
    let line = " ";
46
    for (let x = 0; x < WIDTH; x += 1) {
47
      const cell = y * WIDTH + x;
48
      const tile = view.floor.tiles[cell] ?? "#";
49
      const lit = view.visible.has(cell);
50
      const known = view.seen.has(cell);
51
      if (cell === delver.cell) {
52
        line += paint(YOU, "@", true);
53
        continue;
54
      }
55
      if (!lit && !known) {
56
        line += " ";
57
        continue;
58
      }
59
      if (lit) {
60
        const monster = view.monsters.find((m) => m.cell === cell);
61
        if (monster) {
62
          line += paint(BEAST, breedOf(monster.kind).glyph, true);
63
          continue;
64
        }
65
        if (view.others.has(cell)) {
66
          line += paint(OTHER, "@", true);
67
          continue;
68
        }
69
        if (caches.has(cell) === false && view.floor.tiles[cell] === "." && isCache(view, cell)) {
70
          line += paint(GOLD, "$");
71
          continue;
72
        }
73
      }
74
      if (view.graves.has(cell)) {
75
        line += paint(lit ? BONE : MEMORY, "†");
76
        continue;
77
      }
78
      if (tile === ">" || tile === "<") {
79
        line += paint(lit ? STAIR : MEMORY, tile, lit);
80
        continue;
81
      }
82
      if (!lit) {
83
        line += paint(MEMORY, tile === "#" ? "#" : "·");
84
        continue;
85
      }
86
      line += paint(tile === "#" ? LIT_WALL : LIT_FLOOR, tile === "#" ? "#" : "·");
87
    }
88
    out(`${line}\r\n`);
89
  }
90
  const bar = `${"♥".repeat(Math.max(0, delver.hp))}${paint(DARK, "·".repeat(Math.max(0, delver.max_hp - delver.hp)))}`;
91
  out(`\r\n ${paint(BEAST, bar)}  ${paint(GOLD, `${delver.gold}g`)}  ${paint(MEMORY, delver.name)}\r\n`);
92
  for (const line of store.heard(db, delver.id, 4)) out(` ${paint(MEMORY, line)}\r\n`);
93
  out(`\r\n ${paint(DARK, "hjkl/arrows move · . rest · > descend · < climb · q quit")}\r\n`);
94
}
95
96
// A cache is only a rumour until you are standing next to it; the glyph shows
97
// where one is *known* to sit, which is what the web page does too.
98
function isCache(view: ReturnType<typeof viewFor>, cell: number): boolean {
99
  return cacheCellsFor(view.floor.depth).includes(cell);
100
}
101
const cacheMemo = new Map<number, number[]>();
102
function cacheCellsFor(depth: number): number[] {
103
  const had = cacheMemo.get(depth);
104
  if (had) return had;
105
  const cells = cacheCells(floorAt(depth));
106
  cacheMemo.set(depth, cells);
107
  return cells;
108
}
109
110
const KEYS: Record<string, string> = {
111
  h: "west", j: "south", k: "north", l: "east",
112
  [`${esc}A`]: "north", [`${esc}B`]: "south", [`${esc}C`]: "east", [`${esc}D`]: "west",
113
  ".": "wait", " ": "wait", ">": "descend", "<": "ascend",
114
};
115
116
function key(data: Buffer): void {
117
  const pressed = data.toString("utf8");
118
  if (pressed === "q" || pressed === "\x03" || pressed === "\x04") {
119
    out(`${esc}2J${esc}H`);
120
    db.close();
121
    process.exit(0);
122
  }
123
  const action = KEYS[pressed];
124
  if (action !== undefined && isAction(action)) act(db, hand, action);
125
  draw();
126
}
127
128
if (process.stdin.isTTY) process.stdin.setRawMode(true);
129
process.stdin.on("data", key);
130
process.stdout.on("resize", draw);
131
// Somebody else's turn is your turn too: the floor moves while you stand still.
132
setInterval(draw, 3000);
133
draw();
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts