Compare commits
No commits in common. "main" and "string-calculator" have entirely different histories.
main
...
string-cal
5 changed files with 17 additions and 406 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
# https://dart.dev/guides/libraries/private-files
|
# https://dart.dev/guides/libraries/private-files
|
||||||
# Created by `dart pub`
|
# Created by `dart pub`
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
tdd_*.md
|
|
||||||
|
|
|
||||||
121
README.md
121
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
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -403,7 +323,6 @@ dart test test/roman_numerals_test.dart
|
||||||
dart test test/bowling_game_test.dart
|
dart test test/bowling_game_test.dart
|
||||||
dart test test/gilded_rose_test.dart
|
dart test test/gilded_rose_test.dart
|
||||||
dart test test/string_calculator_test.dart
|
dart test test/string_calculator_test.dart
|
||||||
dart test test/mars_rover_test.dart
|
|
||||||
|
|
||||||
# Run with coverage
|
# Run with coverage
|
||||||
dart test --coverage
|
dart test --coverage
|
||||||
|
|
@ -711,19 +630,19 @@ Decision: Skip refactor commit—code is already excellent.
|
||||||
|
|
||||||
## Comparing the Katas
|
## 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 |
|
| Aspect | Roman Numerals | Bowling Game | Gilded Rose | String Calculator |
|
||||||
|--------|----------------|--------------|-------------|-------------------|------------|
|
|--------|----------------|--------------|-------------|-------------------|
|
||||||
| **Complexity** | Beginner | Intermediate | Advanced | Beginner | Intermediate |
|
| **Complexity** | Beginner | Intermediate | Advanced | Beginner |
|
||||||
| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting | Greenfield TDD |
|
| **Approach** | Greenfield TDD | Greenfield TDD | Legacy refactoring | Bug hunting |
|
||||||
| **State** | Stateless | Stateful | Stateful | Stateless | Stateful |
|
| **State** | Stateless | Stateful | Stateful | Stateless |
|
||||||
| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing | Command pattern |
|
| **Algorithm** | Table-driven | Frame iteration | Strategy pattern | String parsing |
|
||||||
| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs | Navigation & wrapping |
|
| **Key Challenge** | Pattern recognition | State & bonuses | Refactoring safely | Finding bugs |
|
||||||
| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes | Command + Value Objects |
|
| **Design Pattern** | Value Object | Implicit strategy | Explicit strategy | Filters & pipes |
|
||||||
| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production | ~95 production |
|
| **Lines of Code** | ~45 production | ~30 production | ~120 production | ~25 production |
|
||||||
| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests | 23 tests |
|
| **Test Count** | ~15 tests | ~10 tests | ~17 tests | 6 tests |
|
||||||
| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs | Enums carry behavior |
|
| **Aha! Moment** | Table = data | Simple → complex | Refactor = safe | Tests find bugs |
|
||||||
|
|
||||||
### What Each Kata Teaches
|
### What Each Kata Teaches
|
||||||
|
|
||||||
|
|
@ -750,24 +669,16 @@ Decision: Skip refactor commit—code is already excellent.
|
||||||
- Incremental debugging with clear commits
|
- Incremental debugging with clear commits
|
||||||
- Regression prevention through test accumulation
|
- 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
|
### Progressive Learning Path
|
||||||
|
|
||||||
1. **Roman Numerals first:** Learn TDD fundamentals without state complexity
|
1. **Roman Numerals first:** Learn TDD fundamentals without state complexity
|
||||||
2. **Bowling Game second:** Apply TDD to stateful problems
|
2. **Bowling Game second:** Apply TDD to stateful problems
|
||||||
3. **Mars Rover third:** Master Command pattern and value objects
|
3. **Gilded Rose third:** Master refactoring legacy code with tests as safety net
|
||||||
4. **Gilded Rose fourth:** Refactor legacy code with tests as safety net
|
4. **String Calculator fourth:** Practice bug hunting and fixing with TDD
|
||||||
5. **String Calculator fifth:** Practice bug hunting and fixing with TDD
|
5. **Next kata:** Choose based on what you want to practice:
|
||||||
6. **Next kata:** Choose based on what you want to practice:
|
- **Mars Rover:** Command pattern, multiple behaviors
|
||||||
- **Prime Factors:** Mathematical decomposition, algorithmic thinking
|
- **Prime Factors:** Mathematical decomposition, algorithmic thinking
|
||||||
- **Tennis Scoring:** State machines, domain language
|
- **Tennis Scoring:** State machines, domain language
|
||||||
- **FizzBuzz:** Classic conditional logic exercise
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class Item {
|
||||||
/// Quality bounds - domain constraints
|
/// Quality bounds - domain constraints
|
||||||
const int _minQuality = 0;
|
const int _minQuality = 0;
|
||||||
const int _maxQuality = 50;
|
const int _maxQuality = 50;
|
||||||
|
const int _legendaryQuality = 80; // unused, just for documentation
|
||||||
|
|
||||||
/// Helper methods for quality management
|
/// Helper methods for quality management
|
||||||
void _degradeQuality(Item item, int amount) {
|
void _degradeQuality(Item item, int amount) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue