#!/usr/bin/env python3 # agiGame.py - build small AGI v2 games from scratch for JoeyAGI's tests: # LOGIC code from a Python description, PICTURE command lists, VIEWs from # text art, SOUNDs, WORDS.TOK, OBJECT, the four directories and VOL.0. # # Everything here follows the public AGI specifications (the LOGIC command # table, chapter 5's directory and VOL formats, chapters 6-10). Nothing is # copied from a Sierra game, so the games it writes can live in the repo's # tests and be fed to Sierra's own interpreter as well (scripts/oracle.sh). # # L = Logic() # with L.If(("isset", 5)): # L.load_pic(...) # commands by name, '.' written as '_' # L.return_() # writeGame(dir, logics={0: L.build()}, pics={...}, views={...}) import os import struct # Action commands in number order, with their argument counts. ACTIONS = """return 0 increment 1 decrement 1 assignn 2 assignv 2 addn 2 addv 2 subn 2 subv 2 lindirectv 2 rindirect 2 lindirectn 2 set 1 reset 1 toggle 1 set.v 1 reset.v 1 toggle.v 1 new.room 1 new.room.v 1 load.logics 1 load.logics.v 1 call 1 call.v 1 load.pic 1 draw.pic 1 show.pic 0 discard.pic 1 overlay.pic 1 show.pri.screen 0 load.view 1 load.view.v 1 discard.view 1 animate.obj 1 unanimate.all 0 draw 1 erase 1 position 3 position.v 3 get.posn 3 reposition 3 set.view 2 set.view.v 2 set.loop 2 set.loop.v 2 fix.loop 1 release.loop 1 set.cel 2 set.cel.v 2 last.cel 2 current.cel 2 current.loop 2 current.view 2 number.of.loops 2 set.priority 2 set.priority.v 2 release.priority 1 get.priority 2 stop.update 1 start.update 1 force.update 1 ignore.horizon 1 observe.horizon 1 set.horizon 1 object.on.water 1 object.on.land 1 object.on.anything 1 ignore.objs 1 observe.objs 1 distance 3 stop.cycling 1 start.cycling 1 normal.cycle 1 end.of.loop 2 reverse.cycle 1 reverse.loop 2 cycle.time 2 stop.motion 1 start.motion 1 step.size 2 step.time 2 move.obj 5 move.obj.v 5 follow.ego 3 wander 1 normal.motion 1 set.dir 2 get.dir 2 ignore.blocks 1 observe.blocks 1 block 4 unblock 0 get 1 get.v 1 drop 1 put 2 put.v 2 get.room.v 2 load.sound 1 sound 2 stop.sound 0 print 1 print.v 1 display 3 display.v 3 clear.lines 3 text.screen 0 graphics 0 set.cursor.char 1 set.text.attribute 2 shake.screen 1 configure.screen 3 status.line.on 0 status.line.off 0 set.string 2 get.string 5 word.to.string 2 parse 1 get.num 2 prevent.input 0 accept.input 0 set.key 3 add.to.pic 7 add.to.pic.v 7 status 0 save.game 0 restore.game 0 init.disk 0 restart.game 0 show.obj 1 random 3 program.control 0 player.control 0 obj.status.v 1 quit 1 show.mem 0 pause 0 echo.line 0 cancel.line 0 init.joy 0 toggle.monitor 0 version 0 script.size 1 set.game.id 1 log 1 set.scan.start 0 reset.scan.start 0 reposition.to 3 reposition.to.v 3 trace.on 0 trace.info 3 print.at 4 print.at.v 4 discard.view.v 1 clear.text.rect 5 set.upper.left 2 set.menu 1 set.menu.item 2 submit.menu 0 enable.item 1 disable.item 1 menu.input 0 show.obj.v 1 open.dialogue 0 close.dialogue 0 mul.n 2 mul.v 2 div.n 2 div.v 2 close.window 0""" # Test commands in number order (said takes a word list). TESTS = """_ 0 equaln 2 equalv 2 lessn 2 lessv 2 greatern 2 greaterv 2 isset 1 isset.v 1 has 1 obj.in.room 2 posn 5 controller 1 have.key 0 said -1 compare.strings 2 obj.in.box 5 center.posn 5 right.posn 5""" ACT = {} for _i, _line in enumerate(ACTIONS.splitlines()): _n, _c = _line.split() ACT[_n] = (_i, int(_c)) TST = {} for _i, _line in enumerate(TESTS.splitlines()): _n, _c = _line.split() TST[_n] = (_i, int(_c)) CRYPT_KEY = b"Avis Durgan" COND_START = 0xFF COND_END = 0xFF COND_OR = 0xFC COND_NOT = 0xFD CODE_GOTO = 0xFE VOL_SIGNATURE = b"\x12\x34" DIR_EMPTY = b"\xff\xff\xff" LETTERS = 26 WORD_CHAR_XOR = 0x7F WORD_LAST_CHAR = 0x80 VOICES = 4 SOUND_END = b"\xff\xff" # PICTURE commands (specification 7.2). PIC_COLOR = 0xF0 PIC_NO_COLOR = 0xF1 PIC_PRIORITY = 0xF2 PIC_NO_PRIORITY = 0xF3 PIC_Y_CORNER = 0xF4 PIC_X_CORNER = 0xF5 PIC_LINE = 0xF6 PIC_REL_LINE = 0xF7 PIC_FILL = 0xF8 PIC_PEN = 0xF9 PIC_PLOT = 0xFA PIC_END = 0xFF def crypt(data): return bytes(c ^ CRYPT_KEY[i % len(CRYPT_KEY)] for i, c in enumerate(data)) class Logic: def __init__(self): self.code = bytearray() self.msgs = [] def m(self, text): # The message number for text (from 1), added on first use. if text not in self.msgs: self.msgs.append(text) return self.msgs.index(text) + 1 def __getattr__(self, name): cmd = name.rstrip("_").replace("_", ".") if cmd not in ACT: raise AttributeError(name) def emit(*args): op, n = ACT[cmd] if len(args) != n: raise ValueError("%s takes %d arguments" % (cmd, n)) self.code.append(op) for a in args: self.code.append(a & 0xFF) return emit def cond(self, tests): # A term is ("name", args...), ("!", "name", args...) or # ("or", [terms]); said takes word numbers. out = bytearray([COND_START]) def term(t): b = bytearray() if t[0] == "or": b.append(COND_OR) for s in t[1]: b += term(s) b.append(COND_OR) return b if t[0] == "!": b.append(COND_NOT) t = t[1:] num, n = TST[t[0]] b.append(num) if t[0] == "said": b.append(len(t) - 1) for w in t[1:]: b += struct.pack("> 4) & 0x3F, 0x80 | (v << 5) | (divisor & 0x0F)]) body += struct.pack("H", out, 2 * letter)[0] == 0: struct.pack_into(">H", out, 2 * letter, len(out)) prev = "" share = 0 limit = min(len(prev), len(w) - 1) while share < limit and prev[share] == w[share]: share += 1 out.append(share) tail = w[share:] for k, ch in enumerate(tail): b = ord(ch) ^ WORD_CHAR_XOR if k == len(tail) - 1: b |= WORD_LAST_CHAR out.append(b) out += struct.pack(">H", table[w]) prev = w out.append(0) return bytes(out) def objects(items, maxObjects=16): # OBJECT from [(name, room)]: the header, an entry per item, the # names; offsets count from the first entry. Encrypted. entries = bytearray() names = bytearray() tableLen = 3 * len(items) for name, room in items: entries += struct.pack("> 16) & 0x0F, (off >> 8) & 0xFF, off & 0xFF]) with open(os.path.join(path, dirName), "wb") as f: f.write(d) with open(os.path.join(path, "VOL.0"), "wb") as f: f.write(vol) with open(os.path.join(path, "WORDS.TOK"), "wb") as f: f.write(words(vocabulary or {"a": 0})) with open(os.path.join(path, "OBJECT"), "wb") as f: f.write(objects(items or [("?", 0)], maxObjects))