2019: d25: ex1: add solution

This commit is contained in:
Bruno BELANYI 2024-12-27 18:02:54 -05:00
parent 6d7f16f4a9
commit 455b1250a8

486
2019/d25/ex1/ex1.py Executable file
View file

@ -0,0 +1,486 @@
#!/usr/bin/env python
import itertools
import sys
from collections import deque
from copy import deepcopy
from dataclasses import dataclass, field
from enum import IntEnum, StrEnum
from typing import List, NamedTuple
class ParameterMode(IntEnum):
POSITION = 0 # Acts on address
IMMEDIATE = 1 # Acts on the immediate value
RELATIVE = 2 # Acts on offset to relative base
class Instruction(NamedTuple):
address: int # The address of the instruction, for convenience
op: int # The opcode
p1_mode: ParameterMode # Which mode is the first parameter in
p2_mode: ParameterMode # Which mode is the second parameter in
p3_mode: ParameterMode # Which mode is the third parameter in
def lookup_ops(index: int, memory: List[int]) -> Instruction:
digits = list(map(int, str(memory[index])))
a, b, c, d, e = [0] * (5 - len(digits)) + digits # Pad with default values
return Instruction(
address=index,
op=d * 10 + e,
p1_mode=ParameterMode(c),
p2_mode=ParameterMode(b),
p3_mode=ParameterMode(a),
)
class InputInterrupt(Exception):
pass
class OutputInterrupt(Exception):
pass
@dataclass
class Computer:
memory: List[int] # Memory space
rip: int = 0 # Instruction pointer
input_list: List[int] = field(default_factory=list)
output_list: List[int] = field(default_factory=list)
is_halted: bool = field(default=False, init=False)
relative_base: int = field(default=0, init=False)
def run(self) -> None:
while not self.is_halted:
self.run_single()
def run_no_output_interrupt(self) -> None:
while not self.is_halted:
try:
self.run_single()
except OutputInterrupt:
continue
def run_until_input_interrupt(self) -> None:
while not self.is_halted:
try:
self.run_no_output_interrupt()
except InputInterrupt:
return
def run_single(self) -> None: # Returns True when halted
instr = lookup_ops(self.rip, self.memory)
if instr.op == 99: # Halt
self.is_halted = True
elif instr.op == 1: # Sum
self._do_addition(instr)
elif instr.op == 2: # Multiplication
self._do_multiplication(instr)
elif instr.op == 3: # Load from input
self._do_input(instr)
elif instr.op == 4: # Store to output
self._do_output(instr)
elif instr.op == 5: # Jump if true
self._do_jump_if_true(instr)
elif instr.op == 6: # Jump if false
self._do_jump_if_false(instr)
elif instr.op == 7: # Less than
self._do_less_than(instr)
elif instr.op == 8: # Equal to
self._do_equal_to(instr)
elif instr.op == 9: # Change relative base
self._do_change_relative_base(instr)
else:
assert False # Sanity check
def _fill_to_addres(self, address: int) -> None:
values = address - len(self.memory) + 1
if values <= 0:
return
for __ in range(values):
self.memory.append(0)
def _get_value(self, mode: ParameterMode, val: int) -> int:
if mode == ParameterMode.POSITION:
assert 0 <= val # Sanity check
self._fill_to_addres(val)
return self.memory[val]
elif mode == ParameterMode.RELATIVE:
val += self.relative_base
assert 0 <= val # Sanity check
self._fill_to_addres(val)
return self.memory[val]
assert mode == ParameterMode.IMMEDIATE # Sanity check
return val
def _set_value(self, mode: ParameterMode, address: int, value: int) -> None:
if mode == ParameterMode.RELATIVE:
address += self.relative_base
else:
assert mode == ParameterMode.POSITION # Sanity check
assert address >= 0 # Sanity check
self._fill_to_addres(address)
self.memory[address] = value
def _do_addition(self, instr: Instruction) -> None:
lhs = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
rhs = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
dest = self.memory[instr.address + 3]
self._set_value(instr.p3_mode, dest, lhs + rhs)
self.rip += 4 # Length of the instruction
def _do_multiplication(self, instr: Instruction) -> None:
lhs = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
rhs = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
dest = self.memory[instr.address + 3]
self._set_value(instr.p3_mode, dest, lhs * rhs)
self.rip += 4 # Length of the instruction
def _do_input(self, instr: Instruction) -> None:
if len(self.input_list) == 0:
raise InputInterrupt # No input, halt until an input is provided
value = int(self.input_list.pop(0))
param = self.memory[instr.address + 1]
self._set_value(instr.p1_mode, param, value)
self.rip += 2 # Length of the instruction
def _do_output(self, instr: Instruction) -> None:
value = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
self.output_list.append(value)
self.rip += 2 # Length of the instruction
raise OutputInterrupt # Alert that we got an output to give
def _do_jump_if_true(self, instr: Instruction) -> None:
cond = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
value = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
if cond != 0:
self.rip = value
else:
self.rip += 3 # Length of the instruction
def _do_jump_if_false(self, instr: Instruction) -> None:
cond = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
value = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
if cond == 0:
self.rip = value
else:
self.rip += 3 # Length of the instruction
def _do_less_than(self, instr: Instruction) -> None:
lhs = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
rhs = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
dest = self.memory[instr.address + 3]
self._set_value(instr.p3_mode, dest, 1 if lhs < rhs else 0)
self.rip += 4 # Length of the instruction
def _do_equal_to(self, instr: Instruction) -> None:
lhs = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
rhs = self._get_value(instr.p2_mode, self.memory[instr.address + 2])
dest = self.memory[instr.address + 3]
self._set_value(instr.p3_mode, dest, 1 if lhs == rhs else 0)
self.rip += 4 # Length of the instruction
def _do_change_relative_base(self, instr: Instruction) -> None:
value = self._get_value(instr.p1_mode, self.memory[instr.address + 1])
self.relative_base += value
self.rip += 2 # Length of the instruction
class Move(StrEnum):
NORTH = "north"
SOUTH = "south"
EAST = "east"
WEST = "west"
def opposite(self) -> "Move":
if self == Move.NORTH:
return Move.SOUTH
if self == Move.SOUTH:
return Move.NORTH
if self == Move.EAST:
return Move.WEST
if self == Move.WEST:
return Move.EAST
assert False # Sanity check
@dataclass
class Graph:
nodes: dict[str, dict[Move, str]] = field(default_factory=dict)
explored: set[str] = field(default_factory=set)
def add_room(self, room: str) -> None:
self.nodes.setdefault(room, {})
def add_door(self, room: str, move: Move) -> None:
if move in self.nodes[room]:
return
other_room = f"{move} of {room}"
self.nodes[room][move] = other_room
self.add_room(other_room)
self.nodes[other_room].setdefault(move.opposite(), room)
def set_visited(self, room: str) -> None:
self.explored.add(room)
# FIXME: repetition
def goto(self, start: str, end: str) -> list[Move]:
queue: deque[tuple[str, list[Move]]] = deque([(start, [])])
visited: set[str] = {start}
while queue:
room, path = queue.popleft()
visited.add(room)
if room == end:
return path
for dir, neighbour in self.nodes[room].items():
if neighbour in visited:
continue
queue.append((neighbour, path + [dir]))
assert False # Sanity check
def visit_unexplored(self, start: str) -> tuple[str, list[Move]] | None:
queue: deque[tuple[str, list[Move]]] = deque([(start, [])])
visited: set[str] = {start}
while queue:
room, path = queue.popleft()
visited.add(room)
if room not in self.explored:
return room, path
for dir, neighbour in self.nodes[room].items():
if neighbour in visited:
continue
queue.append((neighbour, path + [dir]))
return None
def rename(self, old: str, new: str) -> None:
if old == new or old not in self.nodes:
return
self.nodes[new] = self.nodes.pop(old)
for neighbours in self.nodes.values():
for move in Move:
if move not in neighbours:
continue
neighbours[move] = new if neighbours[move] == old else neighbours[move]
if old in self.explored:
self.explored.remove(old)
self.explored.add(new)
def get_room_name(output: list[str]) -> str:
for line in output:
if not line.startswith("== "):
continue
return line.replace("==", "").strip()
assert False # Sanity check
def add_doors(
output: list[str],
room: str,
graph: Graph,
) -> None:
DOORS_LEAD = "Doors here lead:"
if DOORS_LEAD not in output:
return
doors_lead_index = output.index(DOORS_LEAD)
doors_lead_end = output.index("", doors_lead_index)
directions = [
Move(line.removeprefix("- "))
for line in output[doors_lead_index:doors_lead_end]
if line.startswith("- ")
]
for dir in directions:
graph.add_door(room, dir)
def gather_items_in_room(
output: list[str],
) -> list[str]:
ITEMS_HERE = "Items here:"
# XXX: hard-coded list of items
CURSED_ITEMS = {
"escape pod",
"giant electromagnet",
"infinite loop",
"molten lava",
"photons",
}
if ITEMS_HERE not in output:
return []
items_here_index = output.index(ITEMS_HERE)
items_here_end = output.index("", items_here_index)
items = [
line.removeprefix("- ")
for line in output[items_here_index:items_here_end]
if line.startswith("- ")
]
return [f"take {item}" for item in items if item not in CURSED_ITEMS]
def explore_rooms(
output: list[str],
room: str,
graph: Graph,
) -> tuple[str, list[str], bool]:
SECURITY = "Security Checkpoint"
# Commands we want to execute
commands: list[str] = []
# Get the actual room name, and fix the graph to refer to it
actual_room = get_room_name(output)
graph.rename(room, actual_room)
room = actual_room
# Mark this room as visited
graph.set_visited(room)
# Add new doors to graph, except security checkpoint which won't let us through
if room != SECURITY:
add_doors(output, room, graph)
# Gather items in room
commands += gather_items_in_room(output)
# Go to an unexplored room if possible
if (res := graph.visit_unexplored(room)) is not None:
room, path = res
commands += path
else:
# Go to security room otherwise, with all our items
return SECURITY, list(map(str, graph.goto(room, SECURITY))), False
return room, commands, True
def get_last_output(droid: Computer) -> list[str]:
output = "".join(map(chr, droid.output_list))
droid.output_list.clear()
res: list[str] = []
for line in output.splitlines()[::-1]:
res.append(line)
if line.startswith("== "):
break
return res[::-1]
def gather_items(droid: Computer) -> Computer:
# Avoid changing the input droid
droid = deepcopy(droid)
room = "Starting room"
graph = Graph()
graph.add_room(room)
needs_items = True
while needs_items:
droid.run_until_input_interrupt()
output = get_last_output(droid)
room, commands, needs_items = explore_rooms(output, room, graph)
commands.append("") # Account for final new-line
droid.input_list.extend(map(ord, "\n".join(commands)))
# Finish going back to security
droid.run_until_input_interrupt()
# And drain last output
get_last_output(droid)
return droid
def get_current_items(droid: Computer) -> set[str]:
# Avoid changing the input droid
droid = deepcopy(droid)
assert not droid.input_list # Sanity check
assert not droid.output_list # Sanity check
droid.input_list = list(map(ord, "inv\n"))
droid.run_until_input_interrupt()
return {
line.removeprefix("- ")
for line in "".join(map(chr, droid.output_list)).splitlines()
if line.startswith("- ")
}
def go_through_security(droid: Computer) -> Computer:
items = get_current_items(droid)
for r in range(0, len(items)):
for keep in itertools.combinations(items, r):
drop = items - set(keep)
# Try on a temporary droid, use `droid` as a save point
tmp_droid = deepcopy(droid)
tmp_droid.input_list.extend(
map(ord, "".join(f"drop {item}\n" for item in drop))
)
# XXX: hard-coded direction of final room
tmp_droid.input_list.extend(map(ord, "west\n"))
try:
tmp_droid.run_no_output_interrupt()
except InputInterrupt:
# This set of items failed, try again
continue
# We halted, return the droid
return tmp_droid
assert False
def solve(input_str: str) -> int:
memory = [int(n) for n in input_str.split(",")]
droid = Computer(memory)
# Explore the ship and gather all items
droid = gather_items(droid)
# Go through checkpoint weight plate
droid = go_through_security(droid)
final_output = "".join(map(chr, droid.output_list))
# Most terrible parsing of the year
return int(final_output.splitlines()[-1].split("typing ")[1].split()[0])
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()