diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 5c722946..d08ba04b 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -55,9 +55,6 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; - /// Determines when the player has only one ball left. - bool get isLastBall => balls == 1; - /// Shortcut method to check if the given [i] /// is activated. bool isLetterActivated(int i) => activatedBonusLetters.contains(i); diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index 50687f24..11a1660d 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,4 +1,5 @@ import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/flame/flame.dart'; @@ -68,22 +69,34 @@ class BonusBallController extends BallController { } } +/// {@template launched_ball_controller} /// {@macro ball_controller} +/// +/// A [LaunchedBallController] changes the [GameState.balls] count. +/// {@endtemplate} class LaunchedBallController extends BallController - with HasGameRef { - /// {@macro ball_controller} + with HasGameRef, BlocComponent { + /// {@macro launched_ball_controller} LaunchedBallController(Ball ball) : super(ball); + @override + bool listenWhen(GameState? previousState, GameState newState) { + return (previousState?.balls ?? 0) < newState.balls; + } + + @override + void onNewState(GameState state) { + super.onNewState(state); + component.shouldRemove = true; + if (state.balls > 0) gameRef.spawnBall(); + } + /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// any are left. /// /// {@macro ball_controller_lost} @override void lost() { - final bloc = gameRef.read()..add(const BallLost()); - - // TODO(alestiago): Consider the use of onNewState instead. - final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; - if (shouldBallRespwan) gameRef.spawnBall(); + gameRef.read().add(const BallLost()); } } diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index a4d2f45d..737500b4 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -39,99 +39,131 @@ void main() { }); group('LaunchedBallController', () { - group('lost', () { - late GameBloc gameBloc; - late Ball ball; - - setUp(() { - gameBloc = MockGameBloc(); - ball = Ball(baseColor: const Color(0xFF00FFFF)); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); + late Ball ball; + late GameBloc gameBloc; - final tester = flameBlocTester( - game: PinballGameTest.create, - gameBloc: () => gameBloc, + setUp(() { + ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), ); + }); - tester.testGameWidget( - 'removes ball', + final tester = flameBlocTester( + game: PinballGameTest.create, + gameBloc: () => gameBloc, + ); + + flameTester.testGameWidget( + 'lost adds BallLost to GameBloc', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await ball.add(controller); + await game.add(ball); + await game.ready(); + + controller.lost(); + }, + verify: (game, tester) async { + verify(() => gameBloc.add(const BallLost())).called(1); + }, + ); + + group('listenWhen', () { + flameTester.testGameWidget( + 'listens when a ball has been lost', verify: (game, tester) async { - await game.add(ball); final controller = LaunchedBallController(ball); await ball.add(controller); + await game.add(ball); await game.ready(); - controller.lost(); - await game.ready(); + final previousState = MockGameState(); + final newState = MockGameState(); + when(() => previousState.balls).thenReturn(3); + when(() => newState.balls).thenReturn(2); - expect(game.contains(ball), isFalse); + expect(controller.listenWhen(previousState, newState), isTrue); }, ); - tester.testGameWidget( - 'adds BallLost to GameBloc', + flameTester.testGameWidget( + 'does not listen when a ball has not been lost', verify: (game, tester) async { final controller = LaunchedBallController(ball); await ball.add(controller); await game.add(ball); await game.ready(); - controller.lost(); + final previousState = MockGameState(); + final newState = MockGameState(); + when(() => previousState.balls).thenReturn(3); + when(() => newState.balls).thenReturn(3); - verify(() => gameBloc.add(const BallLost())).called(1); + expect(controller.listenWhen(previousState, newState), isTrue); }, ); + }); + + group('onNewState', () { + setUp(() { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); tester.testGameWidget( - 'adds a new ball if the game is not over', + 'removes ball', + setUp: (game, _) => game.ready(), verify: (game, tester) async { final controller = LaunchedBallController(ball); - await ball.add(controller); await game.add(ball); await game.ready(); - final previousBalls = game.descendants().whereType().length; - controller.lost(); - await game.ready(); - final currentBalls = game.descendants().whereType().length; + final state = MockGameState(); + when(() => state.balls).thenReturn(any()); + controller.onNewState(MockGameState()); - expect(previousBalls, equals(currentBalls)); + expect(game.contains(ball), isFalse); }, ); tester.testGameWidget( - 'no ball is added on game over', - verify: (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState( - score: 10, - balls: 1, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ), - ); + 'spawns a new ball when the ball is not the last one', + setUp: (game, tester) async { final controller = LaunchedBallController(ball); - await ball.add(controller); await game.add(ball); await game.ready(); - final previousBalls = game.descendants().whereType().toList(); - controller.lost(); + final state = MockGameState(); + when(() => state.balls).thenReturn(2); + + controller.onNewState(state); + }, + verify: (game, tester) async { + expect(game.contains(ball), isFalse); + }, + ); + + tester.testGameWidget( + 'does not spawn a new ball is the last one', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await game.add(ball); await game.ready(); - final currentBalls = game.descendants().whereType().length; - expect( - currentBalls, - equals((previousBalls..remove(ball)).length), - ); + final state = MockGameState(); + when(() => state.balls).thenReturn(1); + + controller.onNewState(state); + }, + verify: (game, tester) async { + expect(game.contains(ball), isFalse); }, ); });