NAV-STORY-003 — Obstacle Detection
Story
As an operator,
I want to register obstacles on the plateau,
so that rovers stop before hitting them and report their last safe position.
Architecture Reference: 05-building-blocks.md — Plateau, MoveForward; 01-introduction.md — FR-7; 06-runtime.md — Scenario 4; 11-risks-and-technical-debts.md — TD-1; 02-constraints.md — DC-6
Scenarios
SCENARIO 1: Rover stops before hitting obstacle
Scenario ID: NAV-STORY-003-S1
GIVEN
WHEN
THEN
SCENARIO 2: Obstacle-stopped rover is marked in output
Scenario ID: NAV-STORY-003-S2
GIVEN
WHEN
THEN
SCENARIO 3: No obstacles means normal behavior
Scenario ID: NAV-STORY-003-S3
GIVEN
WHEN
THEN
SCENARIO 4: Remaining commands are skipped after obstacle
Scenario ID: NAV-STORY-003-S4
GIVEN
WHEN
THEN
Backend Sub-Story
Story ID: NAV-BE-003.1
As a developer I want Plateau.is_blocked() and ObstacleEncountered exception so that obstacle detection is a domain rule that propagates to the application layer.
Architecture Reference: 05-building-blocks.md — Plateau, MoveForward; 04-solution-strategy.md — domain isolation; 11-risks-and-technical-debts.md — TD-1
Scenarios:
SCENARIO 1: MoveForward raises ObstacleEncountered on blocked cell
Scenario ID: NAV-BE-003.1-S1
GIVEN
WHEN
THEN
SCENARIO 2: MissionController catches exception and stops rover mission
Scenario ID: NAV-BE-003.1-S2
GIVEN
WHEN
THEN
Domain changes — mars_rover/domain/plateau.py
from dataclasses import dataclass
@dataclass(frozen=True)
class Plateau:
width: int
height: int
obstacles: frozenset[tuple[int, int]] = frozenset()
def is_within(self, x: int, y: int) -> bool:
return 0 <= x <= self.width and 0 <= y <= self.height
def is_blocked(self, x: int, y: int) -> bool:
return (x, y) in self.obstacles
Domain changes — mars_rover/domain/commands.py
class ObstacleEncountered(Exception):
"""Raised when MoveForward would enter an obstacle cell."""
# Update MoveForward.__call__ only — __init__ is unchanged from NAV-STORY-001
class MoveForward:
def __init__(self, plateau: "Plateau") -> None:
self._plateau = plateau # unchanged
def __call__(self, rover: "Rover") -> None:
dx, dy = rover.heading.delta()
new_x, new_y = rover.x + dx, rover.y + dy
if not self._plateau.is_within(new_x, new_y):
return # boundary safe-stop
if self._plateau.is_blocked(new_x, new_y):
raise ObstacleEncountered() # caller ends the mission
rover.x = new_x
rover.y = new_y
Application changes — MissionController.run()
⚠️ Interface change: run() return type changes from list[Rover] to list[tuple[Rover, bool]]. The __main__.py entry point must be updated accordingly (see below).
from mars_rover.domain.commands import ObstacleEncountered
def run(self, missions: Sequence[tuple[Rover, str]]) -> list[tuple[Rover, bool]]:
command_map = {
"L": TurnLeft(),
"R": TurnRight(),
"M": MoveForward(self._plateau),
}
results: list[tuple[Rover, bool]] = []
for rover, command_string in missions:
obstacle_stopped = False
for ch in command_string:
try:
rover.execute(command_map[ch])
except ObstacleEncountered:
obstacle_stopped = True
break
results.append((rover, obstacle_stopped))
return results
Entry point update — mars_rover/__main__.py
# Updated loop to unpack (rover, obstacle_stopped) tuples
for rover, obstacle_stopped in controller.run(missions):
print(formatter.format(rover, obstacle_stopped=obstacle_stopped))
Unit tests — tests/domain/test_obstacle.py
import pytest
from mars_rover.domain.commands import MoveForward, ObstacleEncountered
from mars_rover.domain.heading import Heading
from mars_rover.domain.plateau import Plateau
from mars_rover.domain.rover import Rover
def test_obstacle_stops_rover():
plateau = Plateau(5, 5, obstacles=frozenset({(2, 2)}))
rover = Rover(1, 2, Heading.E)
with pytest.raises(ObstacleEncountered):
MoveForward(plateau)(rover)
assert rover.x == 1 and rover.y == 2
def test_no_obstacle_moves_normally():
plateau = Plateau(5, 5, obstacles=frozenset())
rover = Rover(1, 2, Heading.E)
MoveForward(plateau)(rover)
assert rover.x == 2 and rover.y == 2
def test_obstacle_output_prefix():
from mars_rover.adapters.output_formatter import OutputFormatter
rover = Rover(1, 2, Heading.E)
assert OutputFormatter().format(rover, obstacle_stopped=True) == "O:1 2 E"
def test_mission_stops_at_obstacle_remaining_commands_skipped():
from mars_rover.application.mission_controller import MissionController
plateau = Plateau(5, 5, obstacles=frozenset({(2, 2)}))
rover = Rover(1, 2, Heading.E)
controller = MissionController(plateau)
results = controller.run([(rover, "MMM")])
final_rover, obstacle_stopped = results[0]
assert obstacle_stopped is True
assert final_rover.x == 1 and final_rover.y == 2 # never moved
Frontend Sub-Story
Story ID: NAV-FE-003.1
As an operator I want to see the O: prefix for obstacle-stopped rovers so that I can distinguish between normal completion and obstacle encounters.
Architecture Reference: 03-context.md — System → Operator interface
Scenarios:
SCENARIO 1: Obstacle-stopped rover has O: prefix in output
Scenario ID: NAV-FE-003.1-S1
GIVEN
WHEN
THEN
Example output:
Input format for obstacles is not defined by the original kata — a suggested extension:
(Second line = obstacle coordinates, one per line before rover blocks.)
Infrastructure Sub-Story
Story ID: NAV-INFRA-003.1
As a developer I want obstacle detection functionality to be containerized and testable so that obstacle handling works consistently across environments.
Architecture Reference: 07-deployment.md — deployment topology; 11-risks-and-technical-debts.md — TD-1; 02-constraints.md — DC-6
SCENARIO 1: Container processes obstacle-stopped rovers and marks output correctly
Scenario ID: NAV-INFRA-003.1-S1
GIVEN
WHEN
THEN
Obstacle-stopped rover is marked with “O:” prefix in output
Final position reflects the last safe position before obstacle
Container completes successfully with exit code 0
Other rovers continue processing normally
SCENARIO 3: Dockerfile builds with obstacle detection dependencies
Scenario ID: NAV-INFRA-003.1-S3
GIVEN
WHEN
THEN
The build includes obstacle detection code and dependencies
Plateau obstacle checking is available in the container
The container can handle ObstacleEncountered exceptions correctly
Build completes without errors
SCENARIO 4: Test suite validates obstacle detection inside container
Scenario ID: NAV-INFRA-003.1-S4
GIVEN
WHEN
THEN
All obstacle detection tests run inside the container
Tests validate obstacle stopping and output formatting
pytest discovers and executes all obstacle-related tests
Container exits with code 0 on test success