The game has 177 messages, 80 room descriptions and names and 61 object names, and holds none of them as text. A message is a small program for RUN_MESSAGE, and a name is three dictionary references. This page takes one message apart, runs it for three different speakers through the game's own code, measures what the scheme saves, and follows the text to the screen.

The bytecode

RUN_MESSAGE walks the message a byte at a time (read):

ByteMeans
bit 7 setthe first of a two-byte word reference, high byte first: twelve bits of dictionary offset from $6000 and a flag nibble; flags 2, 3 and 6 also end the message (plainly, with a full stop, or with a new line). PRINT_WORD prints it
$60-$7Fone of the 32 common words in COMMON_WORDS, a byte each
$20-$5Fa literal character: punctuation, and words the dictionary lacks, spelled out
$00-$13a control code: its handler from CONTROL_CODES is called, and the message goes on
$14-$16a control code that ends the message

The control codes, with their handlers read from the table:

CodeHandlerPrints
$00MC_PUSHED_OBJECTan object whose record the caller pushed, with no article
$01MC_PUSHED_WORDa word the caller pushed
$02MC_JUMPjump by the signed byte after it
$03MC_INSTRUMENT_NOUNthe instrument's noun (WEAPON_NAME)
$04MC_PUSHED_WITH_ARTICLEa word the caller pushed, with its article
$05MC_NOTHINGnothing
$06MC_ACTORthe actor's name, or YOU
$07MC_TARGETthe target, with its article
$08MC_BACKSPACEa backspace: the next word joins the last
$09MC_INSTRUMENTthe instrument, with its article
$0AMC_NOTHINGnothing
$0BMC_SUBMESSAGEthe message the signed byte after it points at
$0CMC_ACTOR_HISHIS, or YOUR when the actor is the player
$0DPRINT_LITERALa new line (the literal character)
$0EMC_TARGET_HISHIS, or YOUR, for the target
$0FMC_NOTHINGnothing
$10MC_ACTOR_ISthe actor and IS, or YOU ARE
$11MC_TARGET_ISthe target and IS, or YOU ARE
$12MC_NOTHINGnothing
$13MC_PUSHED_ISa pushed object and IS
$14MC_END_LINEend: a new line
$15MC_END_STOPend: a full stop and a new line
$16MC_ENDend

And the common words, the byte that stands for each:

$60 A$61 AND$62 ARE$63 AT$64 BE$65 BLOW$66 BUT$67 CANNOT
$68 CARRYING$69 DO$6A DOOR$6B DRAGON$6C FALL$6D FROM$6E HERE$6F I
$70 IN$71 IS$72 IT$73 NOT$74 OF$75 ON$76 SEE$77 SOME
$78 THE$79 THERE$7A THIS$7B TO$7C TOO$7D WHAT$7E WITH$7F YOU

One message, taken apart

