These moved from the Coffey-Labs org to jcoffey-dev, where they belong. GitHub redirects a transferred repository, so nothing was broken by leaving them -- but a source link that lands via a redirect is still naming an owner that no longer holds the code, and the redirect only lasts as long as nobody creates a repository of the same name in the old place. Only the seven repositories that actually moved are rewritten. Every other Coffey-Labs URL is left alone: ihasmail, cairnobs and the rest are still there.
188 lines
9.3 KiB
Markdown
188 lines
9.3 KiB
Markdown
# 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/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://github.com/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 300–3400 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.
|