Mindshadow (Interplay / Activision)
Mindshadow is a text adventure game, ported to the Spectrum among many other platforms. I'm not sure what was the original (possibly the Apple II?) but given the various gameplay videos on YouTube, the Spectrum version doesn't look like the best of the ports.
The Apple II version, on startup, says 'ADVENT VERSION 1.0', suggesting that on that platform, there may be a clear distinction between the interpreter ('ADVENT') and the adventure database. This isn't the case in the Spectrum port; several of the special cases are implemented in Z80 code.
Here's some detail on the methods the game uses to store its data:
Words
At 8CCAh (part 1) / 96B2h (part 2) is a sorted table of words. The table is formed:
DB first_char DB characters ;for word 0 DB first_char DB characters ;for word 1 ... ;etc.
first_char is 00h-1Eh. 1Fh marks the end of the table.
At startup, the program scans the entire list of words, and builds a lookup table at 5B00h with 4 bytes per entry:
DW word_number ;0FFFFh for an unused entry DW address
When printing a word, the game engine searches this table backward from the end until it finds an entry with word_number less than or equal to the required ID. Starting at that point, it then expands each word in turn until the required one is reached.
For example, the game engine wants to print word 72. It scans the table at 5B00h until it reaches 5B2Ch, which says that word 69 is found at 8E15h. It then expands words 69, 70, 71 and 72 into the buffer:
Word 69: 0, 'ENTER' buffer contains 'ENTER' Word 70: 5, '.' buffer now contains 'ENTER.' Word 71: 1, 'nglish' buffer now contains 'English' Word 72: 2, 'tering' buffer now contains 'Entering'
So the buffer now contains 'entering', as required.
Messages
Messages start at 7601h (part 1) / 7AA4h (part 2). Messages consist of
a sequence of 2-byte word IDs, in big-endian byte order, terminated by 0FFh.
The second message in part 1, for example, is 00 B8 00 ED 03 62 02 FF 05 52 03 95 FF
:
00 B8: Word 0B8h, 'You' 00 ED: Word 0EDh, 'are' 03 62: Word 362h, 'lost' 02 FF: Word 2FFh, 'in' 05 52: Word 552h, 'the' 03 95: Word 395h, 'mountains.' FF End of message
Vocab
The nouns table is at 0A4C0h (part 1) / 0B23Eh (part 2). The verbs table is at 0A71Ch (part 1) / 0B49Ah (part 2).
Each entry is a big-endian word. If the top bit is set, then this is the last word in this group of synonyms. The other bits give the word number.
For example, the part 1 noun table begins:
00 D0: Word 0D0h, 'all'. Noun group 0. 82 39: Word 239h, 'everything'. Noun group 0. Top bit set, end of group. 05 16: Word 516h, 'stones'. Noun group 1. 04 70: Word 470h, 'rubble'. Noun group 1. 03 F2: Word 3F2h, 'pebbles'. Noun group 1. 04 65: Word 465h, 'rocks'. Noun group 1. 84 65: Word 465h, 'rocks'. Noun group 1. Top bit set, end of group. 01 F2: Word 1F2h, 'diagram'. Noun group 2.
Word 0FFFh appears to be used as a placeholder for unknown words.
Maps
Mindshadow is divided into five zones, each of up to 36 rooms in a 6×6 grid. Part 1 consists of two zones, part 2 of three:
Zone 0: Island Zone 1: Pirate ship Zone 2: London docks Zone 3: Luxembourg Zone 4: The second floor of the Luxembourg Hotel
The zone table is at 7589h (part 1) / 7A2Ch (part 2). Each entry in it is 40 bytes. The first 36 give the room connections and description number:
Bit 7: Cannot go West Bit 6: Cannot go North Bit 5: Cannot go East Bit 4: Cannot go South Bits 3-0: Room description number 0-15
The byte at offset 36 is the start of room description messages for the zone. This will be added to the room description number to produce a message number.
At offset 37 is the number of the start room for the zone.
Bytecode
At 6136h (both versions) is the bytecode defining the game logic.
As a rule, the bytecode interpreter, when called, will execute only one
instruction. To execute more than one, they need to be wrapped in a
BEGIN ... END block. Compare the way that in C, an if()
can be
followed either by a single statement, or a block surrounded by curly
brackets.
In general, an instruction is 2 bytes long:
DB tttaaaaa, bbbbbbbb ;T is instruction type, ;A is parameter 1, ;B is parameter 2
However, if the type bits are 000
or 011
, the
instruction is a single byte.
The meanings of the type bits are:
000: BEGIN a block. The interpreter will be called repeatedly until the byte it encounters is 01h (END). 001: Match word. Bits 1-0 of the first instruction byte give the word type: 00 => Verb 2. If the main verb is a movement word, this will give the direction. 01 => Verb 1. The main verb in the player's input. 10 => Noun 1. The main noun in the player's input. 11 => Noun 2. The second noun (for inputs like 'KILL ORC WITH SWORD') The second instruction byte gives the verb / noun number to compare to. If there is a match, the bytecode interpreter will be called recursively on the following instruction. If not, it will be skipped. If the following byte is then 60h (ELSE) then the instruction following it will be executed if there was no match, skipped otherwise. 010: Check condition. As with the 'match word' check above, the following instruction will be executed if the condition is true, skipped if false; and if there is an ELSE following, the instruction after that will be executed if the condition is false, skipped if it is true. Bits 4-0 of the first instruction byte give a parameter, 0-31. The second instruction byte describes the test or operation to perform: 00h => Add the parameter to the current room number (parameter is sign-extended to twos complement, so 0-15 are positive, 16-31 become -16 to -1). Then move the player to that room. 01h => Try to move the player via an exit. Parameter is 0 for North, 1 for East, 2 for South, 3 for West. 02h => Go to the start room of a zone. The parameter specifies the zone. 03h => The parameter specifies a zone. Condition is true if the player is in that zone, false if not. 04h => The parameter specifies a room number. Condition is true if the player is in that room, false if not. 05h => Describe the location. 06h => Restart the game (not used). 07h => Set a boolean flag. The parameter gives the number of the flag, 0-31. 08h => Same as 07h. 09h => Clear a flag. 0Ah => Condition is true if the specified flag is set. 0Bh => List inventory. 0Ch => The parameter is the number of an object. That object will be selected for future operations. 0Dh => The parameter is the number of an object, less 16. Add 16 and use that object for future operations. 0Eh => Condition is true if the current object is available (carried or in the current room). 0Fh => Condition is true if the current object is carried. 10h => Attempt to take the current object. 11h => Attempt to drop the current object. 12h => Bring the current object into play (reset its 'destroyed' flag). 13h => Remove the current object from play (set its 'destroyed' flag). 14h => Condition is true if the current object does not have its 'destroyed' flag set. 15h => Condition is true if the number of objects carried ls less than the parameter. 16h => Moves object to room 15. 17h => Print a stock message. The parameter is: 0 => "Object: Dropped." 1 => "Object: Taken." 2 => "What do you want to do with the object?" 3 => "I don't know the word word." 4 => "I don't know how to word." Other values => "You can't command." 18h => Implements TAKE ALL. 19h => Implements DROP ALL. 1Ah => Reparse input? 1Bh => Paint graphic number param + 70. 1Ch => Save/load operations. The parameter is: 0 => RAM load 1 => RAM save 2 => Cassette load 3 => Cassette save Other values => Cassette save, then reboot. 1Dh => Clear graphics and display text screen. 1Eh => Lock up the computer (not used). Possibly for debugging? 100: The parameter and the following byte are combined to produce a 13-bit message number. This message is displayed. 101: Not used. This combines its parameter and the following byte in the same way, but then reboots the computer. 110: As 010, but there is no conditional execution. Other values: The bytecode interpreter returns, leaving the instruction pointer pointing at the offending byte. This is how it responds on finding byte 01h (END block) or 60h (ELSE).
For example, here's (some of) the code to handle giving rum to the Captain:
21 03 IF verb is 3, 'bribe' 22 06 IF noun is 6, 'rum' 00 BEGIN 23 1F IF noun2 is 31, 'captain' C0 1A Reparse input 40 0F IF current object is carried 48 04 IF currently in room 8 (beach) 44 0A IF flag 4 is set (pirate ship is present) 00 BEGIN C7 0C Reference object 7, an empty bottle 40 14 IF that object exists 00 BEGIN 80 E8 Message 250, 'the captain, disgusted, sails off' C4 09 Clear flag 4 (pirate ship leaves) C0 05 Redescribe room 01 END 60 ELSE (object 7 does not exist) 00 BEGIN C6 0C Reference object 6, a bottle of rum C0 13 Destroy that object 80 75 Message 135, 'the captain takes you aboard his ship' C1 02 Move the player to zone 1, the pirate ship 01 END 01 END 60 ELSE ; Pirate ship not present C2 08 Set flag 2 60 ELSE ; Not currently in room 8 C2 08 Set flag 2 60 ELSE ; Current object not carried C2 08 Set flag 2 01 END
Graphics
There is a lookup table of graphics at 0ACDCh (part 1) / 0BA5Bh (part 2). Each entry is a word, pointing to a 0-terminated string of polygons.
The first byte of a polygon specifies the brush to use:
For brushes 0,1,2: Bits 7 and 6 are the brush number. Bits 5-0 then give the attribute to use. For brushes 3-10: Bits 7 and 6 are both set. Bits 5-3 give the brush number less 3. Bits 2-0 give the paper colour (ink colour is always 0).
The brushes are:
Number Pattern Write mask 00h 055h 0FFh 01h 000h 000h 02h 0FFh 000h 03h 0FFh 0FFh 04h 0AAh 000h 05h 0AAh 0AAh 06h 0AAh 0AAh 07h 088h 0FFh 08h 0CCh 000h 09h 088h 088h 0Ah 0EEh 033h
Following the brush are pairs of coordinates:
DB x1,y1 DB x2,y2 etc.
until a coordinate pair is encountered with bit 7 of Y set. These coordinates then form a polygon to be filled with the specified brush.
Y-coordinates work down from the top of the screen.
John Elliott 2017-11-21