From 1f31a68df5bc01d3612db83326b08a000bd57892 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:19:34 +0700 Subject: [PATCH 01/11] RED: test gutter game --- lib/bowling_game.dart | 9 +++++++++ test/bowling_game_test.dart | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 lib/bowling_game.dart create mode 100644 test/bowling_game_test.dart diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart new file mode 100644 index 0000000..838360e --- /dev/null +++ b/lib/bowling_game.dart @@ -0,0 +1,9 @@ +class BowlingGame { + void roll(int pins) { + throw UnimplementedError(); + } + + int score() { + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart new file mode 100644 index 0000000..f80bb94 --- /dev/null +++ b/test/bowling_game_test.dart @@ -0,0 +1,16 @@ +import 'package:tdd_katas/bowling_game.dart'; +import 'package:test/test.dart'; + +void main() { + group('Bowling Game', () { + test('gutter game - all zeros', () { + final game = BowlingGame(); + + for (int i = 0; i < 20; i++) { + game.roll(0); + } + + expect(game.score(), 0); + }); + }); +} From 9a22a3e5fedd12efbc1a6f11d8ffeeba607cd3c9 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:21:13 +0700 Subject: [PATCH 02/11] GREEN: score() returns hardcoded 0 --- lib/bowling_game.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index 838360e..a900cb8 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -1,9 +1,7 @@ class BowlingGame { - void roll(int pins) { - throw UnimplementedError(); - } - + void roll(int pins) {} + int score() { - throw UnimplementedError(); + return 0; } -} \ No newline at end of file +} From e4167c03a5702a21bde2abe547d184d0bc83584d Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:22:23 +0700 Subject: [PATCH 03/11] RED: test all ones --- test/bowling_game_test.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart index f80bb94..71e9aa1 100644 --- a/test/bowling_game_test.dart +++ b/test/bowling_game_test.dart @@ -12,5 +12,15 @@ void main() { expect(game.score(), 0); }); + + test('all ones - score is 20', () { + final game = BowlingGame(); + + for (int i = 0; i < 20; i++) { + game.roll(1); + } + + expect(game.score(), 20); + }); }); } From b4cdd039ae7c08a1e7687d5432fdd8880993a3a4 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:24:12 +0700 Subject: [PATCH 04/11] GREEN: save score state and add knocked pins on each roll --- lib/bowling_game.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index a900cb8..b4983c5 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -1,7 +1,11 @@ class BowlingGame { - void roll(int pins) {} + int _score = 0; + + void roll(int pins) { + _score += pins; + } int score() { - return 0; + return _score; } } From acf96ad482aeff857290c9886b719629b2e561ce Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:36:11 +0700 Subject: [PATCH 05/11] REFACTOR: tests clean up --- test/bowling_game_test.dart | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart index 71e9aa1..870b869 100644 --- a/test/bowling_game_test.dart +++ b/test/bowling_game_test.dart @@ -3,23 +3,25 @@ import 'package:test/test.dart'; void main() { group('Bowling Game', () { - test('gutter game - all zeros', () { - final game = BowlingGame(); + late BowlingGame game; - for (int i = 0; i < 20; i++) { - game.roll(0); + setUp(() { + game = BowlingGame(); + }); + + void rollMany(int times, int pins) { + for (int i = 0; i < times; i++) { + game.roll(pins); } + } + test('gutter game - all zeros', () { + rollMany(20, 0); expect(game.score(), 0); }); test('all ones - score is 20', () { - final game = BowlingGame(); - - for (int i = 0; i < 20; i++) { - game.roll(1); - } - + rollMany(20, 1); expect(game.score(), 20); }); }); From f34c820c659d4302841fc755d8b169e7e225ae1e Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:37:52 +0700 Subject: [PATCH 06/11] RED: test one spare --- test/bowling_game_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart index 870b869..c278f51 100644 --- a/test/bowling_game_test.dart +++ b/test/bowling_game_test.dart @@ -24,5 +24,14 @@ void main() { rollMany(20, 1); expect(game.score(), 20); }); + + test('one spare', () { + game.roll(5); + game.roll(5); // spare + game.roll(3); // bonus for spare + rollMany(17, 0); // rest are gutter balls + + expect(game.score(), 16); // 10 (spare) + 3 (bonus) + 3 (normal roll) + }); }); } From b6007c8115ab4a546ca638cd5ccb07dba9ba8376 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:52:49 +0700 Subject: [PATCH 07/11] GREEN: store rolls in a list and check for spare --- lib/bowling_game.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index b4983c5..e58e92b 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -1,11 +1,24 @@ class BowlingGame { - int _score = 0; + final List _rolls = []; void roll(int pins) { - _score += pins; + _rolls.add(pins); } int score() { - return _score; + int totalScore = 0; + int rollIndex = 0; + + for (int frame = 0; frame < 10; frame++) { + if (_rolls[rollIndex] + _rolls[rollIndex + 1] == 10) { + totalScore += 10 + _rolls[rollIndex + 2]; + rollIndex += 2; + } else { + totalScore += _rolls[rollIndex] + _rolls[rollIndex + 1]; + rollIndex += 2; + } + } + + return totalScore; } } From 7c24567bfa4f543f3a4f15f3024b65f73ec388f7 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 11:55:31 +0700 Subject: [PATCH 08/11] REFACTOR: extract spare checking logic --- lib/bowling_game.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index e58e92b..2cc3615 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -10,7 +10,7 @@ class BowlingGame { int rollIndex = 0; for (int frame = 0; frame < 10; frame++) { - if (_rolls[rollIndex] + _rolls[rollIndex + 1] == 10) { + if (_isSpare(rollIndex)) { totalScore += 10 + _rolls[rollIndex + 2]; rollIndex += 2; } else { @@ -21,4 +21,8 @@ class BowlingGame { return totalScore; } + + bool _isSpare(int rollIndex) { + return _rolls[rollIndex] + _rolls[rollIndex + 1] == 10; + } } From 9ca4accd7b6ab70a80d1f4ba0516f62d93e0cf05 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 12:33:22 +0700 Subject: [PATCH 09/11] RED: test one strike --- test/bowling_game_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart index c278f51..14fafe0 100644 --- a/test/bowling_game_test.dart +++ b/test/bowling_game_test.dart @@ -33,5 +33,14 @@ void main() { expect(game.score(), 16); // 10 (spare) + 3 (bonus) + 3 (normal roll) }); + + test('one strike', () { + 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 (strike) + 3 + 4 (frame 2) = 24 + }); }); } From 8474b20e857caa934f090e918c3074df18c12a26 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 12:34:55 +0700 Subject: [PATCH 10/11] GREEN: calculate strike --- lib/bowling_game.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index 2cc3615..f1494d2 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -10,7 +10,10 @@ class BowlingGame { int rollIndex = 0; for (int frame = 0; frame < 10; frame++) { - if (_isSpare(rollIndex)) { + if (_isStrike(rollIndex)) { + totalScore += 10 + _rolls[rollIndex + 1] + _rolls[rollIndex + 2]; + rollIndex += 1; + } else if (_isSpare(rollIndex)) { totalScore += 10 + _rolls[rollIndex + 2]; rollIndex += 2; } else { @@ -25,4 +28,8 @@ class BowlingGame { bool _isSpare(int rollIndex) { return _rolls[rollIndex] + _rolls[rollIndex + 1] == 10; } + + bool _isStrike(int rollIndex) { + return _rolls[rollIndex] == 10; + } } From a93a6abb28fc25924160f7a7285ded8e2bfdd863 Mon Sep 17 00:00:00 2001 From: fiatcode Date: Tue, 10 Feb 2026 12:36:39 +0700 Subject: [PATCH 11/11] REFACTOR: break down the logic into dedicated methods, tests cleaned up --- lib/bowling_game.dart | 51 +++++++++++++++++++-------- test/bowling_game_test.dart | 70 +++++++++++++++++++++++++++---------- 2 files changed, 88 insertions(+), 33 deletions(-) diff --git a/lib/bowling_game.dart b/lib/bowling_game.dart index f1494d2..0d7a54a 100644 --- a/lib/bowling_game.dart +++ b/lib/bowling_game.dart @@ -1,35 +1,58 @@ class BowlingGame { + static const _totalFrames = 10; + static const _allPins = 10; + static const _rollsInNormalFrame = 2; + static const _rollsInStrike = 1; + final List _rolls = []; - void roll(int pins) { - _rolls.add(pins); - } + void roll(int pins) => _rolls.add(pins); int score() { - int totalScore = 0; + int total = 0; int rollIndex = 0; - for (int frame = 0; frame < 10; frame++) { + for (int frame = 0; frame < _totalFrames; frame++) { if (_isStrike(rollIndex)) { - totalScore += 10 + _rolls[rollIndex + 1] + _rolls[rollIndex + 2]; - rollIndex += 1; + total += _strikeScore(rollIndex); + rollIndex += _rollsInStrike; } else if (_isSpare(rollIndex)) { - totalScore += 10 + _rolls[rollIndex + 2]; - rollIndex += 2; + total += _spareScore(rollIndex); + rollIndex += _rollsInNormalFrame; } else { - totalScore += _rolls[rollIndex] + _rolls[rollIndex + 1]; - rollIndex += 2; + total += _normalScore(rollIndex); + rollIndex += _rollsInNormalFrame; } } - return totalScore; + 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] == 10; + return _rolls[rollIndex] + _rolls[rollIndex + 1] == _allPins; } bool _isStrike(int rollIndex) { - return _rolls[rollIndex] == 10; + return _rolls[rollIndex] == _allPins; } } diff --git a/test/bowling_game_test.dart b/test/bowling_game_test.dart index 14fafe0..cc2f463 100644 --- a/test/bowling_game_test.dart +++ b/test/bowling_game_test.dart @@ -2,7 +2,7 @@ import 'package:tdd_katas/bowling_game.dart'; import 'package:test/test.dart'; void main() { - group('Bowling Game', () { + group('Bowling Game Scoring', () { late BowlingGame game; setUp(() { @@ -15,32 +15,64 @@ void main() { } } - test('gutter game - all zeros', () { - rollMany(20, 0); - expect(game.score(), 0); + 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); + }); }); - test('all ones - score is 20', () { - 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); + }); }); - test('one spare', () { - game.roll(5); - game.roll(5); // spare - game.roll(3); // bonus for spare - rollMany(17, 0); // rest are gutter balls + 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(), 16); // 10 (spare) + 3 (bonus) + 3 (normal roll) + expect(game.score(), 24); // 10 + 3 + 4 (bonus) + 7 + }); + + test('perfect game - twelve consecutive strikes', () { + rollMany(12, 10); + expect(game.score(), 300); + }); }); - test('one strike', () { - game.roll(10); // Strike! - game.roll(3); - game.roll(4); // Next 2 rolls are bonus - rollMany(16, 0); + 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); - expect(game.score(), 24); // 10 + 3 + 4 (strike) + 3 + 4 (frame 2) = 24 + // Frame 1: 10 + 5 + 5 = 20 + // Frame 2: 10 + 7 = 17 + // Frame 3: 7 + 2 = 9 + expect(game.score(), 46); + }); }); }); }