diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 507c4a51..dc31bbf3 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -12,18 +12,29 @@ class GameBloc extends Bloc { on(_onBallLost); on(_onScored); on(_onIncreasedMultiplier); - on(_onAppliedMultiplier); - on(_onResetMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); } void _onBallLost(BallLost event, Emitter emit) { + var score = state.score; + var multiplier = state.multiplier; + var ballsLeft = event.balls; + var rounds = state.rounds; + + if (ballsLeft < 1) { + score = score * state.multiplier; + multiplier = 1; + ballsLeft = 1; + rounds = state.rounds - 1; + } + emit( state.copyWith( - balls: state.balls - 1, - score: state.score * state.multiplier, - multiplier: 1, + score: score, + multiplier: multiplier, + balls: ballsLeft, + rounds: rounds, ), ); } @@ -42,18 +53,6 @@ class GameBloc extends Bloc { } } - void _onAppliedMultiplier(MultiplierApplied event, Emitter emit) { - if (!state.isGameOver) { - emit(state.copyWith(score: state.score * state.multiplier)); - } - } - - void _onResetMultiplier(MultiplierReset event, Emitter emit) { - if (!state.isGameOver) { - emit(state.copyWith(multiplier: 1)); - } - } - void _onBonusActivated(BonusActivated event, Emitter emit) { emit( state.copyWith( diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index 7a22f415..d177eb6a 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -12,10 +12,14 @@ abstract class GameEvent extends Equatable { /// {@endtemplate} class BallLost extends GameEvent { /// {@macro ball_lost_game_event} - const BallLost(); + const BallLost({ + required this.balls, + }) : assert(balls >= 0, "Balls left can't be negative"); + + final int balls; @override - List get props => []; + List get props => [balls]; } /// {@template scored_game_event} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 88071471..c85f503a 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -23,15 +23,18 @@ class GameState extends Equatable { required this.score, required this.multiplier, required this.balls, + required this.rounds, required this.bonusHistory, }) : assert(score >= 0, "Score can't be negative"), assert(multiplier > 0, 'Multiplier must be greater than zero'), - assert(balls >= 0, "Number of balls can't be negative"); + assert(balls >= 0, "Number of balls can't be negative"), + assert(rounds >= 0, "Number of rounds can't be negative"); const GameState.initial() : score = 0, multiplier = 1, - balls = 3, + balls = 1, + rounds = 3, bonusHistory = const []; /// The current score of the game. @@ -42,20 +45,29 @@ class GameState extends Equatable { /// The number of balls left in the game. /// - /// When the number of balls is 0, the game is over. + /// When the number of balls is 0, round is lost. final int balls; + /// The number of rounds left in the game. + /// + /// When the number of rounds is 0, the game is over. + final int rounds; + /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; + /// Determines when the round is over. + bool get isRoundOver => balls == 0; + /// Determines when the game is over. - bool get isGameOver => balls == 0; + bool get isGameOver => rounds == 0; GameState copyWith({ int? score, int? multiplier, int? balls, + int? rounds, List? bonusHistory, }) { assert( @@ -67,6 +79,7 @@ class GameState extends Equatable { score: score ?? this.score, multiplier: multiplier ?? this.multiplier, balls: balls ?? this.balls, + rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -76,6 +89,7 @@ class GameState extends Equatable { score, multiplier, balls, + rounds, bonusHistory, ]; } diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 6b983cea..669e2076 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -73,7 +73,8 @@ class BallController extends ComponentController @override void onRemove() { super.onRemove(); - gameRef.read().add(const BallLost()); + final remainingBalls = gameRef.children.whereType().length; + gameRef.read().add(BallLost(balls: remainingBalls)); } } @@ -81,7 +82,4 @@ class BallController extends ComponentController class DebugBallController extends BallController { /// {@macro ball_controller} DebugBallController(Ball component) : super(component); - - @override - void onRemove() {} } diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 00eedd2b..213144d4 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -29,7 +29,7 @@ class GameHud extends StatelessWidget { Wrap( direction: Axis.vertical, children: [ - for (var i = 0; i < state.balls; i++) + for (var i = 0; i < state.rounds; i++) const Padding( padding: EdgeInsets.only(top: 6, right: 6), child: CircleAvatar( diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 45f68bdb..8223acd8 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -4,24 +4,26 @@ import 'package:pinball/game/game.dart'; void main() { group('GameBloc', () { - test('initial state has 3 balls and empty score', () { + test('initial state has 3 rounds, 1 ball and empty score', () { final gameBloc = GameBloc(); expect(gameBloc.state.score, equals(0)); - expect(gameBloc.state.balls, equals(3)); + expect(gameBloc.state.balls, equals(1)); + expect(gameBloc.state.rounds, equals(3)); }); - group('LostBall', () { + group('BallLost', () { blocTest( 'decreases number of balls', build: GameBloc.new, act: (bloc) { - bloc.add(const BallLost()); + bloc.add(const BallLost(balls: 0)); }, expect: () => [ const GameState( score: 0, multiplier: 1, - balls: 2, + balls: 1, + rounds: 2, bonusHistory: [], ), ], @@ -40,13 +42,15 @@ void main() { const GameState( score: 2, multiplier: 1, - balls: 3, + balls: 1, + rounds: 1, bonusHistory: [], ), const GameState( score: 5, multiplier: 1, - balls: 3, + balls: 1, + rounds: 1, bonusHistory: [], ), ], @@ -57,8 +61,8 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { - for (var i = 0; i < bloc.state.balls; i++) { - bloc.add(const BallLost()); + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const BallLost(balls: 0)); } bloc.add(const Scored(points: 2)); }, @@ -66,19 +70,22 @@ void main() { const GameState( score: 0, multiplier: 1, - balls: 2, + balls: 1, + rounds: 2, bonusHistory: [], ), const GameState( score: 0, multiplier: 1, balls: 1, + rounds: 1, bonusHistory: [], ), const GameState( score: 0, multiplier: 1, balls: 0, + rounds: 0, bonusHistory: [], ), ], @@ -98,12 +105,14 @@ void main() { score: 0, multiplier: 2, balls: 3, + rounds: 3, bonusHistory: [], ), const GameState( score: 0, multiplier: 3, balls: 3, + rounds: 3, bonusHistory: [], ), ], @@ -114,8 +123,8 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { - for (var i = 0; i < bloc.state.balls; i++) { - bloc.add(const BallLost()); + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const BallLost(balls: 0)); } bloc.add(const MultiplierIncreased()); }, @@ -124,18 +133,21 @@ void main() { score: 0, multiplier: 1, balls: 2, + rounds: 2, bonusHistory: [], ), const GameState( score: 0, multiplier: 1, balls: 1, + rounds: 1, bonusHistory: [], ), const GameState( score: 0, multiplier: 1, balls: 0, + rounds: 0, bonusHistory: [], ), ], @@ -150,6 +162,7 @@ void main() { score: 5, multiplier: 3, balls: 2, + rounds: 2, bonusHistory: [], ), act: (bloc) { @@ -160,6 +173,7 @@ void main() { score: 15, multiplier: 3, balls: 2, + rounds: 2, bonusHistory: [], ), ], @@ -174,6 +188,7 @@ void main() { score: 0, multiplier: 3, balls: 2, + rounds: 2, bonusHistory: [], ), act: (bloc) { @@ -184,6 +199,7 @@ void main() { score: 0, multiplier: 1, balls: 2, + rounds: 2, bonusHistory: [], ), ], @@ -203,12 +219,14 @@ void main() { score: 0, multiplier: 1, balls: 3, + rounds: 2, bonusHistory: [GameBonus.googleWord], ), GameState( score: 0, multiplier: 1, balls: 3, + rounds: 2, bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], ), ], @@ -226,6 +244,7 @@ void main() { score: 0, multiplier: 1, balls: 3, + rounds: 2, bonusHistory: [GameBonus.sparkyTurboCharge], ), ], diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index ce0712ff..067b95a0 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -7,13 +7,17 @@ void main() { group('GameEvent', () { group('BallLost', () { test('can be instantiated', () { - expect(const BallLost(), isNotNull); + expect(const BallLost(balls: 1), isNotNull); }); test('supports value equality', () { expect( - BallLost(), - equals(const BallLost()), + BallLost(balls: 1), + equals(const BallLost(balls: 1)), + ); + expect( + BallLost(balls: 2), + isNot(equals(const BallLost(balls: 1))), ); }); }); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 7ba58f2f..89e4f418 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -11,6 +11,7 @@ void main() { score: 0, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: const [], ), equals( @@ -18,6 +19,7 @@ void main() { score: 0, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: [], ), ), @@ -31,6 +33,7 @@ void main() { score: 0, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: [], ), isNotNull, @@ -44,9 +47,10 @@ void main() { () { expect( () => GameState( - balls: -1, score: 0, multiplier: 1, + balls: -1, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -60,9 +64,10 @@ void main() { () { expect( () => GameState( - balls: 0, score: -1, multiplier: 1, + balls: 0, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -76,9 +81,10 @@ void main() { () { expect( () => GameState( - balls: 0, score: 1, multiplier: 0, + balls: 0, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -86,26 +92,73 @@ void main() { }, ); - group('isGameOver', () { + test( + 'throws AssertionError ' + 'when rounds is negative', + () { + expect( + () => GameState( + score: 1, + multiplier: 1, + balls: 0, + rounds: -1, + bonusHistory: const [], + ), + throwsAssertionError, + ); + }, + ); + + group('isRoundOver', () { test( 'is true ' 'when no balls are left', () { const gameState = GameState( - balls: 0, score: 0, multiplier: 1, + balls: 0, + rounds: 1, bonusHistory: [], ); - expect(gameState.isGameOver, isTrue); + expect(gameState.isRoundOver, isTrue); }); test( 'is false ' 'when one 1 ball left', () { const gameState = GameState( + score: 0, + multiplier: 1, balls: 1, + rounds: 0, + bonusHistory: [], + ); + expect(gameState.isRoundOver, isFalse); + }); + }); + + group('isGameOver', () { + test( + 'is true ' + 'when no rounds are left', () { + const gameState = GameState( + score: 0, + multiplier: 1, + balls: 0, + rounds: 0, + bonusHistory: [], + ); + expect(gameState.isGameOver, isTrue); + }); + + test( + 'is false ' + 'when one 1 round left', () { + const gameState = GameState( score: 0, multiplier: 1, + balls: 0, + rounds: 1, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -118,9 +171,10 @@ void main() { 'when scored is decreased', () { const gameState = GameState( - balls: 0, score: 2, multiplier: 1, + balls: 0, + rounds: 3, bonusHistory: [], ); expect( @@ -135,9 +189,10 @@ void main() { 'when no argument specified', () { const gameState = GameState( - balls: 0, score: 2, multiplier: 1, + balls: 0, + rounds: 3, bonusHistory: [], ); expect( @@ -155,12 +210,14 @@ void main() { score: 2, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, multiplier: gameState.multiplier + 1, balls: gameState.balls + 1, + rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], ); expect(gameState, isNot(equals(otherGameState))); @@ -170,6 +227,7 @@ void main() { score: otherGameState.score, multiplier: otherGameState.multiplier, balls: otherGameState.balls, + rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index e615d508..30018329 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -62,7 +62,7 @@ void main() { controller.lost(); }, verify: (game, tester) async { - verify(() => gameBloc.add(const BallLost())).called(1); + verify(() => gameBloc.add(const BallLost(balls: 0))).called(1); }, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 753f2175..0c645b22 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -25,6 +25,7 @@ void main() { score: 0, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index ede76843..5a4adc12 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -22,6 +22,7 @@ void main() { score: 0, multiplier: 1, balls: 0, + rounds: 3, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_flow_controller_test.dart index 13069a86..63fd3fac 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -17,6 +17,7 @@ void main() { score: 10, multiplier: 1, balls: 0, + rounds: 0, bonusHistory: const [], ); @@ -70,6 +71,7 @@ void main() { score: 10, multiplier: 1, balls: 0, + rounds: 0, bonusHistory: const [], ), ); diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 88762b98..1038d9b9 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -13,6 +13,7 @@ void main() { score: 10, multiplier: 1, balls: 2, + rounds: 3, bonusHistory: [], ); @@ -45,12 +46,12 @@ void main() { ); testWidgets( - 'renders the current ball number', + 'renders the current round number', (tester) async { await _pumpHud(tester); expect( find.byType(CircleAvatar), - findsNWidgets(initialState.balls), + findsNWidgets(initialState.rounds), ); }, ); @@ -65,14 +66,14 @@ void main() { expect(find.text('20'), findsOneWidget); }); - testWidgets('updates the ball number', (tester) async { + testWidgets('updates the rounds number', (tester) async { await _pumpHud(tester); expect( find.byType(CircleAvatar), - findsNWidgets(initialState.balls), + findsNWidgets(initialState.rounds), ); - _mockState(initialState.copyWith(balls: 1)); + _mockState(initialState.copyWith(rounds: 1)); await tester.pump(); expect(