diff --git a/.gitignore b/.gitignore index 1449474..3a85790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ -tdd_*.md diff --git a/README.md b/README.md index d8ef3b1..25fa79d 100644 --- a/README.md +++ b/README.md @@ -58,337 +58,23 @@ integerToRoman(0); // Throws: ArgumentError --- -### 2. Bowling Game ✅ +### 2. [Next Kata Name] 🚧 -**Implementation:** [`lib/bowling_game.dart`](lib/bowling_game.dart) -**Tests:** [`test/bowling_game_test.dart`](test/bowling_game_test.dart) +**Status:** Not yet started +**Implementation:** `lib/[next_kata].dart` +**Tests:** `test/[next_kata]_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.** +*Coming soon...* --- -### 3. Gilded Rose ✅ +### 3. [Third Kata Name] 🚧 -**Implementation:** [`lib/gilded_rose.dart`](lib/gilded_rose.dart) -**Tests:** [`test/gilded_rose_test.dart`](test/gilded_rose_test.dart) +**Status:** Not yet started +**Implementation:** `lib/[third_kata].dart` +**Tests:** `test/[third_kata]_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. +*Coming soon...* --- @@ -400,10 +86,6 @@ 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 @@ -478,312 +160,13 @@ Boundary tests for the valid range (1-3999) --- -## 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) +- [Roman Numerals Rules](https://en.wikipedia.org/wiki/Roman_numerals) - [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. diff --git a/bin/tdd_katas.dart b/bin/tdd_katas.dart index 24f0e29..987a845 100644 --- a/bin/tdd_katas.dart +++ b/bin/tdd_katas.dart @@ -1,3 +1,5 @@ +import 'package:tdd_katas/roman_numerals.dart' as roman_numerals; + void main(List arguments) { - print('TDD Katas exercises, please read the README.md file.'); + print('Hello world: ${roman_numerals.integerToRoman(1)}'); } diff --git a/lib/gilded_rose.dart b/lib/gilded_rose.dart deleted file mode 100644 index 6a813d9..0000000 --- a/lib/gilded_rose.dart +++ /dev/null @@ -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 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(); - } - } -} diff --git a/lib/mars_rover.dart b/lib/mars_rover.dart deleted file mode 100644 index 221e450..0000000 --- a/lib/mars_rover.dart +++ /dev/null @@ -1,106 +0,0 @@ -enum Direction { - north('N'), - east('E'), - south('S'), - west('W'); - - final String code; - const Direction(this.code); - - static Direction fromCode(String code) { - return Direction.values.firstWhere((d) => d.code == code); - } - - Direction turnLeft() { - return Direction.values[(index + 3) % 4]; - } - - Direction turnRight() { - return Direction.values[(index + 1) % 4]; - } -} - -class Position { - final int x; - final int y; - - Position(this.x, this.y); - - Position moveNorth() => Position(x, y + 1); - Position moveEast() => Position(x + 1, y); - Position moveSouth() => Position(x, y - 1); - Position moveWest() => Position(x - 1, y); -} - -class Plateau { - final int width; - final int height; - - Plateau(this.width, this.height); - - Position wrap(Position position) { - final wrappedX = _wrapCoordinate(position.x, width); - final wrappedY = _wrapCoordinate(position.y, height); - return Position(wrappedX, wrappedY); - } - - int _wrapCoordinate(int value, int max) { - final wrapped = value % (max + 1); - return wrapped < 0 ? wrapped + max + 1 : wrapped; - } -} - -class Rover { - Position _position; - Direction _direction; - final Plateau plateau; - - Rover({ - required int x, - required int y, - required String direction, - int plateauWidth = 100, - int plateauHeight = 100, - }) : _position = Position(x, y), - _direction = Direction.fromCode(direction), - plateau = Plateau(plateauWidth, plateauHeight); - - int get x => _position.x; - int get y => _position.y; - String get direction => _direction.code; - - void turnLeft() { - _direction = _direction.turnLeft(); - } - - void turnRight() { - _direction = _direction.turnRight(); - } - - void moveForward() { - final newPosition = switch (_direction) { - Direction.north => _position.moveNorth(), - Direction.east => _position.moveEast(), - Direction.south => _position.moveSouth(), - Direction.west => _position.moveWest(), - }; - _position = plateau.wrap(newPosition); - } - - void execute(String commands) { - for (var i = 0; i < commands.length; i++) { - final command = commands[i]; - switch (command) { - case 'L': - turnLeft(); - break; - case 'R': - turnRight(); - break; - case 'M': - moveForward(); - break; - } - } - } -} diff --git a/lib/string_calculator.dart b/lib/string_calculator.dart deleted file mode 100644 index 6e5ec31..0000000 --- a/lib/string_calculator.dart +++ /dev/null @@ -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; - } -} diff --git a/test/gilded_rose_test.dart b/test/gilded_rose_test.dart deleted file mode 100644 index f79d761..0000000 --- a/test/gilded_rose_test.dart +++ /dev/null @@ -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 _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 - -'''; -} diff --git a/test/mars_rover_test.dart b/test/mars_rover_test.dart deleted file mode 100644 index 53e4540..0000000 --- a/test/mars_rover_test.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:tdd_katas/mars_rover.dart'; -import 'package:test/test.dart'; - -void main() { - group('Mars Rover:', () { - test('rover reports initial position and direction', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - - expect(rover.x, equals(0)); - expect(rover.y, equals(0)); - expect(rover.direction, equals('N')); - }); - - group('Turning Left:', () { - test('from North faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnLeft(); - expect(rover.direction, equals('W')); - }); - - test('from West faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnLeft(); - expect(rover.direction, equals('S')); - }); - - test('from South faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnLeft(); - expect(rover.direction, equals('E')); - }); - - test('from East faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnLeft(); - expect(rover.direction, equals('N')); - }); - }); - - group('Turning Right:', () { - test('from North faces East', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.turnRight(); - expect(rover.direction, equals('E')); - }); - - test('from East faces South', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.turnRight(); - expect(rover.direction, equals('S')); - }); - - test('from South faces West', () { - final rover = Rover(x: 0, y: 0, direction: 'S'); - rover.turnRight(); - expect(rover.direction, equals('W')); - }); - - test('from West faces North', () { - final rover = Rover(x: 0, y: 0, direction: 'W'); - rover.turnRight(); - expect(rover.direction, equals('N')); - }); - }); - - group('Moving Forward:', () { - test('facing North increases Y', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(1)); - }); - - test('facing East increases X', () { - final rover = Rover(x: 0, y: 0, direction: 'E'); - rover.moveForward(); - expect(rover.x, equals(1)); - expect(rover.y, equals(0)); - }); - - test('facing South decreases Y', () { - final rover = Rover(x: 0, y: 5, direction: 'S'); - rover.moveForward(); - expect(rover.x, equals(0)); - expect(rover.y, equals(4)); - }); - - test('facing West decreases X', () { - final rover = Rover(x: 5, y: 0, direction: 'W'); - rover.moveForward(); - expect(rover.x, equals(4)); - expect(rover.y, equals(0)); - }); - }); - - group('Executing Command Sequences:', () { - test('single command L', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('L'); - expect(rover.direction, equals('W')); - }); - - test('single command R', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('R'); - expect(rover.direction, equals('E')); - }); - - test('single command M', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('M'); - expect(rover.y, equals(1)); - }); - - test('complex sequence MMRMMLM', () { - final rover = Rover(x: 0, y: 0, direction: 'N'); - rover.execute('MMRMMLM'); - expect(rover.x, equals(2)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - - test('example from Mars Rover kata', () { - final rover = Rover(x: 1, y: 2, direction: 'N'); - rover.execute('LMLMLMLMM'); - expect(rover.x, equals(1)); - expect(rover.y, equals(3)); - expect(rover.direction, equals('N')); - }); - }); - - group('Plateau Boundaries:', () { - test('wraps around when moving North past boundary', () { - final rover = Rover( - x: 0, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(0)); - }); - - test('wraps around when moving East past boundary', () { - final rover = Rover( - x: 5, - y: 0, - direction: 'E', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(0)); - }); - - test('wraps around when moving South past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'S', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.y, equals(5)); - }); - - test('wraps around when moving West past boundary', () { - final rover = Rover( - x: 0, - y: 0, - direction: 'W', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.moveForward(); - expect(rover.x, equals(5)); - }); - - test('example with wrapping', () { - final rover = Rover( - x: 5, - y: 5, - direction: 'N', - plateauWidth: 5, - plateauHeight: 5, - ); - rover.execute('MMM'); - expect(rover.y, equals(2)); - }); - }); - }); -} diff --git a/test/string_calculator_test.dart b/test/string_calculator_test.dart deleted file mode 100644 index 1d4ee44..0000000 --- a/test/string_calculator_test.dart +++ /dev/null @@ -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)); - }); - }); -}