# # runtime.py: run a parsed Exidy Sorceror Basic file (for Wizard's castle) # # ### license # # NOTE: this is not a full/proper runtime. It works for Wizard's castle. # import sys import parse import random import re # This is the namespace for use in eval(). EV_NS = { } # Current program counter (line number), and step within the line. PC = None STEP = None # Current position in the DATA values READ_POS = 0 # The contents from the DATA statements DATA_VALUES = None # Stack of (nested) FOR loops. Line number, and statement index. FOR_STACK = [ ] # Stack of GOSUB returns. Line number, and statement index. GOSUB_STACK = [ ] # "Memory" for POKE/PEEK operation. MEMORY = { } def prepare_ns(fn): global EV_NS EV_NS = INJECT.copy() for c, f in fn.items(): EV_NS['FN' + c] = _make_macro(f) EV_NS['C_S'] = Array(34, '') EV_NS['I_S'] = Array(34, '') EV_NS['R_S'] = Array(4, '') EV_NS['W_S'] = Array(8, '') EV_NS['E_S'] = Array(8, '') EV_NS['C_A'] = Array2D(3, 4, 0) EV_NS['T_A'] = Array(8, 0) EV_NS['O'] = Array(3, 0) EV_NS['R'] = Array(3, 0) def _make_macro(mtext): # Return a lambda with MTEXT properly bound. return lambda q: _eval_macro(mtext, q) class Array(object): def __init__(self, size, init): self.data = [init] * size def __call__(self, i): return self.data[i - 1] def assign(self, i, value): self.data[i - 1] = value class Array2D(object): def __init__(self, x, y, init): self.data = [[init]*y for i in range(x)] def __call__(self, x, y): return self.data[x - 1][y - 1] def assign(self, x, y, value): self.data[x - 1][y - 1] = value def _eval_macro(mtext, q): ns = EV_NS.copy() ### hrm. check if we're overwriting an actual Q. ns['Q'] = q return eval(mtext, ns) def next_pc(pc): ### we could sort the PROG and find the next, but we know it is +10 return pc + 10 def execute(prog, data, fn): prepare_ns(fn) global DATA_VALUES DATA_VALUES = data global PC, STEP PC = 10 STEP = 0 while True: try: exec_line(prog[PC]) except EndProgram: return except: print() print('ERROR:', PC, prog[PC]) raise def exec_line(stmts): global PC, STEP while True: #print('PC/STEP:', PC, STEP) if STEP >= len(stmts): break stmt = stmts[STEP] try: DISPATCH[stmt[0]](*stmt[1:]) except FlowChange: return STEP += 1 PC = next_pc(PC) STEP = 0 _RE_INDEXED = re.compile('([A-Z_]+)\\((.*?)(?:,(.*))?\\)') def stmt_assign(varname, expr): value = eval(expr, EV_NS) match = _RE_INDEXED.match(varname) if match: array_name = match.group(1) array = EV_NS[array_name] args = (eval(match.group(2), EV_NS),) if match.group(3): # C(x,y) is the only two-dimensional array. The second index is always an int. args += (int(match.group(3)),) args += (value,) array.assign(*args) else: EV_NS[varname] = value def stmt_print(value): print(eval(value, EV_NS)) def stmt_pnocr(value): sys.stdout.write(eval(value, EV_NS)) def stmt_if(cond): global PC, STEP v = eval(cond, EV_NS) if not v: # Effectively, move to the next line. STEP = 9999 def stmt_goto(lineno): global PC, STEP PC = lineno STEP = 0 raise FlowChange def stmt_gosub(lineno): global PC, STEP GOSUB_STACK.append((PC, STEP)) PC = lineno STEP = 0 raise FlowChange def stmt_return(): global PC, STEP PC, STEP = GOSUB_STACK.pop() STEP += 1 raise FlowChange def stmt_end(): raise EndProgram def stmt_on(cond, which, lines): global PC, STEP value = eval(cond, EV_NS) if which == 'GOSUB': GOSUB_STACK.append((PC, STEP)) PC = lines[value - 1] # 1-based indexing STEP = 0 raise FlowChange def stmt_for(varname, start, end): FOR_STACK.append((varname, eval(end, EV_NS), PC, STEP)) EV_NS[varname] = eval(start, EV_NS) def stmt_next(): global PC, STEP varname, end, loop_PC, loop_STEP = FOR_STACK[-1] EV_NS[varname] += 1 if EV_NS[varname] <= end: PC = loop_PC STEP = loop_STEP + 1 # May go to next PC, if STEP is larger than the line raise FlowChange # The loop is done. Toss the info (possibly exposing an outer loop). FOR_STACK.pop() def stmt_input(varname, prompt): EV_NS[varname] = input(prompt) def stmt_poke(addr, value): addr = eval(addr, EV_NS) value = eval(value, EV_NS) #print('POKE:', addr, value) MEMORY[addr] = value def stmt_read(varlist): global READ_POS for v in varlist: value = DATA_VALUES[READ_POS] READ_POS += 1 # All variables are: X$ or X$(Q). varname = v[0] + '_S' if '(' in v: q = EV_NS['Q'] # Array assignment, indexed by Q. EV_NS[varname].assign(q, value) else: EV_NS[varname] = value def stmt_restore(): global READ_POS READ_POS = 0 DISPATCH = { parse.ASSIGN: stmt_assign, parse.STMT_PRINT: stmt_print, parse.STMT_PNOCR: stmt_pnocr, parse.STMT_IF: stmt_if, parse.STMT_THEN: lambda: None, parse.STMT_GOTO: stmt_goto, parse.STMT_GOSUB: stmt_gosub, parse.STMT_RETURN: stmt_return, parse.STMT_END: stmt_end, parse.STMT_ON: stmt_on, parse.STMT_FOR: stmt_for, parse.STMT_NEXT: stmt_next, parse.STMT_INPUT: stmt_input, parse.STMT_NOOP: lambda: None, parse.STMT_POKE: stmt_poke, parse.STMT_READ: stmt_read, parse.STMT_RESTORE: stmt_restore, } def _compare(left, op, right): return { '<': lambda x, y: -(x < y), '>': lambda x, y: -(x > y), '<=': lambda x, y: -(x <= y), '>=': lambda x, y: -(x >= y), '<>': lambda x, y: -(x != y), '=': lambda x, y: -(x == y), }[op](left, right) def _peek(addr): # Location -2049 is read without a prior write. Unused value, so return 0. if addr == -2049: return 0 return MEMORY[addr] # The various names to inject into the expression evaluation. INJECT = { '_CMP': _compare, 'LEN': len, 'CHR_S': lambda c: '', 'LEFT_S': lambda s, l: s[:l], 'MID_S': lambda s, i, l: s[i-1:i-1+l], 'RIGHT_S': lambda s, i: s[-i:], 'PEEK': _peek, 'USR': lambda x: 0, 'RND': lambda x: random.random(), 'TAB': lambda n: ' '*n, 'VAL': int, 'ASC': ord, # Never initialized in the program, so do it manually. 'MC': 0, 'H': 0, # It is possible to quit before this is assigned, so initialize it. 'Q3': 0, } class EndProgram(Exception): pass class FlowChange(Exception): pass if __name__ == '__main__': if len(sys.argv) != 2: print('USAGE: %s wiz.bas' % (__filename__,)) prog = parse.read_raw_program(open(sys.argv[1])) execute(prog, parse.DATA_VALUES, parse.FN)