IllogiGames/Cv dungeon documentation
Sensible Section |
|
|
What It Is[edit | edit source]
It's a hack that grewsome. It's written in Perl which should tell you a lot.
I've got some experience so it's not as gruesome as it could have been but it's none the less pretty intense.
What it Also Is[edit | edit source]
It is also purely lexical in its operation. There are no fancy data structures, no parse tree, just some tables. I mean, it's written in Perl, f'gosh sakes, which is not exactly the most object-friendly language in the Universe, and it was just a reformatter before it grewsome.
Expression parsing is recursive descent (lack of a lexer limits the options just a bit), done by identifying parenthesized subexpressions which themselves contain no parens and recursively evaluating them, substituting them back into the original expression, and then recursively evaluating that. Precedence is handled the same way -- like, identify a "+", split, evaluate both halves, paste back together, and recurse, and so forth. It's all done with regular expressions, of course (Perl, remember?). There's at least one regexp match every five lines throughout the entire script, I think.
But what is it that's useful?[edit | edit source]
It's here. And it's a dungeon compiler. Sortof. More or less.
It's got macros, and states, and state attributes, and it makes it pretty straightforward to write a (somewhat limited) classic Adventure-style game in the form of a click-link browser based game which will run on a Wiki. (At least, it'll run on Illogicopedia -- on other wikis YMMV, it all depends on the extensions and MediaWiki version.)
The main thing a click-link game lacks, in general, is memory. You go from point A to point B, and stuff changes; you come back to point A and by golly it's all changed back. You pick up a fish at the dock, you walk back to the office, and the fish has mysteriously vanished. That's NG for an adventure game; it forces the player to go unidirectionally through the game, which not only takes a lot of the "real feel" out of the game, it also makes it wicked hard to write any kind of sensible puzzles into it.
So, this script brings memory to the table. However, the only state we've got to play with is the room number, so the entire memory is encoded in the room number. Donc, the memory is limited.
An Example of the Limitations[edit | edit source]
Suppose you have a dungeon of 10 rooms, and you have 5 objects, and you can pick up any object and drop it in any location.
Suppose you pick up all the objects, and then wander through the rooms and drop in the random places. That takes you, perhaps, 20 moves between rooms, along with 10 "get" commands and 10 "drop" commands. In other words, not many moves -- the game has still barely begun.
How many states can that bring you to?
Each object can be in any of 10 rooms, and there are 5 objects. That comes to 10^5 = 10,000 states. In addition to that, you can be in any of the 10 rooms, which tosses another factor of 10 into the mix for a total of 100,000 possible states.
Aside from the question of whether the DPL extensions even work with 100,000 forums on a page, if we assume each room takes 15 lines of text to describe (counting links, header, whatnot, 15 lines isn't much), that brings us to a Wiki page with 1.5 million lines of text on it. And all it describes is a measly 10 room dungeon with 5 objects which can be picked up and dropped, and nothing else. Boost the number of rooms to 100 and the size of the resulting file goes up by another factor of 100,000, which is obviously not going to fly.
So, some severe restrictions need to be put on the game design, or it just isn't going to work as a Wiki page, starting with this: You can't drop arbitrary objects in arbitrary places. It's just not feasible.
What This Translator Lacks[edit | edit source]
You cannot split input across two lines. If something is expected to be on one line, by golly, it better be on one line. Forget about hiding newlines with backslashes. Backslash is just another character. For some situations, it would be easy to fix that, but the processing is irregular enough that it might be tricky to get it right across the board.
It has no parser. Or maybe it has a parser but no lexer. Anyway it only has one of them, not two, which is sloppy and slipshod and suboptimal. Consequently it is singularly stupid about lexemes -- in short, it never heard of them.
Expressions have no unary minus. This is not entirely unrelated to the fact that we have no lexer. (Or maybe we have no parser, and only have a lexer.)
The script never got designed. It kind of happened, and then it happened some more, and then it kept happening for another day or two, and then there we were, it was far too late to design it then.
The dungeon definition language which it parses (or doesn't parse, or whatever) also never got designed. This is typically considered a major flaw in a language. (Since it didn't start out to be a language, it wasn't realized that there was a flaw here until it was too late to do anything about it.)
Passes and Stuff[edit | edit source]
It runs two full passes over the input file, and rescans parts of it multiple times in the course of expanding everything. The actual number of times it touches each line varies depending on how hard it's working that day. If I were a compiler guru I would speak eruditely of the number of phases it runs but I'm not so I won't.
It runs astonishingly slowly, particularly given that (a) Perl is pretty darn fast these days and (b) I'm running it on an eight processor 3.something GHz reasonably up to date and spiffy 64 bit machine. If you tried to run this thing on your cell phone I doubt it would ever complete anything before you hung up on it.
Macros[edit | edit source]
It's got macros which look a lot like the classic cpp macros. Do not be fooled. They are entirely different.
The definitions are processed right up front, as the input is read, just after the comments are stripped.
The expansions are done later, when the rooms are being defined. In fact the expansions are done as many times as it takes to get them to settle down. Consequently definition order doesn't matter all that much, unless you redefine things on the fly (which should work fine but it hasn't been tested so it probably doesn't).
And conditionals are expanded very very late, when the rooms are being instantiated, just after state substitution and just before we parse links. So, you can use conditionals inside macros, but you can't define macros inside conditionals, which is probably the exact opposite of what you were expecting. It also means conditionals can slosh across macro boundaries, which certainly wasn't what you were expecting.
Syntax of the macro definitions is a little odd. Since we can't hide newlines (which is ugly anyway so good riddance) there are two forms, depending on whether you're doing a one-liner or a multi-liner:
#define .fooble(&a,&b) &a-snit-&b
and:
#defbegin .fooble(&a,&b) mousebarf &a &b; catclaws + snoodlefit #defend
Finally, macro expansion is done inside room expansion which means macros can be used inside rooms but won't work so well elsewhere, and it means you can't define a macro which builds more than one room at a time. This hasn't proved to be a significant limitation in practice so I haven't fixed it (or even decided it's definitely a bug).
And finally finally, you may have wondered about the fact that I started the example macro names above with a dot, and the macro parameters with an &. Is that necessary? Well, sortof. Remember, we don't lex. Everything is done with regular expressions, operating on homogeneous lines of text. Macro name, argument name, and state name matching is done by searching for a vaguely alphanumeric string (i.e., '\w*'), which may begin with '.', '&', or '%', and which ends at a word boundary. The match strings used look something like this: /... ([\.\&\%]\w+)\b .../
Note that beginnings and endings are different. Macro instances must end on word boundaries but can begin anywhere.
If you define the macro "A", the macro "AA", and the macro "AAA", and then you write "AAAA" it's anybody's guess what's going to happen. And that is why you should begin your macro names with "." ; it makes the expansion unambiguous. (It's defined this way because I found it useful. I realize you probably think it's totally retarded.)
States[edit | edit source]
A state is associated with an inventory list and assorted other attributes. States are defined using the '#state' directive, which is parsed at the same time we're parsing "#define" stuff. A state definition looks like this:
#state(%Start) h:You are wearing a suit and carrying a briefcase. You have a number of additional items on your person. +:Bazooka |* A shoulder-fired rocket launcher +:Apple |* An apple with a caterpillar in it +:Bomb |* A 20 kiloton atomic bomb +:Chimp |* A monkey +a:Office |You are in the office #stateend
That will generate a special room containing an inventory list named "01-Inventory-%Start". The list will look like this:
You are wearing a suit and carrying a briefcase.
You have a number of additional items on your person.
- A shoulder-fired rocket launcher
- An apple with a caterpillar in it
- A 20 kiloton atomic bomb
- A monkey
The words between the ":" and the "|" are tags, and are for use in state expressions (which we'll explain shortly and which are wicked useful).
The line with the "+a:" on it is an attribute. It's like an inventory member which doesn't get listed in the inventory. It's solely for use in state expressions.
You can base one state on another one. That looks like this:
#state(%Detonated,%Start) h:You are wearing a dilapidated suit and carrying a burst briefcase. You have a number of additional items on your person. -:Bomb | +:ICBM |* A Titan ICBM with a 10 megaton warhead +:Elephant|* An Indian elephant wearing a howdah +:Scotch |* A fifth of Glenlivet +a:Crater |You are in the crater #stateend
This defines state "%Detonated" which has the same inventory list as "%Start" except the header has been replaced, the 'Bomb' item has been removed from the list, and we've added an ICBM, an elephant, and a bottle of Scotch. We're still in the office (that attribute is carried over) but we're also in a crater.
Note that once an inventory item has been defined with a short tag, you can refer to it in further inventory lists just using the short tag. When we get to attribute expressions we'll find that you must use the short tags in expressions. In fact if you never use an item in an expression, then it doesn't need to have a short tag.
Simple Rooms[edit | edit source]
A simple room definition looks like this:
================================================================ Inside-dragon You are inside the dragon. (I guess it was hungry, after all.) By the light of your flashlight you can see a number of ribs and other inside-dragon sorts of things. It smells pretty vile in here. You probably have a few minutes before the dragon gets around to digesting you. What would you like to do? * [[../Read-book|Read a book]] * [[../Play-cards|Play a game of solitaire]] * [[../Boom|Detonate the warhead on the ICBM]] [[../01-Inventory-.ILIST(%Dragon)|''Take Inventory'']]
There are three things worth discussing here.
The Room Header and Name[edit | edit source]
The line of equals signs tells cv_dungeon a room definition follows. The room name is on the line following that line. So, the room name here is Inside-dragon.
These are both necessary.
The Links[edit | edit source]
The links in the list of "What would you like to do?" items all look suspiciously like relative links to Wiki pages which are located in the same subdirectory as the game file. And in fact that's exactly what they are.
cv_dungeon recognizes that format. (It had to use some format or other, and the reason for choosing this one is that, historically, it started out as a script to reformat an adventure which had been done as a collection of separate pages into a single-page thing.)
In the last phase of processing, they'll be replaced with "Goto" template transclusions with appropriate arguments.
The Inventory List[edit | edit source]
The link to the inventory list points to an automatically generated page. In this case it's the inventory list associated with the state "%Dragon" (which we assume you've defined). The ".ILIST" macro is built in, and returns the inventory list number for a state, just for this purpose.
Like all other relative links to pages in the same directory, the inventory link will be replaced with an appropriate "Goto" when the output is generated.
Room States[edit | edit source]
This is where the rubber hits the road.
Suppose you can get into the dragon either before or after you blow up the office. In the former case, you'd be in state %Start and the latter you'd be in state %Detonated. To do that in a Wiki based game, you need two copies of the room. To do that, you put a state list after the line of equals signs, before the room name. Since you can't have two rooms with the same name, you would normally append the state number onto the room name.
It would look like this:
================================================================ X=%Start,%Detonated Inside-dragon-X You are inside the dragon. (I guess it was hungry, after all.) By the light of your flashlight you can see a number of ribs and a bunch of other inside-dragon stuff. It smells pretty vile in here. You probably have a few minutes before the dragon gets around to digesting you. What would you like to do? * [[../Read-book-X|Read a book]] * [[../Play-cards-X|Play a game of solitaire]] * [[../Send-monkey-X|Send the monkey to get help]] * [[../Dragon-sneeze-X|Tickle the dragon's ribs]] [[../01-Inventory-.ILIST(X)|''Take Inventory'']]
This is basically a macro definition and expansion, combined. The script will walk over the list of values following the "X=" and replace "X" with each of them in turn. So, we'll get a copy of the room named "Inside-dragon-%Start" and a copy named "Inside-dragon-%Detonated".
Similarly, wherever "X" appears in the room definition, it'll be replaced with the state value. In particular, the links in this room will also point to state-specific instances of the other rooms (and that is how the thing remembers what you've done as you walk around the dungeon).
And, in fact, the state name will be replaced with the state number, so the room names would actually end in "0" or "1" in this example, since state %Start and state %Detonated were 0 and 1 in our example.
Changing State and Conditionals[edit | edit source]
To make changing states easier, you can include multiple state lists before the room name. They will be walked in parallel. So, we could do this:
================================================================ X=%Start,%Detonated ; Q=%Detonated,%Vaporized Inside-dragon-X You are inside the dragon. (I guess it was hungry, after all.) There is a pool of digestive juices here. It smells pretty vile in here. You probably have a few minutes before the dragon gets around to digesting you. What would you like to do? #if (X == %Start) * [[../Dragon-splattered-Q|Set off the atomic bomb]] #else * [[../Dragon-splattered-Q|Detonate the ICBM]] #endif * [[../Go-swim-X|Go for a swim in the pool]] * [[../Wait-in-dragon-X|Wait a while and see what happens]] [[../01-Inventory-.ILIST(X)|''Take Inventory'']]
"X" and "Q" will be substituted in parallel, with corresponding members of their lists. Thus, the link "Set off the atomic bomb" would point to Dragon-splattered-%Detonated if you were in %Start state, or to Dragon-splattered-%Vaporized if you were in %Detonated state.
The conditional, "#if (X == %Start)...", is being used here to change the message on the link depending on whether it's the atomic bomb or the ICBM which you're carrying at the moment.
Attribute Expressions[edit | edit source]
What we've described so far involves a lot of hand coding, and if the state lists get complicated, it gets pretty messy. What we'll describe now is the ability to select states based on their attributes.
A state expression is something between square brackets: "[state1, state2, ...]" It can consist of a comma separated list of state names, in which case it's used for a set inclusion test. So,
#if (X == [%Start,%Detonated]) ...
will expand the 'if' clause if X is equal to either %Start or %Detonated.
A state expression can also be used in the room state line.
Several functions and some operators which can be placed inside the square brackets make this more useful. The functions include:
- %all() -- Returns a list of all states
- %with(a,b,c...) -- Returns a list of all states which have inventory or attribute items with the tags 'a', and 'b', and 'c', and so forth.
- %without(a) -- Same as with but it reverses the sense
- %map(expr,a1,a2,b1,b2,...) -- Evaluates state expression 'expr' and then replaces all occurrences of state a1 with state a2, state b1 with state b2, and so forth.
- %toggle(expr,a1,a2,b1,b2,...) -- Same as %map but in addition it replaces a2 with a1, b2 with b1, and so forth.
The special value "*", in place of a state expression, expands to the last state expression used. That makes it a lot easier to use "%toggle" and "%map" to set up the list of "target" room states for actions that change your state.
The infix operators include &, +, and -, which do the obvious. "&" forms the list which contains states that were on both lists; '+' forms a list which contains anything that was on either list, and '-' forms a list which contains everything from the left hand member which wasn't on the right hand member.
Thus, if you've attached the attribute 'Eaten' to each state that's inside the dragon, then in place of 'X=%Start,...' in the room definition for the dragon, you could say "X = [%with(Eaten)]". This makes it easy to add additional inside-dragon states (you might draw graffiti on the dragon's insides, for instance, and define a new state for that, or define a few states to let the clock run out before you get digested).
Expressions[edit | edit source]
The expression evaluator is called for expressions in #if() and #elseif() operators, and it's called if you use the ".EXPR()" built in macro.
It's pretty ordinary, with multiplication, division, addition, subtraction, and mod available. It also understands that if it encounters a [...] thing it should call the state expression parser to expand it.
Special Macros[edit | edit source]
In addition to .EXPR, which we just mentioned, there are several builtin macros (which are actually useful, unlike .EXPR which is good for debugging and not a lot else). Here are all of them.
- .ILIST(state) -- Inventory list number for "state" (mentioned previously)
- .PERM(perm_num,perm_length,member) -- Extremely useful macro for random-connecting rooms. It takes three arguments:
- perm_num -- This is the number of the permutation. Each time you call .PERM with the same permutation number it uses the same permutation. The first time you pass it a particular permutation number, it computes a random permutation and associates it with that number. Note that the random sequence will be the same every time cv_dungeon is run. (There should also be an explicit '.SRAND' call but currently there isn't.)
- perm_length -- The length of the permutation. Only used on the first call for a given permutation, when it's creating it.
- member -- The current member of the permutation. The returned value will be the next member of the permutation. So, for instance, if permutation #3 was set up as [3 7 2 0 4 1 6 5] and you call it as ".PERM(3, 8, 2)" it will return 0. If you call it as ".PERM(3, 8, 1)" it will return 6. Get the idea? The first number might represent a direction (N, S, E, W --> 1, 2, 3, 4, maybe) and the member might be the room number.
- .RINDEX(room-name) -- Returns the room number associated with a name. This is useful if you're setting up a special link or template in which cv_dungeon won't recognize the string as a room name. In particular it's helpful when using the {{RandomChoose|...}} template.
- .EXPR(expression) -- Already discussed
- .ERROR -- Barf out. If an alternative isn't supposed to happen you can stick in a .ERROR macro to catch it if it does.