Skip to content

Instantly share code, notes, and snippets.

@gordol
Last active March 28, 2025 03:22
Show Gist options
  • Select an option

  • Save gordol/3cca642a9f2234dd9365edbad3b9fc6d to your computer and use it in GitHub Desktop.

Select an option

Save gordol/3cca642a9f2234dd9365edbad3b9fc6d to your computer and use it in GitHub Desktop.
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'
#!/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