2019: d25: ex1: add solution
This commit is contained in:
parent
6d7f16f4a9
commit
455b1250a8
486
2019/d25/ex1/ex1.py
Executable file
486
2019/d25/ex1/ex1.py
Executable 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()
|
Loading…
Reference in a new issue