The message at $AF20 is one of the wounds a fight can print. Each element as RUN_MESSAGE reads it, and what the game printed while it was the element in hand -- probed at $72F4, where RUN_MESSAGE picks up the next element, and at PRINT_CHAR -- for three speakers: the player attacking Thorin with the sword, Thorin attacking the player with it, and the warg attacking the wood elf bare-handed (so there is no instrument's noun to print):

AtBytesKindHoldsyou to thorinthorin to youvicious warg to wood elf
$AF2006control code$06: the actor's name, or YOUyouthorinthe vicious warg
$AF2188 3EwordBRANDISH, flags $0 brandish brandishes brandishes
$AF230Ccontrol code$0C: HIS, or YOUR when the actor is the player your his his
$AF2403control code$03: the instrument's noun (WEAPON_NAME) sword sword-
$AF252Cliteral',',,,
$AF2666common word$66: BUT but but but
$AF2711control code$11: the target and IS, or YOU ARE thorin is you are the (new line)elf is
$AF2875common word$75: ON on (new line)on on
$AF29B9 6FwordGUARD, flags $3; ends with a full stop guard. (new line) guard. (new line) guard. (new line)

Whole, as the three printed it:

Each was started at the beginning of a story line. Where the next word would have passed the 42nd column, PRINT_WORD began a new line before it -- the (new line) in the table -- and the full stop that ends the message is followed by one too.

9 elements, 11 bytes. The same bytes say YOU or THORIN, YOUR or HIS, ARE or IS; and the verb agrees with its subject: PRINT_WORD adds an ending when the sentence is about someone other than the player (ACTING ($B6EA) not zero), if the word allows one (bit 7 of its second dictionary byte) and its flags do not forbid it: $40 always inflects, $50 never, $10 agrees with the target instead of the actor, anything else with the actor.

Endings and articles

The ending is named by bits 5-7 of the word's third byte, from ENDINGS; the fourth is how CARRY becomes CARRIES -- a backspace takes the Y back off:

EndingBytesAdds
073 00 00 00s
165 73 00 00es
269 65 73 00ies
308 69 65 73(backspace)ies
464 00 00 00d
569 6E 67 00ing

A noun's reference carries its article (ARTICLE): bit 7 marks a proper name, printed with no article and a capital (except YOU); otherwise bits 4-6 pick one of four from ARTICLES -- or, in the input window and while an action is being narrated, from the second four, which say THE for everything but SOME:

ChoiceNormallyNarrating
0THETHE
1ATHE
2ANTHE
3SOMESOME

Sentences about actions are not stored at all: NARRATE_ACTION builds them from the action's pattern (see How a sentence is understood), which is why the characters' doings read so alike.

What it saves

Measured over all 177 stored messages, $AD7D on, walked by the build's decoder (the same tests RUN_MESSAGE makes): 2397 bytes hold 611 word references, 413 common-word bytes, 526 literal characters and 227 control codes. Spelled out, the fixed text is 5645 characters with its spaces -- 3560 from the references, 1559 from the common words and 526 literals -- so 2.4 characters to the byte before any name is printed. Run through RUN_MESSAGE itself with the player speaking, the 159 messages that do not print something a caller pushed (18 do) printed 5898 characters from 2259 bytes: 2.6 to 1. A room or object name costs six bytes -- three references -- however long its words, and there are 80 rooms and 61 objects. The word list the messages draw on, SECOND_LIST, is reached by no index at all: a message names each word by its offset.

Six places inside messages are ways in of their own: five share another message's tail (the east bank of the black river is the end of the west bank's description), and one starts on the second byte of a word reference, which it reads as a control code.

To the screen: two windows

Everything goes out through PRINT_CHAR, which asks PRINT_GATE first: nothing is printed unless the action is for real (DOING_IT) and printing is on (the actor can be seen). Then INPUT_STYLE ($B701) chooses the window. The story goes to STORY_CHAR: row 17, in the game's own six-pixel font (FONT, drawn by NARROW_CHAR at any pixel, so 42 columns fit), every letter lower case except the first of a sentence, the whole window scrolling up over the picture. The parser's replies, the prompt and the tape messages go to INPUT_CHAR: rows 19-23, in the ROM's font and capitals. The screen after the line followed on the parsing page and then XYZZY, drawn from screen memory:

The story above the wavy divider, scrolled up over where Bag End's picture was; below it the input window, with the prompt and the parser's complaint about XYZZY.

The story above the wavy divider, scrolled up over where Bag End's picture was; below it the input window, with the prompt and the parser's complaint about XYZZY.

Line breaking. PRINT_WORD builds each word in WORD_BUFFER first and, if it would not fit on the line, starts a new one before it: words are never split (read, and visible above).

The pause. At the end of each story line STORY_CHAR counts down NO_PAUSE_LINES ($B716); while it lasts there is no pause, and once it is 0 the game polls the keyboard up to 32768 times before scrolling -- about 0.58 s by the instruction timings -- or less if a key is pressed. The main loop sets it to 9 before every line typed. Measured on the line followed on the parsing page, the time from the end of each story line to its scroll:

LineNO_PAUSE_LINEST-statesTime
191030.00 s
281030.00 s
371030.00 s
461030.00 s
551030.00 s
641030.00 s
731030.00 s
821030.00 s
911030.00 s
1002,031,6780.58 s
1102,031,6780.58 s
1202,031,6780.58 s

The first nine lines scroll at once; the first of them waited only for the ENTER the line was typed with to be let go. From the tenth, each line waits the full count.

What is confirmed, and what is not