From 07ddb1f7b6846c99fc1a7cf1f7313833f98c756f Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 31 Mar 2022 14:44:47 +0100 Subject: [PATCH] refactor: made ball controller listen to bloc (#116) * refactor: included BonusBallController * feat: made LaunchedBallControler react to states * refactor: removed isLastBall * fix: solved tests * fix: solved BonusLetterActivated test * refactor: used ensureAdd --- lib/game/bloc/game_state.dart | 3 - lib/game/components/controlled_ball.dart | 49 +++-- test/game/bloc/game_state_test.dart | 32 --- test/game/components/bonus_word_test.dart | 7 +- .../game/components/controlled_ball_test.dart | 192 ++++++++++++------ test/helpers/test_game.dart | 3 +- 6 files changed, 164 insertions(+), 122 deletions(-) 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 463c158f..257d4f1d 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'; @@ -28,19 +29,19 @@ class ControlledBall extends Ball with Controls { ControlledBall.bonus({ required PinballTheme theme, }) : super(baseColor: theme.characterTheme.ballColor) { - controller = BallController(this); + controller = BonusBallController(this); } /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = BallController(this); + controller = BonusBallController(this); } } /// {@template ball_controller} /// Controller attached to a [Ball] that handles its game related logic. /// {@endtemplate} -class BallController extends ComponentController { +abstract class BallController extends ComponentController { /// {@macro ball_controller} BallController(Ball ball) : super(ball); @@ -50,30 +51,52 @@ class BallController extends ComponentController { /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// a [BottomWall]. /// {@endtemplate} - @mustCallSuper + void lost(); +} + +/// {@template bonus_ball_controller} +/// {@macro ball_controller} +/// +/// A [BonusBallController] doesn't change the [GameState.balls] count. +/// {@endtemplate} +class BonusBallController extends BallController { + /// {@macro bonus_ball_controller} + BonusBallController(Ball component) : super(component); + + @override void lost() { component.shouldRemove = true; } } +/// {@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 > 1) gameRef.spawnBall(); + } + /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// any are left. /// /// {@macro ball_controller_lost} @override void lost() { - super.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/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 9ca913ab..ed80d192 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -103,38 +103,6 @@ void main() { }); }); - group('isLastBall', () { - test( - 'is true ' - 'when there is only one ball left', - () { - const gameState = GameState( - balls: 1, - score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLastBall, isTrue); - }, - ); - - test( - 'is false ' - 'when there are more balls left', - () { - const gameState = GameState( - balls: 2, - score: 0, - activatedBonusLetters: [], - activatedDashNests: {}, - bonusHistory: [], - ); - expect(gameState.isLastBall, isFalse); - }, - ); - }); - group('isLetterActivated', () { test( 'is true when the letter is activated', diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index f02adceb..f48d60ee 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -196,7 +196,7 @@ void main() { group('bonus letter activation', () { late GameBloc gameBloc; - final tester = flameBlocTester( + final tester = flameBlocTester( // TODO(alestiago): Use TestGame once BonusLetter has controller. game: PinballGameTest.create, gameBloc: () => gameBloc, @@ -217,13 +217,8 @@ void main() { await game.ready(); final bonusLetter = game.descendants().whereType().first; - await game.add(bonusLetter); - await game.ready(); - bonusLetter.activate(); await game.ready(); - - await tester.pump(); }, verify: (game, tester) async { verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); diff --git a/test/game/components/controlled_ball_test.dart b/test/game/components/controlled_ball_test.dart index 9cf1dd7e..dcd075ca 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -15,20 +15,26 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); - group('BallController', () { + group('BonusBallController', () { late Ball ball; setUp(() { ball = Ball(baseColor: const Color(0xFF00FFFF)); }); + test('can be instantiated', () { + expect( + BonusBallController(ball), + isA(), + ); + }); + flameTester.test( 'lost removes ball', (game) async { await game.add(ball); - final controller = BallController(ball); - await ball.add(controller); - await game.ready(); + final controller = BonusBallController(ball); + await ball.ensureAdd(controller); controller.lost(); await game.ready(); @@ -39,13 +45,20 @@ void main() { }); group('LaunchedBallController', () { - group('lost', () { - late GameBloc gameBloc; + test('can be instantiated', () { + expect( + LaunchedBallController(MockBall()), + isA(), + ); + }); + + group('description', () { late Ball ball; + late GameBloc gameBloc; setUp(() { - gameBloc = MockGameBloc(); ball = Ball(baseColor: const Color(0xFF00FFFF)); + gameBloc = MockGameBloc(); whenListen( gameBloc, const Stream.empty(), @@ -59,81 +72,126 @@ void main() { ); tester.testGameWidget( - 'removes ball', - verify: (game, tester) async { - await game.add(ball); + 'lost adds BallLost to GameBloc', + setUp: (game, tester) async { final controller = LaunchedBallController(ball); await ball.add(controller); - await game.ready(); + await game.ensureAdd(ball); controller.lost(); - await game.ready(); - - expect(game.contains(ball), isFalse); }, - ); - - tester.testGameWidget( - 'adds BallLost to GameBloc', verify: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); - - controller.lost(); - verify(() => gameBloc.add(const BallLost())).called(1); }, ); - tester.testGameWidget( - 'adds a new ball if the game is not over', - verify: (game, tester) async { - final controller = LaunchedBallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); + group('listenWhen', () { + tester.testGameWidget( + 'listens when a ball has been lost', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + + await ball.add(controller); + await game.ensureAdd(ball); + }, + verify: (game, tester) async { + final controller = + game.descendants().whereType().first; + + final previousState = MockGameState(); + final newState = MockGameState(); + when(() => previousState.balls).thenReturn(3); + when(() => newState.balls).thenReturn(2); + + expect(controller.listenWhen(previousState, newState), isTrue); + }, + ); - final previousBalls = game.descendants().whereType().length; - controller.lost(); - await game.ready(); - final currentBalls = game.descendants().whereType().length; + tester.testGameWidget( + 'does not listen when a ball has not been lost', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + + await ball.add(controller); + await game.ensureAdd(ball); + }, + verify: (game, tester) async { + final controller = + game.descendants().whereType().first; + + final previousState = MockGameState(); + final newState = MockGameState(); + when(() => previousState.balls).thenReturn(3); + when(() => newState.balls).thenReturn(3); + + expect(controller.listenWhen(previousState, newState), isFalse); + }, + ); + }); - expect(previousBalls, equals(currentBalls)); - }, - ); + group('onNewState', () { + tester.testGameWidget( + 'removes ball', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final state = MockGameState(); + when(() => state.balls).thenReturn(1); + controller.onNewState(state); + await game.ready(); + }, + verify: (game, tester) async { + 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: [], - ), - ); - final controller = BallController(ball); - await ball.add(controller); - await game.add(ball); - await game.ready(); + tester.testGameWidget( + '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.ensureAdd(ball); - final previousBalls = game.descendants().whereType().toList(); - controller.lost(); - await game.ready(); - final currentBalls = game.descendants().whereType().length; + final state = MockGameState(); + when(() => state.balls).thenReturn(2); - expect( - currentBalls, - equals((previousBalls..remove(ball)).length), - ); - }, - ); + final previousBalls = game.descendants().whereType().toList(); + controller.onNewState(state); + await game.ready(); + + final currentBalls = game.descendants().whereType(); + + expect(currentBalls.contains(ball), isFalse); + expect(currentBalls.length, equals(previousBalls.length)); + }, + ); + + tester.testGameWidget( + 'does not spawn a new ball is the last one', + setUp: (game, tester) async { + final controller = LaunchedBallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final state = MockGameState(); + when(() => state.balls).thenReturn(1); + + final previousBalls = game.descendants().whereType().toList(); + controller.onNewState(state); + await game.ready(); + + final currentBalls = game.descendants().whereType(); + + expect(currentBalls.contains(ball), isFalse); + expect( + currentBalls.length, + equals((previousBalls..remove(ball)).length), + ); + }, + ); + }); }); }); } diff --git a/test/helpers/test_game.dart b/test/helpers/test_game.dart index a1219868..3c6ff42f 100644 --- a/test/helpers/test_game.dart +++ b/test/helpers/test_game.dart @@ -1,6 +1,7 @@ +import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -class TestGame extends Forge2DGame { +class TestGame extends Forge2DGame with FlameBloc { TestGame() { images.prefix = ''; }