Last active
March 28, 2025 03:22
-
-
Save gordol/3cca642a9f2234dd9365edbad3b9fc6d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from typing import Optional, List, Literal, Tuple, Generator, Union | |
| class PizzaGrid: | |
| """A simple class to encapsulate the needed state and helper constructs to orchestrate pizza deliveries. | |
| You may pass in optional start_coords as a tuple of x/y coodinates. | |
| You can spin up a goat to help the human courier by setting the with_goat boolean. | |
| You can ignore invalid commands, or throw an exception, with the ignore_invalid_commands boolean. | |
| """ | |
| def __init__(self, start_coords:Optional[Tuple[int, int]]=(0, 0), with_goat:Optional[bool]=False, ignore_invalid_commands:Optional[bool]=True): | |
| """ | |
| :param start_coordsOptional[Tuple[int, int]]: Optional tuple of x/y start coordinates (Default value = (0, 0) | |
| :param with_goat:bool: Optional boolean to dispatch a goat also (Default value = False) | |
| :param ignore_invalid_commands:bool: Optional boolean control behavior upon invalid commands (Default value = True) | |
| """ | |
| self.deliveries: List[Tuple[int, int, type(self.Deliverer), str]] = [] | |
| self.dispatcher = PizzaGrid.Dispatcher(self) | |
| self.goat = PizzaGrid.Goat(self, start_coords) if with_goat else None | |
| self.human = PizzaGrid.Human(self, start_coords) | |
| self.ignore_invalid_commands = ignore_invalid_commands | |
| def dispatch(self, commands:Optional[str]=None) -> None: | |
| """Dispatches the courier(s) | |
| :param commands:Optional[str]: (Default value = None) | |
| """ | |
| self.dispatcher.dispatch(commands) | |
| def summary(self, verbose:bool=False): | |
| """Show a summary of the delivery | |
| :param verbose:bool: Show commands that were executed, and total deliveries (Default value = False) | |
| """ | |
| if verbose: | |
| for delivery in self.deliveries: | |
| print('Delivered to (%s, %s) with %s by command %s' % delivery) | |
| print('Total deliveries: %s' % self.total_deliveries) | |
| print('Total unique deliveries: %s' % self.total_unique_deliveries) | |
| @property | |
| def total_deliveries(self) -> int: | |
| """Counts total deliveries""" | |
| return len(self.deliveries) | |
| @property | |
| def total_unique_deliveries(self) -> int: | |
| """Counts total unique deliveries""" | |
| return len(set([(delivery[0], delivery[1]) for delivery in self.deliveries])) | |
| class CommandException(Exception): | |
| pass | |
| class InvalidCommand(CommandException): | |
| def __init__(self, command): | |
| self.message = 'Command: "%s" is not valid' % command | |
| class Dispatcher: | |
| """Reads commands, and sends them to instances of PizzaGrid.Deliverer""" | |
| def __init__(self, grid): | |
| self.grid = grid | |
| def command_stream(self, commands:str=None) -> Generator[Tuple[int, str], None, None]: | |
| """Yields a stream of commands, either from input, or from the PizzaDeliveryInput.txt file. | |
| :param commands:str: String of commands to override the input file (Default value = None) | |
| """ | |
| cursor = 0 | |
| if commands: | |
| for command in commands: | |
| yield (cursor, command) | |
| cursor += 1 | |
| else: | |
| with open('PizzaDeliveryInput.txt', 'r') as f: | |
| command = f.read(1) | |
| while command: | |
| yield (cursor, command) | |
| cursor += 1 | |
| command = f.read(1) | |
| def dispatch(self, commands:Optional[str]) -> None: | |
| """Dispatches a Deliverer | |
| :param commands:Optional[str]: An optional string of command overrides may be passed in to facilitate extensibility (ex. test harness support) | |
| """ | |
| for cursor, command in self.command_stream(commands): | |
| try: | |
| if self.grid.goat: | |
| if cursor % 2: | |
| self.grid.human.parse(command) | |
| else: | |
| self.grid.goat.parse(command) | |
| else: | |
| self.grid.human.parse(command) | |
| except PizzaGrid.InvalidCommand: | |
| if self.grid.ignore_invalid_commands: | |
| pass | |
| else: | |
| raise | |
| class Deliverer: | |
| """Holds the Deliverer state and handles command parsing from the Dispatcher""" | |
| def __init__(self, grid, start_coords=(0, 0)): | |
| self.grid = grid | |
| self.x = start_coords[0] | |
| self.y = start_coords[1] | |
| self.command = None | |
| #deliver to the starting point first, upon dispatch/instantiation | |
| self.deliver() | |
| self.command_map = { | |
| '<': self.west, | |
| '>': self.east, | |
| '^': self.north, | |
| 'v': self.south, | |
| } | |
| def deliver(self) -> None: | |
| """Adds a delivery to the grid state""" | |
| self.grid.deliveries.append((self.x, self.y, self, self.command)) | |
| def parse(self, command:Literal['^', 'v', '<', '>']): | |
| """Parse a command code and execute the appropriate movement action, then deliver. | |
| :param command:str: A command string, should be one of: <, >, ^, v | |
| """ | |
| if command in self.command_map: | |
| self.command = command | |
| self.command_map[command]() | |
| self.deliver() | |
| else: | |
| raise PizzaGrid.InvalidCommand(command) | |
| def north(self) -> None: | |
| """Move north""" | |
| self.y += 1 | |
| def south(self) -> None: | |
| """Move south""" | |
| self.y -= 1 | |
| def west(self) -> None: | |
| """Move west""" | |
| self.x -= 1 | |
| def east(self) -> None: | |
| """Move east""" | |
| self.x += 1 | |
| class Goat(Deliverer): | |
| """A goat is a type of Deliverer""" | |
| def __repr__(self) -> str: | |
| return 'Goat' | |
| class Human(Deliverer): | |
| """A human is a type of deliverer""" | |
| def __repr__(self) -> str: | |
| return 'Human' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import unittest | |
| from pizza import PizzaGrid | |
| class TestPart1WithHumanOnly(unittest.TestCase): | |
| def setUp(self): | |
| self.grid = PizzaGrid() | |
| def test_two_deliveries(self): | |
| self.grid.dispatch('>') | |
| assert self.grid.total_unique_deliveries == 2 | |
| def test_five_deliveries_4_houses(self): | |
| self.grid.dispatch('^>v<') | |
| assert self.grid.total_unique_deliveries == 4 | |
| def test_five_deliveries_4_houses_start_house_got_two_pizzas(self): | |
| self.grid.dispatch('^>v<') | |
| assert len([d for d in self.grid.deliveries if (d[0], d[1]) == (0, 0)]) == 2 | |
| def test_ten_deliveries_back_and_forth_2_houses(self): | |
| self.grid.dispatch('^v^v^v^v^v') | |
| assert self.grid.total_deliveries == 11 | |
| assert self.grid.total_unique_deliveries == 2 | |
| class TestPart2WithGoat(unittest.TestCase): | |
| def setUp(self): | |
| self.grid = PizzaGrid(with_goat=True) | |
| def test_three_houses_human_goes_north(self): | |
| self.grid.dispatch('^v') | |
| assert self.grid.total_unique_deliveries == 3 | |
| assert self.grid.deliveries[2][0] == 0 | |
| assert self.grid.deliveries[2][1] == 1 | |
| def test_three_houses_goat_goes_south(self): | |
| self.grid.dispatch('^v') | |
| assert self.grid.total_unique_deliveries == 3 | |
| assert self.grid.deliveries[3][0] == 0 | |
| assert self.grid.deliveries[3][1] == -1 | |
| def test_three_houses_both_end_at_start(self): | |
| self.grid.dispatch('^>v<') | |
| assert self.grid.total_unique_deliveries == 3 | |
| assert self.grid.deliveries[-1][0] == 0 | |
| assert self.grid.deliveries[-1][1] == 0 | |
| assert self.grid.deliveries[-2][0] == 0 | |
| assert self.grid.deliveries[-2][1] == 0 | |
| def test_three_houses_with_4_pizzas_at_start(self): | |
| self.grid.dispatch('^>v<') | |
| assert len([d for d in self.grid.deliveries if (d[0], d[1]) == (0, 0)]) == 4 | |
| def test_eleven_houses(self): | |
| self.grid.dispatch('^v^v^v^v^v') | |
| assert self.grid.total_unique_deliveries == 11 | |
| def test_eleven_houses_human_goes_north(self): | |
| self.grid.dispatch('^v^v^v^v^v') | |
| assert self.grid.deliveries[2][0] == 0 | |
| assert self.grid.deliveries[2][1] == 1 | |
| assert self.grid.deliveries[4][0] == 0 | |
| assert self.grid.deliveries[4][1] == 2 | |
| assert self.grid.deliveries[6][0] == 0 | |
| assert self.grid.deliveries[6][1] == 3 | |
| assert self.grid.deliveries[8][0] == 0 | |
| assert self.grid.deliveries[8][1] == 4 | |
| def test_eleven_houses_goat_goes_south(self): | |
| self.grid.dispatch('^v^v^v^v^v') | |
| assert self.grid.deliveries[3][0] == 0 | |
| assert self.grid.deliveries[3][1] == -1 | |
| assert self.grid.deliveries[5][0] == 0 | |
| assert self.grid.deliveries[5][1] == -2 | |
| assert self.grid.deliveries[7][0] == 0 | |
| assert self.grid.deliveries[7][1] == -3 | |
| assert self.grid.deliveries[9][0] == 0 | |
| assert self.grid.deliveries[9][1] == -4 | |
| class TestInvalidCommand(unittest.TestCase): | |
| def setUp(self): | |
| self.grid = PizzaGrid(ignore_invalid_commands=False) | |
| def test_three_houses_with_invalid_command_expected_failure(self): | |
| """this should raise an exception, and we should only have two deliveries""" | |
| with self.assertRaises(PizzaGrid.InvalidCommand) as context: | |
| self.grid.dispatch('^lv') | |
| self.assertTrue('InvalidCommand: l' in context.exception) | |
| assert self.grid.total_unique_deliveries == 2 | |
| assert self.grid.deliveries[-1][0] == 0 | |
| assert self.grid.deliveries[-1][1] == 1 | |
| class TestIgnoreInvalidCommand(unittest.TestCase): | |
| def setUp(self): | |
| self.grid = PizzaGrid(with_goat=False, ignore_invalid_commands=True) | |
| def test_three_houses_without_goat_invalid_command_ignored(self): | |
| """this should behave as usual, with 4 deliveries""" | |
| self.grid.dispatch('^>lv<') | |
| assert self.grid.total_unique_deliveries == 4 | |
| class TestIgnoreInvalidCommandWithGoat(unittest.TestCase): | |
| """this is a a slightly odd test case to make sure behavior is as expected during invalid commands | |
| the dispatcher still alternates between humand and goat. | |
| the one that receives an invalid command should remain in place. | |
| """ | |
| def setUp(self): | |
| self.grid = PizzaGrid(with_goat=True, ignore_invalid_commands=True) | |
| def test_three_houses_with_goat_invalid_command_ignored(self): | |
| self.grid.dispatch('^>lv<') | |
| assert self.grid.total_unique_deliveries == 5 | |
| assert self.grid.deliveries[-1][0] == -1 | |
| assert self.grid.deliveries[-1][1] == 1 | |
| assert self.grid.deliveries[-2][0] == 1 | |
| assert self.grid.deliveries[-2][1] == -1 | |
| if __name__ == '__main__': | |
| unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment