Hunt the Wumpus, in two skins

A browser recreation of Gregory Yob's 1973 game. The rules are his and are
free to use; the sentences he wrote to explain them are not, and none of them
are here. Every line this game prints was written for it. NOTICE.md sets out
what is ours, what is not, and why -- the same argument the lemonade stand
makes about its own text, made before shipping rather than after.

Two skins over one game, and the period one is a Teletype rather than a
television. Wumpus is six years older than the Atari the lemonade stand runs
on. It lived on a timesharing service you telephoned and it printed on paper,
which is why the game asks you to remember things: there is no screen to look
back at. So 1973 is fanfold and black ink and a print head hammering out one
character at a time, and the remaster is the same cave with the lamp on. The
rules never differ, and neither does a single word.

The cave is proved rather than trusted. The adjacency table is typed out by
hand, because a generated dodecahedron comes out with an arbitrary numbering
and this is the numbering every listing has used since 1973 -- so a transcript
from a magazine still makes sense against it. Typed out means it can be typed
wrong, and a wrong table would not crash: it would quietly stop being a
dodecahedron while the game still played. cave.test.ts checks degree three,
symmetry, connectivity, a diameter of exactly five, and three pentagons
through every room.

The map is not allowed to know more than the paper. CaveMap draws only from
the rooms the hunter has stood in and the warnings those rooms gave, and it
marks suspects rather than answers. That guarantee is asserted in the engine
rather than in the component, because it is a property of the engine and would
still have to hold if the map were thrown away.

Drawing a solid flat took two attempts. Rooms 6 to 15 are not two pentagons at
different depths, they are one ten-room ring alternating inward and outward
wiring, and getting that wrong still draws and still looks cave-shaped. It is
much harder to read. The test checks the property that actually matters --
that no tunnel crosses another -- and was confirmed to fail against the bad
layout before it was believed.

The dial-in is a real Bell 103 call rather than a noise: dial tone at 350 and
440 Hz, Touch-Tone dialling, ringback at 440 and 480, the far modem's 2225 Hz
mark tone, then both ends keying at once through a telephone band. The screech
everybody remembers is a V.34 handshake from the 1990s and could not have
existed here.

