Files
jcoffey-dev ff4da7321c Point links at git.coffeylabs.org after the move from GitHub
GitHub took the organization's repos and GHCR offline on 2026-09-20. Repo,
release, raw-file and clone links now go to Gitea at git.coffeylabs.org,
container images to registry.coffeylabs.org, and GitLab-style /-/blob paths
to Gitea's /src/branch form. Go module paths are identifiers and stay as
they are; links to GitHub issues and pull requests are left as history.
2026-09-22 09:09:33 -07:00

188 lines
9.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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://git.coffeylabs.org/jcoffey-dev/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://git.coffeylabs.org/jcoffey-dev/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.