diff --git a/libsynacor/__init__.py b/libsynacor/__init__.py new file mode 100644 index 0000000..9595d2b --- /dev/null +++ b/libsynacor/__init__.py @@ -0,0 +1,19 @@ +# synacor.py - An implementation of the Synacor Challenge +# Copyright © 2017 RunasSudo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from .binfile import * +from .bytecode import * +from .cpu import * diff --git a/libsynacor/binfile.py b/libsynacor/binfile.py new file mode 100644 index 0000000..941f122 --- /dev/null +++ b/libsynacor/binfile.py @@ -0,0 +1,27 @@ +# synacor.py - An implementation of the Synacor Challenge +# Copyright © 2017 RunasSudo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +def memory_from_file(data): + import struct + SYN_MEM = [0] * 32768 + SYN_PTR = 0 + while True: + byteData = data.read(2) + if len(byteData) < 2: + break + SYN_MEM[SYN_PTR] = struct.unpack('. + +class Operand: + @staticmethod + def read_op(word): + if 0 <= word <= 32767: + return OpLiteral(word) + if 32768 <= word <= 32775: + return OpRegister(word - 32768) + raise Exception('Invalid word {}') + +class OpLiteral(Operand): + def __init__(self, value): + self.value = value + def get(self, cpu): + return self.value + def set(self, cpu, value): + raise Exception('Attempted to set literal value {} to {} at {}'.format(self.value, value, cpu.SYN_PTR)) + +class OpRegister(Operand): + def __init__(self, register): + self.register = register + def get(self, cpu): + return cpu.SYN_REG[self.register] + def set(self, cpu, value): + cpu.SYN_REG[self.register] = value + +instructions_by_opcode = {} +instructions_by_name = {} + +def instruction(opcode, name, nargs=0): + def decorator(cls): + cls.opcode = opcode + cls.name = name + cls.nargs = nargs + + instructions_by_opcode[opcode] = cls + instructions_by_name[name] = cls + + return cls + return decorator + +class Instruction: + def __init__(self): + self.args = [] + + @staticmethod + def next_instruction(data, idx): + opcode = Operand.read_op(data[idx]) + idx += 1 + + if not isinstance(opcode, OpLiteral): + raise Exception('Invalid value for opcode {} at {}'.format(data[idx], idx)) + opcode_value = opcode.get(None) + if opcode_value not in instructions_by_opcode: + raise Exception('Invalid opcode {} at {}'.format(opcode_value, idx)) + + instruction = instructions_by_opcode[opcode_value]() + for _ in range(instruction.nargs): + instruction.args.append(Operand.read_op(data[idx])) + idx += 1 + + return instruction, idx + +@instruction(21, 'nop') +class InstructionNop(Instruction): + def run(self, cpu): + pass + +@instruction(0, 'halt') +class InstructionHalt(Instruction): + def run(self, cpu): + import sys + sys.exit(0) + +@instruction(1, 'set', 2) +class InstructionSet(Instruction): + def run(self, cpu): + self.args[0].set(cpu, self.args[1].get(cpu)) + +@instruction(2, 'push', 1) +class InstructionPush(Instruction): + def run(self, cpu): + cpu.SYN_STK.append(self.args[0].get(cpu)) + +@instruction(3, 'pop', 1) +class InstructionPop(Instruction): + def run(self, cpu): + if len(cpu.SYN_STK) == 0: + raise Exception('Attempted to pop from empty stack at {}'.format(cpu.SYN_PTR)) + self.args[0].set(cpu, cpu.SYN_STK.pop()) + +@instruction(4, 'eq', 3) +class InstructionEq(Instruction): + def run(self, cpu): + self.args[0].set(cpu, 1 if self.args[1].get(cpu) == self.args[2].get(cpu) else 0) + +@instruction(5, 'gt', 3) +class InstructionGt(Instruction): + def run(self, cpu): + self.args[0].set(cpu, 1 if self.args[1].get(cpu) > self.args[2].get(cpu) else 0) + +@instruction(6, 'jmp', 1) +class InstructionJmp(Instruction): + def run(self, cpu): + cpu.SYN_PTR = self.args[0].get(cpu) + +@instruction(7, 'jt', 2) +class InstructionJt(Instruction): + def run(self, cpu): + if self.args[0].get(cpu) != 0: + cpu.SYN_PTR = self.args[1].get(cpu) + +@instruction(8, 'jf', 2) +class InstructionJf(Instruction): + def run(self, cpu): + if self.args[0].get(cpu) == 0: + cpu.SYN_PTR = self.args[1].get(cpu) + +@instruction(9, 'add', 3) +class InstructionAdd(Instruction): + def run(self, cpu): + self.args[0].set(cpu, (self.args[1].get(cpu) + self.args[2].get(cpu)) % 32768) + +@instruction(10, 'mult', 3) +class InstructionMult(Instruction): + def run(self, cpu): + self.args[0].set(cpu, (self.args[1].get(cpu) * self.args[2].get(cpu)) % 32768) + +@instruction(11, 'mod', 3) +class InstructionMod(Instruction): + def run(self, cpu): + self.args[0].set(cpu, self.args[1].get(cpu) % self.args[2].get(cpu)) + +@instruction(12, 'and', 3) +class InstructionAnd(Instruction): + def run(self, cpu): + self.args[0].set(cpu, self.args[1].get(cpu) & self.args[2].get(cpu)) + +@instruction(13, 'or', 3) +class InstructionOr(Instruction): + def run(self, cpu): + self.args[0].set(cpu, self.args[1].get(cpu) | self.args[2].get(cpu)) + +@instruction(14, 'not', 2) +class InstructionNot(Instruction): + def run(self, cpu): + self.args[0].set(cpu, ~self.args[1].get(cpu) % 32768) + +@instruction(15, 'rmem', 2) +class InstructionRmem(Instruction): + def run(self, cpu): + self.args[0].set(cpu, cpu.SYN_MEM[self.args[1].get(cpu)]) + +@instruction(16, 'wmem', 2) +class InstructionWmem(Instruction): + def run(self, cpu): + cpu.SYN_MEM[self.args[0].get(cpu)] = self.args[1].get(cpu) + +@instruction(17, 'call', 1) +class InstructionCall(Instruction): + def run(self, cpu): + cpu.SYN_STK.append(cpu.SYN_PTR) + cpu.SYN_PTR = self.args[0].get(cpu) + +@instruction(18, 'ret') +class InstructionRet(Instruction): + def run(self, cpu): + if len(cpu.SYN_STK) == 0: + raise Exception('Attempted to return with empty stack at {}'.format(cpu.SYN_PTR)) + cpu.SYN_PTR = cpu.SYN_STK.pop() + +@instruction(19, 'out', 1) +class InstructionOut(Instruction): + def run(self, cpu): + print(chr(self.args[0].get(cpu)), end='') + +@instruction(20, 'in', 1) +class InstructionIn(Instruction): + def run(self, cpu): + import sys + + while len(cpu.SYN_STDIN_BUF) == 0: + line = sys.stdin.readline() + cpu.SYN_STDIN_BUF = list(line) + + self.args[0].set(cpu, ord(cpu.SYN_STDIN_BUF.pop(0))) diff --git a/libsynacor/cpu.py b/libsynacor/cpu.py new file mode 100644 index 0000000..b23c809 --- /dev/null +++ b/libsynacor/cpu.py @@ -0,0 +1,33 @@ +# synacor.py - An implementation of the Synacor Challenge +# Copyright © 2017 RunasSudo +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from libsynacor import * + +class CPU: + def __init__(self): + self.SYN_PTR = 0 + self.SYN_MEM = [0] * 32768 + self.SYN_REG = [0] * 8 + self.SYN_STK = [] + self.SYN_STDIN_BUF = [] + + def swallow_op(self): + op = Operand.read_op(self.SYN_MEM[self.SYN_PTR]) + self.SYN_PTR += 1 + + def step(self): + instruction, self.SYN_PTR = Instruction.next_instruction(self.SYN_MEM, self.SYN_PTR) + instruction.run(self) diff --git a/synacor.py b/synacor.py index 593341d..db96583 100755 --- a/synacor.py +++ b/synacor.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # synacor.py - An implementation of the Synacor Challenge -# Copyright © 2016 RunasSudo +# Copyright © 2017 RunasSudo # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by @@ -15,136 +15,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import struct # for bytes<-->word handling -import sys # for args, stdin +from libsynacor import * -SYN_PTR = 0 -SYN_MEM = [0] * 32768 -SYN_REG = [0] * 8 -SYN_STK = [] -SYN_STDIN_BUF = [] +import sys -DBG_FLAG = False -DBG_CSTK = [] +cpu = CPU() +with open(sys.argv[1], 'rb') as data: + cpu.SYN_MEM = memory_from_file(data) -class OpLiteral: - def __init__(self, value): - self.value = value; - def get(self): - return self.value; - def set(self, value): - raise Exception('Attempted to set literal value {} to {} at {}'.format(self.value, value, SYN_PTR)) - -class OpRegister: - def __init__(self, register): - self.register = register; - def get(self): - return SYN_REG[self.register]; - def set(self, value): - SYN_REG[self.register] = value; - -def readOp(word): - if 0 <= word <= 32767: - return OpLiteral(word) - if 32768 <= word <= 32775: - return OpRegister(word - 32768) - raise Exception('Invalid word {} at {}'.format(word, SYN_PTR)) - -def swallowOp(): - global SYN_PTR - op = readOp(SYN_MEM[SYN_PTR]) - SYN_PTR += 1 - return op - -if len(sys.argv) > 1: - dbg_args = sys.argv[1:] - with open(dbg_args[0] + '.py', 'r') as f: - exec(f.read(), globals(), locals()) -else: - # Read code into memory - with open('challenge.bin', 'rb') as data: - while True: - byteData = data.read(2) - if len(byteData) < 2: - break - SYN_MEM[SYN_PTR] = struct.unpack(' swallowOp().get() else 0) - elif instruction == 6: #JMP - SYN_PTR = swallowOp().get() - elif instruction == 7: #JT (jump if not zero) - a, b = swallowOp().get(), swallowOp().get() - if a != 0: - SYN_PTR = b - elif instruction == 8: #JF (jump if zero) - a, b = swallowOp().get(), swallowOp().get() - if a == 0: - SYN_PTR = b - elif instruction == 9: #ADD - swallowOp().set((swallowOp().get() + swallowOp().get()) % 32768) - elif instruction == 10: #MULT - swallowOp().set((swallowOp().get() * swallowOp().get()) % 32768) - elif instruction == 11: #MOD - swallowOp().set(swallowOp().get() % swallowOp().get()) - elif instruction == 12: #AND - swallowOp().set(swallowOp().get() & swallowOp().get()) - elif instruction == 13: #OR - swallowOp().set(swallowOp().get() | swallowOp().get()) - elif instruction == 14: #NOT - swallowOp().set(~swallowOp().get() % 32768) # mathemagic - elif instruction == 15: #RMEM - swallowOp().set(SYN_MEM[swallowOp().get()]) - elif instruction == 16: #WMEM - a, b = swallowOp().get(), swallowOp().get() - SYN_MEM[a] = b # order of operations - elif instruction == 17: #CALL - SYN_STK.append(SYN_PTR + 1) # skip one word for the operand - addr = swallowOp().get() - DBG_CSTK.append(addr) - SYN_PTR = addr - elif instruction == 18: #RET - if len(SYN_STK) == 0: - raise Exception('Attempted to return with empty stack at {}'.format(SYN_PTR)) - SYN_PTR = SYN_STK.pop() - - if len(DBG_CSTK) > 0: - DBG_CSTK.pop() - elif instruction == 19: #OUT - print(chr(swallowOp().get()), end='') - elif instruction == 20: #IN - while len(SYN_STDIN_BUF) == 0: - line = sys.stdin.readline() - if line.startswith('.'): # debug command - dbg_args = line.rstrip()[1:].split() - with open(dbg_args[0] + '.py', 'r') as f: - SYN_PTR -= 1; exec(f.read(), globals(), locals()); SYN_PTR += 1 - else: - SYN_STDIN_BUF = list(line) - - swallowOp().set(ord(SYN_STDIN_BUF.pop(0))) - else: - raise Exception('Unimplemented opcode {} at {}'.format(instruction, SYN_PTR)) + cpu.step()