Skip to content

Commit

Permalink
add turn off command
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Sabatini committed Sep 10, 2024
1 parent 273e001 commit 1176f01
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 43 deletions.
5 changes: 5 additions & 0 deletions app/command_handler/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class Move(Command):
id: MarsRoverId


@dataclasses.dataclass
class TurnOff(Command):
id: MarsRoverId


@dataclasses.dataclass
class NotifyObstacleHit(Command):
message: str
17 changes: 17 additions & 0 deletions app/command_handler/turn_off_command_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from app.command_handler.commands import TurnOff
from app.ddd.basics import CommandHandler
from app.domain.events import MarsRoverTurnedOff
from app.domain.mars_rover import MarsRover
from app.infrastructure.mars_rover_repository import InMemoryMarsRoverRepository


class TurnOffCommandHandler(CommandHandler):
def __init__(self, repo: InMemoryMarsRoverRepository):
self.repo = repo

def handle(self, command: TurnOff) -> MarsRoverTurnedOff:
mars_rover: MarsRover = self.repo.get_by_id(command.id)
event = mars_rover.turn_off()
self.repo.save(mars_rover)

return event
5 changes: 2 additions & 3 deletions app/ddd/command_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ def __init__(self,

self.commands: List[Command] = []

def submit(self, commands: List[Command]):
for c in commands:
self.commands.append(c)
def submit(self, command: Command):
self.commands.append(command)

def run(self):
while self.commands:
Expand Down
49 changes: 36 additions & 13 deletions app/domain/mars_rover.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from app.ddd.basics import Aggregate
from app.domain.direction import Direction
from app.domain.events import MarsRoverMoved, ObstacleHit, MarsRoverStarted
from app.domain.events import MarsRoverMoved, ObstacleHit, MarsRoverStarted, MarsRoverTurnedOff
from app.domain.mars_rover_id import MarsRoverId
from app.domain.point import Point
from app.domain.world import World
Expand All @@ -14,6 +14,7 @@ class MarsRoverStatus(enum.Enum):
STARTED = "STARTED"
MOVED = "MOVED"
OBSTACLE_HIT = "OBSTACLE_HIT"
TURNED_OFF = "TURNED_OFF"


@dataclasses.dataclass
Expand All @@ -27,7 +28,10 @@ def start(self) -> MarsRoverStarted:
self.status = MarsRoverStatus.STARTED
return MarsRoverStarted.create(self.id)

def turn_right(self) -> MarsRoverMoved:
def turn_right(self) -> MarsRoverMoved | MarsRoverTurnedOff:
if self._is_turned_off():
return MarsRoverTurnedOff.create(self.id)

match self.direction:
case Direction.NORTH:
self.direction = Direction.EAST
Expand All @@ -41,7 +45,10 @@ def turn_right(self) -> MarsRoverMoved:
self.status = MarsRoverStatus.MOVED
return MarsRoverMoved.create(id=self.id)

def turn_left(self) -> MarsRoverMoved:
def turn_left(self) -> MarsRoverMoved | MarsRoverTurnedOff:
if self._is_turned_off():
return MarsRoverTurnedOff.create(self.id)

match self.direction:
case Direction.NORTH:
self.direction = Direction.WEST
Expand All @@ -55,23 +62,32 @@ def turn_left(self) -> MarsRoverMoved:
self.status = MarsRoverStatus.MOVED
return MarsRoverMoved.create(id=self.id)

def move(self) -> MarsRoverMoved | ObstacleHit:
def move(self) -> MarsRoverMoved | ObstacleHit | MarsRoverTurnedOff:
if self._is_turned_off():
return MarsRoverTurnedOff.create(self.id)

rover_x = self.actual_point.x
rover_y = self.actual_point.y

world_x = self.world.x()
world_y = self.world.y()

match self.direction:
case Direction.NORTH:
next_y = (self.actual_point.y + 1) % self.world.dimension[1]
next_point = Point.create(self.actual_point.x, next_y)
next_y = (rover_y + 1) % world_y
next_point = Point.create(rover_x, next_y)

case Direction.SOUTH:
next_y = (self.actual_point.y - 1) % self.world.dimension[1]
next_point = Point.create(self.actual_point.x, next_y)
next_y = (rover_y - 1) % world_y
next_point = Point.create(rover_x, next_y)

case Direction.WEST:
next_x = (self.actual_point.x - 1) % self.world.dimension[0]
next_point = Point.create(next_x, self.actual_point.y)
next_x = (rover_x - 1) % world_x
next_point = Point.create(next_x, rover_y)

case Direction.EAST:
next_x = (self.actual_point.x + 1) % self.world.dimension[0]
next_point = Point.create(next_x, self.actual_point.y)
next_x = (rover_x + 1) % world_x
next_point = Point.create(next_x, rover_y)

if self.world.hit_obstacles(next_point):
self.status = MarsRoverStatus.OBSTACLE_HIT
Expand All @@ -81,11 +97,18 @@ def move(self) -> MarsRoverMoved | ObstacleHit:
self.status = MarsRoverStatus.MOVED
return MarsRoverMoved.create(id=self.id)

def turn_off(self) -> MarsRoverTurnedOff:
self.status = MarsRoverStatus.TURNED_OFF
return MarsRoverTurnedOff.create(id=self.id)

def coordinate(self):
hit_obstacles = "O:" if self._is_obstacle_hit() else ""
hit_obstacles = "O:" if self._is_obstacle_hit() or self._is_turned_off() else ""

return f"{hit_obstacles}{self.actual_point.x}:{self.actual_point.y}:{self.direction.value}"

def _is_turned_off(self):
return MarsRoverStatus.TURNED_OFF == self.status

def _is_obstacle_hit(self):
return self.status == MarsRoverStatus.OBSTACLE_HIT

Expand Down
6 changes: 6 additions & 0 deletions app/domain/world.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ class World:
dimension: Tuple[int, int]
obstacles: Obstacles

def x(self):
return self.dimension[0]

def y(self):
return self.dimension[1]

def hit_obstacles(self, point: Point):
for p in self.obstacles.points:
if p == point:
Expand Down
6 changes: 3 additions & 3 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ def group_by(storage):
random_commands = generate_random_commands(commands_length)
commands[id] = random_commands

runner.execute(id, commands[id])
runner.execute(id.value, commands[id])

grouped_paths = group_by(paths_table)

for id in mars_rover_ids_table:
print(f"################################################################################")
print(f"RoverId: {id}")
mars_rover = repository.get_by_id(MarsRoverId(id))
mars_rover = repository.get_by_id(id)
print(f"Commands: {commands.get(id)}")
print(f"Actual Coordinate: {mars_rover.coordinate()}")
print(f"Paths: {grouped_paths.get(id)}, length: {len(grouped_paths.get(id))}")
print(f"Paths: {grouped_paths.get(id.value)}, length: {len(grouped_paths.get(id.value))}")
print(f"Obstacles: {obstacles_table}")
7 changes: 6 additions & 1 deletion app/policy/policies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from app.command_handler.commands import NotifyObstacleHit
from app.command_handler.commands import NotifyObstacleHit, TurnOff
from app.ddd.basics import Policy, Command
from app.domain.events import ObstacleHit


class TurnOffObstacleHitPolicy(Policy):
def apply(self, event: ObstacleHit) -> Command:
return TurnOff(id=event.id)


class NotifyObstacleHitPolicy(Policy):
def apply(self, event: ObstacleHit) -> Command:
return NotifyObstacleHit(message=f"Rover {event.id.value} hit obstacle")
2 changes: 1 addition & 1 deletion app/projection/mars_rover_start_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ def project(self, event: MarsRoverStarted):
raw = {"id": mars_rover.id.value, "actual_point": mars_rover.coordinate()}

self.paths_storage.append(raw)
self.mars_rover_storage.append(mars_rover.id.value)
self.mars_rover_storage.append(mars_rover.id)
18 changes: 10 additions & 8 deletions app/service/mars_rover_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import List, Dict, Tuple, Set

from app.command_handler.commands import TurnRight, TurnLeft, Move, StartMarsRover
from app.ddd.basics import Command
from app.ddd.command_dispatcher import InMemoryCommandDispatcher
from app.domain.direction import Direction
from app.domain.mars_rover_id import MarsRoverId
Expand Down Expand Up @@ -39,14 +40,15 @@ def with_world(self, obstacles: List[Tuple[int, int]], world_dimension: Tuple[in
return self

def start_rover(self):
start = StartMarsRover(initial_point=self.initial_point,
initial_direction=self.initial_direction,
world=self.world)
self.command_dispatcher.submit([start])
command = StartMarsRover(initial_point=self.initial_point,
initial_direction=self.initial_direction,
world=self.world)

self.command_dispatcher.submit(command)
self.command_dispatcher.run()

def execute(self, rover_id: str, commands: str):
parsed_commands = [self.command_map[c](MarsRoverId(rover_id)) for c in commands]

self.command_dispatcher.submit(commands=parsed_commands)
self.command_dispatcher.run()
commands: List[Command] = [self.command_map[c](MarsRoverId(rover_id)) for c in commands]
for c in commands:
self.command_dispatcher.submit(command=c)
self.command_dispatcher.run()
9 changes: 7 additions & 2 deletions app/workflow_factory.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from typing import List, Dict, Tuple, Set

from app.command_handler.commands import TurnRight, TurnLeft, Move, StartMarsRover, NotifyObstacleHit
from app.command_handler.commands import TurnRight, TurnLeft, Move, StartMarsRover, NotifyObstacleHit, TurnOff
from app.command_handler.move_command_handlers import MoveCommandHandler
from app.command_handler.notify_obstacle_command_handler import NotifyObstacleCommandHandler
from app.command_handler.start_mars_rover_command_handlers import StartMarsRoverCommandHandler
from app.command_handler.turn_left_command_handlers import TurnLeftCommandHandler
from app.command_handler.turn_off_command_handlers import TurnOffCommandHandler
from app.command_handler.turn_right_command_handlers import TurnRightCommandHandler
from app.ddd.command_dispatcher import InMemoryCommandDispatcher
from app.ddd.command_dispatcher_builder import InMemoryCommandDispatcherBuilder
from app.domain.events import MarsRoverMoved, ObstacleHit, MarsRoverStarted
from app.domain.mars_rover_id import MarsRoverId
from app.infrastructure.mars_rover_repository import InMemoryMarsRoverRepository
from app.policy.policies import NotifyObstacleHitPolicy
from app.policy.policies import NotifyObstacleHitPolicy, TurnOffObstacleHitPolicy
from app.projection.mars_rover_ostacles_projection import MarsRoverObstaclesProjection
from app.projection.mars_rover_path_projection import MarsRoverPathProjection
from app.projection.mars_rover_start_projection import MarsRoverStartProjection
Expand All @@ -25,8 +26,10 @@ def create_command_dispatcher(mars_rover_repo: InMemoryMarsRoverRepository,
turn_right = TurnRightCommandHandler(repo=mars_rover_repo)
turn_left = TurnLeftCommandHandler(repo=mars_rover_repo)
move = MoveCommandHandler(repo=mars_rover_repo)
turn_off = TurnOffCommandHandler(repo=mars_rover_repo)
notify_obstacle = NotifyObstacleCommandHandler()

turn_off_policy = TurnOffObstacleHitPolicy()
notify_obstacle_policy = NotifyObstacleHitPolicy()

start_projection = MarsRoverStartProjection(repo=mars_rover_repo,
Expand All @@ -41,7 +44,9 @@ def create_command_dispatcher(mars_rover_repo: InMemoryMarsRoverRepository,
.with_command_handler(TurnRight, turn_right)
.with_command_handler(TurnLeft, turn_left)
.with_command_handler(Move, move)
.with_command_handler(TurnOff, turn_off)
.with_command_handler(NotifyObstacleHit, notify_obstacle)
.with_policy(ObstacleHit, turn_off_policy)
.with_policy(ObstacleHit, notify_obstacle_policy)
.with_projection(MarsRoverStarted, start_projection)
.with_projection(MarsRoverMoved, path_projection)
Expand Down
24 changes: 12 additions & 12 deletions test/test_e2e.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
from typing import List, Tuple, Set

from app.domain.direction import Direction
from app.domain.mars_rover import MarsRover
Expand All @@ -15,9 +16,9 @@ class TestE2E(unittest.TestCase):
#
def test_execute_some_commands(self):
repo = InMemoryMarsRoverRepository()
mars_rover_start_view = []
mars_rover_start_view: List[MarsRoverId] = []
mars_rover_path_view = []
obstacle_view = []
obstacle_view: Set[Tuple[int, int]] = set()

runner = (
MarsRoverRunner(repository=repo,
Expand All @@ -26,23 +27,22 @@ def test_execute_some_commands(self):
obstacle_view=obstacle_view)
.with_initial_point(x=0, y=0)
.with_initial_direction(direction=Direction.NORTH)
.with_world(world_dimension=(4, 4),
obstacles=[])
.with_world(world_dimension=(4, 4), obstacles=[])
)
runner.start_rover()

id = mars_rover_start_view[0]

runner.execute(rover_id=id, commands="RMLMM")
runner.execute(rover_id=id.value, commands="RMLMM")

actual: MarsRover = repo.get_by_id(MarsRoverId(id))
actual: MarsRover = repo.get_by_id(id)
self.assertEqual("1:2:N", actual.coordinate())
self.assertEqual("MOVED", actual.status.value)

expected_path = ["0:0:N", "0:0:E", "1:0:E", "1:0:N", "1:1:N", "1:2:N"]
self._assert_paths(expected=expected_path, actual=mars_rover_path_view)

self.assertListEqual([], obstacle_view)
self.assertSetEqual(set(), obstacle_view)

# x x x x
# x x o x
Expand Down Expand Up @@ -70,20 +70,20 @@ def test_hit_obstacle(self):

id = mars_rover_start_view[0]

runner.execute(rover_id=id, commands="RMMLMMMMMM")
runner.execute(rover_id=id.value, commands="RMMLMMMMMMMMMMMRMMMM")

actual: MarsRover = repo.get_by_id(MarsRoverId(id))
actual: MarsRover = repo.get_by_id(id)
self.assertEqual("TURNED_OFF", actual.status.value)
self.assertEqual("O:2:1:N", actual.coordinate())
self.assertEqual("OBSTACLE_HIT", actual.status.value)

expected_path = ["0:0:N", "0:0:E", "1:0:E", "2:0:E", "2:0:N", "2:1:N"]
self._assert_paths(expected=expected_path, actual=mars_rover_path_view)

expected_obstacles = {(2, 2)}
self._assert_obstacles(expected=expected_obstacles, actual=obstacle_view)

def _assert_obstacles(self, expected, actual):
self.assertEqual(expected, actual)
def _assert_obstacles(self, expected: set, actual: set):
self.assertSetEqual(expected, actual)

def _assert_paths(self, expected, actual):
actual_path = [p["actual_point"] for p in actual]
Expand Down

0 comments on commit 1176f01

Please sign in to comment.