38 tests. Typecheck clean.
This commit is contained in:
2026-09-08 22:32:31 -07:00
commit 6485f9b161
37 changed files with 8618 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
# Hunt the Wumpus
A browser recreation of the 1973 cave game — the one you played on a Teletype,
by smell, with five arrows you could not aim straight.
It ships in two skins over one game. **1973** is the machine as it was: a
printing terminal, black ink on fanfold paper, the print head hammering out one
character at a time and nothing on screen you have not read. **Remaster** is
the same cave with the lamp on — a cel-shaded map of the dodecahedron, warnings
drawn as marks over the rooms they came from. The rules never differ between
them, and neither does a single word the machine prints. Switch any time with
the button under the frame.
React + TypeScript + Vite, and not one binary asset: every mark on the map is
SVG generated from the graph in code, and every note is generated by a small
synth on the Web Audio API.
## Playing
```
npm install
npm run dev
```
The leaderboard is a separate service; run
[games-scores](https://github.com/Coffey-Labs/games-scores) alongside this if
you want one, or do not, and the board will say so rather than break.
You dial in rather than switch on, because in 1973 the game was not on your
machine — it was on somebody else's, down a telephone line. Pressing DIAL is
also what unlocks the audio: browsers want a gesture before they will make a
sound, and that happens to be exactly the gesture the game already wanted.
One to four hunters share the keyboard, hot-seat style. A turn is one whole
cave rather than one move — you walk yours until it kills you or you kill it,
and only then does the next hunter sit down. Taking turns move by move would
mean three people watching a fourth think.
- The cave is always a **dodecahedron**: twenty rooms, three tunnels from each.
That never changes. What changes is where things are.
- **Two bottomless pits.** Walk into one and you keep going.
- **Two roosts of super bats.** They will not hurt you. They will pick you up
and put you down somewhere at random, and they do not care what is already
there.
- **One wumpus**, asleep. Nothing down there troubles it — it is too heavy to
fall down a pit and too heavy for the bats to lift.
- Standing **one tunnel** from any of them, you are told something is close.
You are never told which room. Three tunnels and one warning is the whole
puzzle.
- **Five crooked arrows.** Each can be aimed through as many as five rooms in a
row. Name a room the arrow cannot reach from where it is and it does not stop
and does not fail — it takes a tunnel of its own choosing, and one of those
leads back to you.
- A shot that misses **usually wakes the wumpus**, which then moves one room or
stays where it is. If it arrives where you are, that is the end of it.
Kill it and you go deeper: a fresh cave, the same twenty rooms, and whatever
arrows you did not spend. Die and you are finished. **CLIMB OUT** closes the
books whenever you like and shows the standings.
## High scores
One board, shared by everybody. Ending an expedition posts every hunter in the
party, and the top 50 comes back ranked best first: wumpodes bagged, then
arrows still in the quiver, then fewest rooms walked. Runs from the expedition
you just finished are picked out.
There is no way to clear it. A hunter leaves the list only by being pushed off
the bottom by a better one.
The board is not in this repository and is not ours alone: every game on the
site shares one service, [games-scores](https://github.com/Coffey-Labs/games-scores),
which is one container and one volume however many games there are. All this
repository holds is the client in `src/game/highscores.ts`, which names the
game on every call and is otherwise exactly what it would be if the board were
ours — rows come back in this game's own field names.
It is the one part of the game that needs a line out. With the service
unreachable the game plays exactly as normal and the board says so plainly
rather than breaking; posting happens behind the closing standings, so a slow
network never holds up the end of a run. That repository's README has the API,
and what a board with no accounts can and cannot promise.
## How it is put together
```
src/
game/ pure simulation - no React, no DOM
cave.ts the dodecahedron: adjacency, and where to draw it
constants.ts hazard counts, arrows, probabilities, every line of text
engine.ts one turn: move, shoot, hazards, the wumpus waking
reducer.ts the turn/phase machine
rng.ts seeded mulberry32, so an expedition can be replayed
audio/
synth.ts pulse voices, a drum kit, filters, look-ahead sequencer
tunes.ts the chiptune
dread.ts the remaster's band
skin.ts which of the two skins is on, and where it is kept
components/
Teletype the transcript, printed a character at a time
CaveMap the remaster's map, generated from the graph
HuntScreen picks between them, so no screen needs to care
```
`engine.ts` takes a cave and a command and hands back the cave that follows,
plus the lines the machine would have printed. Both skins render those lines;
neither one decides anything. That is what makes it possible to run five
hundred hunts in a test and check that a hunter walking blind dies almost every
time.
### The cave, and proving it is one
The adjacency table is written out by hand rather than generated, because a
generated dodecahedron comes out with an arbitrary numbering and this one is
the numbering every listing has used since 1973. Written out means it can be
typed wrong, and a wrong table would not crash — it would quietly stop being a
dodecahedron while the game still played. So `cave.test.ts` proves it: twenty
rooms, three tunnels each, symmetric, connected, diameter exactly five, and
every room on three pentagons.
### The map, and what it is not allowed to know
The remaster must not be an easier game than the transcript. `CaveMap` may only
draw what has already been printed: it reads the rooms the hunter has stood in
and the warnings those rooms gave, and it marks the *suspects* — never the
answer. The reveal on the report screen is the one exception, and the hunt is
over by then.
That guarantee is asserted in `engine.test.ts` rather than in the component,
because it is a property of the engine and would still have to hold if the map
were thrown away tomorrow.
### Drawing a solid on paper
A dodecahedron cannot be drawn flat without lying about something, so the map
is its Schlegel diagram: one face pushed out to become the rim, the other
eleven nested inside. Rings of 5, 10 and 5 — and the middle one is the part
that is easy to get wrong, because rooms 6 to 15 are not two pentagons at
different depths but a single ten-room ring alternating between the rooms wired
inward and the rooms wired outward. Getting that wrong still draws, still
scales, still looks broadly cave-shaped, and is much harder to read. So the
test checks the property that actually matters: no tunnel crosses another.
## Sound
Audio starts on the first gesture, as browsers require.
The 1973 skin has one instrument that is not a lie: the print head. An ASR-33
printed ten characters a second and you could hear every one of them, so the
transcript is typed rather than shown, and every character is a solenoid thump
with a little bandpassed clatter on it.
The dial-in is a real Bell 103 call, in order and at the real frequencies:
dial tone at 350 + 440 Hz, Touch-Tone dialling off the DTMF grid, ringback at
440 + 480, the far modem putting up its 2225 Hz mark tone, and then both ends
talking at once — the originating coupler keying 1070 and 1270 Hz while the
answering machine keys 2025 and 2225, which is full duplex and is where most of
the texture comes from. All of it through a 3003400 Hz telephone band with a
resonance around 900 Hz for the two rubber cups.
Worth being clear that this is *not* the sound most people mean by "modem".
That one is a V.32 or V.34 handshake from the 1990s — the ascending probe tones
and the long grinding echo-cancel training — and none of it existed in 1973 or
could have. A Model 33 at 110 baud through an acoustic coupler is Bell 103, and
Bell 103 is a much quieter animal. The boot prints each stage as it happens,
off the same schedule the audio is built from, and can be skipped.
The music is the one thing the period skin claims that the period did not have.
The real game was silent. What plays instead is the music it would have got
when it followed everybody home: two pulse voices, a triangle bass, filtered
noise for a kit, and a minor key, which is roughly what a 1981 home-computer
port of a cave game sounded like.
The remaster gets a different band, and deliberately not the lemonade stand's
funk. Same engine, other end of the scale: D phrygian, no shuffle, a sub bass
that sits on the root and refuses to move, a resonant filter sweep on a
detuned saw, and toms instead of a backbeat — a snare on two and four would
make it music to walk to, and nothing down there walks in time.
Both run off one scheduler that queues notes 200 ms ahead, so the groove does
not stutter when React re-renders. `MUSIC` and `SOUND` toggle independently.
## Provenance
The rules are Yob's and are free to use. The sentences he wrote to explain them
are not, and none of them are here — every line this game prints was written
for it. [NOTICE.md](NOTICE.md) sets out what is ours, what is not, and why.