diff --git a/.gitignore b/.gitignore index 1449474..3a85790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ -tdd_*.md diff --git a/README.md b/README.md index d8ef3b1..803531d 100644 --- a/README.md +++ b/README.md @@ -312,86 +312,6 @@ calculator.add('2,1001'); // Returns: 2 (1001 ignored) --- -### 5. Mars Rover ✅ - -**Implementation:** [`lib/mars_rover.dart`](lib/mars_rover.dart) -**Tests:** [`test/mars_rover_test.dart`](test/mars_rover_test.dart) - -A Command Pattern kata simulating a robotic rover navigating a plateau on Mars. Demonstrates clean separation of concerns, value objects, and command-based control. - -#### Domain Context - -A rover explores a rectangular plateau with coordinate-based navigation: - -**Core Concepts:** -- **Position:** (x, y) coordinates on the plateau grid -- **Direction:** Cardinal directions (N, E, S, W) -- **Plateau:** Grid with defined boundaries that wrap around (toroidal topology) - -**Commands:** -- `L` - Turn left 90 degrees (changes direction, not position) -- `R` - Turn right 90 degrees (changes direction, not position) -- `M` - Move forward one grid point in current direction - -**Example Navigation:** -``` -Starting: (0,0) facing North -Commands: "MMRMMLM" -- MM: Move to (0,2) facing North -- R: Turn to face East (still at 0,2) -- MM: Move to (2,2) facing East -- L: Turn to face North (still at 2,2) -- M: Move to (2,3) facing North -Result: (2,3) facing North -``` - -#### Key Design Decisions - -**Direction Enum:** Encapsulates rotation logic using modular arithmetic. Each direction knows how to turn left/right, eliminating conditional branching. - -**Value Objects:** -- `Position` is immutable—movement returns new position instances -- `Plateau` encapsulates boundary wrapping logic -- Prevents invalid states at the type level - -**Command Pattern (Implicit):** The `execute()` method delegates to command handlers (`turnLeft()`, `turnRight()`, `moveForward()`). Each command is isolated and testable. - -**Wrapping Logic:** Plateau boundaries wrap around (toroidal topology). Moving past edge (e.g., x=5→6 on 5x5 grid) wraps to opposite side (x=0). - -#### Usage - -```dart -import 'package:tdd_katas/mars_rover.dart'; - -// Create rover at position (1,2) facing North on 5x5 plateau -final rover = Rover( - x: 1, - y: 2, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, -); - -rover.execute('LMLMLMLMM'); - -print('Position: (${rover.x}, ${rover.y})'); // Position: (1, 3) -print('Direction: ${rover.direction}'); // Direction: N -``` - -#### The "Aha!" Moments - -1. **Enums as Behavior Carriers:** Direction enum doesn't just store values—it encapsulates rotation logic. Turning left/right becomes `direction.turnLeft()`, eliminating lookup tables. - -2. **Value Objects Prevent Bugs:** Immutable `Position` means movement can't corrupt state. New position calculated, validated, then assigned. Boundary wrapping isolated in `Plateau`. - -3. **Switch Expressions Shine:** Modern Dart's `switch` expression makes direction-based movement elegant and exhaustive. Compiler enforces handling all directions. - -4. **Modular Arithmetic for Rotation:** `(index + 1) % 4` handles right rotation elegantly. No if-statements, no edge cases—math models the domain perfectly. - -5. **Refactoring Without Fear:** Two refactoring commits drastically improved code structure. Tests stayed green throughout, proving behavior preservation. - ---- - ## Running Tests ```bash @@ -403,7 +323,6 @@ dart test test/roman_numerals_test.dart dart test test/bowling_game_test.dart dart test test/gilded_rose_test.dart dart test test/string_calculator_test.dart -dart test test/mars_rover_test.dart # Run with coverage dart test --coverage @@ -711,19 +630,19 @@ Decision: Skip refactor commit—code is already excellent. ## Comparing the Katas -### All Five Katas at a Glance +### All Four Katas at a Glance -| Aspect | Roman Numerals | Bowling Game | Gilded Rose | String Calculator | Mars Rover | -|--------|----------------|--------------|-------------|-------------------|------------| -| **Complexity** | Beginner | Intermediate | Advanced | Beginner | Intermediate | -| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting | Greenfield TDD | -| **State** | Stateless | Stateful | Stateful | Stateless | Stateful | -| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing | Command pattern | -| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs | Navigation & wrapping | -| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes | Command + Value Objects | -| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production | ~95 production | -| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests | 23 tests | -| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs | Enums carry behavior | +| Aspect | Roman Numerals | Bowling Game | Gilded Rose | String Calculator | +|--------|----------------|--------------|-------------|-------------------| +| **Complexity** | Beginner | Intermediate | Advanced | Beginner | +| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting | +| **State** | Stateless | Stateful | Stateful | Stateless | +| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing | +| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs | +| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes | +| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production | +| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests | +| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs | ### What Each Kata Teaches @@ -750,24 +669,16 @@ Decision: Skip refactor commit—code is already excellent. - Incremental debugging with clear commits - Regression prevention through test accumulation -**Mars Rover:** -- Command pattern for behavior delegation -- Value Objects for domain modeling (Position, Plateau, Direction) -- Enums as behavior carriers, not just constants -- Coordinate systems and wrapping logic -- Progressive refactoring with confidence - ### Progressive Learning Path 1. **Roman Numerals first:** Learn TDD fundamentals without state complexity 2. **Bowling Game second:** Apply TDD to stateful problems -3. **Mars Rover third:** Master Command pattern and value objects -4. **Gilded Rose fourth:** Refactor legacy code with tests as safety net -5. **String Calculator fifth:** Practice bug hunting and fixing with TDD -6. **Next kata:** Choose based on what you want to practice: +3. **Gilded Rose third:** Master refactoring legacy code with tests as safety net +4. **String Calculator fourth:** Practice bug hunting and fixing with TDD +5. **Next kata:** Choose based on what you want to practice: + - **Mars Rover:** Command pattern, multiple behaviors - **Prime Factors:** Mathematical decomposition, algorithmic thinking - **Tennis Scoring:** State machines, domain language - - **FizzBuzz:** Classic conditional logic exercise --- diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart index 6a813d9..01c7722 100644 --- a/lib/gilded_rose.dart +++ b/lib/gilded_rose.dart @@ -14,6 +14,7 @@ class Item { /// Quality bounds - domain constraints const int _minQuality = 0; const int _maxQuality = 50; +const int _legendaryQuality = 80; // unused, just for documentation /// Helper methods for quality management void _degradeQuality(Item item, int amount) { diff --git a/lib/mars_rover.dart b/lib/mars_rover.dart deleted file mode 100644 index 221e450..0000000 --- a/lib/mars_rover.dart +++ /dev/null @@ -1,106 +0,0 @@ -enum Direction { - north('N'), - east('E'), - south('S'), - west('W'); - - final String code; - const Direction(this.code); - - static Direction fromCode(String code) { - return Direction.values.firstWhere((d) => d.code == code); - } - - Direction turnLeft() { - return Direction.values[(index + 3) % 4]; - } - - Direction turnRight() { - return Direction.values[(index + 1) % 4]; - } -} - -class Position { - final int x; - final int y; - - Position(this.x, this.y); - - Position moveNorth() => Position(x, y + 1); - Position moveEast() => Position(x + 1, y); - Position moveSouth() => Position(x, y - 1); - Position moveWest() => Position(x - 1, y); -} - -class Plateau { - final int width; - final int height; - - Plateau(this.width, this.height); - - Position wrap(Position position) { - final wrappedX = _wrapCoordinate(position.x, width); - final wrappedY = _wrapCoordinate(position.y, height); - return Position(wrappedX, wrappedY); - } - - int _wrapCoordinate(int value, int max) { - final wrapped = value % (max + 1); - return wrapped < 0 ? wrapped + max + 1 : wrapped; - } -} - -class Rover { - Position _position; - Direction _direction; - final Plateau plateau; - - Rover({ - required int x, - required int y, - required String direction, - int plateauWidth = 100, - int plateauHeight = 100, - }) : _position = Position(x, y), - _direction = Direction.fromCode(direction), - plateau = Plateau(plateauWidth, plateauHeight); - - int get x => _position.x; - int get y => _position.y; - String get direction => _direction.code; - - void turnLeft() { - _direction = _direction.turnLeft(); - } - - void turnRight() { - _direction = _direction.turnRight(); - } - - void moveForward() { - final newPosition = switch (_direction) { - Direction.north => _position.moveNorth(), - Direction.east => _position.moveEast(), - Direction.south => _position.moveSouth(), - Direction.west => _position.moveWest(), - }; - _position = plateau.wrap(newPosition); - } - - void execute(String commands) { - for (var i = 0; i < commands.length; i++) { - final command = commands[i]; - switch (command) { - case 'L': - turnLeft(); - break; - case 'R': - turnRight(); - break; - case 'M': - moveForward(); - break; - } - } - } -} diff --git a/test/mars_rover_test.dart b/test/mars_rover_test.dart deleted file mode 100644 index 53e4540..0000000 --- a/test/mars_rover_test.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:tdd_katas/mars_rover.dart'; -import 'package:test/test.dart'; - -void main() { - group('Mars Rover:', () { - test('rover reports initial position and direction', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - - expect(rover.x, equals(0)); - expect(rover.y, equals(0)); - expect(rover.direction, equals('N')); - }); - - group('Turning Left:', () { - test('from North faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnLeft(); - expect(rover.direction, equals('W')); - }); - - test('from West faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnLeft(); - expect(rover.direction, equals('S')); - }); - - test('from South faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnLeft(); - expect(rover.direction, equals('E')); - }); - - test('from East faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnLeft(); - expect(rover.direction, equals('N')); - }); - }); - - group('Turning Right:', () { - test('from North faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnRight(); - expect(rover.direction, equals('E')); - }); - - test('from East faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnRight(); - expect(rover.direction, equals('S')); - }); - - test('from South faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnRight(); - expect(rover.direction, equals('W')); - }); - - test('from West faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnRight(); - expect(rover.direction, equals('N')); - }); - }); - - group('Moving Forward:', () { - test('facing North increases Y', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(1)); - }); - - test('facing East increases X', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.moveForward(); - expect(rover.x, equals(1)); - expect(rover.y, equals(0)); - }); - - test('facing South decreases Y', () { - final rover = Rover(x: 0, y: 5, direction: 'S'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(4)); - }); - - test('facing West decreases X', () { - final rover = Rover(x: 5, y: 0, direction: 'W'); - rover.moveForward(); - expect(rover.x, equals(4)); - expect(rover.y, equals(0)); - }); - }); - - group('Executing Command Sequences:', () { - test('single command L', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('L'); - expect(rover.direction, equals('W')); - }); - - test('single command R', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('R'); - expect(rover.direction, equals('E')); - }); - - test('single command M', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('M'); - expect(rover.y, equals(1)); - }); - - test('complex sequence MMRMMLM', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('MMRMMLM'); - expect(rover.x, equals(2)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - - test('example from Mars Rover kata', () { - final rover = Rover(x: 1, y: 2, direction: 'N'); - rover.execute('LMLMLMLMM'); - expect(rover.x, equals(1)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - }); - - group('Plateau Boundaries:', () { - test('wraps around when moving North past boundary', () { - final rover = Rover( - x: 0, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(0)); - }); - - test('wraps around when moving East past boundary', () { - final rover = Rover( - x: 5, - y: 0, - direction: 'E', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(0)); - }); - - test('wraps around when moving South past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'S', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(5)); - }); - - test('wraps around when moving West past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'W', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(5)); - }); - - test('example with wrapping', () { - final rover = Rover( - x: 5, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.execute('MMM'); - expect(rover.y, equals(2)); - }); - }); - }); -}