""" tubes.py Playing with adventofcode.com/2017/day/19 i.e. "a series of tubes". running it ---------- $ python tubes.py --- a series of tubes - adventofcode.com/2017/day/19 --- Animate "example 1"? (y/n) n The symbols of maze "example 1" are ABCDEF. Animate "example 2"? (y/n) n The symbols of maze "example 2" are SUPER. Animate "adventofcode input"? (y/n) n The symbols of maze "adventofcode input" are PVBSCMEQHY. global variables ---------------- up directions (pairs of integers) down left right maze_example_1 mazes (lists of strings) maze_example_2 maze_input functions --------- maze = maze_from_string(maze_string) maze = maze_from_file(filename) (row_min, row_max, col_min, col_max) = get_min_max(maze) symbol = look(maze, position) position = (row, column) = start_position(maze) new_position = next_position(position, direction) partial_maze_string = display(maze, position, window=True, window_width=20) (position, direction, syms) = move(maze, position, direction, syms) clear_terminal() symbols = traverse(maze, animate=False) main() implementation notes -------------------- A maze is represented as as list of strings which can be treated as a matrix of characters i.e. maze[row][col]], where col means column and the top left is the (row=0, col=0) corner. A position in the maze is a pair (row, column), and similarly a direction is a pair like (1, 0) giving the row and column offsets. While moving through a maze, the state that keeps track of things is position coords of where we are in the maze (pair of integers) direction offsets needed to change position (ditto) symbols list of letters seen as we travel the maze I started with one maze as just a hardcoded string, then decided it would be simpler to access as a list of strings, and then when it came time to work on a big one needed a way to read one in from a file ... which is why there are several maze conversion function here. The three mazes looked at are example 1 a small maze in a text string example 2 a small maze in the file './maze_example_2.txt' maze input the big maze from adventofcode, './maze_input.txt' Are we having fun yet? Tested with python 3.5. --------------------------------------------------------------------- Jim Mahoney | cs.marlboro.college | Oct 2018 | MIT License """ import time # The first example in the advent of code site is : maze_string_1 = """ | | +--+ A | C F---|----E|--+ | | | D +B-+ +--+ """ # Global variables for the four possible directions : (up, down, left, right) = ((-1, 0), (1, 0), (0, -1), (0, 1)) def maze_from_string(maze_string): """ return maze as list of strings given one long string """ # split on newlines and leave out empty lines lines = maze_string.split('\n') result = [] for line in lines: if len(line) > 1: result.append(line) return result def maze_from_file(filename): """ return maze as list of strings from file """ # remove newlines and leave out empty lines lines = open(filename).readlines() result = [] for line in lines: if len(line) > 1: if line[-1] == '\n': result.append(line[0:-1]) # remove trailing newline else: result.append(line) return result # And here are the three mazes that we'll play around with. # (Since these are global names, I can use them in doctests.) maze_example_1 = maze_from_string(maze_string_1) maze_example_2 = maze_from_file('maze_example_2.txt') maze_input = maze_from_file('maze_input.txt') def get_min_max(maze): """ return (row_min, row_max, col_min, col_max) of maze >>> get_min_max(maze_example_1) (0, 5, 0, 15) """ # The rows should all be the same length, # so I'll just use the length of maze[0] to get the width. (row_min, row_max) = (0, len(maze) - 1) (col_min, col_max) = (0, len(maze[0]) - 1) return (row_min, row_max, col_min, col_max) def look(maze, position): """ Return character at given (row, col) position in the maze, and a space if position is past the edge of the maze. >>> look(maze_example_1, (1,8)) '+' >>> look(maze_example_1, (1000, 23)) ' ' """ (row, col) = position (row_min, row_max, col_min, col_max) = get_min_max(maze) if row < row_min or row > row_max or col < col_min or col > col_max: return ' ' else: return maze[row][col] def start_position(maze): """ Return coords of '|' in top row (row=0) of maze >>> start_position(maze_example_1) (0, 5) """ row = 0 for col in range(len(maze[1])): if look(maze, (row, col)) == '|': return (row, col) return (-1, -1) # error return - we shouldn't get here. def display(maze, position, window=True, window_width=20): """ Return the whole maze or a window around the current position as a one string with newlines between rows and a '*' at the current position. """ result = '' (current_row, current_col) = position if window: halfwidth = int(window_width/2) (row_min, row_max, col_min, col_max) = (position[0] - halfwidth, position[0] + halfwidth, position[1] - halfwidth, position[1] + halfwidth) else: (row_min, row_max, col_min, col_max) = get_min_max(maze) # Build the result explicitly symbol by symbol with look() # so things work even if the window extends past the maze edges. for row in range(row_min, row_max + 1): for col in range(col_min, col_max + 1): if row == current_row and col == current_col: result = result + '*' else: result = result + look(maze, (row, col)) result = result + '\n' # next line return result def next_position(position, direction): """ return position + direction >>> next_position((3, 4), up) (2, 4) >>> next_position((3, 4), right) (3, 5) """ return (position[0] + direction[0], position[1] + direction[1]) def move(maze, position, direction, letters): """ return (new_position, new_direction, new_letters) after one move. """ new_position = next_position(position, direction) symbol = look(maze, new_position) if symbol in ('|', '-'): # keep going in same direction return (new_position, direction, letters) elif symbol in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': # keep going , remember letter return (new_position, direction, letters + [symbol]) elif symbol == '+': if direction in (up, down): # Turn to go left or right. # Choose which by looking each way and finding a '-' or a letter. on_left = next_position(new_position, left) on_right = next_position(new_position, right) if look(maze, on_left) == '-': return (new_position, left, letters) elif look(maze, on_right) == '-': return (new_position, right, letters) elif look(maze, on_left) in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': return (new_position, left, letters) else: return (new_position, right, letters) else: # Direction must be left or right, so turn to go up or down. # Choose which by looking each way and finding '|' or a letter. above = next_position(new_position, up) below = next_position(new_position, down) if look(maze, above) == '|': return (new_position, up, letters) elif look(maze, below) == '|': return (new_position, down, letters) elif look(maze, above) in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': return (new_position, up, letters) else: return (new_position, down, letters) else: # Something else happened - probably the space at end of the maze. return (new_position, direction, letters) def clear_terminal(): """ clear terminal and put cursor top left """ # google "python3 clear terminal" # https://stackoverflow.com/questions/2084508/clear-terminal-in-python # google "python3 put cursor top left" # https://stackoverflow.com/questions/40103919/move-console-cursor-up # ... which also gives a link to a list of terminal escape sequences : # http://ascii-table.com/ansi-escape-sequences-vt-100.php escape = chr(27) # ANSII ESC character clear_screen = escape + '[2J' cursor_top_left = escape + '[H' print(clear_screen) print(cursor_top_left) def traverse(maze, animate=False): """ Walk through the maze. Return the symbols seen. Optionally display animation showing part of maze at current position """ (where, direction, symbols) = (start_position(maze), down, []) while look(maze, where) != ' ': # Stop when we hit a space. if animate: clear_terminal() print(display(maze, where)) # The default shows part of maze. time.sleep(0.1) (where, direction, symbols) = move(maze, where, direction, symbols) return symbols def main(): print('--- a series of tubes - adventofcode.com/2017/day/19 ---') names = ['example 1', 'example 2', 'adventofcode input'] mazes = [maze_example_1, maze_example_2, maze_input] for which in range(3): query = input('Animate "{}"? (y/n) '.format(names[which])) result = ''.join(traverse(mazes[which], animate=query[0]=='y')) print('The symbols of maze "{}" are {}.'.format(names[which], result)) if __name__ == '__main__': import doctest doctest.testmod() main()