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

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

Keyboard shortcuts