Compare commits
No commits in common. "main" and "roman-numerals-kata" have entirely different histories.
main
...
roman-nume
11 changed files with 5 additions and 2221 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
|
|
||||||
|
|
|
||||||
795
README.md
795
README.md
|
|
@ -1,793 +1,2 @@
|
||||||
# TDD Katas Collection
|
A sample command-line application with an entrypoint in `bin/`, library code
|
||||||
|
in `lib/`, and example unit test in `test/`.
|
||||||
A collection of Test-Driven Development (TDD) exercises implementing classic programming katas, following Uncle Bob's Clean Code principles and Domain-Driven Design tactical patterns.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
These katas demonstrate:
|
|
||||||
|
|
||||||
- **Test-Driven Development:** Production code written only to pass failing tests
|
|
||||||
- **Clean Code Practices:** Intent-revealing names, single responsibility, domain-focused abstractions
|
|
||||||
- **Domain-Driven Design:** Value Objects enforce domain constraints at the type level
|
|
||||||
- **The Craftsman's Way:** Quality is not a trade-off for speed; it is the only way to go fast
|
|
||||||
|
|
||||||
## Completed Katas
|
|
||||||
|
|
||||||
### 1. Roman Numerals ✅
|
|
||||||
|
|
||||||
**Implementation:** [`lib/roman_numerals.dart`](lib/roman_numerals.dart)
|
|
||||||
**Tests:** [`test/roman_numerals_test.dart`](test/roman_numerals_test.dart)
|
|
||||||
|
|
||||||
Converts integers (1-3999) to Roman numeral notation using a table-driven greedy algorithm.
|
|
||||||
|
|
||||||
#### Domain Context
|
|
||||||
|
|
||||||
Roman numerals represent numbers using seven basic symbols with specific combination rules:
|
|
||||||
|
|
||||||
**Symbols:**
|
|
||||||
- `I` = 1, `V` = 5, `X` = 10, `L` = 50, `C` = 100, `D` = 500, `M` = 1000
|
|
||||||
|
|
||||||
**Domain Rules:**
|
|
||||||
1. **Additive Notation:** Symbols placed in descending order are summed (e.g., `VI` = 6)
|
|
||||||
2. **Subtractive Notation:** Smaller symbol before larger subtracts (e.g., `IV` = 4)
|
|
||||||
3. **Repetition Limit:** Symbols repeat maximum three times (e.g., `III` = 3, not `IIII`)
|
|
||||||
4. **Valid Range:** Classical Roman numerals represent 1-3999
|
|
||||||
|
|
||||||
**Subtractive Pairs (Domain Constraint):**
|
|
||||||
Only specific pairs use subtractive notation:
|
|
||||||
- `IV` (4), `IX` (9)
|
|
||||||
- `XL` (40), `XC` (90)
|
|
||||||
- `CD` (400), `CM` (900)
|
|
||||||
|
|
||||||
#### Key Design Decisions
|
|
||||||
|
|
||||||
**Value Object Pattern:** `RomanNumeralInput` enforces domain invariants (1-3999 range). Invalid inputs are impossible to construct.
|
|
||||||
|
|
||||||
**Table-Driven Algorithm:** The `conversionRules` table is the **domain model**—it directly represents Roman numeral encoding rules.
|
|
||||||
|
|
||||||
**Greedy Decomposition:** The algorithm mirrors how Romans actually encoded numbers: repeatedly subtract the largest applicable value.
|
|
||||||
|
|
||||||
#### Usage
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:tdd_katas/roman_numerals.dart';
|
|
||||||
|
|
||||||
integerToRoman(1994); // Returns: 'MCMXCIV'
|
|
||||||
integerToRoman(0); // Throws: ArgumentError
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Bowling Game ✅
|
|
||||||
|
|
||||||
**Implementation:** [`lib/bowling_game.dart`](lib/bowling_game.dart)
|
|
||||||
**Tests:** [`test/bowling_game_test.dart`](test/bowling_game_test.dart)
|
|
||||||
|
|
||||||
Calculates scores for a bowling game following official scoring rules with look-ahead bonus logic for strikes and spares.
|
|
||||||
|
|
||||||
#### Domain Context
|
|
||||||
|
|
||||||
A bowling game consists of 10 frames where players roll a ball to knock down pins:
|
|
||||||
|
|
||||||
**Scoring Rules:**
|
|
||||||
1. **Normal Frame:** Sum of pins knocked down (e.g., 3 + 4 = 7)
|
|
||||||
2. **Spare (/):** All 10 pins in 2 rolls → Score = 10 + next 1 roll
|
|
||||||
3. **Strike (X):** All 10 pins in 1 roll → Score = 10 + next 2 rolls
|
|
||||||
4. **10th Frame:** Bonus rolls awarded if spare or strike achieved
|
|
||||||
|
|
||||||
#### Key Design Decisions
|
|
||||||
|
|
||||||
**State Management:** Stores all rolls in a list and calculates score by iterating through frames, not individual rolls.
|
|
||||||
|
|
||||||
**Look-Ahead Logic:** Spares and strikes require examining future rolls for bonus calculation—the algorithm walks forward strategically.
|
|
||||||
|
|
||||||
**Frame Advancement:** Strikes consume 1 roll, spares/normal frames consume 2 rolls—the algorithm tracks position correctly.
|
|
||||||
|
|
||||||
#### Usage
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:tdd_katas/bowling_game.dart';
|
|
||||||
|
|
||||||
final game = BowlingGame();
|
|
||||||
game.roll(10); // Strike!
|
|
||||||
game.roll(3);
|
|
||||||
game.roll(4);
|
|
||||||
// ... continue rolling
|
|
||||||
print(game.score()); // Calculates total with bonuses
|
|
||||||
```
|
|
||||||
|
|
||||||
#### The "Aha!" Moment
|
|
||||||
|
|
||||||
Tests for simple cases (gutter game, one spare, one strike) drove an algorithm that automatically handles complex scenarios like perfect games (300 points) without explicit implementation. **This is TDD's magic—correct abstractions emerge naturally.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Gilded Rose ✅
|
|
||||||
|
|
||||||
**Implementation:** [`lib/gilded_rose.dart`](lib/gilded_rose.dart)
|
|
||||||
**Tests:** [`test/gilded_rose_test.dart`](test/gilded_rose_test.dart)
|
|
||||||
|
|
||||||
A legacy code refactoring kata demonstrating how to safely transform deeply nested conditionals into clean, extensible code using characterization tests and the Strategy pattern.
|
|
||||||
|
|
||||||
#### Domain Context
|
|
||||||
|
|
||||||
An inn's inventory system that updates item quality daily based on complex business rules:
|
|
||||||
|
|
||||||
**Item Types:**
|
|
||||||
|
|
||||||
1. **Normal Items:** Quality decreases by 1/day, 2/day after sell-by date
|
|
||||||
2. **Aged Brie:** Quality increases by 1/day, 2/day after expiration (improves with age!)
|
|
||||||
3. **Sulfuras (Legendary):** Never changes, quality always 80, never "expires"
|
|
||||||
4. **Backstage Passes:** Complex appreciation:
|
|
||||||
- More than 10 days: +1 quality/day
|
|
||||||
- 10 days or less: +2 quality/day
|
|
||||||
- 5 days or less: +3 quality/day
|
|
||||||
- After concert (sellIn < 0): Quality drops to 0
|
|
||||||
5. **Conjured Items:** Degrade twice as fast as normal items (2/day, 4/day after expiration)
|
|
||||||
|
|
||||||
**Domain Constraints:**
|
|
||||||
- Quality never negative (≥ 0)
|
|
||||||
- Quality never exceeds 50 (except Sulfuras at 80)
|
|
||||||
- Cannot modify the `Item` class (goblin constraint!)
|
|
||||||
|
|
||||||
#### Key Design Decisions
|
|
||||||
|
|
||||||
**Characterization Testing:** Before touching legacy code, created 17 tests to capture existing behavior as a "safety net." Includes a Golden Master test simulating 30 days.
|
|
||||||
|
|
||||||
**Strategy Pattern:** Each item type gets its own updater class implementing `ItemUpdater` interface. Eliminates conditional branching and enables Open-Closed Principle.
|
|
||||||
|
|
||||||
**Helper Methods & Constants:** `_degradeQuality()` and `_improveQuality()` with automatic clamping eliminate scattered boundary checks. Domain constants (`_minQuality`, `_maxQuality`) remove magic numbers.
|
|
||||||
|
|
||||||
**Factory Pattern:** `_selectUpdater()` method chooses the appropriate strategy based on item name, enabling polymorphic dispatch.
|
|
||||||
|
|
||||||
#### The Refactoring Journey
|
|
||||||
|
|
||||||
**Phase 1: Understand Legacy Code**
|
|
||||||
```dart
|
|
||||||
// 60+ lines of 7-8 level nested conditionals
|
|
||||||
// Nearly incomprehensible logic mixing all item types
|
|
||||||
```
|
|
||||||
|
|
||||||
**Phase 2: Characterization Tests (GREEN Phase)**
|
|
||||||
- 14 comprehensive tests covering all item types
|
|
||||||
- Golden Master test: 30-day simulation baseline
|
|
||||||
- Result: **Safety net established** ✅
|
|
||||||
|
|
||||||
**Phase 3: Refactor with Confidence (3 steps)**
|
|
||||||
|
|
||||||
**Step 1 - Extract Methods (REFACTOR):**
|
|
||||||
```dart
|
|
||||||
// Before: 60 lines of nested hell
|
|
||||||
// After: Clean if-else delegating to 4 private methods
|
|
||||||
_updateNormalItem(), _updateAgedBrie(),
|
|
||||||
_updateBackstagePasses(), _updateSulfuras()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2 - Strategy Pattern (REFACTOR):**
|
|
||||||
```dart
|
|
||||||
abstract class ItemUpdater {
|
|
||||||
void update(Item item);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NormalItemUpdater implements ItemUpdater { ... }
|
|
||||||
class AgedBrieUpdater implements ItemUpdater { ... }
|
|
||||||
// ... 4 concrete strategies
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 3 - Domain Helpers (REFACTOR):**
|
|
||||||
```dart
|
|
||||||
const int _minQuality = 0;
|
|
||||||
const int _maxQuality = 50;
|
|
||||||
|
|
||||||
void _degradeQuality(Item item, int amount) {
|
|
||||||
item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Phase 4: Add Conjured Items (RED-GREEN)**
|
|
||||||
|
|
||||||
**RED:** Added 3 failing tests for Conjured items behavior
|
|
||||||
**GREEN:** Created `ConjuredItemUpdater` class—**one class, one condition**
|
|
||||||
**Result:** Feature added in minutes thanks to refactoring!
|
|
||||||
|
|
||||||
#### Usage
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:tdd_katas/gilded_rose.dart';
|
|
||||||
|
|
||||||
final items = [
|
|
||||||
Item('Normal Sword', 10, 20),
|
|
||||||
Item('Aged Brie', 2, 0),
|
|
||||||
Item('Sulfuras, Hand of Ragnaros', 0, 80),
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 15, 20),
|
|
||||||
Item('Conjured Mana Cake', 3, 6),
|
|
||||||
];
|
|
||||||
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
gildedRose.updateQuality(); // Updates all items per domain rules
|
|
||||||
```
|
|
||||||
|
|
||||||
#### The "Aha!" Moments
|
|
||||||
|
|
||||||
1. **Characterization Tests = Freedom:** With tests in place, aggressive refactoring felt safe. Every change validated instantly.
|
|
||||||
|
|
||||||
2. **Strategy Pattern = Extensibility:** Adding Conjured items took **5 minutes**. Before refactoring, it would have meant diving into nested conditionals and risking bugs.
|
|
||||||
|
|
||||||
3. **Small Steps = Big Wins:** Three refactoring commits transformed spaghetti into clean code. Each step kept tests GREEN, proving behavior preservation.
|
|
||||||
|
|
||||||
4. **Open-Closed Principle in Action:** New item types don't modify existing code—they just add new updater classes. The system is "open for extension, closed for modification."
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. String Calculator ✅
|
|
||||||
|
|
||||||
**Implementation:** [`lib/string_calculator.dart`](lib/string_calculator.dart)
|
|
||||||
**Tests:** [`test/string_calculator_test.dart`](test/string_calculator_test.dart)
|
|
||||||
|
|
||||||
A Bug Hunt Kata demonstrating how to use TDD to discover and fix bugs in existing code. Each bug is exposed with a RED test, then fixed with GREEN implementation.
|
|
||||||
|
|
||||||
#### Domain Context
|
|
||||||
|
|
||||||
A simple calculator that sums numbers from a string input with various delimiter support:
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
1. **Empty String:** Returns 0
|
|
||||||
2. **Single Number:** Returns that number (`"5"` → 5)
|
|
||||||
3. **Comma Delimiter:** Sums comma-separated numbers (`"1,2,3"` → 6)
|
|
||||||
4. **Custom Delimiters:** Supports format `"//[delimiter]\n[numbers]"` (`"//;\n1;2"` → 3)
|
|
||||||
5. **Ignore Large Numbers:** Numbers > 1000 are ignored (`"2,1001"` → 2)
|
|
||||||
|
|
||||||
#### The Bug Hunt Approach
|
|
||||||
|
|
||||||
**Different from Previous Katas:** This wasn't built test-first. Instead, we started with **buggy working code** and used tests to expose and fix bugs one by one.
|
|
||||||
|
|
||||||
**Bug Hunt Process:**
|
|
||||||
1. **RED:** Write test exposing a specific bug
|
|
||||||
2. **GREEN:** Fix only that bug
|
|
||||||
3. **Commit:** Document the bug found and fixed
|
|
||||||
4. **Repeat:** Move to next bug
|
|
||||||
|
|
||||||
#### Bugs Found & Fixed
|
|
||||||
|
|
||||||
**Bug #1: Empty String Returns Wrong Value**
|
|
||||||
- **Bug:** Returned 1 instead of 0
|
|
||||||
- **Test:** `expect(calculator.add(''), equals(0))`
|
|
||||||
- **Fix:** Changed return value from 1 to 0
|
|
||||||
- **Commits:** RED → GREEN
|
|
||||||
|
|
||||||
**Bug #2: Single Number Off-By-One**
|
|
||||||
- **Bug:** Added +1 to parsed number
|
|
||||||
- **Test:** `expect(calculator.add('5'), equals(5))`
|
|
||||||
- **Expected:** 5, **Actual:** 6
|
|
||||||
- **Fix:** Removed `+ 1` from parsing
|
|
||||||
- **Commits:** RED → GREEN
|
|
||||||
|
|
||||||
**Bug #3: Summation Loop Misses Last Element**
|
|
||||||
- **Bug:** Loop condition `i < length - 1` skipped last item
|
|
||||||
- **Test:** `expect(calculator.add('1,2'), equals(3))`
|
|
||||||
- **Expected:** 3, **Actual:** 1
|
|
||||||
- **Fix:** Changed to `i < length`
|
|
||||||
- **Commits:** RED → GREEN
|
|
||||||
|
|
||||||
**Bug #4: Custom Delimiter Not Extracted**
|
|
||||||
- **Bug:** Delimiter extraction line was commented out
|
|
||||||
- **Test:** `expect(calculator.add('//;\n1;2'), equals(3))`
|
|
||||||
- **Error:** FormatException trying to parse '1;2'
|
|
||||||
- **Fix:** Uncommented `delimiter = parts[0].substring(2)`
|
|
||||||
- **Commits:** RED → GREEN
|
|
||||||
|
|
||||||
**Bug #5: Missing Feature - Ignore Numbers > 1000**
|
|
||||||
- **Bug:** All numbers included in sum
|
|
||||||
- **Test:** `expect(calculator.add('2,1001'), equals(2))`
|
|
||||||
- **Expected:** 2, **Actual:** 1003
|
|
||||||
- **Fix:** Added `.where((n) => n <= 1000)` filter
|
|
||||||
- **Commits:** RED → GREEN
|
|
||||||
|
|
||||||
#### Usage
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'package:tdd_katas/string_calculator.dart';
|
|
||||||
|
|
||||||
final calculator = StringCalculator();
|
|
||||||
|
|
||||||
calculator.add(''); // Returns: 0
|
|
||||||
calculator.add('5'); // Returns: 5
|
|
||||||
calculator.add('1,2,3'); // Returns: 6
|
|
||||||
calculator.add('//;\n1;2'); // Returns: 3
|
|
||||||
calculator.add('2,1001'); // Returns: 2 (1001 ignored)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### The "Aha!" Moments
|
|
||||||
|
|
||||||
1. **Tests as Bug Detectors:** Each test acted like a spotlight, illuminating exactly ONE bug at a time. No guessing—the test tells you what's broken.
|
|
||||||
|
|
||||||
2. **RED-GREEN Still Works:** Even when fixing bugs (not adding features), the RED-GREEN rhythm provides safety. You're never fixing blind.
|
|
||||||
|
|
||||||
3. **Regression Prevention:** After fixing each bug, ALL previous tests stay green. This proves you didn't break something while fixing something else.
|
|
||||||
|
|
||||||
4. **Incremental Debugging:** Fixing one bug at a time with commits creates a clear audit trail. You can see exactly what each bug was and how it was fixed.
|
|
||||||
|
|
||||||
5. **Real-World Skill:** This mirrors production work—most code you touch is existing code with bugs, not greenfield TDD.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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
|
|
||||||
# Run all tests
|
|
||||||
dart test
|
|
||||||
|
|
||||||
# Run specific test file
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Philosophy
|
|
||||||
|
|
||||||
### The Craftsman's Standard
|
|
||||||
|
|
||||||
> "Clean code that works." — Ron Jeffries
|
|
||||||
|
|
||||||
Every kata in this collection follows:
|
|
||||||
- **Uncle Bob's Clean Code:** Intent-revealing names, functions do one thing, no comments needed
|
|
||||||
- **Kent Beck's TDD:** Red-Green-Refactor discipline, tests first
|
|
||||||
- **Eric Evans' DDD:** Domain concepts drive the model, tactical patterns enforce boundaries
|
|
||||||
- **The Boy Scout Rule:** Every commit leaves the code cleaner than before
|
|
||||||
|
|
||||||
### TDD Discipline Applied
|
|
||||||
|
|
||||||
1. **Red:** Write a failing test
|
|
||||||
2. **Green:** Write the simplest code to pass
|
|
||||||
3. **Refactor:** Clean up duplication, improve names
|
|
||||||
4. **Repeat:** Let the design emerge from tests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Roman Numerals: Detailed Journey
|
|
||||||
|
|
||||||
### Test Strategy
|
|
||||||
|
|
||||||
Tests are organized by **domain concepts**, not technical structure:
|
|
||||||
|
|
||||||
**Basic Symbols:**
|
|
||||||
Tests for the seven fundamental symbols (I, V, X, L, C, D, M)
|
|
||||||
|
|
||||||
**Subtractive Notation:**
|
|
||||||
Tests for all six subtractive pairs, verifying the domain rule
|
|
||||||
|
|
||||||
**Additive Combinations:**
|
|
||||||
Tests for repeated symbols and multi-symbol sequences
|
|
||||||
|
|
||||||
**Complex Edge Cases:**
|
|
||||||
Stress tests combining multiple rules:
|
|
||||||
- `1994 → MCMXCIV` (year notation)
|
|
||||||
- `3999 → MMMCMXCIX` (maximum valid value)
|
|
||||||
- `444 → CDXLIV` (all subtractive positions)
|
|
||||||
|
|
||||||
**Constraint Validation:**
|
|
||||||
Boundary tests for the valid range (1-3999)
|
|
||||||
|
|
||||||
### Development Timeline
|
|
||||||
|
|
||||||
1. **Red:** Tests for 1-5 (basic additive, first subtractive case)
|
|
||||||
2. **Green:** Minimal implementation with conditionals
|
|
||||||
3. **Refactor:** Extract symbol mapping, clarify intent
|
|
||||||
4. **Red:** Tests for 6-10 (reveals pattern)
|
|
||||||
5. **Green:** Extend conditionals
|
|
||||||
6. **Refactor:** Recognize duplication → Table-driven approach emerges
|
|
||||||
7. **Red:** Tests for 40-1000 (remaining symbols)
|
|
||||||
8. **Green:** Extend conversion table (algorithm unchanged)
|
|
||||||
9. **Red:** Edge cases and constraint tests
|
|
||||||
10. **Green:** Add `RomanNumeralInput` Value Object
|
|
||||||
11. **Refactor:** Extract validation, organize tests by domain concept
|
|
||||||
|
|
||||||
### Key Insights
|
|
||||||
|
|
||||||
**The Algorithm Never Changed:** After the table-driven refactoring, adding 40-1000 required **zero logic modifications**. This validates the abstraction.
|
|
||||||
|
|
||||||
**Type System as Domain Enforcer:** `RomanNumeralInput` makes invalid states unrepresentable. You cannot construct a Roman numeral for 0 or 4000—the compiler prevents it.
|
|
||||||
|
|
||||||
**Tests as Living Documentation:** Test names use **ubiquitous language** from the Roman numeral domain. A domain expert could read the test file and recognize the rules they explained.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Bowling Game: Detailed Journey
|
|
||||||
|
|
||||||
### Test Strategy
|
|
||||||
|
|
||||||
Tests are organized by **scoring complexity**, mirroring how the domain rules build on each other:
|
|
||||||
|
|
||||||
**Basic Scoring:**
|
|
||||||
- Gutter game (all zeros)
|
|
||||||
- All ones (simple addition)
|
|
||||||
|
|
||||||
**Spare Bonus (next 1 roll):**
|
|
||||||
- One spare in first frame
|
|
||||||
- All spares (150 points)
|
|
||||||
|
|
||||||
**Strike Bonus (next 2 rolls):**
|
|
||||||
- One strike in first frame
|
|
||||||
- Perfect game (300 points)
|
|
||||||
|
|
||||||
**Complex Scenarios:**
|
|
||||||
- Combinations of strikes, spares, and normal frames
|
|
||||||
|
|
||||||
### Development Timeline
|
|
||||||
|
|
||||||
1. **Red:** Gutter game test
|
|
||||||
2. **Green:** Return 0 (simplest implementation)
|
|
||||||
3. **Red:** All ones test
|
|
||||||
4. **Green:** Store rolls, sum them in `score()`
|
|
||||||
5. **Refactor:** Extract `rollMany()` helper, add `setUp()`
|
|
||||||
6. **Red:** One spare test
|
|
||||||
7. **Green:** Detect spare, add look-ahead bonus (+1 roll)
|
|
||||||
8. **Refactor:** Extract `_isSpare()` helper
|
|
||||||
9. **Red:** One strike test
|
|
||||||
10. **Green:** Detect strike, add look-ahead bonus (+2 rolls)
|
|
||||||
11. **Refactor:** Extract `_isStrike()`, clean up frame advancement
|
|
||||||
12. **Validate:** Perfect game test passes without modification!
|
|
||||||
|
|
||||||
### Key Insights
|
|
||||||
|
|
||||||
**Emergent Design:** The algorithm structure wasn't planned upfront. Tests for simple cases forced:
|
|
||||||
- Frame-based iteration (not roll-based)
|
|
||||||
- Index tracking (advancing by 1 or 2)
|
|
||||||
- Look-ahead logic (accessing future rolls)
|
|
||||||
|
|
||||||
**The Perfect Game Moment:** Writing code to handle "one spare" and "one strike" automatically handled "12 consecutive strikes" (300 points). The algorithm correctly models the domain, so all valid games work.
|
|
||||||
|
|
||||||
**State vs. Behavior:** Initially tempting to model Frame objects with state. TDD revealed a simpler truth: just store rolls and calculate on-demand. No frame objects needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gilded Rose: Detailed Journey
|
|
||||||
|
|
||||||
### The Legacy Code Challenge
|
|
||||||
|
|
||||||
Unlike the previous two katas (greenfield TDD), Gilded Rose simulates **real-world legacy code refactoring**. You inherit messy, working code with no tests and must:
|
|
||||||
1. Understand what it does (without breaking it)
|
|
||||||
2. Add tests to capture behavior
|
|
||||||
3. Refactor safely
|
|
||||||
4. Add new features
|
|
||||||
|
|
||||||
### Test Strategy
|
|
||||||
|
|
||||||
Tests are organized by **item type behavior** and include a **Golden Master**:
|
|
||||||
|
|
||||||
**Normal Items:**
|
|
||||||
- Quality degradation (1/day, 2/day after expiration)
|
|
||||||
- Quality never negative
|
|
||||||
|
|
||||||
**Aged Brie:**
|
|
||||||
- Quality appreciation (improves with age)
|
|
||||||
- Respects quality cap (≤ 50)
|
|
||||||
|
|
||||||
**Sulfuras (Legendary Items):**
|
|
||||||
- Never changes (quality, sellIn)
|
|
||||||
- Always quality 80
|
|
||||||
|
|
||||||
**Backstage Passes:**
|
|
||||||
- Threshold-based appreciation (10 days, 5 days)
|
|
||||||
- Drops to 0 after concert
|
|
||||||
|
|
||||||
**Conjured Items (new feature):**
|
|
||||||
- Degrades 2x faster than normal items
|
|
||||||
|
|
||||||
**Golden Master Test:**
|
|
||||||
- 30-day simulation with all item types
|
|
||||||
- Captures baseline output before refactoring
|
|
||||||
- Detects any behavioral regression
|
|
||||||
|
|
||||||
### Refactoring Timeline
|
|
||||||
|
|
||||||
**Phase 1: Create Legacy Code**
|
|
||||||
- Intentionally nested 7-8 levels deep
|
|
||||||
- Mixed concerns (all item types in one method)
|
|
||||||
- Magic numbers scattered throughout
|
|
||||||
- Result: Represents realistic legacy code
|
|
||||||
|
|
||||||
**Phase 2: Characterization Tests (GREEN)**
|
|
||||||
- 14 comprehensive tests written before any refactoring
|
|
||||||
- Golden Master baseline captured
|
|
||||||
- Commit: `"GREEN: Add characterization tests"`
|
|
||||||
- Result: **Safety net established** ✅
|
|
||||||
|
|
||||||
**Phase 3: Refactor in Small Steps (3 REFACTOR commits)**
|
|
||||||
|
|
||||||
**Step 1 - Extract Methods:**
|
|
||||||
```dart
|
|
||||||
// Before: 60 lines, items[i] everywhere, deeply nested
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
|
||||||
if (items[i].name != 'Aged Brie' && ...) {
|
|
||||||
if (items[i].quality > 0) {
|
|
||||||
if (items[i].name != 'Sulfuras...') {
|
|
||||||
// ... 5 more levels ...
|
|
||||||
|
|
||||||
// After: Clean delegation, readable
|
|
||||||
for (final item in items) {
|
|
||||||
if (item.name == 'Sulfuras, Hand of Ragnaros') {
|
|
||||||
_updateSulfuras(item);
|
|
||||||
} else if (item.name == 'Aged Brie') {
|
|
||||||
_updateAgedBrie(item);
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Commit: `"REFACTOR: Extract item type methods from nested conditionals"`
|
|
||||||
|
|
||||||
**Step 2 - Introduce Strategy Pattern:**
|
|
||||||
```dart
|
|
||||||
abstract class ItemUpdater {
|
|
||||||
void update(Item item);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NormalItemUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
_degradeQuality(item, 1);
|
|
||||||
item.sellIn -= 1;
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
_degradeQuality(item, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ... 4 concrete strategies
|
|
||||||
|
|
||||||
ItemUpdater _selectUpdater(Item item) { ... }
|
|
||||||
```
|
|
||||||
Commit: `"REFACTOR: Introduce Strategy pattern for item types"`
|
|
||||||
|
|
||||||
**Step 3 - Extract Domain Helpers:**
|
|
||||||
```dart
|
|
||||||
const int _minQuality = 0;
|
|
||||||
const int _maxQuality = 50;
|
|
||||||
|
|
||||||
void _degradeQuality(Item item, int amount) {
|
|
||||||
item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _improveQuality(Item item, int amount) {
|
|
||||||
item.quality = (item.quality + amount).clamp(_minQuality, _maxQuality);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Commit: `"REFACTOR: Extract helper methods and domain constants"`
|
|
||||||
|
|
||||||
All 14 tests stayed **GREEN** throughout! 🟢
|
|
||||||
|
|
||||||
**Phase 4: Add Conjured Items (RED-GREEN-REFACTOR)**
|
|
||||||
|
|
||||||
**RED:** Write failing tests
|
|
||||||
```dart
|
|
||||||
test('degrade in quality twice as fast as normal items', () {
|
|
||||||
final items = [Item('Conjured Mana Cake', 10, 20)];
|
|
||||||
GildedRose(items).updateQuality();
|
|
||||||
expect(items[0].quality, equals(18)); // -2 instead of -1
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Result: 2 tests **FAILING** ❌ (Expected: 18, Actual: 19)
|
|
||||||
Commit: `"RED: Add failing tests for Conjured items"`
|
|
||||||
|
|
||||||
**GREEN:** Implement minimal solution
|
|
||||||
```dart
|
|
||||||
class ConjuredItemUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
_degradeQuality(item, 2); // 2x normal rate
|
|
||||||
item.sellIn -= 1;
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
_degradeQuality(item, 2); // 4x total after expiration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemUpdater _selectUpdater(Item item) {
|
|
||||||
// ... existing checks ...
|
|
||||||
} else if (item.name.startsWith('Conjured')) {
|
|
||||||
return ConjuredItemUpdater();
|
|
||||||
} else {
|
|
||||||
return NormalItemUpdater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Result: All 17 tests **PASSING** ✅
|
|
||||||
Commit: `"GREEN: Implement Conjured items degrading twice as fast"`
|
|
||||||
|
|
||||||
**REFACTOR:** Already clean! No duplication, clear names, reusing helpers.
|
|
||||||
Decision: Skip refactor commit—code is already excellent.
|
|
||||||
|
|
||||||
### Development Metrics
|
|
||||||
|
|
||||||
| Metric | Before Refactoring | After Refactoring |
|
|
||||||
|--------|-------------------|-------------------|
|
|
||||||
| **Cyclomatic Complexity** | ~25 (very high) | ~3 per class (low) |
|
|
||||||
| **Lines per Method** | 60+ | 5-10 |
|
|
||||||
| **Max Nesting** | 7-8 levels | 2 levels |
|
|
||||||
| **Time to Add Feature** | Hours (risky) | Minutes (safe) |
|
|
||||||
| **Test Coverage** | 0% → 100% | 100% (maintained) |
|
|
||||||
|
|
||||||
### Key Insights
|
|
||||||
|
|
||||||
**Characterization Tests Are Your Lifeline:** Without tests, refactoring is guesswork. With tests, it's engineering. Every change validated in milliseconds.
|
|
||||||
|
|
||||||
**Small Steps = Low Risk:** Three refactoring commits, each preserving behavior. No "big bang" rewrite—steady, safe progress.
|
|
||||||
|
|
||||||
**Strategy Pattern = Future-Proofing:** Adding Conjured items demonstrated the payoff:
|
|
||||||
- **Before refactoring:** Would require diving into nested conditionals, risking bugs
|
|
||||||
- **After refactoring:** One new class, one condition, done in 5 minutes
|
|
||||||
|
|
||||||
**Golden Master Testing:** The 30-day simulation test caught edge cases that individual unit tests missed. It serves as a comprehensive regression detector.
|
|
||||||
|
|
||||||
**Open-Closed Principle Validated:** New item types extend the system without modifying existing updater classes. The design is "open for extension, closed for modification."
|
|
||||||
|
|
||||||
**Refactoring ≠ Rewriting:** We never changed what the code does, only how it's structured. Tests prove behavioral equivalence at every step.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comparing the Katas
|
|
||||||
|
|
||||||
### All Five 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 |
|
|
||||||
|
|
||||||
### What Each Kata Teaches
|
|
||||||
|
|
||||||
**Roman Numerals:**
|
|
||||||
**Roman Numerals:**
|
|
||||||
- Converting domain rules into data structures
|
|
||||||
- Value Objects for enforcing constraints
|
|
||||||
- When to stop coding (algorithm emerges naturally)
|
|
||||||
|
|
||||||
**Bowling Game:**
|
|
||||||
- State management without over-engineering
|
|
||||||
- Look-ahead logic in sequential data
|
|
||||||
- How correct abstractions scale beyond test cases
|
|
||||||
|
|
||||||
**Gilded Rose:**
|
|
||||||
- Safely refactoring legacy code with characterization tests
|
|
||||||
- Strategy pattern for eliminating conditional complexity
|
|
||||||
- Open-Closed Principle for extensibility
|
|
||||||
- Working effectively with code you didn't write
|
|
||||||
|
|
||||||
**String Calculator:**
|
|
||||||
- Using tests to expose bugs in existing code
|
|
||||||
- Bug hunting with RED-GREEN discipline
|
|
||||||
- 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:
|
|
||||||
- **Prime Factors:** Mathematical decomposition, algorithmic thinking
|
|
||||||
- **Tennis Scoring:** State machines, domain language
|
|
||||||
- **FizzBuzz:** Classic conditional logic exercise
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### General TDD & Clean Code
|
|
||||||
- [Gilded Rose Kata](https://github.com/emilybache/GildedRose-Refactoring-Kata)
|
|
||||||
- [Clean Code by Robert C. Martin](https://www.oreilly.com/library/view/clean-code-a/9780136083238/)
|
|
||||||
- [Domain-Driven Design by Eric Evans](https://www.domainlanguage.com/ddd/)
|
|
||||||
- [Test-Driven Development by Kent Beck](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530)
|
|
||||||
|
|
||||||
### Kata-Specific
|
|
||||||
- [Roman Numerals Rules](https://en.wikipedia.org/wiki/Roman_numerals)
|
|
||||||
- [Bowling Scoring Rules](https://en.wikipedia.org/wiki/Ten-pin_bowling#Scoring)
|
|
||||||
- [Uncle Bob's Bowling Game Kata](http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This is a learning exercise. Use freely for educational purposes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Following The Craftsman's Way: Quality is not negotiable.**
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:tdd_katas/roman_numerals.dart' as roman_numerals;
|
||||||
|
|
||||||
void main(List<String> arguments) {
|
void main(List<String> arguments) {
|
||||||
print('TDD Katas exercises, please read the README.md file.');
|
print('Hello world: ${roman_numerals.integerToRoman(1)}');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
class BowlingGame {
|
|
||||||
static const _totalFrames = 10;
|
|
||||||
static const _allPins = 10;
|
|
||||||
static const _rollsInNormalFrame = 2;
|
|
||||||
static const _rollsInStrike = 1;
|
|
||||||
|
|
||||||
final List<int> _rolls = [];
|
|
||||||
|
|
||||||
void roll(int pins) => _rolls.add(pins);
|
|
||||||
|
|
||||||
int score() {
|
|
||||||
int total = 0;
|
|
||||||
int rollIndex = 0;
|
|
||||||
|
|
||||||
for (int frame = 0; frame < _totalFrames; frame++) {
|
|
||||||
if (_isStrike(rollIndex)) {
|
|
||||||
total += _strikeScore(rollIndex);
|
|
||||||
rollIndex += _rollsInStrike;
|
|
||||||
} else if (_isSpare(rollIndex)) {
|
|
||||||
total += _spareScore(rollIndex);
|
|
||||||
rollIndex += _rollsInNormalFrame;
|
|
||||||
} else {
|
|
||||||
total += _normalScore(rollIndex);
|
|
||||||
rollIndex += _rollsInNormalFrame;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
int _strikeScore(int rollIndex) {
|
|
||||||
return _allPins + _nextTwoRollsBonus(rollIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
int _spareScore(int rollIndex) {
|
|
||||||
return _allPins + _nextRollBonus(rollIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
int _normalScore(int rollIndex) {
|
|
||||||
return _rolls[rollIndex] + _rolls[rollIndex + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
int _nextTwoRollsBonus(int rollIndex) {
|
|
||||||
return _rolls[rollIndex + 1] + _rolls[rollIndex + 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
int _nextRollBonus(int rollIndex) {
|
|
||||||
return _rolls[rollIndex + 2];
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isSpare(int rollIndex) {
|
|
||||||
return _rolls[rollIndex] + _rolls[rollIndex + 1] == _allPins;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isStrike(int rollIndex) {
|
|
||||||
return _rolls[rollIndex] == _allPins;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
// WARNING: DO NOT modify the Item class - the goblin in the corner will insta-rage!
|
|
||||||
|
|
||||||
class Item {
|
|
||||||
String name;
|
|
||||||
int sellIn;
|
|
||||||
int quality;
|
|
||||||
|
|
||||||
Item(this.name, this.sellIn, this.quality);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '$name, $sellIn, $quality';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Quality bounds - domain constraints
|
|
||||||
const int _minQuality = 0;
|
|
||||||
const int _maxQuality = 50;
|
|
||||||
|
|
||||||
/// Helper methods for quality management
|
|
||||||
void _degradeQuality(Item item, int amount) {
|
|
||||||
item.quality = (item.quality - amount).clamp(_minQuality, _maxQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _improveQuality(Item item, int amount) {
|
|
||||||
item.quality = (item.quality + amount).clamp(_minQuality, _maxQuality);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strategy pattern: Each item type has its own update behavior
|
|
||||||
abstract class ItemUpdater {
|
|
||||||
void update(Item item);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NormalItemUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
// Quality decreases by 1 each day
|
|
||||||
_degradeQuality(item, 1);
|
|
||||||
|
|
||||||
item.sellIn -= 1;
|
|
||||||
|
|
||||||
// After sell-by date, quality degrades twice as fast
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
_degradeQuality(item, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AgedBrieUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
// Quality increases as it ages
|
|
||||||
_improveQuality(item, 1);
|
|
||||||
|
|
||||||
item.sellIn -= 1;
|
|
||||||
|
|
||||||
// After sell-by date, quality increases twice as fast
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
_improveQuality(item, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BackstagePassUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
// Base quality increase
|
|
||||||
_improveQuality(item, 1);
|
|
||||||
|
|
||||||
// 10 days or less: +1 additional
|
|
||||||
if (item.sellIn <= 10) {
|
|
||||||
_improveQuality(item, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5 days or less: +1 additional (total +3/day)
|
|
||||||
if (item.sellIn <= 5) {
|
|
||||||
_improveQuality(item, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.sellIn -= 1;
|
|
||||||
|
|
||||||
// After concert, quality drops to 0
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
item.quality = _minQuality;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SulfurasUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
// Legendary item never changes
|
|
||||||
// Quality always 80, sellIn never decreases
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConjuredItemUpdater implements ItemUpdater {
|
|
||||||
@override
|
|
||||||
void update(Item item) {
|
|
||||||
// Conjured items degrade twice as fast as normal items
|
|
||||||
_degradeQuality(item, 2);
|
|
||||||
|
|
||||||
item.sellIn -= 1;
|
|
||||||
|
|
||||||
// After sell-by date, quality degrades twice as fast (4x total)
|
|
||||||
if (item.sellIn < 0) {
|
|
||||||
_degradeQuality(item, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GildedRose {
|
|
||||||
List<Item> items;
|
|
||||||
|
|
||||||
GildedRose(this.items);
|
|
||||||
|
|
||||||
void updateQuality() {
|
|
||||||
for (final item in items) {
|
|
||||||
final updater = _selectUpdater(item);
|
|
||||||
updater.update(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemUpdater _selectUpdater(Item item) {
|
|
||||||
if (item.name == 'Sulfuras, Hand of Ragnaros') {
|
|
||||||
return SulfurasUpdater();
|
|
||||||
} else if (item.name == 'Aged Brie') {
|
|
||||||
return AgedBrieUpdater();
|
|
||||||
} else if (item.name == 'Backstage passes to a TAFKAL80ETC concert') {
|
|
||||||
return BackstagePassUpdater();
|
|
||||||
} else if (item.name.startsWith('Conjured')) {
|
|
||||||
return ConjuredItemUpdater();
|
|
||||||
} else {
|
|
||||||
return NormalItemUpdater();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,55 +0,0 @@
|
||||||
/// String Calculator - Buggy Implementation
|
|
||||||
/// This code has intentional bugs for the Bug Hunt Kata exercise
|
|
||||||
///
|
|
||||||
/// Requirements:
|
|
||||||
/// 1. Empty string returns 0
|
|
||||||
/// 2. Single number returns that number
|
|
||||||
/// 3. Two numbers comma-delimited returns sum
|
|
||||||
/// 4. Handle newlines as delimiters
|
|
||||||
/// 5. Support custom delimiters: "//[delimiter]\n[numbers]"
|
|
||||||
library;
|
|
||||||
|
|
||||||
class StringCalculator {
|
|
||||||
int add(String numbers) {
|
|
||||||
// Bug 1: Empty string handling
|
|
||||||
if (numbers.isEmpty) {
|
|
||||||
return 0; // Fixed: Return 0 for empty string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug 2: Single number parsing
|
|
||||||
if (!numbers.contains(',') &&
|
|
||||||
!numbers.contains('\n') &&
|
|
||||||
!numbers.startsWith('//')) {
|
|
||||||
return int.parse(numbers); // Fixed: Removed off-by-one error
|
|
||||||
}
|
|
||||||
|
|
||||||
String delimiter = ',';
|
|
||||||
String numbersToProcess = numbers;
|
|
||||||
|
|
||||||
// Custom delimiter support
|
|
||||||
if (numbers.startsWith('//')) {
|
|
||||||
// Bug 4: Custom delimiter not actually used
|
|
||||||
final parts = numbers.split('\n');
|
|
||||||
delimiter = parts[0].substring(2); // Fixed: Extract custom delimiter
|
|
||||||
numbersToProcess = parts.skip(1).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug 3 & 4: Delimiter handling issues
|
|
||||||
final numList = numbersToProcess
|
|
||||||
.replaceAll('\n', delimiter)
|
|
||||||
.split(delimiter)
|
|
||||||
.where((s) => s.isNotEmpty)
|
|
||||||
.map((s) => int.parse(s))
|
|
||||||
.where((n) => n <= 1000) // Filter out numbers > 1000
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Bug 3: Off-by-one in summation
|
|
||||||
int sum = 0;
|
|
||||||
for (int i = 0; i < numList.length; i++) {
|
|
||||||
// Fixed: Include last element
|
|
||||||
sum += numList[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import 'package:tdd_katas/bowling_game.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('Bowling Game Scoring', () {
|
|
||||||
late BowlingGame game;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
game = BowlingGame();
|
|
||||||
});
|
|
||||||
|
|
||||||
void rollMany(int times, int pins) {
|
|
||||||
for (int i = 0; i < times; i++) {
|
|
||||||
game.roll(pins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group('Basic Scoring', () {
|
|
||||||
test('gutter game - no pins knocked', () {
|
|
||||||
rollMany(20, 0);
|
|
||||||
expect(game.score(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all ones - simple addition', () {
|
|
||||||
rollMany(20, 1);
|
|
||||||
expect(game.score(), 20);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Spare Bonus (next 1 roll)', () {
|
|
||||||
test('one spare in first frame', () {
|
|
||||||
game.roll(5);
|
|
||||||
game.roll(5); // spare
|
|
||||||
game.roll(3); // bonus for spare
|
|
||||||
rollMany(17, 0);
|
|
||||||
|
|
||||||
expect(game.score(), 16); // 10 + 3 (bonus) + 3
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all spares with 5 pins each', () {
|
|
||||||
rollMany(21, 5); // 10 frames of 5,5 + 1 bonus roll
|
|
||||||
expect(game.score(), 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Strike Bonus (next 2 rolls)', () {
|
|
||||||
test('one strike in first frame', () {
|
|
||||||
game.roll(10); // Strike!
|
|
||||||
game.roll(3);
|
|
||||||
game.roll(4); // Next 2 rolls are bonus
|
|
||||||
rollMany(16, 0);
|
|
||||||
|
|
||||||
expect(game.score(), 24); // 10 + 3 + 4 (bonus) + 7
|
|
||||||
});
|
|
||||||
|
|
||||||
test('perfect game - twelve consecutive strikes', () {
|
|
||||||
rollMany(12, 10);
|
|
||||||
expect(game.score(), 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Complex Scenarios', () {
|
|
||||||
test('combination of strikes, spares, and normal frames', () {
|
|
||||||
game.roll(10); // Frame 1: Strike
|
|
||||||
game.roll(5);
|
|
||||||
game.roll(5); // Frame 2: Spare
|
|
||||||
game.roll(7);
|
|
||||||
game.roll(2); // Frame 3: Normal
|
|
||||||
rollMany(15, 0);
|
|
||||||
|
|
||||||
// Frame 1: 10 + 5 + 5 = 20
|
|
||||||
// Frame 2: 10 + 7 = 17
|
|
||||||
// Frame 3: 7 + 2 = 9
|
|
||||||
expect(game.score(), 46);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,759 +0,0 @@
|
||||||
import 'package:tdd_katas/gilded_rose.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('Gilded Rose Inn - Characterization Tests', () {
|
|
||||||
group('Normal items', () {
|
|
||||||
test('quality and sellIn both decrease each day', () {
|
|
||||||
final items = [Item('Normal Item', 10, 20)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(9));
|
|
||||||
expect(items[0].quality, equals(19));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality degrades twice as fast after sell-by date', () {
|
|
||||||
final items = [Item('Normal Item', 0, 10)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(-1));
|
|
||||||
expect(items[0].quality, equals(8)); // -2 quality
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality never becomes negative', () {
|
|
||||||
final items = [Item('Normal Item', 5, 0)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(0));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Aged Brie - appreciates over time', () {
|
|
||||||
test('quality increases as it ages', () {
|
|
||||||
final items = [Item('Aged Brie', 10, 20)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(9));
|
|
||||||
expect(items[0].quality, equals(21));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality increases twice as fast after sell-by date', () {
|
|
||||||
final items = [Item('Aged Brie', 0, 20)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(-1));
|
|
||||||
expect(items[0].quality, equals(22)); // +2 quality
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality never exceeds 50', () {
|
|
||||||
final items = [Item('Aged Brie', 5, 50)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(50));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Sulfuras - legendary item', () {
|
|
||||||
test('quality never changes', () {
|
|
||||||
final items = [Item('Sulfuras, Hand of Ragnaros', 10, 80)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(80));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sellIn never decreases', () {
|
|
||||||
final items = [Item('Sulfuras, Hand of Ragnaros', 10, 80)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(10));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Backstage passes - concert tickets', () {
|
|
||||||
test('quality increases by 1 when more than 10 days away', () {
|
|
||||||
final items = [
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 15, 20),
|
|
||||||
];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(21)); // +1
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality increases by 2 when 10 days or less', () {
|
|
||||||
final items = [
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 10, 20),
|
|
||||||
];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(22)); // +2
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality increases by 3 when 5 days or less', () {
|
|
||||||
final items = [
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 5, 20),
|
|
||||||
];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(23)); // +3
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality drops to 0 after concert', () {
|
|
||||||
final items = [
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 0, 20),
|
|
||||||
];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].sellIn, equals(-1));
|
|
||||||
expect(items[0].quality, equals(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quality never exceeds 50', () {
|
|
||||||
final items = [
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 5, 49),
|
|
||||||
];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(50)); // capped at 50
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Conjured items', () {
|
|
||||||
test('degrade in quality twice as fast as normal items', () {
|
|
||||||
final items = [Item('Conjured Mana Cake', 10, 20)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(18)); // -2 instead of -1
|
|
||||||
expect(items[0].sellIn, equals(9));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('degrade twice as fast after sell-by date', () {
|
|
||||||
final items = [Item('Conjured Mana Cake', 0, 20)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(16)); // -4 instead of -2
|
|
||||||
expect(items[0].sellIn, equals(-1));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('never have negative quality', () {
|
|
||||||
final items = [Item('Conjured Mana Cake', 5, 1)];
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
|
|
||||||
expect(items[0].quality, equals(0));
|
|
||||||
expect(items[0].sellIn, equals(4));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Golden Master - 30-day simulation', () {
|
|
||||||
test('captures complete behavior of all item types over time', () {
|
|
||||||
final items = _createAllItemTypes();
|
|
||||||
final gildedRose = GildedRose(items);
|
|
||||||
final output = StringBuffer();
|
|
||||||
|
|
||||||
for (var day = 0; day <= 30; day++) {
|
|
||||||
output.writeln('-------- day $day --------');
|
|
||||||
output.writeln('name, sellIn, quality');
|
|
||||||
for (final item in items) {
|
|
||||||
output.writeln(item);
|
|
||||||
}
|
|
||||||
output.writeln();
|
|
||||||
gildedRose.updateQuality();
|
|
||||||
}
|
|
||||||
|
|
||||||
final expected = _goldenMasterBaseline();
|
|
||||||
expect(
|
|
||||||
output.toString(),
|
|
||||||
equals(expected),
|
|
||||||
reason: 'Golden Master mismatch - legacy behavior changed!',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Item> _createAllItemTypes() {
|
|
||||||
return [
|
|
||||||
// Normal items at various states
|
|
||||||
Item('Normal Item', 10, 20),
|
|
||||||
Item('Normal Item', 2, 5),
|
|
||||||
Item('Normal Item', 0, 10),
|
|
||||||
Item('Normal Item', -1, 10),
|
|
||||||
// Aged Brie
|
|
||||||
Item('Aged Brie', 10, 20),
|
|
||||||
Item('Aged Brie', 0, 30),
|
|
||||||
Item('Aged Brie', 5, 49),
|
|
||||||
// Sulfuras
|
|
||||||
Item('Sulfuras, Hand of Ragnaros', 10, 80),
|
|
||||||
Item('Sulfuras, Hand of Ragnaros', -1, 80),
|
|
||||||
// Backstage passes at critical thresholds
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 15, 20),
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 10, 20),
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 5, 20),
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 1, 20),
|
|
||||||
Item('Backstage passes to a TAFKAL80ETC concert', 0, 20),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
String _goldenMasterBaseline() {
|
|
||||||
// Generated by running the legacy code
|
|
||||||
// This captures the exact behavior - our safety net!
|
|
||||||
return '''-------- day 0 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 10, 20
|
|
||||||
Normal Item, 2, 5
|
|
||||||
Normal Item, 0, 10
|
|
||||||
Normal Item, -1, 10
|
|
||||||
Aged Brie, 10, 20
|
|
||||||
Aged Brie, 0, 30
|
|
||||||
Aged Brie, 5, 49
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 15, 20
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 10, 20
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 5, 20
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 1, 20
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 0, 20
|
|
||||||
|
|
||||||
-------- day 1 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 9, 19
|
|
||||||
Normal Item, 1, 4
|
|
||||||
Normal Item, -1, 8
|
|
||||||
Normal Item, -2, 8
|
|
||||||
Aged Brie, 9, 21
|
|
||||||
Aged Brie, -1, 32
|
|
||||||
Aged Brie, 4, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 14, 21
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 9, 22
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 4, 23
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 0, 23
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -1, 0
|
|
||||||
|
|
||||||
-------- day 2 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 8, 18
|
|
||||||
Normal Item, 0, 3
|
|
||||||
Normal Item, -2, 6
|
|
||||||
Normal Item, -3, 6
|
|
||||||
Aged Brie, 8, 22
|
|
||||||
Aged Brie, -2, 34
|
|
||||||
Aged Brie, 3, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 13, 22
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 8, 24
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 3, 26
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -1, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -2, 0
|
|
||||||
|
|
||||||
-------- day 3 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 7, 17
|
|
||||||
Normal Item, -1, 1
|
|
||||||
Normal Item, -3, 4
|
|
||||||
Normal Item, -4, 4
|
|
||||||
Aged Brie, 7, 23
|
|
||||||
Aged Brie, -3, 36
|
|
||||||
Aged Brie, 2, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 12, 23
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 7, 26
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 2, 29
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -2, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -3, 0
|
|
||||||
|
|
||||||
-------- day 4 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 6, 16
|
|
||||||
Normal Item, -2, 0
|
|
||||||
Normal Item, -4, 2
|
|
||||||
Normal Item, -5, 2
|
|
||||||
Aged Brie, 6, 24
|
|
||||||
Aged Brie, -4, 38
|
|
||||||
Aged Brie, 1, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 11, 24
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 6, 28
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 1, 32
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -3, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -4, 0
|
|
||||||
|
|
||||||
-------- day 5 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 5, 15
|
|
||||||
Normal Item, -3, 0
|
|
||||||
Normal Item, -5, 0
|
|
||||||
Normal Item, -6, 0
|
|
||||||
Aged Brie, 5, 25
|
|
||||||
Aged Brie, -5, 40
|
|
||||||
Aged Brie, 0, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 10, 25
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 5, 30
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 0, 35
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -4, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -5, 0
|
|
||||||
|
|
||||||
-------- day 6 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 4, 14
|
|
||||||
Normal Item, -4, 0
|
|
||||||
Normal Item, -6, 0
|
|
||||||
Normal Item, -7, 0
|
|
||||||
Aged Brie, 4, 26
|
|
||||||
Aged Brie, -6, 42
|
|
||||||
Aged Brie, -1, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 9, 27
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 4, 33
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -1, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -5, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -6, 0
|
|
||||||
|
|
||||||
-------- day 7 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 3, 13
|
|
||||||
Normal Item, -5, 0
|
|
||||||
Normal Item, -7, 0
|
|
||||||
Normal Item, -8, 0
|
|
||||||
Aged Brie, 3, 27
|
|
||||||
Aged Brie, -7, 44
|
|
||||||
Aged Brie, -2, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 8, 29
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 3, 36
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -2, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -6, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -7, 0
|
|
||||||
|
|
||||||
-------- day 8 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 2, 12
|
|
||||||
Normal Item, -6, 0
|
|
||||||
Normal Item, -8, 0
|
|
||||||
Normal Item, -9, 0
|
|
||||||
Aged Brie, 2, 28
|
|
||||||
Aged Brie, -8, 46
|
|
||||||
Aged Brie, -3, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 7, 31
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 2, 39
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -3, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -7, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -8, 0
|
|
||||||
|
|
||||||
-------- day 9 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 1, 11
|
|
||||||
Normal Item, -7, 0
|
|
||||||
Normal Item, -9, 0
|
|
||||||
Normal Item, -10, 0
|
|
||||||
Aged Brie, 1, 29
|
|
||||||
Aged Brie, -9, 48
|
|
||||||
Aged Brie, -4, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 6, 33
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 1, 42
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -4, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -8, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -9, 0
|
|
||||||
|
|
||||||
-------- day 10 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, 0, 10
|
|
||||||
Normal Item, -8, 0
|
|
||||||
Normal Item, -10, 0
|
|
||||||
Normal Item, -11, 0
|
|
||||||
Aged Brie, 0, 30
|
|
||||||
Aged Brie, -10, 50
|
|
||||||
Aged Brie, -5, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 5, 35
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 0, 45
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -5, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -9, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -10, 0
|
|
||||||
|
|
||||||
-------- day 11 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -1, 8
|
|
||||||
Normal Item, -9, 0
|
|
||||||
Normal Item, -11, 0
|
|
||||||
Normal Item, -12, 0
|
|
||||||
Aged Brie, -1, 32
|
|
||||||
Aged Brie, -11, 50
|
|
||||||
Aged Brie, -6, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 4, 38
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -1, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -6, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -10, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -11, 0
|
|
||||||
|
|
||||||
-------- day 12 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -2, 6
|
|
||||||
Normal Item, -10, 0
|
|
||||||
Normal Item, -12, 0
|
|
||||||
Normal Item, -13, 0
|
|
||||||
Aged Brie, -2, 34
|
|
||||||
Aged Brie, -12, 50
|
|
||||||
Aged Brie, -7, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 3, 41
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -2, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -7, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -11, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -12, 0
|
|
||||||
|
|
||||||
-------- day 13 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -3, 4
|
|
||||||
Normal Item, -11, 0
|
|
||||||
Normal Item, -13, 0
|
|
||||||
Normal Item, -14, 0
|
|
||||||
Aged Brie, -3, 36
|
|
||||||
Aged Brie, -13, 50
|
|
||||||
Aged Brie, -8, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 2, 44
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -3, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -8, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -12, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -13, 0
|
|
||||||
|
|
||||||
-------- day 14 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -4, 2
|
|
||||||
Normal Item, -12, 0
|
|
||||||
Normal Item, -14, 0
|
|
||||||
Normal Item, -15, 0
|
|
||||||
Aged Brie, -4, 38
|
|
||||||
Aged Brie, -14, 50
|
|
||||||
Aged Brie, -9, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 1, 47
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -4, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -9, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -13, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -14, 0
|
|
||||||
|
|
||||||
-------- day 15 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -5, 0
|
|
||||||
Normal Item, -13, 0
|
|
||||||
Normal Item, -15, 0
|
|
||||||
Normal Item, -16, 0
|
|
||||||
Aged Brie, -5, 40
|
|
||||||
Aged Brie, -15, 50
|
|
||||||
Aged Brie, -10, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, 0, 50
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -5, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -10, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -14, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -15, 0
|
|
||||||
|
|
||||||
-------- day 16 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -6, 0
|
|
||||||
Normal Item, -14, 0
|
|
||||||
Normal Item, -16, 0
|
|
||||||
Normal Item, -17, 0
|
|
||||||
Aged Brie, -6, 42
|
|
||||||
Aged Brie, -16, 50
|
|
||||||
Aged Brie, -11, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -1, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -6, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -11, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -15, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -16, 0
|
|
||||||
|
|
||||||
-------- day 17 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -7, 0
|
|
||||||
Normal Item, -15, 0
|
|
||||||
Normal Item, -17, 0
|
|
||||||
Normal Item, -18, 0
|
|
||||||
Aged Brie, -7, 44
|
|
||||||
Aged Brie, -17, 50
|
|
||||||
Aged Brie, -12, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -2, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -7, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -12, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -16, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -17, 0
|
|
||||||
|
|
||||||
-------- day 18 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -8, 0
|
|
||||||
Normal Item, -16, 0
|
|
||||||
Normal Item, -18, 0
|
|
||||||
Normal Item, -19, 0
|
|
||||||
Aged Brie, -8, 46
|
|
||||||
Aged Brie, -18, 50
|
|
||||||
Aged Brie, -13, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -3, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -8, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -13, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -17, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -18, 0
|
|
||||||
|
|
||||||
-------- day 19 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -9, 0
|
|
||||||
Normal Item, -17, 0
|
|
||||||
Normal Item, -19, 0
|
|
||||||
Normal Item, -20, 0
|
|
||||||
Aged Brie, -9, 48
|
|
||||||
Aged Brie, -19, 50
|
|
||||||
Aged Brie, -14, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -4, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -9, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -14, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -18, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -19, 0
|
|
||||||
|
|
||||||
-------- day 20 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -10, 0
|
|
||||||
Normal Item, -18, 0
|
|
||||||
Normal Item, -20, 0
|
|
||||||
Normal Item, -21, 0
|
|
||||||
Aged Brie, -10, 50
|
|
||||||
Aged Brie, -20, 50
|
|
||||||
Aged Brie, -15, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -5, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -10, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -15, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -19, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -20, 0
|
|
||||||
|
|
||||||
-------- day 21 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -11, 0
|
|
||||||
Normal Item, -19, 0
|
|
||||||
Normal Item, -21, 0
|
|
||||||
Normal Item, -22, 0
|
|
||||||
Aged Brie, -11, 50
|
|
||||||
Aged Brie, -21, 50
|
|
||||||
Aged Brie, -16, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -6, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -11, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -16, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -20, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -21, 0
|
|
||||||
|
|
||||||
-------- day 22 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -12, 0
|
|
||||||
Normal Item, -20, 0
|
|
||||||
Normal Item, -22, 0
|
|
||||||
Normal Item, -23, 0
|
|
||||||
Aged Brie, -12, 50
|
|
||||||
Aged Brie, -22, 50
|
|
||||||
Aged Brie, -17, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -7, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -12, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -17, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -21, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -22, 0
|
|
||||||
|
|
||||||
-------- day 23 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -13, 0
|
|
||||||
Normal Item, -21, 0
|
|
||||||
Normal Item, -23, 0
|
|
||||||
Normal Item, -24, 0
|
|
||||||
Aged Brie, -13, 50
|
|
||||||
Aged Brie, -23, 50
|
|
||||||
Aged Brie, -18, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -8, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -13, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -18, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -22, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -23, 0
|
|
||||||
|
|
||||||
-------- day 24 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -14, 0
|
|
||||||
Normal Item, -22, 0
|
|
||||||
Normal Item, -24, 0
|
|
||||||
Normal Item, -25, 0
|
|
||||||
Aged Brie, -14, 50
|
|
||||||
Aged Brie, -24, 50
|
|
||||||
Aged Brie, -19, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -9, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -14, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -19, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -23, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -24, 0
|
|
||||||
|
|
||||||
-------- day 25 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -15, 0
|
|
||||||
Normal Item, -23, 0
|
|
||||||
Normal Item, -25, 0
|
|
||||||
Normal Item, -26, 0
|
|
||||||
Aged Brie, -15, 50
|
|
||||||
Aged Brie, -25, 50
|
|
||||||
Aged Brie, -20, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -10, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -15, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -20, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -24, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -25, 0
|
|
||||||
|
|
||||||
-------- day 26 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -16, 0
|
|
||||||
Normal Item, -24, 0
|
|
||||||
Normal Item, -26, 0
|
|
||||||
Normal Item, -27, 0
|
|
||||||
Aged Brie, -16, 50
|
|
||||||
Aged Brie, -26, 50
|
|
||||||
Aged Brie, -21, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -11, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -16, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -21, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -25, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -26, 0
|
|
||||||
|
|
||||||
-------- day 27 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -17, 0
|
|
||||||
Normal Item, -25, 0
|
|
||||||
Normal Item, -27, 0
|
|
||||||
Normal Item, -28, 0
|
|
||||||
Aged Brie, -17, 50
|
|
||||||
Aged Brie, -27, 50
|
|
||||||
Aged Brie, -22, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -12, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -17, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -22, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -26, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -27, 0
|
|
||||||
|
|
||||||
-------- day 28 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -18, 0
|
|
||||||
Normal Item, -26, 0
|
|
||||||
Normal Item, -28, 0
|
|
||||||
Normal Item, -29, 0
|
|
||||||
Aged Brie, -18, 50
|
|
||||||
Aged Brie, -28, 50
|
|
||||||
Aged Brie, -23, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -13, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -18, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -23, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -27, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -28, 0
|
|
||||||
|
|
||||||
-------- day 29 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -19, 0
|
|
||||||
Normal Item, -27, 0
|
|
||||||
Normal Item, -29, 0
|
|
||||||
Normal Item, -30, 0
|
|
||||||
Aged Brie, -19, 50
|
|
||||||
Aged Brie, -29, 50
|
|
||||||
Aged Brie, -24, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -14, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -19, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -24, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -28, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -29, 0
|
|
||||||
|
|
||||||
-------- day 30 --------
|
|
||||||
name, sellIn, quality
|
|
||||||
Normal Item, -20, 0
|
|
||||||
Normal Item, -28, 0
|
|
||||||
Normal Item, -30, 0
|
|
||||||
Normal Item, -31, 0
|
|
||||||
Aged Brie, -20, 50
|
|
||||||
Aged Brie, -30, 50
|
|
||||||
Aged Brie, -25, 50
|
|
||||||
Sulfuras, Hand of Ragnaros, 10, 80
|
|
||||||
Sulfuras, Hand of Ragnaros, -1, 80
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -15, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -20, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -25, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -29, 0
|
|
||||||
Backstage passes to a TAFKAL80ETC concert, -30, 0
|
|
||||||
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
|
|
@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import 'package:tdd_katas/string_calculator.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('String Calculator - Bug Hunt', () {
|
|
||||||
late StringCalculator calculator;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
calculator = StringCalculator();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('empty string returns 0', () {
|
|
||||||
expect(calculator.add(''), equals(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('single number returns that number', () {
|
|
||||||
expect(calculator.add('5'), equals(5));
|
|
||||||
expect(calculator.add('42'), equals(42));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('two comma-delimited numbers return sum', () {
|
|
||||||
expect(calculator.add('1,2'), equals(3));
|
|
||||||
expect(calculator.add('10,20'), equals(30));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('multiple comma-delimited numbers return sum', () {
|
|
||||||
expect(calculator.add('1,2,3'), equals(6));
|
|
||||||
expect(calculator.add('5,10,15,20'), equals(50));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('custom delimiter works correctly', () {
|
|
||||||
expect(calculator.add('//;\n1;2'), equals(3));
|
|
||||||
expect(calculator.add('//|\n10|20|30'), equals(60));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('numbers greater than 1000 are ignored', () {
|
|
||||||
expect(calculator.add('2,1001'), equals(2));
|
|
||||||
expect(calculator.add('1000,1001,2'), equals(1002));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue