chore: create new project structure and aoc.py runner script

This commit is contained in:
2025-08-04 16:23:06 +02:00
parent f76375d835
commit e2964c6c36
91 changed files with 177 additions and 113 deletions

View File

@@ -0,0 +1,23 @@
# https://www.reddit.com/r/adventofcode/comments/egq9xn/2019_day_9_intcode_benchmarking_suite/
from intcode import interpret_intcode
def isqrt(n):
program = [3,1,109,149,21101,0,15,0,20101,0,1,1,1105,1,18,204,1,99,22101,0,1,2,22101,0,1,1,21101,0,43,3,22101,0,1,4,22101,0,2,5,109,3,1105,1,78,109,-3,22102,-1,1,1,22201,1,4,3,22102,-1,1,1,1208,3,0,62,2105,-1,0,1208,3,1,69,2105,-1,0,22101,0,4,1,1105,1,26,1207,1,1,83,2105,-1,0,21101,0,102,3,22101,0,2,4,22101,0,1,5,109,3,1105,1,115,109,-3,22201,1,4,1,21101,0,2,2,1105,1,115,2102,-1,2,140,2101,0,2,133,22101,0,1,2,20001,133,140,1,1207,2,-1,136,2105,-1,0,21201,2,-1,2,22101,1,1,1,1105,1,131]
res = interpret_intcode(program, stdin=[n])
return res.stdout.pop()
def sum_of_primes(n):
program = [3,100,1007,100,2,7,1105,-1,87,1007,100,1,14,1105,-1,27,101,-2,100,100,101,1,101,101,1105,1,9,101,105,101,105,101,2,104,104,101,1,102,102,1,102,102,103,101,1,103,103,7,102,101,52,1106,-1,87,101,105,102,59,1005,-1,65,1,103,104,104,101,105,102,83,1,103,83,83,7,83,105,78,1106,-1,35,1101,0,1,-1,1105,1,69,4,104,99]
res = interpret_intcode(program, stdin=[n])
return res.stdout.pop()
if __name__ == '__main__':
#import timeit
#print(timeit.timeit("test()", globals=locals()))
print(isqrt(111))
print(sum_of_primes(10))

View File

@@ -0,0 +1,194 @@
#! /usr/bin/env python3
from collections import defaultdict
from enum import Enum
import logging
import os
loglevel = (os.getenv("LOGLEVEL") or "WARN").upper()
logging.basicConfig(format="%(levelname)s:%(message)s", level=loglevel)
def get_nth_digit(n, number):
"Returns the nth digit of the input number"
return number // 10 ** n % 10
class Operation(Enum):
ADDITION = 1
MULTIPLICATION = 2
INPUT = 3
OUTPUT = 4
JMP_IF_TRUE = 5
JMP_IF_FALSE = 6
LESS_THAN = 7
EQUALS = 8
BASE = 9
TERMINATION = 99
class Mode(Enum):
POSITION = 0
IMMEDIATE = 1
RELATIVE = 2
class Instruction:
def __init__(self, memory, ip, base):
self.ip = ip
self.memory = memory
self.opcode = memory[ip]
# A B C D E
# 0 1 1 0 3
# A B C modes, DE opcode
self.operation = Operation(self.opcode % 100)
self.modes = [Mode(get_nth_digit(n, self.opcode)) for n in range(2, 5)]
self.handler_name = f"handle_{self.operation.name.lower()}"
self.handler = getattr(self, self.handler_name, self.handle_unknown)
self.input = None
self.output = None
self.halted = False
self.base = base
def __repr__(self):
return f"[{self.opcode}] Instruction({self.operation}, {self.modes})"
def handle(self):
return self.handler()
def handle_addition(self):
first, second = self._get_parameters()
logging.debug(f"ADD {first} {second}")
result = first + second
address = self._get_write_addr(3)
logging.debug(f"{address}")
self._write(address, result)
self.ip += 4
return self.ip
def handle_multiplication(self):
first, second = self._get_parameters()
logging.debug(f"MUL {first} {second}")
result = first * second
address = self._get_write_addr(3)
self._write(address, result)
self.ip += 4
return self.ip
def handle_input(self):
address = self._get_write_addr(1)
self._write(address, self.input)
logging.debug(f"INP {address} {self.input}")
self.ip += 2
return self.ip
def handle_output(self):
self.output = self._get_value(1)
logging.debug(f"OUT {self.output}")
self.ip += 2
return self.ip
def handle_jmp_if_true(self):
first, second = self._get_parameters()
logging.debug(f"JMPT {first} {second} {first != 0}")
return second if first != 0 else self.ip + 3
def handle_jmp_if_false(self):
first, second = self._get_parameters()
logging.debug(f"JMPF {first} {second}")
return second if first == 0 else self.ip + 3
def handle_less_than(self):
first, second = self._get_parameters()
logging.debug(f"LT {first} {second} {first < second}")
address = self._get_write_addr(3)
self._write(address, int(first < second))
self.ip += 4
return self.ip
def handle_equals(self):
first, second = self._get_parameters()
logging.debug(f"EQ {first} {second}")
address = self._get_write_addr(3)
self._write(address, int(first == second))
self.ip += 4
return self.ip
def handle_termination(self):
logging.debug("HALT")
self.halted = True
return self.ip
def handle_base(self):
self.base += self._get_value(1)
logging.debug(f"BASE {self.base}")
self.ip += 2
return self.ip
def handle_unknown(self):
raise ValueError(f"Unknown operation <{self.operation}> @ [{self.ip}]")
def _get_value(self, offset):
value = self.memory[self.ip + offset]
match self.modes[offset - 1]:
case Mode.POSITION: return self.memory[value] if value < len(self.memory) else 0
case Mode.IMMEDIATE: return value
case Mode.RELATIVE: return self.memory[value + self.base]
case _: raise ValueError(f"{self.modes[i]}")
def _get_write_addr(self, offset):
value = self.memory[self.ip + offset]
match self.modes[offset - 1]:
case Mode.POSITION: return value
case Mode.RELATIVE: return value + self.base
case _: raise ValueError(f"{self.modes[i]}")
def _get_parameters(self):
first = self._get_value(1)
second = self._get_value(2)
return first, second
def _write(self, address, value):
while address >= len(self.memory):
self.memory += [0] * len(self.memory)
self.memory[address] = value
class Interpreter:
def __init__(self, program, stdin=[]):
self.ip = 0
self.stdin = stdin
self.stdout = []
self.memory = program[::]
self.halted = False
self.base = 0
def __repr__(self):
return f"Interpreter(ip={self.ip}, stdin={self.stdin}, stdout={self.stdout}, halted={self.halted})"
def interpret(self, break_on_input=False, break_on_output=True):
while not self.halted:
# fetch next instruction
instruction = Instruction(self.memory, self.ip, self.base)
# pause if INP + break on input
if instruction.operation == Operation.INPUT:
if break_on_input and self.stdin == []:
break
else:
instruction.input = self.stdin.pop(0)
# execute instruction
self.ip = instruction.handle()
self.base = instruction.base
logging.debug(f"IP {self.ip}")
if instruction.output is not None:
self.stdout.append(instruction.output)
if break_on_output:
break
if instruction.halted:
self.halted = True
def interpret_intcode(program, stdin=[]):
interpreter = Interpreter(program, stdin)
interpreter.interpret(break_on_output=False)
return interpreter

View File

@@ -0,0 +1,111 @@
# Run with pytest
import logging
from intcode import *
def run_test(program, expected_mem=None, stdin=[], expected_out=None):
mem = program[::]
out = interpret_intcode(mem, stdin=stdin)
if expected_mem is not None:
assert expected_mem == out.memory
if expected_out is not None:
assert expected_out == out.stdout
def test_day2():
tests = [
[[1,0,0,0,99], [2,0,0,0,99]], # ADD 1 + 1 to location 0
[[2,3,0,3,99], [2,3,0,6,99]], # MUL 2 * 3 to location 3
[[2,4,4,5,99,0], [2,4,4,5,99,9801]], # MUL 99 * 99 to location 5 (9801)
[[1,1,1,4,99,5,6,0,99], [30,1,1,4,2,5,6,0,99]],
]
for program, expected in tests:
run_test(program, expected)
def test_day5_basic():
# Using position mode, consider whether the input is equal to 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,9,8,9,10,9,4,9,99,-1,8]
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[8], expected_out=[1])
# Using position mode, consider whether the input is less than 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,9,7,9,10,9,4,9,99,-1,8]
run_test(prog, stdin=[7], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Using immediate mode, consider whether the input is equal to 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,3,1108,-1,8,3,4,3,99]
run_test(prog, stdin=[8], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Using immediate mode, consider whether the input is less than 8; output 1
# (if it is) or 0 (if it is not).
prog = [3,3,1107,-1,8,3,4,3,99]
run_test(prog, stdin=[7], expected_out=[1])
run_test(prog, stdin=[9], expected_out=[0])
# Here are some jump tests that take an input, then output 0 if the input
# was zero or 1 if the input was non-zero:
prog = [3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9] # using position mode
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[4], expected_out=[1])
prog = [3,12,6,12,15,1,13,14,13,4,13,99,-1,0,1,9] # using immediate mode
run_test(prog, stdin=[0], expected_out=[0])
run_test(prog, stdin=[4], expected_out=[1])
def test_day5_larger():
"""
The above example program uses an input instruction to ask for a single
number. The program will then output 999 if the input value is below 8,
output 1000 if the input value is equal to 8, or output 1001 if the input
value is greater than 8.
"""
prog = [3,21,1008,21,8,20,1005,20,22,107,8,21,20,1006,20,31,
1106,0,36,98,0,0,1002,21,125,20,4,20,1105,1,46,104,
999,1105,1,46,1101,1000,1,20,4,20,1105,1,46,98,99]
run_test(prog, stdin=[7], expected_out=[999])
run_test(prog, stdin=[8], expected_out=[1000])
run_test(prog, stdin=[9], expected_out=[1001])
def test_day9_base():
program = [109, 19, 109, 6, 99]
interpreter = Interpreter(program)
interpreter.base = 2000
interpreter.interpret(break_on_output=False)
assert interpreter.base == 2025
def test_day9_relative():
program = [109,6,21001,9,25,1,104,0,99,49]
ret = interpret_intcode(program)
assert ret.stdout == [74]
def test_day9_quine():
quine = [109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99]
run_test(quine, expected_out=quine)
def test_day9_relative():
run_test([109, -1, 4, 1, 99], expected_out=[-1])
run_test([109, -1, 104, 1, 99], expected_out=[1])
run_test([109, -1, 204, 1, 99], expected_out=[109])
run_test([109, 1, 9, 2, 204, -6, 99], expected_out=[204])
run_test([109, 1, 109, 9, 204, -6, 99], expected_out=[204])
run_test([109, 1, 209, -1, 204, -106, 99], expected_out=[204])
run_test([109, 1, 3, 3, 204, 2, 99], stdin=[69], expected_out=[69])
run_test([109, 1, 203, 2, 204, 2, 99], stdin=[1337], expected_out=[1337])
def test_fac():
# factorial test
fac = [3,29,1007,29,2,28,1005,28,24,2,27,29,27,1001,29,-1,29,1101,0,0,28,1006,28,2,4,27,99,1,0,0]
res = interpret_intcode(fac, [4])
assert res.stdout == [24]