class Droid: def __init__(self) -> None: with open_fixture("day15") as fp: data = decode(fp.readline()) self.vm = IntcodeVM(data) self.grid = Grid() self.pos = Position(0, 0) self.oxygen_system_pos = Position(0, 0) @property def cell(self) -> Cell: return self.grid[self.pos] def move(self, direction: Direction) -> bool: self.vm.io_push(direction.value) while not self.vm.stdout: self.vm.step() status = Status(self.vm.io_pop()) pos = self.pos.move(direction) self.grid.set_status(pos, status) if status.passable: self.pos = pos if status == Status.OXYGENATED: self.oxygen_system_pos = pos return True return False def explore(self) -> bool: # first explore unexplored cells for direction in Direction.all(): npos = self.pos.move(direction) if npos not in self.grid: if self.move(direction): return True # invariant: all neighbors are known # quit if home again if not self.pos: return False # move back towards home options = {} for direction in Direction.all(): npos = self.pos.move(direction) cell = self.grid[npos] if cell.status == Status.OKAY: options[cell.distance] = direction continue assert options best = sorted(options.keys())[0] assert best < self.cell.distance direction = options[best] assert self.move(direction) return True
class Robot: def __init__(self, program: Data) -> None: self.vm = IntcodeVM(program) self.grid = Grid() self.orientation = Orientation.UP self.position = (0, 0) def step(self) -> bool: # get current color self.vm.io_push(self.grid[self.position].value) # run the program until output is availble while len(self.vm.stdout) < 2: if not self.vm.step(): return False # paint color = Color(self.vm.io_pop()) self.grid[self.position] = color # turn direction = self.vm.io_pop() if direction: self.orientation = self.orientation.turn_right() else: self.orientation = self.orientation.turn_left() # move if self.orientation == Orientation.UP: self.position = (self.position[0], self.position[1] - 1) if self.orientation == Orientation.RIGHT: self.position = (self.position[0] + 1, self.position[1]) if self.orientation == Orientation.DOWN: self.position = (self.position[0], self.position[1] + 1) if self.orientation == Orientation.LEFT: self.position = (self.position[0] - 1, self.position[1]) return not self.vm.halted def run(self) -> None: while self.step(): pass
class ArcadeCabinet: def __init__(self, data: Data) -> None: self.vm = IntcodeVM(data, trap_input=self._trap_input) self.score = 0 self.frame = 0 self.init_block_count = 0 self.last_good_frame = 0 self.last_good_state = data.copy() self.last_miss_x = 0 self.last_paddle_x = 0 self.failed = False self.moves: Optional[List[int]] = None self.stdscr: Any = None def step(self) -> bool: if not self.vm.step(): return False if len(self.vm.stdout) == 3: self._trap_draw_tile() return True def _trap_draw_tile(self) -> None: x = self.vm.io_pop() y = self.vm.io_pop() if x == -1 and y == 0: # update score self.score = self.vm.io_pop() return # paint a tile tile = Tile(self.vm.io_pop()) if tile == Tile.BLOCK: # track count for part 1 self.init_block_count += 1 if tile == Tile.PADDLE: self.last_paddle_x = x if tile == Tile.BALL: # did the ball hit the paddle if y == 20 and x in [ self.last_paddle_x - 1, self.last_paddle_x, self.last_paddle_x + 1, ]: self.last_good_frame = self.frame self.last_good_state = self.vm.data.copy() # did the ball go past the paddle if y == 21: self.failed = True self.last_miss_x = x # update curses window if self.stdscr is None: return self.stdscr.addstr(y, x, str(tile)) def _trap_input(self) -> int: """ Called whenever the vm is starved for input, between each iteration of the program state. """ self.frame += 1 v = 0 # neutral paddle movement if self.moves: v = self.moves[0] self.moves = self.moves[1:] stdscr = self.stdscr if stdscr is None: return v # paint curses window stdscr.addstr(24, 0, f"Score: {self.score} Frame: {self.frame}") stdscr.refresh() sleep(0.01) return v def run(self, stdscr=None, moves: Optional[List[int]] = None) -> bool: # if stdscr: self.stdscr = stdscr self.moves = moves while not self.vm.halted: self.step() return not self.failed
class Ascii: def __init__( self, wake_up: bool = False, ): with open_fixture("day17") as fp: data = decode(fp.readline()) self.vm = IntcodeVM(data) if wake_up: self.vm.data[0] = 2 def putc(self, c: int) -> None: """ Block until the character is written to the vm input buffer. """ assert not self.vm.stdin assert not self.vm.stdout self.vm.io_push(c) while self.vm.step() and self.vm.stdin: assert not self.vm.stdout def write(self, s: str) -> None: """ Block until the whole string is written to the vm input buffer. """ for c in s: self.putc(ord(c)) def getc(self) -> str: """ Block until a character is read from the vm output buffer. """ assert not self.vm.stdin assert not self.vm.stdout while self.vm.step(): if self.vm.stdout: return chr(self.vm.io_pop()) return "" def readline(self) -> str: """ Block until a line is read from the vm output buffer. """ with StringIO() as s: while True: c = self.getc() if not c: break s.write(c) if c == "\n": break return s.getvalue() def readgrid(self) -> str: """ Block until a whole grid is read from the vm output buffer. """ with StringIO() as s: for _ in range((GRID_WIDTH + 1) * GRID_HEIGHT): c = self.getc() s.write(c) if not c: break assert not self.vm.stdout return s.getvalue() def get_dust_count(self) -> int: """ Run the program to completion and return the sum of dust collected. """ while self.vm.step(): continue return self.vm.stdout[len(self.vm.stdout) - 1]