├── .gitignore ├── .idea ├── codeStyleSettings.xml └── vcs.xml ├── LICENSE ├── README.md ├── advent ├── game.lua ├── p8advent ├── __init__.py ├── lzwlib.py ├── textlib.py └── tool.py └── tests ├── testdata ├── pager.lua └── test_game.lua └── textlib_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dan Sanderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # p8advent 2 | 3 | This will eventually be an adventure game toolkit for the [Pico-8 virtual 4 | console](http://www.lexaloffle.com/pico-8.php). Right now it's just some data 5 | format and workflow experiments. :) 6 | 7 | Currently the only thing here are some experiments with packing text strings 8 | into cart data. The `p8advent` tool (see `tool.py`) uses a specially-marked 9 | Lua source file to create a Pico-8 cart with string literals extracted into 10 | the cart data region, packed using a given text packing library. 11 | 12 | `textlib` was an early attempt at a dictionary-based packing library that 13 | focused on English words, inspired by methods used by old text adventure 14 | games. It stores both the code stream and the dictionary in cart data, and so 15 | requires minimal RAM to access strings. In the context of Pico-8, it's not 16 | very satisfying: the compression rate for a long wordy text (_A Tale of Two 17 | Cities_ by Charles Dickens) only compressed to about 75% the original size. 18 | Simply packing the 6-bit character set into 8-bit strings (a reasonable 19 | method not yet implemented here) would be as effective, so a fancier 20 | algorithm has to do better than this. 21 | 22 | `lzwlib` uses the LZW compression algorithm with variable-width codes. All 23 | strings share the same dictionary to maximize packing, but are stored 24 | byte-aligned with headers so they can be accessed directly. This requires 25 | that the Lua code reconstruct the dictionary in RAM before accessing any 26 | strings. The Dickens test text compresses to 48% when using as much Lua RAM 27 | as possible for the largest possible dictionary. Capping the dictionary at 28 | 4,096 entries bumps this up to about 60% for this text. 29 | 30 | Of course, ToTC is not a typical text corpus for a game, even a large 31 | text-based game. I'll need to make an actual game that uses a lot of text to 32 | demonstrate that fancy packing techniques are actually profitable. It seems 33 | likely that a game that uses both text and graphics and wants to store 34 | strings could simply use a bit stream of 6-bit characters. 35 | -------------------------------------------------------------------------------- /advent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | sys.path.append('../picotool') 5 | 6 | from p8advent import tool 7 | 8 | if __name__ == '__main__': 9 | sys.exit(tool.main(sys.argv[1:])) 10 | -------------------------------------------------------------------------------- /game.lua: -------------------------------------------------------------------------------- 1 | function _update() 2 | end 3 | 4 | function _draw() 5 | end 6 | -------------------------------------------------------------------------------- /p8advent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dansanderson/p8advent/9257a4aebc88840da8e0d4af5172f60ea0576604/p8advent/__init__.py -------------------------------------------------------------------------------- /p8advent/lzwlib.py: -------------------------------------------------------------------------------- 1 | """An LZW-based string packer. 2 | 3 | LzwLib stores a set of strings as compressed binary data. It can also 4 | generate Pico-8 Lua code capable of accessing a string given a string's ID ( 5 | returned by the encoder method). The goal is to make it easy to write Pico-8 6 | games that use a large quantity of English text without storing that text in 7 | the code region of the cart. 8 | 9 | All strings added to the data structure contribute to a single decoder 10 | dictionary. The Lua client generates this dictionary from the complete LZW 11 | data the first time a string is accessed. 12 | 13 | The ID of a string is a 16-bit value equal to the address of the compressed 14 | data for the string in memory. The compressed string is stored as two bytes 15 | representing its compressed size (LSB first) followed by the codes. The 16 | ID is returned encoded as a string of 6-bit characters codes in the "pscii" 17 | character set. 18 | 19 | This is meant to be a drop-in replacement for TextLib, which was a silly idea 20 | that never compressed very well. Unlike TextLib, LzwLib does not distinguish 21 | between word and non-word characters, and preserves all characters including 22 | spaces, with the exception of newlines which are converted to single spaces. 23 | """ 24 | 25 | from collections import OrderedDict 26 | import re 27 | 28 | __all__ = ['LzwLib', 'encode_pscii'] 29 | 30 | 31 | # A character set, which I'm going to call "pscii", consisting of all of the 32 | # characters supported by TextLib. This corresponds to all of the characters 33 | # supported by Pico-8 v0.1.3. Notable missing chars include: $ \ @ ` (I 34 | # believe 0.1.4 will add support for "\".) 35 | CHAR_TABLE = ' !"#%\'()*+,-./0123456789:;<=>?abcdefghijklmnopqrstuvwxyz[]^_{~}' 36 | 37 | # For string-encoding IDs, add a character to make it an even 64 chars. 38 | CHAR_TABLE_FOR_SID = CHAR_TABLE + '@' 39 | 40 | # The Lua code for a string unpacker. 41 | # 42 | # _t(sid) unpacks and returns one or more strings. sid is a string containing 43 | # one or more string-encoded string IDs, three chars each. If sid contains 44 | # multiple three-char IDs, the return value is each of those strings 45 | # concatenated. This allows for the original Lua source to concatenate string 46 | # IDs as if they are the original string, then call _t() at the last moment 47 | # to unpack the aggregate result. 48 | # 49 | # _c(o) converts a character code to a single-character string. 50 | # _o(c) converts a single-character string to its character code (or nil). 51 | # _tlinit is true after the first call to _t(). 52 | # _tladdr is the starting address of the compressed data. 53 | # _ct is the character table. 54 | # 55 | # (Note: Unlike TextLib's P8ADVENT_LUA_PAT, this is not a format pattern.) 56 | P8ADVENT_LUA = """ 57 | _tl={a=nil,t=nil,d=nil,w=nil} 58 | function _tl:c(o) return sub(_tl.t,o+1,o+1) end 59 | function _tl:o(c) 60 | local i 61 | for i=1,#self.t do 62 | if sub(self.t,i,i)==c then return i-1 end 63 | end 64 | return 63 65 | end 66 | function _t(s) 67 | local p,r,c,n,i,a,l,bp,w,b 68 | local tid 69 | if _tl.d == nil then 70 | _tl.d={} 71 | n=bor(peek(_tl.a),shl(peek(_tl.a+1),8)) 72 | a=_tl.a+2 73 | while n>0 do 74 | p=nil 75 | i=bor(peek(a),shl(peek(a+1),8)) 76 | a+=5 77 | bp=0 78 | while i>0 do 79 | c=0 80 | w=_tl.w 81 | for bi=1,w do 82 | b=band(shr(peek(a),bp),1) 83 | c=bor(c,shl(b,bi-1)) 84 | bp+=1 85 | if bp==8 then 86 | a+=1 87 | bp=0 88 | end 89 | end 90 | r=nil 91 | if c<=(#_tl.t-1) then 92 | r=_tl:c(c) 93 | elseif _tl.d[c-#_tl.t+1]~=nil then 94 | r=_tl.d[c-#_tl.t+1] 95 | end 96 | if p~=nil and #_tl.d+#_tl.t0 do 131 | c=0 132 | w=_tl.w 133 | for bi=1,w do 134 | b=band(shr(peek(a),bp),1) 135 | c=bor(c,shl(b,bi-1)) 136 | bp+=1 137 | if bp==8 then 138 | a+=1 139 | bp=0 140 | end 141 | end 142 | l-=1 143 | if c<=(#_tl.t-1) then 144 | r=r.._tl:c(c) 145 | else 146 | r=r.._tl.d[c-#_tl.t+1] 147 | end 148 | tid+=1 149 | if tid==(2^_tl.w) then 150 | _tl.w+=1 151 | end 152 | end 153 | end 154 | return r 155 | end 156 | """ 157 | 158 | LZW_STARTING_WIDTH = 7 159 | 160 | MAX_TABLE_ENTRY_COUNT = 4096 161 | 162 | 163 | def _generate_lua(start_addr): 164 | """Generate the Lua code for the string unpacker. 165 | 166 | Args: 167 | start_addr: The starting address of the data region. 168 | 169 | Returns: 170 | The Lua code, as a string. 171 | """ 172 | # Remove leading spaces to reduce char footprint. 173 | lua = re.sub(r'\n +', '\n', P8ADVENT_LUA) 174 | 175 | return ('{}\n_tl.t="{}"\n_tl.a={}\n_tl.w={}\n_tl.mt={}\n'.format( 176 | lua, 177 | re.sub(r'"', '"..\'"\'.."', CHAR_TABLE), 178 | start_addr, 179 | LZW_STARTING_WIDTH, 180 | MAX_TABLE_ENTRY_COUNT)) 181 | 182 | 183 | class Error(Exception): 184 | """A base class for errors.""" 185 | pass 186 | 187 | 188 | class CharOutOfRange(Error): 189 | """A character was in a string that is not supported by pscii.""" 190 | def __init__(self, *args, **kwargs): 191 | self.char = kwargs.get('char') 192 | self.pos = kwargs.get('pos') 193 | super().__init__(*args, **kwargs) 194 | 195 | def __str__(self): 196 | return ('Character out of range: {}, pos:{}'.format( 197 | repr(self.char), 198 | self.pos)) 199 | 200 | 201 | class TooMuchDataError(Error): 202 | """The compressed data does not fit in the given cart data range. 203 | """ 204 | def __init__(self, msg): 205 | self._msg = msg 206 | 207 | def __str__(self): 208 | return 'Too much data: {}'.format(self._msg) 209 | 210 | 211 | def encode_pscii(s): 212 | """Encode an ASCII string as a bytestring in terms of the character table. 213 | 214 | Args: 215 | s: The Python string to encode. 216 | 217 | Returns: 218 | The bytestring of indexes into CHAR_TABLE. 219 | 220 | Raises: 221 | ValueError: The string contains a character not in CHAR_TABLE. 222 | """ 223 | result = bytearray() 224 | lower_s = s.lower() 225 | i = c = None 226 | try: 227 | for i, c in enumerate(lower_s): 228 | result.append(CHAR_TABLE.index(c)) 229 | except ValueError as e: 230 | raise CharOutOfRange(c, i) 231 | return bytes(result) 232 | 233 | 234 | class LzwLib: 235 | def __init__(self, start_addr=0, end_addr=0x4300): 236 | """Initializer. 237 | 238 | You can use arguments to customize the addresses and maximum memory 239 | ranges for the compressed data in the cart and for the lookup 240 | dictionary in RAM. 241 | 242 | Args: 243 | start_addr: The Pico-8 cart data starting address for the data. 244 | end_addr: The Pico-8 cart data ending address for the data + 1. 245 | """ 246 | self._start_addr = start_addr 247 | self._end_addr = end_addr 248 | 249 | self._string_id_map = dict() 250 | self._data = bytearray() 251 | self._dict = OrderedDict( 252 | (CHAR_TABLE[i], i) for i in range(len(CHAR_TABLE))) 253 | 254 | self._code_width = LZW_STARTING_WIDTH 255 | self._code_bit_pos = 0 256 | self._code_buffer = None 257 | 258 | def id_for_string(self, s): 259 | s = re.sub(r'\s+', ' ', s.lower()) 260 | if s not in self._string_id_map: 261 | # TODO: dict length - 1? 262 | expected_table_id = len(self._dict) 263 | expected_code_width = self._code_width 264 | 265 | self._code_buffer = bytearray() 266 | self._code_bit_pos = 0 267 | sid = self._start_addr + 2 + len(self._data) 268 | start_i = 0 269 | code_count = 0 270 | while start_i < len(s): 271 | end_i = start_i + 1 272 | while end_i < len(s) and s[start_i:end_i] in self._dict: 273 | end_i += 1 274 | created_new_entry = None 275 | if s[start_i:end_i] not in self._dict: 276 | # (Condition may or may not be false at the end of the 277 | # string, so we check.) 278 | if len(self._dict) < MAX_TABLE_ENTRY_COUNT: 279 | self._dict[s[start_i:end_i]] = len(self._dict) 280 | created_new_entry = self._dict[s[start_i:end_i]] 281 | end_i -= 1 282 | 283 | code = self._dict[s[start_i:end_i]] 284 | for i in range(self._code_width): 285 | if self._code_bit_pos == 0: 286 | self._code_buffer.append(0) 287 | self._code_buffer[-1] |= (code & 1) << self._code_bit_pos 288 | code >>= 1 289 | self._code_bit_pos = (self._code_bit_pos + 1) % 8 290 | 291 | if (created_new_entry is not None and 292 | created_new_entry == (2**self._code_width - 1)): 293 | self._code_width += 1 294 | 295 | code_count += 1 296 | start_i = end_i 297 | 298 | self._data.append(code_count & 255) 299 | self._data.append(code_count >> 8) 300 | self._data.append(expected_table_id & 255) 301 | self._data.append(expected_table_id >> 8) 302 | self._data.append(expected_code_width) 303 | self._data.extend(self._code_buffer) 304 | 305 | encoded_sid = (CHAR_TABLE_FOR_SID[sid & 63] + 306 | CHAR_TABLE_FOR_SID[(sid >> 6) & 63] + 307 | CHAR_TABLE_FOR_SID[(sid >> 12) & 63]) 308 | self._string_id_map[s] = encoded_sid 309 | 310 | return self._string_id_map[s] 311 | 312 | def as_bytes(self): 313 | """Get the binary data for the packed text. 314 | 315 | Returns: 316 | The data, as a bytearray. 317 | 318 | Raises: 319 | TooMuchDataError: The given strings do not fit into the memory 320 | ranges given to __init__. 321 | """ 322 | string_count = len(self._string_id_map) 323 | data = (bytearray([string_count & 255, string_count >> 8]) + 324 | self._data) 325 | 326 | total_string_size = sum(len(k) for k in self._string_id_map.keys()) 327 | compressed_data_size = len(data) 328 | lookup_table_count = len(self._dict) 329 | lookup_table_size = sum(len(k) for k in self._dict.keys()) 330 | print( 331 | 'DEBUG: unique string count: {string_count}\n' 332 | 'DEBUG: total unique string size: {total_string_size}\n' 333 | 'DEBUG: lookup table entry count: {lookup_table_count}\n' 334 | 'DEBUG: compressed data size: {compressed_data_size}\n' 335 | 'DEBUG: lookup table size: {lookup_table_size}\n' 336 | .format(**locals())) 337 | 338 | if len(data) > (self._end_addr - self._start_addr): 339 | raise TooMuchDataError( 340 | 'compressed data is too large: {} bytes do not fit between ' 341 | 'addresses {} and {}'.format( 342 | len(data), self._start_addr, self._end_addr)) 343 | return data 344 | 345 | def generate_lua(self): 346 | """Generate the Lua code for accessing this LzwLib. 347 | 348 | Returns: 349 | The Lua code. 350 | """ 351 | return _generate_lua(self._start_addr) 352 | -------------------------------------------------------------------------------- /p8advent/textlib.py: -------------------------------------------------------------------------------- 1 | """A text library. 2 | 3 | TextLib stores a set of strings containing English text as compact binary 4 | data. It can also generate Pico-8 Lua code capable of accessing a string given 5 | the string's string ID (returned by the encoder method). The goal is to make it 6 | easy to write Pico-8 games that use a large quantity of English text without 7 | storing that text in the code region of the cart. 8 | 9 | Strings are not encoded exactly. To save space when storing multi-word 10 | English phrases, word spaces are not stored. The generated Lua code uses 11 | rules about English phrases to calculate word spacing in the final string. 12 | 13 | See p8advent.tool for code that generates a full Pico-8 cart that replaces 14 | string literals in Lua source with string library IDs. To allow code to defer 15 | string assembly until the last minute, the code must explicitly call the t(sid) 16 | function (added during cart processing) to get the string value. String IDs 17 | are encoded as strings, and can be concatenated. (sub() also works if you're 18 | careful: each string ID is three characters long.) 19 | 20 | TextLib uses a technique similar to the one used by old 8-bit text adventure 21 | games. Words are encoded as two bytes: a prefix ID and a suffix ID. A prefix 22 | is a fixed length that you set when you instantiate TextLib, typically 1 or 2. 23 | Each prefix has a list of suffixes indexed by the suffix ID. The decoded word 24 | is simply the prefix followed by the suffix. A string is a sequence of 25 | literal non-word characters and word byte pairs. See as_bytes() for a 26 | description of the complete binary representation. 27 | 28 | (It's debatable whether this is the best way to compress a set of short English 29 | phrases for a text game. It's also debatable whether a Pico-8 text game 30 | benefits from storing its text in a compacted form in cart data vs. in the 31 | code region. And of course making a text game in Pico-8 is a dubious endeavor 32 | to begin with. I just wanted to play with this technique.) 33 | """ 34 | 35 | __all__ = ['TextLib', 'encode_pscii'] 36 | 37 | from collections import defaultdict 38 | import re 39 | import sys 40 | 41 | 42 | _WORD = re.compile(r'[a-zA-Z\']+') 43 | 44 | 45 | # A character set, which I'm going to call "pscii", consisting of all of the 46 | # characters supported by TextLib. This corresponds to all of the characters 47 | # supported by Pico-8 v0.1.3. Notable missing chars include: $ \ @ ` (I 48 | # believe 0.1.4 will add support for "\".) 49 | CHAR_TABLE = ' !"#%\'()*+,-./0123456789:;<=>?abcdefghijklmnopqrstuvwxyz[]^_{~}' 50 | 51 | 52 | # A format pattern for the Lua code to inject. This expects a format key of 53 | # "text_start_addr" equal to the RAM address where the text data begins. 54 | # 55 | # _c(o) converts a character code to a single-character string. 56 | # _o(c) converts a single-character string to its character code (or nil). 57 | # 58 | # _t(sid) calculates the string with the given ID. It uses the string jump 59 | # table to find the character and word codes for the string, then builds the 60 | # result. If the next byte has its high bit set, then it and the following 61 | # byte are the prefix and suffix ID, respectively, of a word in the word 62 | # table. Otherwise it is a character code. For a word, it finds the prefix 63 | # using the word jump table, reads the prefix at that location (of a fixed 64 | # length encoded at pos 0), then scans a list of null-terminated suffixes to 65 | # find the appropriate suffix. 66 | # 67 | # Spaces are added according to English punctuation rules: 68 | # 69 | # * a space between words: "word word" 70 | # * a space after sentence ending punctuation and closing brackets if 71 | # followed by a word: !),.?:;]} 72 | # * a space after a word if followed by opening brackets: ([{ 73 | # * double-quotes (") are treated as brackets, alternating between opening and 74 | # closing brackets 75 | # 76 | # Local variables in _t(): 77 | # 78 | # * ta: The text data start absolute address. 79 | # * r : The result accumulator. 80 | # * sids : A list of string IDs encoded as a string of three-char segments. 81 | # * sid : The (numeric, decoded) string ID. 82 | # * sc : The sentence count. 83 | # * sa : The address of the first byte of the sentence string. 84 | # This pointer is advanced during the sentence string loop. 85 | # * sae : The address of the last byte of the sentence string + 1. 86 | # * psa : The value at the sentence string pointer. 87 | # * pi : The prefix index. 88 | # * si : The suffix index. 89 | # * wa : The address of the first byte of the prefix for the word. 90 | # * pl : The prefix length. 91 | # * pli : Prefix char index (0-based). 92 | # * was : The address of the start of the word table. 93 | # * lww : True if the last string part was a word. 94 | # * lwep : True if the last string part was sentence-ending or 95 | # bracket-closing punctuation. 96 | # * qt : True if the next double-quote is bracket-closing. 97 | # 98 | # TODO: Treat ~ (61) as a paragraph break, reset double-quote state. 99 | CHAR_TABLE_LUA = re.sub(r'"', '"..\'"\'.."', CHAR_TABLE) 100 | CHAR_TABLE_LUA = re.sub(r'{', '{{', CHAR_TABLE_LUA) 101 | CHAR_TABLE_LUA = re.sub(r'}', '}}', CHAR_TABLE_LUA) 102 | P8ADVENT_LUA_PAT = ( 103 | '_ct="' + CHAR_TABLE_LUA + '"\n' + 104 | """ 105 | function _c(o) return sub(_ct,o+1,o+1) end 106 | function _o(c) 107 | local i 108 | for i=1,#_ct do 109 | if sub(_ct,i,i)==c then return i-1 end 110 | end 111 | return 63 112 | end 113 | function _t(sids) 114 | local ta={text_start_addr} 115 | local sidsi,sid,r,sc,sa,sae,psa,pi,si,wa,pl,pli,was,lww,lwep,qt 116 | pl=peek(ta) 117 | sc=bor(shl(peek(ta+2),8),peek(ta+1)) 118 | was=ta+bor(shl(peek(ta+sc*2+4),8),peek(ta+sc*2+3)) 119 | r='' 120 | lww=false 121 | lwep=false 122 | qt=false 123 | for sidsi=1,#sids,3 do 124 | sid=bor(bor(_o(sub(sids,sidsi,sidsi)), 125 | shl(_o(sub(sids,sidsi+1,sidsi+1)),6)), 126 | shl(_o(sub(sids,sidsi+2,sidsi+2)),12)) 127 | sa=ta+bor(shl(peek(ta+sid*2+4),8),peek(ta+sid*2+3)) 128 | sae=ta+bor(shl(peek(ta+(sid+1)*2+4),8),peek(ta+(sid+1)*2+3)) 129 | while sa 0) r=r.._c(peek(wa+pli)) 138 | end 139 | wa=wa+pl 140 | while si>0 do 141 | while band(peek(wa),128)~=128 and peek(wa)~=0 do wa+=1 end 142 | wa+=1 143 | si-=1 144 | end 145 | repeat 146 | if peek(wa)==0 then break end 147 | r=r.._c(band(peek(wa),127)) 148 | wa+=1 149 | until band(peek(wa-1),128)==128 150 | sa+=1 151 | lww=true 152 | lwep=false 153 | else 154 | if ((lww and ((psa==2 and qt)or(psa==6)or(psa==56)or(psa==60))) or 155 | (lwep and psa==2 and not qt)) then 156 | r=r.." " 157 | end 158 | r=r.._c(psa) 159 | lww=false 160 | lwep=((psa==2 and qt)or(psa==7)or(psa==10)or(psa==12)or(psa==24)or 161 | (psa==25)or(psa==29)or(psa==57)or(psa==62)) 162 | if (psa==2) qt=not qt 163 | end 164 | sa+=1 165 | end 166 | end 167 | return r 168 | end 169 | """) 170 | 171 | 172 | class Error(Exception): 173 | """A base class for errors.""" 174 | pass 175 | 176 | 177 | class TooManyWordsForPrefixError(Error): 178 | """There were too many words with the same prefix. 179 | 180 | If this happens, increase the prefix length and try again. 181 | """ 182 | pass 183 | 184 | 185 | def encode_pscii(s): 186 | """Encode an ASCII string as a bytestring in terms of the character table. 187 | 188 | Args: 189 | s: The Python string to encode. 190 | 191 | Returns: 192 | The bytestring of indexes into CHAR_TABLE. 193 | 194 | Raises: 195 | ValueError: The string contains a character not in CHAR_TABLE. 196 | """ 197 | result = bytearray() 198 | lower_s = s.lower() 199 | ce = None 200 | try: 201 | for c in lower_s: 202 | ce = c 203 | result.append(CHAR_TABLE.index(c)) 204 | except ValueError as e: 205 | sys.stderr.write('Character out of supported range: {}\n'.format( 206 | repr(ce))) 207 | raise 208 | return bytes(result) 209 | 210 | 211 | class TextLib: 212 | def __init__(self, prefix_length=1): 213 | self._prefix_lst = list() 214 | self._word_lib = defaultdict(list) 215 | self._string_lib_map = dict() 216 | self._string_lib_lst = list() 217 | self._prefix_length = prefix_length 218 | 219 | self._total_chars_stored = 0 220 | 221 | def _encode_word(self, w): 222 | """Encodes a word, adding it to the library if necessary. 223 | 224 | The result is a prefix index followed by a lookup index for the suffix. 225 | 226 | If a prefix index grows beyond 127 or a suffix index grows beyond 255, 227 | we raise an exception. If this happens, increase the prefix length 228 | and try again. (A test with a very large document needed a 10-bit 229 | index with a 1-byte prefix, but an 8-bit index with a 2-byte prefix.) 230 | 231 | Args: 232 | w: The word. 233 | 234 | Returns: 235 | A bytestring, either or a pscii bytestring if 236 | the word is shorter than the prefix length. 237 | """ 238 | w = encode_pscii(w) 239 | if len(w) <= self._prefix_length: 240 | w += b'\x00' * (self._prefix_length - len(w)) 241 | prefix = w 242 | suffix = b'' 243 | else: 244 | prefix = w[:self._prefix_length] 245 | suffix = w[self._prefix_length:] 246 | if prefix not in self._word_lib: 247 | self._prefix_lst.append(prefix) 248 | prefix_id = len(self._prefix_lst) - 1 249 | if prefix_id > 127: 250 | raise TooManyWordsForPrefixError() 251 | else: 252 | prefix_id = self._prefix_lst.index(prefix) 253 | 254 | if suffix in self._word_lib[prefix]: 255 | suffix_id = self._word_lib[prefix].index(suffix) 256 | else: 257 | self._word_lib[prefix].append(suffix) 258 | suffix_id = len(self._word_lib[prefix]) - 1 259 | if suffix_id > 255: 260 | raise TooManyWordsForPrefixError() 261 | 262 | # Set high bit of prefix ID. 263 | prefix_id |= 128 264 | 265 | return bytes((prefix_id, suffix_id)) 266 | 267 | def _encode_string(self, s): 268 | """Encodes the symbols of a string. 269 | 270 | Args: 271 | s: The string. 272 | 273 | Returns: 274 | The byte encoding for the string. 275 | """ 276 | result = bytearray() 277 | s_i = 0 278 | while s_i < len(s): 279 | if s[s_i] == ' ': 280 | s_i += 1 281 | continue 282 | m = _WORD.match(s[s_i:]) 283 | if not m: 284 | result.extend(encode_pscii(s[s_i])) 285 | s_i += 1 286 | continue 287 | result.extend(self._encode_word(m.group(0))) 288 | s_i += len(m.group(0)) 289 | return result 290 | 291 | def _encode_string_id(self, id): 292 | """Encodes a string ID as three pscii characters. 293 | 294 | Args: 295 | id: The numeric ID, from 0 to 65535. 296 | 297 | Returns: 298 | The three-character string encoding of the ID. 299 | """ 300 | # Add a special char to the table to make it 64 chars even. 301 | ct = CHAR_TABLE + '@' 302 | w1 = id & 63 303 | w2 = (id >> 6) & 63 304 | w3 = (id >> 12) & 63 305 | return ct[w1] + ct[w2] + ct[w3] 306 | 307 | def id_for_string(self, s): 308 | """Gets the ID for a string, adding it to the library if necessary. 309 | 310 | Args: 311 | s: The string. 312 | 313 | Returns: 314 | The string ID, encoded as a three-character pscii string. 315 | """ 316 | s = re.sub(r'\s+', ' ', s) 317 | self._total_chars_stored += len(s) # for stats 318 | if s not in self._string_lib_map: 319 | self._string_lib_lst.append(self._encode_string(s)) 320 | self._string_lib_map[s] = len(self._string_lib_lst) - 1 321 | return self._encode_string_id(self._string_lib_map[s]) 322 | 323 | def as_bytes(self): 324 | """Dump the entire library in its byte encoding. 325 | 326 | The prefix length and table sizes are not encoded. It is expected 327 | that the generated access code will stay within expected ranges. 328 | TODO: This is dumb. I'm passing these values into the generated Lua, 329 | might as well store them with the bytes. 330 | 331 | 0: The prefix length. 332 | 1 - 2: The number of sentences, S, LSB first. 333 | 3 - 2*S+2: The string jump table, each entry as an offset from pos 0, 334 | two bytes each, LSB first. 335 | 2*S+3 - 2*S+4: The offset of the byte following the last byte of string 336 | S, LSB first. This serves two purposes: it allows the string reader 337 | to read two offsets from the jump table to get the length, and it's 338 | the offset of the word lookup table (W_addr). 339 | 2*S+5 - ...: Encoded strings. If a byte has its high bit set, then it is 340 | a word prefix offset and the next byte is the suffix offset. 341 | Otherwise a given byte is a pscii code. Word spaces are omitted, and 342 | are up to the renderer to provide according to English punctuation 343 | rules. 344 | W_addr - W_addr+2*W-1: The prefix jump table, each entry as an offset 345 | from pos 0, two bytes each, LSB first. 346 | W_addr+2*W - ...: Word entries, null terminated. Each entry starts with 347 | the prefix (in pscii) followed by all of the suffixes (in pscii). 348 | Each suffix's final character has its high bit set. 349 | 350 | Returns: 351 | A bytearray. 352 | """ 353 | longest_string_size = 0 354 | most_lookup_entries_count = 0 355 | total_lookup_entries_count = 0 356 | longest_suffix_size = 0 357 | 358 | string_offset_list = [0] 359 | string_data = bytearray() 360 | for s in self._string_lib_lst: 361 | string_data.extend(s) 362 | string_offset_list.append(len(string_data)) 363 | if len(string_data) > longest_string_size: 364 | longest_string_size = len(string_data) 365 | string_table_offset = 3 + 2 * len(self._string_lib_lst) + 2 366 | string_jump_tbl = bytearray() 367 | for e in string_offset_list: 368 | v = string_table_offset + e 369 | if v >= 65536: 370 | raise TooManyWordsForPrefixError() 371 | string_jump_tbl.append(v & 255) 372 | string_jump_tbl.append(v >> 8) 373 | 374 | lookup_offset_list = [0] 375 | lookup_data = bytearray() 376 | for p in self._prefix_lst: 377 | lookup_data.extend(p) 378 | for suffix in self._word_lib[p]: 379 | if len(suffix) > 0: 380 | lookup_data.extend(suffix) 381 | lookup_data[-1] |= 0x80 382 | else: 383 | lookup_data.append(0) 384 | if len(suffix) > longest_suffix_size: 385 | longest_suffix_size = len(suffix) 386 | lookup_offset_list.append(len(lookup_data)) 387 | if len(self._word_lib[p]) > most_lookup_entries_count: 388 | most_lookup_entries_count = len(self._word_lib[p]) 389 | total_lookup_entries_count += len(self._word_lib[p]) 390 | lookup_table_offset = (3 + len(string_jump_tbl) + len(string_data) + 391 | 2 * len(self._prefix_lst)) 392 | lookup_prefix_tbl = bytearray() 393 | # We don't need the offset past the last lookup: 394 | lookup_offset_list.pop() 395 | for e in lookup_offset_list: 396 | v = lookup_table_offset + e 397 | if v >= 65536: 398 | raise TooManyWordsForPrefixError() 399 | lookup_prefix_tbl.append(v & 255) 400 | lookup_prefix_tbl.append(v >> 8) 401 | 402 | num_of_strings = len(self._string_lib_lst) 403 | num_of_prefixes = len(self._prefix_lst) 404 | 405 | # TODO: remove these, or make them an official feature: 406 | print('DEBUG: num_of_strings = {}'.format(num_of_strings)) 407 | print('DEBUG: num_of_prefixes = {}'.format(num_of_prefixes)) 408 | print('DEBUG: longest_string_size = {}'.format(longest_string_size)) 409 | print('DEBUG: longest_suffix_size = {}'.format(longest_suffix_size)) 410 | print('DEBUG: most_lookup_entries_count = {}'.format(most_lookup_entries_count)) 411 | print('DEBUG: total_lookup_entries_count = {}'.format(total_lookup_entries_count)) 412 | print('DEBUG: original text size = {}'.format(self._total_chars_stored)) 413 | print('DEBUG: total text lib size = {}'.format(len(string_jump_tbl) + 414 | len(string_data) + 415 | len(lookup_prefix_tbl) + 416 | len(lookup_data))) 417 | 418 | return bytes(bytearray([self._prefix_length, 419 | len(self._string_lib_lst) & 255, 420 | len(self._string_lib_lst) >> 8]) + 421 | string_jump_tbl + 422 | string_data + 423 | lookup_prefix_tbl + 424 | lookup_data) 425 | 426 | def generate_lua(self, text_start_addr=0): 427 | """Generate the Lua code for accessing this TextLib. 428 | 429 | Args: 430 | text_start_addr: The starting address for the text bytes region. 431 | """ 432 | return P8ADVENT_LUA_PAT.format(text_start_addr=text_start_addr) 433 | -------------------------------------------------------------------------------- /p8advent/tool.py: -------------------------------------------------------------------------------- 1 | """The main routines for the command-line tool. 2 | 3 | This tool processes a Lua source file into a Pico-8 cart. It adds a simple 4 | syntax rule for string literals: If a string literal is immediately preceded 5 | by a star (*), the string is added to a text lib data structure, and the 6 | string literal is replaced with an encoded string ID (also a string). To get 7 | the original string, the Lua code must call the _t() function (added to the 8 | Lua code by the tool) and pass it the string ID. For example: 9 | 10 | function _update() 11 | msg = *"check it out everyone it's the apocalypse!" 12 | print(_t(msg)) 13 | end 14 | 15 | This becomes: 16 | 17 | function _update() 18 | msg = " " 19 | print(_t(msg)) 20 | end 21 | function _t(sid) 22 | ... 23 | end 24 | 25 | The string data is moved into the graphics region of the Pico-8 cart. The _t() 26 | function uses the encoded ID to locate and unpack the string. 27 | """ 28 | 29 | __all__ = ['main'] 30 | 31 | import argparse 32 | import textwrap 33 | 34 | from pico8.game import game 35 | from pico8.lua import lexer 36 | 37 | from . import lzwlib 38 | 39 | 40 | # To preserve a portion of the gfx region, use 512 * number of sprite rows. 41 | TEXT_START_ADDR = 0 42 | 43 | # To preserve the song/sfx region, use 0x3100. 44 | TEXT_END_ADDR = 0x4300 45 | 46 | 47 | def _get_argparser(): 48 | """Builds and returns the argument parser.""" 49 | parser = argparse.ArgumentParser( 50 | formatter_class=argparse.RawDescriptionHelpFormatter, 51 | usage='%(prog)s [--help] --lua ', 52 | description=textwrap.dedent(''' 53 | ''')) 54 | parser.add_argument( 55 | '--lua', type=str, required=True, 56 | help='the annotated Lua code for the game') 57 | parser.add_argument( 58 | '--startaddr', type=int, default=TEXT_START_ADDR, 59 | help='the Pico-8 cart address to put the text data') 60 | parser.add_argument( 61 | '--endaddr', type=int, default=TEXT_END_ADDR, 62 | help='the Pico-8 cart address the the text data must end; if the text' 63 | 'data is too long, this tool reports an error') 64 | return parser 65 | 66 | 67 | def main(orig_args): 68 | arg_parser = _get_argparser() 69 | args = arg_parser.parse_args(args=orig_args) 70 | 71 | assert args.lua.endswith('.lua') 72 | game_fname = args.lua[:-len('.lua')] + '.p8' 73 | 74 | my_game = game.Game.make_empty_game(filename=game_fname) 75 | my_lexer = lexer.Lexer(version=4) 76 | with open(args.lua) as lua_fh: 77 | my_lexer.process_lines(lua_fh) 78 | 79 | my_textlib = lzwlib.LzwLib(start_addr=args.startaddr, end_addr=args.endaddr) 80 | 81 | saw_star = False 82 | for i, token in enumerate(my_lexer._tokens): 83 | if token.matches(lexer.TokSymbol('*')): 84 | saw_star = True 85 | elif token.matches(lexer.TokString) and saw_star: 86 | sid = my_textlib.id_for_string(token.value) 87 | my_lexer._tokens[i-1] = lexer.TokSpace('') 88 | my_lexer._tokens[i] = lexer.TokString(sid) 89 | saw_star = False 90 | else: 91 | saw_star = False 92 | 93 | textlib_lua = my_textlib.generate_lua() 94 | my_lexer.process_lines(l+'\n' for l in textlib_lua.split('\n')) 95 | 96 | my_game.lua._lexer = my_lexer 97 | my_game.lua._parser.process_tokens(my_game.lua._lexer.tokens) 98 | 99 | text_bytes = my_textlib.as_bytes() 100 | my_game.write_cart_data(text_bytes, args.startaddr) 101 | 102 | with open(game_fname, 'w') as outstr: 103 | my_game.to_p8_file(outstr, filename=game_fname) 104 | 105 | return 0 106 | -------------------------------------------------------------------------------- /tests/testdata/pager.lua: -------------------------------------------------------------------------------- 1 | printer = nil 2 | msgs = { 3 | 4 | *"Book the First--Recalled to Life", 5 | 6 | *"I. The Period", 7 | 8 | *"It was the best of times, it was the worst of times, 9 | it was the age of wisdom, it was the age of foolishness, 10 | it was the epoch of belief, it was the epoch of incredulity, 11 | it was the season of Light, it was the season of Darkness, 12 | it was the spring of hope, it was the winter of despair, 13 | we had everything before us, we had nothing before us, 14 | we were all going direct to Heaven, 15 | we were all going direct the other way-- 16 | in short, the period was so far like the present period, that some of 17 | its noisiest authorities insisted on its being received, for good or for 18 | evil, in the superlative degree of comparison only.", 19 | 20 | *"There were a king with a large jaw and a queen with a plain face, on the 21 | throne of England; there were a king with a large jaw and a queen with 22 | a fair face, on the throne of France. In both countries it was clearer 23 | than crystal to the lords of the State preserves of loaves and fishes, 24 | that things in general were settled for ever.", 25 | 26 | *"It was the year of Our Lord one thousand seven hundred and seventy-five. 27 | Spiritual revelations were conceded to England at that favoured period, 28 | as at this. Mrs. Southcott had recently attained her five-and-twentieth 29 | blessed birthday, of whom a prophetic private in the Life Guards had 30 | heralded the sublime appearance by announcing that arrangements were 31 | made for the swallowing up of London and Westminster. Even the Cock-lane 32 | ghost had been laid only a round dozen of years, after rapping out its 33 | messages, as the spirits of this very year last past (supernaturally 34 | deficient in originality) rapped out theirs. Mere messages in the 35 | earthly order of events had lately come to the English Crown and People, 36 | from a congress of British subjects in America: which, strange 37 | to relate, have proved more important to the human race than any 38 | communications yet received through any of the chickens of the Cock-lane 39 | brood.", 40 | 41 | *"France, less favoured on the whole as to matters spiritual than her 42 | sister of the shield and trident, rolled with exceeding smoothness down 43 | hill, making paper money and spending it. Under the guidance of her 44 | Christian pastors, she entertained herself, besides, with such humane 45 | achievements as sentencing a youth to have his hands cut off, his tongue 46 | torn out with pincers, and his body burned alive, because he had not 47 | kneeled down in the rain to do honour to a dirty procession of monks 48 | which passed within his view, at a distance of some fifty or sixty 49 | yards. It is likely enough that, rooted in the woods of France and 50 | Norway, there were growing trees, when that sufferer was put to death, 51 | already marked by the Woodman, Fate, to come down and be sawn into 52 | boards, to make a certain movable framework with a sack and a knife in 53 | it, terrible in history. It is likely enough that in the rough outhouses 54 | of some tillers of the heavy lands adjacent to Paris, there were 55 | sheltered from the weather that very day, rude carts, bespattered with 56 | rustic mire, snuffed about by pigs, and roosted in by poultry, which 57 | the Farmer, Death, had already set apart to be his tumbrils of 58 | the Revolution. But that Woodman and that Farmer, though they work 59 | unceasingly, work silently, and no one heard them as they went about 60 | with muffled tread: the rather, forasmuch as to entertain any suspicion 61 | that they were awake, was to be atheistical and traitorous.", 62 | 63 | *"In England, there was scarcely an amount of order and protection to 64 | justify much national boasting. Daring burglaries by armed men, and 65 | highway robberies, took place in the capital itself every night; 66 | families were publicly cautioned not to go out of town without removing 67 | their furniture to upholsterers' warehouses for security; the highwayman 68 | in the dark was a City tradesman in the light, and, being recognised and 69 | challenged by his fellow-tradesman whom he stopped in his character of 70 | \"the Captain,\" gallantly shot him through the head and rode away; the 71 | mail was waylaid by seven robbers, and the guard shot three dead, and 72 | then got shot dead himself by the other four, \"in consequence of the 73 | failure of his ammunition:\" after which the mail was robbed in peace; 74 | that magnificent potentate, the Lord Mayor of London, was made to stand 75 | and deliver on Turnham Green, by one highwayman, who despoiled the 76 | illustrious creature in sight of all his retinue; prisoners in London 77 | gaols fought battles with their turnkeys, and the majesty of the law 78 | fired blunderbusses in among them, loaded with rounds of shot and ball; 79 | thieves snipped off diamond crosses from the necks of noble lords at 80 | Court drawing-rooms; musketeers went into St. Giles's, to search 81 | for contraband goods, and the mob fired on the musketeers, and the 82 | musketeers fired on the mob, and nobody thought any of these occurrences 83 | much out of the common way. In the midst of them, the hangman, ever busy 84 | and ever worse than useless, was in constant requisition; now, stringing 85 | up long rows of miscellaneous criminals; now, hanging a housebreaker on 86 | Saturday who had been taken on Tuesday; now, burning people in the 87 | hand at Newgate by the dozen, and now burning pamphlets at the door of 88 | Westminster Hall; to-day, taking the life of an atrocious murderer, 89 | and to-morrow of a wretched pilferer who had robbed a farmer's boy of 90 | sixpence.", 91 | 92 | *"All these things, and a thousand like them, came to pass in and close 93 | upon the dear old year one thousand seven hundred and seventy-five. 94 | Environed by them, while the Woodman and the Farmer worked unheeded, 95 | those two of the large jaws, and those other two of the plain and the 96 | fair faces, trod with stir enough, and carried their divine rights 97 | with a high hand. Thus did the year one thousand seven hundred 98 | and seventy-five conduct their Greatnesses, and myriads of small 99 | creatures--the creatures of this chronicle among the rest--along the 100 | roads that lay before them.", 101 | 102 | *"II. The Mail", 103 | 104 | *"It was the Dover road that lay, on a Friday night late in November, 105 | before the first of the persons with whom this history has business. 106 | The Dover road lay, as to him, beyond the Dover mail, as it lumbered up 107 | Shooter's Hill. He walked up hill in the mire by the side of the mail, 108 | as the rest of the passengers did; not because they had the least relish 109 | for walking exercise, under the circumstances, but because the hill, 110 | and the harness, and the mud, and the mail, were all so heavy, that the 111 | horses had three times already come to a stop, besides once drawing the 112 | coach across the road, with the mutinous intent of taking it back 113 | to Blackheath. Reins and whip and coachman and guard, however, in 114 | combination, had read that article of war which forbade a purpose 115 | otherwise strongly in favour of the argument, that some brute animals 116 | are endued with Reason; and the team had capitulated and returned to 117 | their duty.", 118 | 119 | *"With drooping heads and tremulous tails, they mashed their way through 120 | the thick mud, floundering and stumbling between whiles, as if they were 121 | falling to pieces at the larger joints. As often as the driver rested 122 | them and brought them to a stand, with a wary \"Wo-ho! so-ho-then!\" the 123 | near leader violently shook his head and everything upon it--like an 124 | unusually emphatic horse, denying that the coach could be got up the 125 | hill. Whenever the leader made this rattle, the passenger started, as a 126 | nervous passenger might, and was disturbed in mind.", 127 | 128 | *"There was a steaming mist in all the hollows, and it had roamed in its 129 | forlornness up the hill, like an evil spirit, seeking rest and finding 130 | none. A clammy and intensely cold mist, it made its slow way through the 131 | air in ripples that visibly followed and overspread one another, as the 132 | waves of an unwholesome sea might do. It was dense enough to shut out 133 | everything from the light of the coach-lamps but these its own workings, 134 | and a few yards of road; and the reek of the labouring horses steamed 135 | into it, as if they had made it all.", 136 | 137 | *"Two other passengers, besides the one, were plodding up the hill by the 138 | side of the mail. All three were wrapped to the cheekbones and over the 139 | ears, and wore jack-boots. Not one of the three could have said, from 140 | anything he saw, what either of the other two was like; and each was 141 | hidden under almost as many wrappers from the eyes of the mind, as from 142 | the eyes of the body, of his two companions. In those days, travellers 143 | were very shy of being confidential on a short notice, for anybody on 144 | the road might be a robber or in league with robbers. As to the latter, 145 | when every posting-house and ale-house could produce somebody in 146 | \"the Captain's\" pay, ranging from the landlord to the lowest stable 147 | non-descript, it was the likeliest thing upon the cards. So the guard 148 | of the Dover mail thought to himself, that Friday night in November, one 149 | thousand seven hundred and seventy-five, lumbering up Shooter's Hill, as 150 | he stood on his own particular perch behind the mail, beating his feet, 151 | and keeping an eye and a hand on the arm-chest before him, where a 152 | loaded blunderbuss lay at the top of six or eight loaded horse-pistols, 153 | deposited on a substratum of cutlass.", 154 | 155 | 156 | 157 | *"The Dover mail was in its usual genial position that the guard suspected 158 | the passengers, the passengers suspected one another and the guard, they 159 | all suspected everybody else, and the coachman was sure of nothing but 160 | the horses; as to which cattle he could with a clear conscience have 161 | taken his oath on the two Testaments that they were not fit for the 162 | journey.", 163 | 164 | *"\"Wo-ho!\" said the coachman. \"So, then! One more pull and you're at the 165 | top and be damned to you, for I have had trouble enough to get you to 166 | it!--Joe!\"", 167 | 168 | *"\"Halloa!\" the guard replied.", 169 | 170 | *"\"What o'clock do you make it, Joe?\"", 171 | 172 | *"\"Ten minutes, good, past eleven.\"", 173 | 174 | *"\"My blood!\" ejaculated the vexed coachman, \"and not atop of Shooter's 175 | yet! Tst! Yah! Get on with you!\"", 176 | 177 | *"The emphatic horse, cut short by the whip in a most decided negative, 178 | made a decided scramble for it, and the three other horses followed 179 | suit. Once more, the Dover mail struggled on, with the jack-boots of its 180 | passengers squashing along by its side. They had stopped when the coach 181 | stopped, and they kept close company with it. If any one of the three 182 | had had the hardihood to propose to another to walk on a little ahead 183 | into the mist and darkness, he would have put himself in a fair way of 184 | getting shot instantly as a highwayman.", 185 | 186 | *"The last burst carried the mail to the summit of the hill. The horses 187 | stopped to breathe again, and the guard got down to skid the wheel for 188 | the descent, and open the coach-door to let the passengers in.", 189 | 190 | *"\"Tst! Joe!\" cried the coachman in a warning voice, looking down from his 191 | box.", 192 | 193 | *"\"What do you say, Tom?\"", 194 | 195 | *"They both listened.", 196 | 197 | *"\"I say a horse at a canter coming up, Joe.\"", 198 | 199 | *"\"_I_ say a horse at a gallop, Tom,\" returned the guard, leaving his hold 200 | of the door, and mounting nimbly to his place. \"Gentlemen! In the king's 201 | name, all of you!\"", 202 | 203 | *"With this hurried adjuration, he cocked his blunderbuss, and stood on 204 | the offensive.", 205 | 206 | *"The passenger booked by this history, was on the coach-step, getting in; 207 | the two other passengers were close behind him, and about to follow. He 208 | remained on the step, half in the coach and half out of; they remained 209 | in the road below him. They all looked from the coachman to the guard, 210 | and from the guard to the coachman, and listened. The coachman looked 211 | back and the guard looked back, and even the emphatic leader pricked up 212 | his ears and looked back, without contradicting.", 213 | 214 | *"The stillness consequent on the cessation of the rumbling and labouring 215 | of the coach, added to the stillness of the night, made it very quiet 216 | indeed. The panting of the horses communicated a tremulous motion to 217 | the coach, as if it were in a state of agitation. The hearts of the 218 | passengers beat loud enough perhaps to be heard; but at any rate, the 219 | quiet pause was audibly expressive of people out of breath, and holding 220 | the breath, and having the pulses quickened by expectation.", 221 | 222 | *"The sound of a horse at a gallop came fast and furiously up the hill.", 223 | 224 | *"\"So-ho!\" the guard sang out, as loud as he could roar. \"Yo there! Stand! 225 | I shall fire!\"", 226 | 227 | *"The pace was suddenly checked, and, with much splashing and floundering, 228 | a man's voice called from the mist, \"Is that the Dover mail?\"", 229 | 230 | *"\"Never you mind what it is!\" the guard retorted. \"What are you?\"", 231 | 232 | *"\"_Is_ that the Dover mail?\"", 233 | 234 | *"\"Why do you want to know?\"", 235 | 236 | *"\"I want a passenger, if it is.\"", 237 | 238 | *"\"What passenger?\"", 239 | 240 | *"\"Mr. Jarvis Lorry.\"", 241 | 242 | *"Our booked passenger showed in a moment that it was his name. The guard, 243 | the coachman, and the two other passengers eyed him distrustfully.", 244 | 245 | *"\"Keep where you are,\" the guard called to the voice in the mist, 246 | \"because, if I should make a mistake, it could never be set right in 247 | your lifetime. Gentleman of the name of Lorry answer straight.\"", 248 | 249 | *"\"What is the matter?\" asked the passenger, then, with mildly quavering 250 | speech. \"Who wants me? Is it Jerry?\"", 251 | 252 | *"(\"I don't like Jerry's voice, if it is Jerry,\" growled the guard to 253 | himself. \"He's hoarser than suits me, is Jerry.\")", 254 | 255 | *"\"Yes, Mr. Lorry.\"", 256 | 257 | *"\"What is the matter?\"", 258 | 259 | *"\"A despatch sent after you from over yonder. T. and Co.\"", 260 | 261 | *"\"I know this messenger, guard,\" said Mr. Lorry, getting down into the 262 | road--assisted from behind more swiftly than politely by the other two 263 | passengers, who immediately scrambled into the coach, shut the door, and 264 | pulled up the window. \"He may come close; there's nothing wrong.\"", 265 | 266 | *"\"I hope there ain't, but I can't make so 'Nation sure of that,\" said the 267 | guard, in gruff soliloquy. \"Hallo you!\"", 268 | 269 | *"\"Well! And hallo you!\" said Jerry, more hoarsely than before.", 270 | 271 | *"\"Come on at a footpace! d'ye mind me? And if you've got holsters to that 272 | saddle o' yourn, don't let me see your hand go nigh 'em. For I'm a devil 273 | at a quick mistake, and when I make one it takes the form of Lead. So 274 | now let's look at you.\"", 275 | 276 | *"The figures of a horse and rider came slowly through the eddying mist, 277 | and came to the side of the mail, where the passenger stood. The rider 278 | stooped, and, casting up his eyes at the guard, handed the passenger 279 | a small folded paper. The rider's horse was blown, and both horse and 280 | rider were covered with mud, from the hoofs of the horse to the hat of 281 | the man.", 282 | 283 | *"\"Guard!\" said the passenger, in a tone of quiet business confidence.", 284 | 285 | *"The watchful guard, with his right hand at the stock of his raised 286 | blunderbuss, his left at the barrel, and his eye on the horseman, 287 | answered curtly, \"Sir.\"", 288 | 289 | *"\"There is nothing to apprehend. I belong to Tellson's Bank. You must 290 | know Tellson's Bank in London. I am going to Paris on business. A crown 291 | to drink. I may read this?\"", 292 | 293 | *"\"If so be as you're quick, sir.\"", 294 | 295 | *"He opened it in the light of the coach-lamp on that side, and 296 | read--first to himself and then aloud: \"'Wait at Dover for Mam'selle.' 297 | It's not long, you see, guard. Jerry, say that my answer was, RECALLED 298 | TO LIFE.\"", 299 | 300 | *"Jerry started in his saddle. \"That's a Blazing strange answer, too,\" 301 | said he, at his hoarsest.", 302 | 303 | *"\"Take that message back, and they will know that I received this, as 304 | well as if I wrote. Make the best of your way. Good night.\"", 305 | 306 | *"With those words the passenger opened the coach-door and got in; not at 307 | all assisted by his fellow-passengers, who had expeditiously secreted 308 | their watches and purses in their boots, and were now making a general 309 | pretence of being asleep. With no more definite purpose than to escape 310 | the hazard of originating any other kind of action.", 311 | 312 | *"The coach lumbered on again, with heavier wreaths of mist closing round 313 | it as it began the descent. The guard soon replaced his blunderbuss 314 | in his arm-chest, and, having looked to the rest of its contents, and 315 | having looked to the supplementary pistols that he wore in his belt, 316 | looked to a smaller chest beneath his seat, in which there were a 317 | few smith's tools, a couple of torches, and a tinder-box. For he was 318 | furnished with that completeness that if the coach-lamps had been blown 319 | and stormed out, which did occasionally happen, he had only to shut 320 | himself up inside, keep the flint and steel sparks well off the straw, 321 | and get a light with tolerable safety and ease (if he were lucky) in 322 | five minutes.", 323 | 324 | *"\"Tom!\" softly over the coach roof.", 325 | 326 | *"\"Hallo, Joe.\"", 327 | 328 | *"\"Did you hear the message?\"", 329 | 330 | *"\"I did, Joe.\"", 331 | 332 | *"\"What did you make of it, Tom?\"", 333 | 334 | *"\"Nothing at all, Joe.\"", 335 | 336 | *"\"That's a coincidence, too,\" the guard mused, \"for I made the same of it 337 | myself.\"", 338 | 339 | *"Jerry, left alone in the mist and darkness, dismounted meanwhile, not 340 | only to ease his spent horse, but to wipe the mud from his face, and 341 | shake the wet out of his hat-brim, which might be capable of 342 | holding about half a gallon. After standing with the bridle over his 343 | heavily-splashed arm, until the wheels of the mail were no longer within 344 | hearing and the night was quite still again, he turned to walk down the 345 | hill.", 346 | 347 | *"\"After that there gallop from Temple Bar, old lady, I won't trust your 348 | fore-legs till I get you on the level,\" said this hoarse messenger, 349 | glancing at his mare. \"'Recalled to life.' That's a Blazing strange 350 | message. Much of that wouldn't do for you, Jerry! I say, Jerry! You'd 351 | be in a Blazing bad way, if recalling to life was to come into fashion, 352 | Jerry!\"", 353 | 354 | *"III. The Night Shadows", 355 | 356 | *"A wonderful fact to reflect upon, that every human creature is 357 | constituted to be that profound secret and mystery to every other. A 358 | solemn consideration, when I enter a great city by night, that every 359 | one of those darkly clustered houses encloses its own secret; that every 360 | room in every one of them encloses its own secret; that every beating 361 | heart in the hundreds of thousands of breasts there, is, in some of 362 | its imaginings, a secret to the heart nearest it! Something of the 363 | awfulness, even of Death itself, is referable to this. No more can I 364 | turn the leaves of this dear book that I loved, and vainly hope in time 365 | to read it all. No more can I look into the depths of this unfathomable 366 | water, wherein, as momentary lights glanced into it, I have had glimpses 367 | of buried treasure and other things submerged. It was appointed that the 368 | book should shut with a spring, for ever and for ever, when I had read 369 | but a page. It was appointed that the water should be locked in an 370 | eternal frost, when the light was playing on its surface, and I stood 371 | in ignorance on the shore. My friend is dead, my neighbour is dead, 372 | my love, the darling of my soul, is dead; it is the inexorable 373 | consolidation and perpetuation of the secret that was always in that 374 | individuality, and which I shall carry in mine to my life's end. In 375 | any of the burial-places of this city through which I pass, is there 376 | a sleeper more inscrutable than its busy inhabitants are, in their 377 | innermost personality, to me, or than I am to them?", 378 | 379 | *"As to this, his natural and not to be alienated inheritance, the 380 | messenger on horseback had exactly the same possessions as the King, the 381 | first Minister of State, or the richest merchant in London. So with the 382 | three passengers shut up in the narrow compass of one lumbering old mail 383 | coach; they were mysteries to one another, as complete as if each had 384 | been in his own coach and six, or his own coach and sixty, with the 385 | breadth of a county between him and the next.", 386 | 387 | *"The messenger rode back at an easy trot, stopping pretty often at 388 | ale-houses by the way to drink, but evincing a tendency to keep his 389 | own counsel, and to keep his hat cocked over his eyes. He had eyes that 390 | assorted very well with that decoration, being of a surface black, with 391 | no depth in the colour or form, and much too near together--as if they 392 | were afraid of being found out in something, singly, if they kept too 393 | far apart. They had a sinister expression, under an old cocked-hat like 394 | a three-cornered spittoon, and over a great muffler for the chin and 395 | throat, which descended nearly to the wearer's knees. When he stopped 396 | for drink, he moved this muffler with his left hand, only while he 397 | poured his liquor in with his right; as soon as that was done, he 398 | muffled again.", 399 | 400 | *"\"No, Jerry, no!\" said the messenger, harping on one theme as he rode. 401 | \"It wouldn't do for you, Jerry. Jerry, you honest tradesman, it wouldn't 402 | suit _your_ line of business! Recalled--! Bust me if I don't think he'd 403 | been a drinking!\"", 404 | 405 | *"His message perplexed his mind to that degree that he was fain, several 406 | times, to take off his hat to scratch his head. Except on the crown, 407 | which was raggedly bald, he had stiff, black hair, standing jaggedly all 408 | over it, and growing down hill almost to his broad, blunt nose. It was 409 | so like Smith's work, so much more like the top of a strongly spiked 410 | wall than a head of hair, that the best of players at leap-frog might 411 | have declined him, as the most dangerous man in the world to go over.", 412 | 413 | *"While he trotted back with the message he was to deliver to the night 414 | watchman in his box at the door of Tellson's Bank, by Temple Bar, who 415 | was to deliver it to greater authorities within, the shadows of the 416 | night took such shapes to him as arose out of the message, and took such 417 | shapes to the mare as arose out of _her_ private topics of uneasiness. 418 | They seemed to be numerous, for she shied at every shadow on the road.", 419 | 420 | *"What time, the mail-coach lumbered, jolted, rattled, and bumped upon 421 | its tedious way, with its three fellow-inscrutables inside. To whom, 422 | likewise, the shadows of the night revealed themselves, in the forms 423 | their dozing eyes and wandering thoughts suggested.", 424 | 425 | *"Tellson's Bank had a run upon it in the mail. As the bank 426 | passenger--with an arm drawn through the leathern strap, which did what 427 | lay in it to keep him from pounding against the next passenger, 428 | and driving him into his corner, whenever the coach got a special 429 | jolt--nodded in his place, with half-shut eyes, the little 430 | coach-windows, and the coach-lamp dimly gleaming through them, and the 431 | bulky bundle of opposite passenger, became the bank, and did a great 432 | stroke of business. The rattle of the harness was the chink of money, 433 | and more drafts were honoured in five minutes than even Tellson's, with 434 | all its foreign and home connection, ever paid in thrice the time. Then 435 | the strong-rooms underground, at Tellson's, with such of their valuable 436 | stores and secrets as were known to the passenger (and it was not a 437 | little that he knew about them), opened before him, and he went in among 438 | them with the great keys and the feebly-burning candle, and found them 439 | safe, and strong, and sound, and still, just as he had last seen them.", 440 | 441 | *"But, though the bank was almost always with him, and though the coach 442 | (in a confused way, like the presence of pain under an opiate) was 443 | always with him, there was another current of impression that never 444 | ceased to run, all through the night. He was on his way to dig some one 445 | out of a grave.", 446 | 447 | *"Now, which of the multitude of faces that showed themselves before him 448 | was the true face of the buried person, the shadows of the night did 449 | not indicate; but they were all the faces of a man of five-and-forty by 450 | years, and they differed principally in the passions they expressed, 451 | and in the ghastliness of their worn and wasted state. Pride, contempt, 452 | defiance, stubbornness, submission, lamentation, succeeded one another; 453 | so did varieties of sunken cheek, cadaverous colour, emaciated hands 454 | and figures. But the face was in the main one face, and every head was 455 | prematurely white. A hundred times the dozing passenger inquired of this 456 | spectre:", 457 | 458 | *"\"Buried how long?\"", 459 | 460 | *"The answer was always the same: \"Almost eighteen years.\"", 461 | 462 | *"\"You had abandoned all hope of being dug out?\"", 463 | 464 | *"\"Long ago.\"", 465 | 466 | *"\"You know that you are recalled to life?\"", 467 | 468 | *"\"They tell me so.\"", 469 | 470 | *"\"I hope you care to live?\"", 471 | 472 | *"\"I can't say.\"", 473 | 474 | *"\"Shall I show her to you? Will you come and see her?\"", 475 | 476 | *"The answers to this question were various and contradictory. Sometimes 477 | the broken reply was, \"Wait! It would kill me if I saw her too soon.\" 478 | Sometimes, it was given in a tender rain of tears, and then it was, 479 | \"Take me to her.\" Sometimes it was staring and bewildered, and then it 480 | was, \"I don't know her. I don't understand.\"", 481 | 482 | *"After such imaginary discourse, the passenger in his fancy would dig, 483 | and dig, dig--now with a spade, now with a great key, now with his 484 | hands--to dig this wretched creature out. Got out at last, with earth 485 | hanging about his face and hair, he would suddenly fan away to dust. The 486 | passenger would then start to himself, and lower the window, to get the 487 | reality of mist and rain on his cheek.", 488 | 489 | *"Yet even when his eyes were opened on the mist and rain, on the moving 490 | patch of light from the lamps, and the hedge at the roadside retreating 491 | by jerks, the night shadows outside the coach would fall into the train 492 | of the night shadows within. The real Banking-house by Temple Bar, the 493 | real business of the past day, the real strong rooms, the real express 494 | sent after him, and the real message returned, would all be there. Out 495 | of the midst of them, the ghostly face would rise, and he would accost 496 | it again.", 497 | 498 | *"\"Buried how long?\"", 499 | 500 | *"\"Almost eighteen years.\"", 501 | 502 | *"\"I hope you care to live?\"", 503 | 504 | *"\"I can't say.\"", 505 | 506 | *"Dig--dig--dig--until an impatient movement from one of the two 507 | passengers would admonish him to pull up the window, draw his arm 508 | securely through the leathern strap, and speculate upon the two 509 | slumbering forms, until his mind lost its hold of them, and they again 510 | slid away into the bank and the grave.", 511 | 512 | *"\"Buried how long?\"", 513 | 514 | *"\"Almost eighteen years.\"", 515 | 516 | *"\"You had abandoned all hope of being dug out?\"", 517 | 518 | *"\"Long ago.\"", 519 | 520 | *"The words were still in his hearing as just spoken--distinctly in 521 | his hearing as ever spoken words had been in his life--when the weary 522 | passenger started to the consciousness of daylight, and found that the 523 | shadows of the night were gone.", 524 | 525 | *"He lowered the window, and looked out at the rising sun. There was a 526 | ridge of ploughed land, with a plough upon it where it had been left 527 | last night when the horses were unyoked; beyond, a quiet coppice-wood, 528 | in which many leaves of burning red and golden yellow still remained 529 | upon the trees. Though the earth was cold and wet, the sky was clear, 530 | and the sun rose bright, placid, and beautiful.", 531 | 532 | *"\"Eighteen years!\" said the passenger, looking at the sun. \"Gracious 533 | Creator of day! To be buried alive for eighteen years!\"", 534 | 535 | *"... To be continued!" 536 | 537 | } 538 | 539 | msg_i = 0 540 | print_speed = 2.5 -- chars per frame; can be fractional 541 | print_speed_ctr = 0 542 | 543 | function new_printer() 544 | local printer = { 545 | cur_x = 0, 546 | cur_y = 0, 547 | cur_str = nil, 548 | cur_pos = nil, 549 | cur_line = nil, 550 | color = 7, 551 | paused = false 552 | } 553 | 554 | function printer:print(s, color) 555 | if (color ~= nil) self.color = color 556 | if self.cur_str ~= nil then 557 | self.cur_str = self.cur_str..s 558 | else 559 | self.cur_str = s 560 | self.cur_pos = 1 561 | self.cur_line = 0 562 | end 563 | end 564 | 565 | function printer:scroll(h) 566 | if self.cur_y > 95 then 567 | memcpy(0x6000, 0x6000 + h*64, (108-h)*64) 568 | else 569 | self.cur_y += h 570 | end 571 | rectfill(0, 108-h, 128, 108, 0) 572 | end 573 | 574 | function printer:update() 575 | local c, p 576 | 577 | rectfill(0, 108, 128, 128, 1) 578 | if self.paused then 579 | print('-- more --', 86, 112, 6) 580 | end 581 | 582 | if btnp(5) and self.paused then 583 | self.paused = false 584 | end 585 | if self.paused then 586 | return 587 | end 588 | 589 | if self.cur_str ~= nil then 590 | c = sub(self.cur_str, self.cur_pos, self.cur_pos) 591 | if c == " " then 592 | p = self.cur_pos + 1 593 | while (p <= #self.cur_str) and 594 | (sub(self.cur_str, p, p) ~= " ") do 595 | p += 1 596 | end 597 | if (p - self.cur_pos) * 4 + self.cur_x > 127 then 598 | self.cur_line +=1 599 | if self.cur_line >= 14 then 600 | self.paused = true 601 | self.cur_line = 0 602 | end 603 | self:scroll(7) 604 | self.cur_x = 0 605 | else 606 | self.cur_x += 4 607 | end 608 | else 609 | print(c, self.cur_x, self.cur_y, self.color) 610 | self.cur_x += 4 611 | end 612 | self.cur_pos += 1 613 | if self.cur_pos > #self.cur_str then 614 | self:scroll(9) -- end of paragraph scroll 615 | self.cur_x = 0 616 | self.cur_str = nil 617 | self.cur_pos = nil 618 | self.cur_line = nil 619 | end 620 | end 621 | end 622 | 623 | return printer 624 | end 625 | 626 | function _init() 627 | cls() 628 | printer = new_printer() 629 | end 630 | 631 | function _update() 632 | print_speed_ctr += print_speed 633 | while print_speed_ctr >= 1 do 634 | printer:update() 635 | print_speed_ctr -= 1 636 | end 637 | 638 | if printer.cur_str == nil and btnp(5) then 639 | printer:print(_t(msgs[msg_i+1])) 640 | msg_i = (msg_i + 1) % #msgs 641 | end 642 | end 643 | 644 | function _draw() 645 | rectfill(0,120,128,128,1) 646 | print(stat(0),0,120,7) 647 | end 648 | -------------------------------------------------------------------------------- /tests/testdata/test_game.lua: -------------------------------------------------------------------------------- 1 | msgs = { 2 | "it was the best of times", 3 | "it was the worst of times", 4 | "some things happened", 5 | "then some more things happened", 6 | "but i don't really know what happened", 7 | "so i probably shouldn't say", 8 | "some things happened", 9 | "that's all i know", 10 | 'he stood up. "no," he said. ok.', 11 | 'then i (his brother) laughed.', 12 | '"uh, maybe?" i shook my head.', 13 | '"ok (or not)." i sighed.' 14 | } 15 | msg_i = #msgs - 1 16 | 17 | function _update() 18 | if btnp(0) then 19 | msg_i = (msg_i + 1) % #msgs 20 | end 21 | end 22 | 23 | function _draw() 24 | cls() 25 | print(_t(msgs[msg_i+1]), 0, 0, 7) 26 | end 27 | -------------------------------------------------------------------------------- /tests/textlib_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from p8advent import textlib 4 | 5 | 6 | class TestTextLib(unittest.TestCase): 7 | def setUp(self): 8 | self.tl = textlib.TextLib(prefix_length=2) 9 | 10 | def test_encode_pscii(self): 11 | self.assertEqual(b'\x1e\x1f\x20\x21\x22\x23\x0f\x10\x11', 12 | textlib.encode_pscii('abcDEF123')) 13 | 14 | def test_encode_word(self): 15 | result = self.tl._encode_word('aardvark') 16 | self.assertEqual(b'\x80\x00', result) 17 | result = self.tl._encode_word('aaron') 18 | self.assertEqual(b'\x80\x01', result) 19 | result = self.tl._encode_word('baron') 20 | self.assertEqual(b'\x81\x00', result) 21 | 22 | def test_encode_word_shorter_than_prefix(self): 23 | result = self.tl._encode_word('a') 24 | self.assertEqual(b'\x80\x00', result) 25 | 26 | def test_encode_word_too_many_prefixes(self): 27 | try: 28 | for a in range(ord('a'), ord('z')): 29 | for b in range(ord('a'), ord('z')): 30 | w = chr(a) + chr(b) + 'x' 31 | self.tl._encode_word(w) 32 | self.fail() 33 | except textlib.TooManyWordsForPrefixError: 34 | pass 35 | 36 | def test_encode_word_too_many_suffixes(self): 37 | try: 38 | for a in range(ord('a'), ord('z')): 39 | for b in range(ord('a'), ord('z')): 40 | w = 'xx' + chr(a) + chr(b) 41 | self.tl._encode_word(w) 42 | self.fail() 43 | except textlib.TooManyWordsForPrefixError: 44 | pass 45 | 46 | def test_encode_string_id(self): 47 | self.assertEqual(' ', self.tl._encode_string_id(0)) 48 | self.assertEqual('! ', self.tl._encode_string_id(1)) 49 | self.assertEqual('" ', self.tl._encode_string_id(2)) 50 | self.assertEqual('# ', self.tl._encode_string_id(3)) 51 | self.assertEqual('% ', self.tl._encode_string_id(4)) 52 | self.assertEqual('@ ', self.tl._encode_string_id(63)) 53 | self.assertEqual(' ! ', self.tl._encode_string_id(64)) 54 | self.assertEqual('!! ', self.tl._encode_string_id(65)) 55 | self.assertEqual('"! ', self.tl._encode_string_id(66)) 56 | self.assertEqual('@@ ', self.tl._encode_string_id(4095)) 57 | self.assertEqual(' !', self.tl._encode_string_id(4096)) 58 | self.assertEqual('! !', self.tl._encode_string_id(4097)) 59 | self.assertEqual('@@1', self.tl._encode_string_id(65535)) 60 | 61 | def test_encode_string(self): 62 | result = self.tl._encode_string('aardvark aaron baron') 63 | self.assertEqual(b'\x80\x00\x80\x01\x81\x00', result) 64 | 65 | result = self.tl._encode_string('"Aardvark? Aaron, baron."') 66 | self.assertEqual(b'\x02\x80\x00\x1d\x80\x01\x0a\x81\x00\x0c\x02', 67 | result) 68 | 69 | def test_id_for_string(self): 70 | result = self.tl.id_for_string('aardvark aaron baron') 71 | self.assertEqual(' ', result) 72 | 73 | result = self.tl.id_for_string('"Aardvark? Aaron, baron."') 74 | self.assertEqual('! ', result) 75 | 76 | result = self.tl.id_for_string('aardvark aaron baron') 77 | self.assertEqual(' ', result) 78 | 79 | def test_as_bytes(self): 80 | # Discard IDs, we just want to load the lib. 81 | self.tl.id_for_string('aardvark aaron baron') 82 | self.tl.id_for_string('"Aardvark? Aaron, baron."') 83 | 84 | result = self.tl.as_bytes() 85 | expected = (b'\x02\x02\x00' + 86 | b'\x09\x00\x0f\x00\x1a\x00' + # string jump table 87 | b'\x80\x00\x80\x01\x81\x00' + # string data 88 | b'\x02\x80\x00\x1d\x80\x01\x0a\x81\x00\x0c\x02' + 89 | b'\x1e\x00\x2b\x00' + # prefix jump table 90 | b'\x1e\x1e\x2f\x21\x33\x1e\x2f\x28\x00' + # lookup data 91 | b'\x2f\x2c\x2b\x00' + 92 | b'\x1f\x1e\x2f\x2c\x2b\x00') 93 | self.assertEqual(expected, result) 94 | 95 | def test_generate_lua(self): 96 | self.tl.id_for_string('aardvark aaron baron') 97 | self.tl.id_for_string('"Aardvark? Aaron, baron."') 98 | 99 | result = self.tl.generate_lua(text_start_addr=512) 100 | self.assertIn('local ta=512\n', result) 101 | 102 | 103 | if __name__ == '__main__': 104 | unittest.main() 105 | --------------------------------------------------------------------------------