Article 158F2 Dome room

Dome room

by
ericlippert
from Fabulous adventures in coding on (#158F2)

You are at the periphery of a large dome, which forms the ceiling of another room below. A wooden railing protects you from a precipitous drop.

> tie the rope to the railing

The rope drops over the side and comes within ten feet of the floor.

> climb down

OK, we're getting good at this, so let's go a little faster. I'm going to post the code at https://github.com/ericlippert/flathead/tree/blog6.

The most important thing to know about the dictionary is: to save space, only the first 6 (or 9, in some versions) characters of a word are actually meaningful to the game. Anything after that is truncated. It is perfectly legal when playing Mini-Zork to say "pick up the screwdribble" and the game will cheerfully pick up the screwdriver, because it stopped disambiguating words after "screwd".

The dictionary of words recognized by the game is stored starting at the byte address found in the word beginning at offset 8 in the story. At that address, memory is organized as follows:

  • One byte contains the number of "word separator" characters. If the game parser wants characters other than spaces to separate words, they are listed here.
  • Then follows that many word separator characters.
  • After that comes one byte that gives the number of bytes in each dictionary table entry. Since words are six characters, which is four bytes of zstring, a dictionary entry has to be at least that many bytes. But a dictionary entry is permitted to be larger; the game may store data of its choice - likely having to do with how sentences are parsed but to be honest I do not know what is stored here - after the four bytes of zstring text. Every entry is of the same size.
  • After that comes two bytes that give the number of dictionary entries.
  • And after that comes the actual entries: string data first, and then extra game data.

Parsing all this out is pretty straightforward given the tools we've built. I've added more wrapper types so that the compiler helps catch my mistakes:

type dictionary_base = Dictionary_base of inttype dictionary_table_base = Dictionary_table_base of inttype dictionary_address = Dictionary_address of inttype dictionary_number = Dictionary of int

I didn't bother to add special types for the tiny little word separator table. Later on we will see that the Z-machine needs to track the addresses of individual dictionary entries, but it is convenient for me when trying to understand the structure to think about indices into that table. Hence I've got both dictionary numbers and dictionary addresses. Again, I want to use these wrapper types so that the compiler tells me when I've made a mistake.

Getting the dictionary base address out of the story is straightforward:

let dictionary_base story = let dictionary_base_offset = Word_address 8 in Dictionary_base (read_word story dictionary_base_offset)

And I think I can let the decoding logic pass without further comment:

let word_separators_base (Dictionary_base base) = Byte_address baselet word_separators_count story = let dict_base = Story.dictionary_base story in let ws_base = word_separators_base dict_base in Story.read_byte story ws_baselet entry_base story = let dict_base = Story.dictionary_base story in let ws_count = word_separators_count story in let ws_base = word_separators_base dict_base in inc_byte_addr_by ws_base (ws_count + 1)let entry_length story = Story.read_byte story (entry_base story)let entry_count story = let (Byte_address addr) = inc_byte_addr (entry_base story) in Story.read_word story (Word_address addr)(* This is the address of the actual dictionary entries. *)let table_base story = let (Byte_address addr) = inc_byte_addr_by (entry_base story) 3 in Dictionary_table_base addrlet entry_address story (Dictionary dictionary_number) = let (Dictionary_table_base base) = table_base story in let length = entry_length story in Dictionary_address (base + dictionary_number * length)let entry story dictionary_number = let (Dictionary_address addr) = entry_address story dictionary_number in Zstring.read story (Zstring addr)

All right, we can now ask for the number of dictionary entries and read out each string.

This is major boring code right here and I want to talk about something interesting, so let's see how we might accumulate all of those strings to display them.

We've already seen how to run loops using tail-recursive auxilliary functions, but I want to be even a bit more functional than that. The defining characteristic of functional programming is that functions are first class, so let's give an example.

In this series I'm going to very frequently want to turn a collection of numbered things - like dictionary words - into a string summarizing them. Rather than writing those little aux loops to accumulate a result, I want to write something a bit higher level. I want a helper that lets me express "turn each item from 0 to n-1 into a string and concatenate the results". Here's what I came up with:

let accumulate_strings_loop to_string start max = let rec aux acc i = if i >= max then acc else aux (acc ^ (to_string i)) (i + 1) in aux "" start

Let's take a look at this carefully. We have three arguments. The start and max arguments are straightforward integers; we will loop starting at start, and stop when we get to max. But what is to_string? It's a function that takes an integer and produces a string; in C# terms it would be a delegate.

We have our standard tail-recursive auxilliary loop that takes an accumulator and either returns the accumulator or does tail recursion. The recursion takes a new value for the accumulator, which is the result of calling to_string concatenated to the previous accumulation.

As we discussed previously, I don't really care that repeated string concatenation is n-squared here. The vast majority of times I use this I'll be just dumping things for debugging purposes or building small strings. If I had a real performance problem then I'd use some kind of string builder, just as I would in C#.

Now that I have this little helper method I can use it to build me a list of all the dictionary words:

let display story = let count = entry_count story in let to_string i = Printf.sprintf "%s " (entry story (Dictionary i)) in accumulate_strings_loop to_string 0 count 

My little to_string function takes an int and produces a string. Notice that of course the nested to_string function is closed over the parameters of its containing function; that is, it can freely use story without taking it as a parameter.

And now if we run this thing"

 let dict = Dictionary.display story in Printf.printf "%s\n" dict

" then we can see every word meaningful in Mini-Zork:

$ve . , #comm #rand #reco #unre " a about across activa advent againair air-p all altar an ancien and answer apply around art ask at attachattack awake away ax axe bag banish bare basket bat bathe beauti beetlebehind bell below beneat birds bite black blade block bloody blow boardboarde boards boat bodies body bolt bones ...

(The $ve, #rand, and so on words allow the developers to tweak aspects of game play for testing. Re-seeding the random number generator, for instance.)

Next time on FAIC: We'll start decoding the object tree.


3899 b.gif?host=ericlippert.com&blog=67759120
External Content
Source RSS or Atom Feed
Feed Location http://ericlippert.com/feed
Feed Title Fabulous adventures in coding
Feed Link https://ericlippert.com/
Reply 0 comments