Endless Cave Code
To the extent possible under law, the person who associated CC0 with this work has waived all copyright and related or neighboring rights to this work.
Introduction
This is all of the code to The Endless Cave. The code is derived from Hunter in Darkness, an IF game that I wrote several years ago.
The code here gives the effect of a maze of 10000 rooms. In fact it’s implemented with just one location. The “exit” links just change the description.
If more than one player were present, the trick would be obvious. (You’d always see every player in the Age, and whenever any player “moved”, every player would see the description change.) Therefore, this Age must use the “solo” instancing model.
The room description (and the exits) are generated by a GenText
property named cavebody
. The random-generator seed is an integer stored in the realm.seed
property. realm.seed
is initially zero; it’s set to zero whenever the player links in. (This gives the impression of linking into a consistent starting chamber.)
You can follow the cavebody
property and its children to understand how the description is generated. The first sentence is always a description (columnhead
); then there are one to three bits of detail (columna, columnb, columnc
). Then there’s a small chance of an extra detail, like a letter carved on the wall or a lost pickaxe. Finally, there are exactly three exits. (I wonder why?)
Each “exit” link changes realm.seed
to a new value from 0 to 9999. This is handled by the do_exit(arg)
code property; arg
is the exit number, from 0 to 2. The math is slightly hairy, but the upshot is that exit 0 changes realm.seed
by -3 to +3; exit 1 changes it from +3330 to +3336; and exit 2 changes it from +6663 to +6669. Values over 10000 wrap around.
Why these constants? It’s more an art than an algorithm, but the effect is that the player tends to orbit among a few seed values near 0, 3333, and 6666. That is, it’s a twisting maze of passages that tend to turn back on themselves. If the player follows a simple rule (like “always take the first exit”), he’ll almost certainly get stuck in a small loop. But it is possible to find passages out of the cluster. In fact, of the 10000 rooms, 9626 are reachable.
Realm properties
seed
: (Value): 0
columnhead
: (GenText):
[ ('this is', 'you are in'), A, Weight( 5, ('large', 'largish', 'small', 'smallish', 'tiny', 'cramped'), 3, SetKey('colheadany', 'f') ), Weight( 4, ([commaifany, 'elongated'], [commaifany, 'oblong'], 'round', 'roundish', 'angular'), 3, SetKey('colheadany2', 'f') ), Weight( 2, [dullifany, 'cave'], 1, 'cavern', 2, [dullifany, 'room'], 1, 'pit', 1, 'dome', ), Opt(0.25, ( ['with', A, ('high', 'low'), 'roof', Opt(0.25, 'overhead')], ['which is', ('crazed', 'mazed', 'laced'), 'with', ('fine', 'deep'), ('cracks', 'crevices')], )) ]
dullifany
: (GenText):
IfKey('colheadany', 'f', IfKey('colheadany2', 'f', 'dull'))
commaifany
: (GenText):
IfKey('colheadany', 'f', None, COMMA)
mazecolor
: (GenText):
( 'white', 'brown', 'dark grey', 'pale grey', 'blue', 'green', 'pink', 'reddish', 'greenish', 'orange', 'yellow', 'yellowish', 'violet' )
columna
: (GenText):
( [('ragged', 'rippling'), ('fringes', 'bands', 'laces'), 'of', mazecolor, ('mineral', 'stone'), ('hang from', 'hang down from'), ('the ceiling', 'above')], ['delicate', mazecolor, 'crystals grow', ('here and there', 'everywhere')], ['needles of', mazecolor, 'calcite spray from the', ('rocks', 'stone', 'walls'), ('here and there', 'everywhere')], ['stalactites hang', ('high', 'low'), 'overhead'], ['stalactites form heavy stone fringes across the', ('roof', 'ceiling')], [mazecolor, 'travertine seems to pour', Opt(0.5, 'endlessly'), 'down the', ('wall nearby', 'walls')], [('lacy', 'ropy'), Opt(0.5, [COMMA, 'twisted']), 'pillars rise from floor to', ('roof', 'ceiling')], ['cascades of', mazecolor, 'flowstone billow across the walls'], [Opt(0.5, ('coarse', 'dirty', 'fine', 'flecked')), mazecolor, 'striations band the walls'], )
columnb
: (GenText):
( [A, ('thick', 'thin', 'heavy', 'watery'), 'layer of', mudlink, 'lies underfoot'], ['the ground is worn into', ('polished flowing', 'flowing polished', 'flowing', 'polished'), 'curves'], ['the floor is', ('crusted', 'covered'), 'with', Opt(0.3, ['fine', COMMA]), ('wet', 'damp', 'moist'), sandlink], [('fine dry', 'dry, fine'), sandlink, ('drifts', 'is drifted'), 'delicately across the', ('floor', 'floor', 'ground')], [('jagged', 'broken', 'rough'), Opt(0.3, 'stone'), 'slabs', ('litter the floor', 'lie around your feet')], [Opt(0.3, ('heavy', 'broad', 'narrow', 'slender')), 'stalagmites cluster', ('thickly', 'all', None), 'around you'], ['gravel and', ('chipped rock', 'chipped stone', 'small rock chips', 'small stone chips'), 'cover the', ('floor', 'ground', 'ground')], )
columnc
: (GenText):
( [('water trickles audibly', 'water drips audibly', 'you can hear water trickling', 'you can hear water rushing', 'you can hear water flowing'), Opt(0.5, [COMMA, 'somewhere']), ('nearby', 'in the distance')], [A, ('small still', 'still small', 'small', 'tiny', 'still'), poollink, 'lies to one side'], ['the air is', ('chill and damp', 'damp and chill')], [('cool', 'chilly', 'cold'), 'air', ('moves against', 'brushes'), 'your', ('face', 'skin')], [A, Opt(0.5, ('tiny', 'small', 'narrow')), 'rivulet of', waterlink, 'trickles across the', ('cave', 'floor', 'ground', 'cave floor')], [('your breathing', 'your breath', 'the sound of your breath'), 'echoes in the stillness'], )
oneletter
: (GenText):
('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
onedigit
: (GenText):
('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
feature
: (GenText):
( [('the', 'a large', 'a rough'), 'letter', oneletter, 'is', ('carved', 'scratched'), 'on the wall here'], ['you see “', _, oneletter, _, onedigit, _, '”', ('carved', 'engraved', 'incised'), 'into the wall here'], [('you notice', 'you see', 'there are'), 'illegible chalk marks on the walls', ('here', 'nearby', None)], [('you see', 'there is'), A, ('enormous', 'huge', 'broad'), ('stone slab', 'boulder', 'chunk of rock'), ('lying', 'resting', 'sitting'), 'in the middle of the room'], None, [A, ('rusty', 'battered', 'broken', 'antique'), Opt(0.4, ('iron', 'steel')), ('pickaxe', 'crowbar', 'spike'), 'lies', ('here', 'nearby')], ['the walls are', ('stained', 'darkened'), 'with soot', Opt(0.5, 'here'), COMMA, ('the traces', 'traces', 'the marks'), 'of some explorer’s lantern'], )
oneexit
: (GenText):
[ Shuffle( [ SetKey('dir', 'left'), SetKey('vdir', 'f') ], [ SetKey('dir', 'right'), SetKey('vdir', 'f') ], [ SetKey('dir', 'ahead'), SetKey('vdir', 'f') ], [ SetKey('dir', 'up'), SetKey('vdir', 't') ], [ SetKey('dir', 'down'), SetKey('vdir', 't') ], ), A, Opt(0.5, Shuffle('twisty', 'narrow', 'wide', 'twisting', 'steep', 'rough', 'low')), Weight( 4, ('tunnel', 'passage', 'crawl', 'crack', 'path'), 2, IfKey( 'vdir', 't', ('chimney', 'pit'), ('canyon', 'gap')) ), exitlink, SwitchKey('dir', 'left', ('goes left', 'leads left', 'heads left', 'runs left'), 'right', ('goes right', 'leads right', 'heads right', 'runs right'), 'up', ('ascends', 'leads up', 'heads up', 'climbs up'), 'down', ('descends', 'leads down', 'heads down', 'runs down'), 'ahead', ('leads ahead', 'leads forward'), ), endexitlink, ]
exitlink
: (GenText):
Shuffle(exit0link, exit1link, exit2link)
sandlink
: (Text):
[sand]
mudlink
: (Text):
[mud]
poollink
: (Text):
[pool]
waterlink
: (Text):
[water|rivulet]
exit0link
: (Code):
link('do_exit(0)')
exit1link
: (Code):
link('do_exit(1)')
exit2link
: (Code):
link('do_exit(2)')
endexitlink
: (Code):
endlink()
cavebody
: (GenText):
[ columnhead, STOP, ( columna, columnb, columnc, [columna, SEMI, columnc], [columna, COMMA, 'and', columnb], [columnb, STOP, columnc], [columnb, STOP, columna, SEMI, columnc], [columna, COMMA, 'and', columnb, STOP, columnc], ), Opt(0.15, [PARA, feature]), PARA, oneexit, Opt(0.15, 'from here'), COMMA, oneexit, COMMA, 'and', oneexit ]
sand
: (Text):
It’s just sand.
mud
: (Text):
It’s just mud.
pool
: (Text):
Just a bit of muddy water that’s gathered at a low point in the cave.
rivulet
: (Text):
Just a bit of muddy water trickling by.
do_exit
: (CodeArgs) (arg)
_diff = (realm.seed % (217+arg*4)) % 6 if _diff >= 3: _diff = _diff-2 else: _diff = -(_diff+1) _diff += arg * 3333 realm.seed = (realm.seed + _diff + 10000) % 10000 unfocus()
Location properties
desc
: (Text):
[[display_scenery]] [$if seed == 0]A [metal plaque|startsign] is bolted to the wall.[$end]
display_scenery
: (Code):
gentext.display(cavebody, seed=seed, cooked=True)
on_enter
: (Code):
if _from is None: realm.seed = 0
portaldesc
: (Text):
You see a nondescript cave, and a metal plaque on the wall.
startsign
: (Text):
“Welcome to the Endless Cave! (Disclaimer: cave is not actually endless.) “This is based on the maze in my IF game ‘Hunter in Darkness’. Like that game, it is built with deterministic text-generation algorithms. There are 10000 rooms, all (probably) different! “Unlike that game, this maze has no exit or clues pointing towards one. So you can wander for a long time. The layout is consistent, so you could map it if you really wanted to. “Enjoy. --Belford”