By John Elliott. Based mainly on reading the source of c4 by Brian Raiter, and peering at various versions of CC with a hex editor.
Ports of Chip's Challenge prior to the Microsoft version tend to use the same format to describe levels. This format also appears to be used by the original Lynx ROM.
Unless otherwise stated, all multi-byte values are little-endian.
In CC for DOS, each level is stored in a separate .PAK file in the 'levels' directory. The names of the .PAK files are hardcoded in the file BF1.EXE, at offset 0x94B9. Each level filename is a fixed 13-character ASCIIZ string; the name element is packed to 8 characters with spaces. For example, 'DIGGER.PAK' is stored as
000095A3 64 69 67 67 65 72 20 20 2E 70 61 6B 00 digger .pak.
There is no master table of passwords. Instead, 256 passwords are generated by the game engine at startup using a pseudo-random number generator, although only the first 149 are used:
1 | BDHP | 33 | BQSN | 65 | UPUN | 97 | IOCS | 129 | HEAN | 161 | JDFE | 193 | EFMV | 225 | WCSO |
2 | JXMJ | 34 | NQFI | 66 | ZIKZ | 98 | TKWD | 130 | XHIZ | 162 | OGFP | 194 | ICNP | 226 | HNAE |
3 | ECBQ | 35 | VDTM | 67 | GGJA | 99 | XUVU | 131 | FIRD | 163 | YJPA | 195 | ZUNX | 227 | SGNH |
4 | YMCJ | 36 | NXIS | 68 | RTDI | 100 | QJXR | 132 | ZYFA | 164 | KBIV | 196 | FHYK | 228 | TIOC |
5 | TQKB | 37 | VQNK | 69 | NLLY | 101 | RPIR | 133 | TIGG | 165 | ZDTQ | 197 | FEOO | 229 | ZUKF |
6 | WNLP | 38 | BIFA | 70 | GCCG | 102 | VDDU | 134 | XPPH | 166 | LQSN | 198 | TPLM | 230 | BPIK |
7 | FXQO | 39 | ICXY | 71 | LAJM | 103 | PTAC | 135 | LYWO | 167 | WKWD | 199 | PLYR | 231 | LEBH |
8 | NHAG | 40 | YWFH | 72 | EKFT | 104 | KWNL | 136 | LUZL | 168 | TQJM | 200 | DEJD | 232 | RDXH |
9 | KCRE | 41 | GKWD | 73 | QCCR | 105 | YNEG | 137 | HPPX | 169 | RABT | 201 | EKFD | 233 | CFLY |
10 | VUWS | 42 | LMFU | 74 | MKNH | 106 | NXYB | 138 | LUJT | 170 | URPI | 202 | IGWB | 234 | PHDA |
11 | CNPE | 43 | UJDP | 75 | MJDV | 107 | ECRE | 139 | VLHH | 171 | ARLT | 203 | LXHT | 235 | KJXP |
12 | WVHI | 44 | TXHL | 76 | NMRH | 108 | LIOC | 140 | SJUK | 172 | NMSB | 204 | DVMV | 236 | UNXY |
13 | OCKS | 45 | OVPZ | 77 | FHIC | 109 | KZQR | 141 | MCJE | 173 | VLNA | 205 | XLUZ | 237 | RUSJ |
14 | BTDY | 46 | HDQJ | 78 | GRMO | 110 | XBAO | 142 | UCRY | 174 | MNAN | 206 | DHPP | 238 | HQSS |
15 | COZQ | 47 | LXPP | 79 | JINU | 111 | KRQJ | 143 | OKOR | 175 | JNTT | 207 | IVIR | 239 | VHIC |
16 | SKKK | 48 | JYSF | 80 | EVUG | 112 | NJLA | 144 | GVXQ | 176 | FHYK | 208 | DQKB | 240 | BAOG |
17 | AJMG | 49 | PPXI | 81 | SCWF | 113 | PTAS | 145 | YBLI | 177 | XTIW | 209 | XPPH | 241 | ICNM |
18 | HMJL | 50 | QBDH | 82 | LLIO | 114 | JWNL | 146 | JHEN | 178 | EJTX | 210 | HAGO | 242 | PYCX |
19 | MRHR | 51 | IGGJ | 83 | OVPJ | 115 | EGRW | 147 | COZA | 179 | QRLT | 211 | SJUN | 243 | SFER |
20 | KGFP | 52 | PPHT | 84 | UVEO | 116 | HXMF | 148 | RGSK | 180 | CSOZ | 212 | USJE | 244 | ICXI |
21 | UGRW | 53 | CGNX | 85 | LEBX | 117 | FPZT | 149 | DIGW | 181 | CZMG | 213 | WCSR | 245 | DASX |
22 | WZIN | 54 | ZMGC | 86 | FLHH | 118 | OSCW | 150 | GNLP | 182 | KBYM | 214 | LDFU | 246 | BDHH |
23 | HUVE | 55 | SJES | 87 | YJYS | 119 | PHTY | 151 | ACKS | 183 | MJLA | 215 | DMYB | 247 | UGZQ |
24 | UNIZ | 56 | FCJE | 88 | WZYV | 120 | FLXP | 152 | BQZP | 184 | MIZX | 216 | ZXMJ | 248 | PXMF |
25 | PQGV | 57 | UBXU | 89 | VCZO | 121 | BPYS | 153 | XUFS | 185 | KWTT | 217 | BIVY | 249 | GNXP |
26 | YVYJ | 58 | YBLT | 90 | OLLM | 122 | SJUM | 154 | EBHR | 186 | UGRW | 218 | TLAK | 250 | OCKS |
27 | IGGZ | 59 | BLDM | 91 | JPQG | 123 | YKZE | 155 | YWNL | 187 | BIFQ | 219 | GZIF | 251 | PHTY |
28 | UJDD | 60 | ZYVI | 92 | DTMI | 124 | TASX | 156 | CFPD | 188 | URPI | 220 | RLYG | 252 | ZTHP |
29 | QGOL | 61 | RMOW | 93 | REKF | 125 | MYRT | 157 | HHDA | 189 | DEZT | 221 | HYRA | 253 | EBHM |
30 | BQZP | 62 | TIGW | 94 | EWCS | 126 | QRLD | 158 | REZT | 190 | GONT | 222 | PQGF | 254 | PPXL |
31 | RYMS | 63 | GOHX | 95 | BIFQ | 127 | JMWZ | 159 | ZMWO | 191 | ASHA | 223 | KRQZ | 255 | CFLX |
32 | PEFS | 64 | IJPQ | 96 | WVHY | 128 | FTLA | 160 | GBUS | 192 | LHXD | 224 | ZQRA | 256 | PHDA |
After the 256 passwords have been generated, the program then generates three more characters and uses them to overwrite the passwords of levels 98, 107 and 114 (which would otherwise be duplicates):
Level | Before | After |
---|---|---|
98 | GKWD | TKWD |
107 | KCRE | ECRE |
114 | KWNL | JWNL |
Each level (.pak) consists of one or more compressed blocks. A block begins with a 4-byte header:
Byte | Length of compression dictionary, 0-255. If this is zero the level is not compressed. |
Byte | Nonzero if another compressed block follows this one; zero if this is the last compressed block in the level. In the levels supplied with CC for DOS, this byte is always zero, and I don't know how well various implementations support nonzero values for it. |
Word | Number of bytes following the dictionary. |
The dictionary then follows. Each entry is three bytes:
Byte | Token to define. |
Byte | Token definition: first byte. |
Byte | Token definition: second byte. |
Each 'definition' byte will be treated either as a reference to another token (if it matches a token already in the dictionary) or a literal byte (otherwise). Note that tokens can be redefined. For example, level 106 (KABLAM) starts with:
1E ; Dictionary has 30 entries 00 ; This is the last block 70 00 ; 112 bytes follow the dictionary 02 2C 2C ; Define dictionary entry 02 as { 2C 2C }. Dictionary ; entry 2C is not defined, so both '2C' are treated ; as literal values. 03 00 00 ; Define dictionary entry 03 as { 00 00 }. 04 02 02 ; Define dictionary entry 04 as { 02 02 }. Dictionary ; entry 02 is defined, so both are expanded; entry ; 4 becomes { 2C 2C 2C 2C } 02 03 03 ; Redefine dictionary entry 02 as { 03 03 }. Dictionary ; entry 03 is defined, so both are expanded; entry ; 2 becomes { 00 00 00 00 }
Here are some C code fragments for reading the dictionary, and expanding an entry.
typedef struct dictent { struct dictent *pLeft; struct dictent *pRight; unsigned char chLeft; unsigned char chRight; } DICTENT; DICTENT dict[256] = { 0 }; DICTENT *lookup[256] = { 0 }; /* Load dictionary */ int dentry = 0; for (n = 0; n < entries; n++) { unsigned char key = read_byte(); unsigned char left = read_byte(); unsigned char right = read_byte(); dict[dentry].pLeft = lookup[left]; dict[dentry].pRight = lookup[right]; dict[dentry].chLeft = left; dict[dentry].chRight = right; lookup[key] = &dict[dentry]; ++dentry; } /* Expand an entry */ int expand(DICTENT *de, unsigned char *dest) { int len = 0; if (de->pLeft) { len = expand(de->pLeft, dest); dest += len; } else { dest[0] = de->chLeft; len = 1; } if (de->pRight) { len += expand(de->pRight, dest); } else { dest[0] = de->chRight; len++; } return len; }
Following the dictionary is a sequence of bytes, which are expanded to the level data. Again, these are treated as dictionary tokens (if defined in the dictionary) or literal values (otherwise).
unsigned char *dest = buffer; { unsigned char c = read_byte(); if (lookup[c]) { dest += expand(lookup[c], dest); } else { *dest++ = c; } }
Once the level has been expanded, the first 1024 bytes will be the map (32 rows of 32 tiles). Tiles are:
00 | Empty | 10 | Fire boots | 20 | Yellow key | 30 | Red button |
01 | Wall | 11 | Force boots | 21 | Green key | 31 | Chip (uncounted) |
02 | Ice | 12 | Water boots | 22 | Blue button | 32 | Blue block wall |
03 | Dirt | 13 | Fire | 23 | Chip (counted) | 33 | Hint button |
04 | Blue block floor | 14 | Water | 24 | Socket | ||
05 | Force floor north | 15 | Thief | 25 | Exit | ||
06 | Force floor east | 16 | Popup wall | 26 | Invisible wall temporary | ||
07 | Force floor south | 17 | Toggle open | 27 | Invisible wall permanent | ||
08 | Force floor west | 18 | Toggle closed | 28 | Gravel | ||
09 | Force floor random | 19 | Green button | 29 | Wall east | ||
0A | Ice corner SE | 1A | Red door | 2A | Wall south | ||
0B | Ice corner SW | 1B | Blue door | 2B | Wall southeast | ||
0C | Ice corner NW | 1C | Yellow door | 2C | Bomb | ||
0D | Ice corner NE | 1D | Green door | 2D | Bear trap | ||
0E | Teleport | 1E | Red key | 2E | Brown button | ||
0F | Ice boots | 1F | Blue key | 2F | Clone machine |
Other tile types tend to crash the game engine.
Following the map is a 384-byte creature list.
000h: DB creature0, creature1, ... ; Up to 128 creature IDs 080h: DB x0, x1, ... ; X-coordinates 100h: DB y0, y1, ... ; Y-coordinates
Creature types are:
04 | Chip north | 05 | Chip east | 06 | Chip south | 07 | Chip west |
08 | Bug north | 09 | Bug east | 0A | Bug south | 0B | Bug west |
0C | Centipede north | 0D | Centipede east | 0E | Centipede south | 0F | Centipede west |
0C | Fireball north | 0D | Fireball east | 0E | Fireball south | 0F | Fireball west |
10 | Glider north | 11 | Glider east | 12 | Glider south | 13 | Glider west |
14 | Ball north | 15 | Ball east | 16 | Ball south | 17 | Ball west |
18 | Block north | 19 | Block east | 1A | Block south | 1B | Block west |
1C | Tank north | 1D | Tank east | 1E | Tank south | 1F | Tank west |
20 | Walker north | 21 | Walker east | 22 | Walker south | 23 | Walker west |
24 | Blob north | 25 | Blob east | 26 | Blob south | 27 | Blob west |
28 | Teeth north | 29 | Teeth east | 2A | Teeth south | 2B | Teeth west |
Other creature types tend to crash the game engine.
The level time is a little-endian word:
DW time
The title is a 0-terminated string, with this encoding:
x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | x8 | x9 | xA | xB | xC | xD | xE | xF | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0x | Newline | Space | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D |
1x | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T |
2x | U | V | W | X | Y | Z | ! | " | ' | ( | ) | , | - | . | : | ; |
3x | ? |
(Note: The Spectrum version port only supports characters 00h-25h, plus 2Dh).
The hint, if present, uses the same encoding as the title except it is terminated with two 00 bytes in succession (a single 00 byte is treated as a newline but does not end the hint).
On the Spectrum version, levels are stored on cassette tape. World of Spectrum has three tape images:
In all versions, each file on the tape contains eight consecutive levels. The files are distinguished by their level set ID: ID zero holds levels 1-8, ID 1 holds levels 9-16, etc.
The TAP format is vastly easier to understand: both as a container, and the way the levels are stored in it. A .TAP file consists of one or more blocks:
DW length ;Number of bytes in the following block DB block ;Data block. The first byte is the type, usually ;00h for a file header, 0FFh for file data.
Each level file is stored in the .TAP as two consecutive blocks, header followed by data:
DW 13h ;Number of bytes in the following block DB 00h ;Block type 0 : header DB 03h ;Spectrum file type (CODE) DB id ;Level set ID, 0-17. DB "*CHIP'S* " ;Remainder of Spectrum filename DW datalen ;Length of level data DW 8000h ;Notional load address (ignored) DW 0 ;Unused field DB check ;XOR of all bytes from block type to end. DW blklen ;Number of bytes in the following block, ;should be datalen + 2 DB 0FFh ;Block type 0FFh : data DB ... level data ... DB check ;XOR of all bytes from block type to end.
For example, this is how the beginning of the first level file looks. I have highlighted the two separate TAP blocks:
00011938 13 00 00 03 00 2A 43 48 .....*CH 00011940 49 50 27 53 2A 20 97 08 00 80 00 00 5A 99 08 FF IP'S* ......Z... 00011950 10 A8 12 A9 F4 A9 27 AB 7E AC 83 AD 7B AE B2 AF ......'.~...{... 00011960 11 00 CB 00 02 00 00 04 02 02 05 04 04 07 05 05 ................
The level file begins with a 16-byte header, giving the addresses of the eight levels it contains (on the assumption that the file has loaded at 0A800h). In the above example, the first level pointer is 0A810h, so Lesson 1 can be found 16 bytes after the start of the file.
The actual levels themelves, once you reach them, are in the same compressed format as the DOS .PAK files.
TZX, as a container format, is described at World of Spectrum. It's a nuisance to work with, because there's no consistent way of specifying the length of a chunk.
Of the two TZX releases, the Erbe rerelease contains the same data as the .TAP download. The original (turbloader) TZX stores the levels in an obfuscated form, which the turboloader decrypts on read. For example, the file containing the first levels has the header
E9 CB 5C F7 98 41 83 C3 25 FE CB 76 2B
After decryption by the loader, this becomes
00 89 08 FF 5F 3D 3D C0 97 08
Of these bytes:
00
is the level ID.
89 08
is the number (0889h) of bytes that will be read from the
file.
97 08
is the number (0897h) of bytes that will end up in memory.
The other bytes are used in decoding the following data block.
The version of the game downloadable from ntnu.no is in the form of a disk image (in the CPC data format). The filesystem is CP/M, with groups of eight levels held in CP/M files:
CHIPS.CHA: Levels 1-8 CHIPS.CHB: Levels 9-16 CHIPS.CHC: Levels 17-24etc.
Each file begins with a 128-byte AMSDOS header. This is followed by the same data as in the Spectrum tape file: eight words giving the level addresses (assuming a load at 0A800h) and then the eight levels.