From 2ad0196e446b7bb6e170b7d8686f8e03dce0eee9 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 5 May 2022 15:56:38 +0100 Subject: [PATCH] refactor: include `GameStatus` on `GameBloc` (#345) --- lib/game/bloc/game_bloc.dart | 15 ++- lib/game/bloc/game_event.dart | 14 +++ lib/game/bloc/game_state.dart | 21 +++- lib/game/components/components.dart | 2 +- lib/game/components/controlled_flipper.dart | 2 +- lib/game/components/controlled_plunger.dart | 2 +- .../components/game_bloc_status_listener.dart | 33 +++++ lib/game/components/game_flow_controller.dart | 46 ------- lib/game/pinball_game.dart | 50 ++++---- lib/game/view/pinball_game_page.dart | 1 - lib/game/view/widgets/game_hud.dart | 3 +- lib/game/view/widgets/score_view.dart | 3 +- .../widgets/start_game_listener.dart | 5 +- .../game/behaviors/scoring_behavior_test.dart | 1 + test/game/bloc/game_bloc_test.dart | 119 +++++++++++++++--- test/game/bloc/game_event_test.dart | 26 ++++ test/game/bloc/game_state_test.dart | 40 ++---- .../components/controlled_flipper_test.dart | 75 ++++++++--- .../components/controlled_plunger_test.dart | 53 +++++--- ...rt => game_bloc_status_listener_test.dart} | 37 +++--- .../behaviors/multiballs_behavior_test.dart | 1 + .../behaviors/multipliers_behavior_test.dart | 2 + test/game/pinball_game_test.dart | 6 +- test/game/view/widgets/game_hud_test.dart | 1 + .../widgets/round_count_display_test.dart | 1 + test/game/view/widgets/score_view_test.dart | 5 +- .../start_game/bloc/start_game_bloc_test.dart | 18 --- .../widgets/start_game_listener_test.dart | 67 +++++----- 28 files changed, 411 insertions(+), 238 deletions(-) create mode 100644 lib/game/components/game_bloc_status_listener.dart delete mode 100644 lib/game/components/game_flow_controller.dart rename test/game/components/{game_flow_controller_test.dart => game_bloc_status_listener_test.dart} (80%) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 43d6005b..b22baa14 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -14,6 +14,16 @@ class GameBloc extends Bloc { on(_onIncreasedMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); + on(_onGameOver); + on(_onGameStarted); + } + + void _onGameStarted(GameStarted _, Emitter emit) { + emit(state.copyWith(status: GameStatus.playing)); + } + + void _onGameOver(GameOver _, Emitter emit) { + emit(state.copyWith(status: GameStatus.gameOver)); } void _onRoundLost(RoundLost event, Emitter emit) { @@ -26,12 +36,13 @@ class GameBloc extends Bloc { roundScore: 0, multiplier: 1, rounds: roundsLeft, + status: roundsLeft == 0 ? GameStatus.gameOver : state.status, ), ); } void _onScored(Scored event, Emitter emit) { - if (!state.isGameOver) { + if (state.status.isPlaying) { emit( state.copyWith(roundScore: state.roundScore + event.points), ); @@ -39,7 +50,7 @@ class GameBloc extends Bloc { } void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { - if (!state.isGameOver) { + if (state.status.isPlaying) { emit( state.copyWith( multiplier: math.min(state.multiplier + 1, 6), diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index c81ce526..6dba8056 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -59,3 +59,17 @@ class MultiplierIncreased extends GameEvent { @override List get props => []; } + +class GameStarted extends GameEvent { + const GameStarted(); + + @override + List get props => []; +} + +class GameOver extends GameEvent { + const GameOver(); + + @override + List get props => []; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 2ccb4405..a9e86720 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -20,6 +20,17 @@ enum GameBonus { androidSpaceship, } +enum GameStatus { + waiting, + playing, + gameOver, +} + +extension GameStatusX on GameStatus { + bool get isPlaying => this == GameStatus.playing; + bool get isGameOver => this == GameStatus.gameOver; +} + /// {@template game_state} /// Represents the state of the pinball game. /// {@endtemplate} @@ -31,13 +42,15 @@ class GameState extends Equatable { required this.multiplier, required this.rounds, required this.bonusHistory, + required this.status, }) : assert(totalScore >= 0, "TotalScore can't be negative"), assert(roundScore >= 0, "Round score can't be negative"), assert(multiplier > 0, 'Multiplier must be greater than zero'), assert(rounds >= 0, "Number of rounds can't be negative"); const GameState.initial() - : totalScore = 0, + : status = GameStatus.waiting, + totalScore = 0, roundScore = 0, multiplier = 1, rounds = 3, @@ -65,8 +78,7 @@ class GameState extends Equatable { /// PinballGame. final List bonusHistory; - /// Determines when the game is over. - bool get isGameOver => rounds == 0; + final GameStatus status; /// The score displayed at the game. int get displayScore => roundScore + totalScore; @@ -78,6 +90,7 @@ class GameState extends Equatable { int? balls, int? rounds, List? bonusHistory, + GameStatus? status, }) { assert( totalScore == null || totalScore >= this.totalScore, @@ -90,6 +103,7 @@ class GameState extends Equatable { multiplier: multiplier ?? this.multiplier, rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, + status: status ?? this.status, ); } @@ -100,5 +114,6 @@ class GameState extends Equatable { multiplier, rounds, bonusHistory, + status, ]; } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 2b132656..c8a71cee 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -8,7 +8,7 @@ export 'controlled_plunger.dart'; export 'dino_desert/dino_desert.dart'; export 'drain.dart'; export 'flutter_forest/flutter_forest.dart'; -export 'game_flow_controller.dart'; +export 'game_bloc_status_listener.dart'; export 'google_word/google_word.dart'; export 'launcher.dart'; export 'multiballs/multiballs.dart'; diff --git a/lib/game/components/controlled_flipper.dart b/lib/game/components/controlled_flipper.dart index 3c82e719..9d5a8164 100644 --- a/lib/game/components/controlled_flipper.dart +++ b/lib/game/components/controlled_flipper.dart @@ -37,7 +37,7 @@ class FlipperController extends ComponentController RawKeyEvent event, Set keysPressed, ) { - if (state?.isGameOver ?? false) return true; + if (state?.status.isGameOver ?? false) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/controlled_plunger.dart b/lib/game/components/controlled_plunger.dart index d6c622f7..999fae5e 100644 --- a/lib/game/components/controlled_plunger.dart +++ b/lib/game/components/controlled_plunger.dart @@ -38,7 +38,7 @@ class PlungerController extends ComponentController RawKeyEvent event, Set keysPressed, ) { - if (state?.isGameOver ?? false) return true; + if (state?.status.isGameOver ?? false) return true; if (!_keys.contains(event.logicalKey)) return true; if (event is RawKeyDownEvent) { diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart new file mode 100644 index 00000000..0012f62b --- /dev/null +++ b/lib/game/components/game_bloc_status_listener.dart @@ -0,0 +1,33 @@ +import 'package:flame/components.dart'; +import 'package:flame_bloc/flame_bloc.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_audio/pinball_audio.dart'; + +/// Listens to the [GameBloc] and updates the game accordingly. +class GameBlocStatusListener extends Component + with BlocComponent, HasGameRef { + @override + bool listenWhen(GameState? previousState, GameState newState) { + return previousState?.status != newState.status; + } + + @override + void onNewState(GameState state) { + switch (state.status) { + case GameStatus.waiting: + break; + case GameStatus.playing: + gameRef.player.play(PinballAudio.backgroundMusic); + gameRef.firstChild()?.focusOnGame(); + gameRef.overlays.remove(PinballGame.playButtonOverlay); + break; + case GameStatus.gameOver: + gameRef.descendants().whereType().first.initialsInput( + score: state.displayScore, + characterIconPath: gameRef.characterTheme.leaderboardIcon.keyName, + ); + gameRef.firstChild()!.focusOnGameOverBackbox(); + break; + } + } +} diff --git a/lib/game/components/game_flow_controller.dart b/lib/game/components/game_flow_controller.dart deleted file mode 100644 index e285eb3a..00000000 --- a/lib/game/components/game_flow_controller.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flame/components.dart'; -import 'package:flame_bloc/flame_bloc.dart'; -import 'package:pinball/game/game.dart'; -import 'package:pinball_audio/pinball_audio.dart'; -import 'package:pinball_flame/pinball_flame.dart'; - -/// {@template game_flow_controller} -/// A [Component] that controls the game over and game restart logic -/// {@endtemplate} -class GameFlowController extends ComponentController - with BlocComponent { - /// {@macro game_flow_controller} - GameFlowController(PinballGame component) : super(component); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - return previousState?.isGameOver != newState.isGameOver; - } - - @override - void onNewState(GameState state) { - if (state.isGameOver) { - _initialsInput(); - } else { - start(); - } - } - - /// Puts the game in the initials input state. - void _initialsInput() { - // TODO(erickzanardo): implement score submission and "navigate" to the - // next page - component.descendants().whereType().first.initialsInput( - score: state?.displayScore ?? 0, - characterIconPath: component.characterTheme.leaderboardIcon.keyName, - ); - component.firstChild()!.focusOnGameOverBackbox(); - } - - /// Puts the game in the playing state. - void start() { - component.player.play(PinballAudio.backgroundMusic); - component.firstChild()?.focusOnGame(); - component.overlays.remove(PinballGame.playButtonOverlay); - } -} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index a6f496af..65a19f29 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -42,11 +42,8 @@ class PinballGame extends PinballForge2DGame final AppLocalizations l10n; - late final GameFlowController gameFlowController; - @override Future onLoad() async { - await add(gameFlowController = GameFlowController(this)); await add(CameraController(this)); final machine = [ @@ -71,26 +68,29 @@ class PinballGame extends PinballForge2DGame SparkyScorch(), ]; - await add( - CanvasComponent( - onSpritePainted: (paint) { - if (paint.filterQuality != FilterQuality.medium) { - paint.filterQuality = FilterQuality.medium; - } - }, - children: [ - ZCanvasComponent( - children: [ - ...machine, - ...decals, - ...characterAreas, - Drain(), - BottomGroup(), - Launcher(), - ], - ), - ], - ), + await addAll( + [ + GameBlocStatusListener(), + CanvasComponent( + onSpritePainted: (paint) { + if (paint.filterQuality != FilterQuality.medium) { + paint.filterQuality = FilterQuality.medium; + } + }, + children: [ + ZCanvasComponent( + children: [ + ...machine, + ...decals, + ...characterAreas, + Drain(), + BottomGroup(), + Launcher(), + ], + ), + ], + ), + ], ); await super.onLoad(); @@ -151,9 +151,7 @@ class _GameBallsController extends ComponentController @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; - final notGameOver = !newState.isGameOver; - - return noBallsLeft && notGameOver; + return noBallsLeft && newState.status.isPlaying; } @override diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 033d5b04..b56e00f4 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -113,7 +113,6 @@ class PinballGameLoadedView extends StatelessWidget { final clampedMargin = leftMargin > 0 ? leftMargin : 0.0; return StartGameListener( - game: game, child: Stack( children: [ Positioned.fill( diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 23d0990a..5f651a60 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -26,7 +26,8 @@ class _GameHudState extends State { @override Widget build(BuildContext context) { - final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + final isGameOver = + context.select((GameBloc bloc) => bloc.state.status.isGameOver); final height = _calculateHeight(context); diff --git a/lib/game/view/widgets/score_view.dart b/lib/game/view/widgets/score_view.dart index c33d13ab..66233598 100644 --- a/lib/game/view/widgets/score_view.dart +++ b/lib/game/view/widgets/score_view.dart @@ -13,7 +13,8 @@ class ScoreView extends StatelessWidget { @override Widget build(BuildContext context) { - final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver); + final isGameOver = + context.select((GameBloc bloc) => bloc.state.status.isGameOver); return Padding( padding: const EdgeInsets.symmetric( diff --git a/lib/start_game/widgets/start_game_listener.dart b/lib/start_game/widgets/start_game_listener.dart index 6184bd91..692116f3 100644 --- a/lib/start_game/widgets/start_game_listener.dart +++ b/lib/start_game/widgets/start_game_listener.dart @@ -17,13 +17,10 @@ class StartGameListener extends StatelessWidget { const StartGameListener({ Key? key, required Widget child, - required PinballGame game, }) : _child = child, - _game = game, super(key: key); final Widget _child; - final PinballGame _game; @override Widget build(BuildContext context) { @@ -34,7 +31,7 @@ class StartGameListener extends StatelessWidget { break; case StartGameStatus.selectCharacter: _onSelectCharacter(context); - _game.gameFlowController.start(); + context.read().add(const GameStarted()); break; case StartGameStatus.howToPlay: _onHowToPlay(context); diff --git a/test/game/behaviors/scoring_behavior_test.dart b/test/game/behaviors/scoring_behavior_test.dart index 47903d8a..5673e165 100644 --- a/test/game/behaviors/scoring_behavior_test.dart +++ b/test/game/behaviors/scoring_behavior_test.dart @@ -57,6 +57,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.playing, ); whenListen(bloc, Stream.value(state), initialState: state); return bloc; diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 5927291b..3e5abb74 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -10,6 +10,34 @@ void main() { expect(gameBloc.state.rounds, equals(3)); }); + blocTest( + 'GameStarted starts the game', + build: GameBloc.new, + act: (bloc) => bloc.add(const GameStarted()), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + GameStatus.playing, + ), + ], + ); + + blocTest( + 'GameOver finishes the game', + build: GameBloc.new, + act: (bloc) => bloc.add(const GameOver()), + expect: () => [ + isA() + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), + ], + ); + group('RoundLost', () { blocTest( 'decreases number of rounds ' @@ -23,6 +51,24 @@ void main() { ], ); + blocTest( + 'sets game over when there are no more rounds', + build: GameBloc.new, + act: (bloc) { + bloc + ..add(const RoundLost()) + ..add(const RoundLost()) + ..add(const RoundLost()); + }, + expect: () => [ + isA()..having((state) => state.rounds, 'rounds', 2), + isA()..having((state) => state.rounds, 'rounds', 1), + isA() + ..having((state) => state.rounds, 'rounds', 0) + ..having((state) => state.status, 'status', GameStatus.gameOver), + ], + ); + blocTest( 'apply multiplier to roundScore and add it to totalScore ' 'when round is lost', @@ -33,6 +79,7 @@ void main() { multiplier: 3, rounds: 2, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) { bloc.add(const RoundLost()); @@ -45,8 +92,7 @@ void main() { ); blocTest( - 'resets multiplier ' - 'when round is lost', + 'resets multiplier when round is lost', build: GameBloc.new, seed: () => const GameState( totalScore: 10, @@ -54,6 +100,7 @@ void main() { multiplier: 3, rounds: 2, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) { bloc.add(const RoundLost()); @@ -66,25 +113,26 @@ void main() { group('Scored', () { blocTest( - 'increases score ' - 'when game is not over', + 'increases score when playing', build: GameBloc.new, act: (bloc) => bloc + ..add(const GameStarted()) ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.roundScore, 'roundScore', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.roundScore, 'roundScore', 5) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having((state) => state.status, 'status', GameStatus.playing), ], ); blocTest( - "doesn't increase score " - 'when game is over', + "doesn't increase score when game is over", build: GameBloc.new, act: (bloc) { for (var i = 0; i < bloc.state.rounds; i++) { @@ -96,15 +144,27 @@ void main() { isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.roundScore, 'roundScore', 0) ..having((state) => state.rounds, 'rounds', 0) - ..having((state) => state.isGameOver, 'isGameOver', true), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); }); @@ -115,15 +175,26 @@ void main() { 'when multiplier is below 6 and game is not over', build: GameBloc.new, act: (bloc) => bloc + ..add(const GameStarted()) ..add(const MultiplierIncreased()) ..add(const MultiplierIncreased()), expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.multiplier, 'multiplier', 2) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 3) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); @@ -137,6 +208,7 @@ void main() { multiplier: 6, rounds: 3, bonusHistory: [], + status: GameStatus.playing, ), act: (bloc) => bloc..add(const MultiplierIncreased()), expect: () => const [], @@ -147,21 +219,36 @@ void main() { 'when game is over', build: GameBloc.new, act: (bloc) { + bloc.add(const GameStarted()); for (var i = 0; i < bloc.state.rounds; i++) { bloc.add(const RoundLost()); } bloc.add(const MultiplierIncreased()); }, expect: () => [ + isA() + ..having((state) => state.status, 'status', GameStatus.playing), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', false), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), isA() ..having((state) => state.multiplier, 'multiplier', 1) - ..having((state) => state.isGameOver, 'isGameOver', true), + ..having( + (state) => state.status, + 'status', + GameStatus.gameOver, + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index 6a39bd67..c4de5792 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -72,6 +72,32 @@ void main() { }); }); + group('GameStarted', () { + test('can be instantiated', () { + expect(const GameStarted(), isNotNull); + }); + + test('supports value equality', () { + expect( + GameStarted(), + equals(const GameStarted()), + ); + }); + }); + + group('GameOver', () { + test('can be instantiated', () { + expect(const GameOver(), isNotNull); + }); + + test('supports value equality', () { + expect( + GameOver(), + equals(const GameOver()), + ); + }); + }); + group('SparkyTurboChargeActivated', () { test('can be instantiated', () { expect(const SparkyTurboChargeActivated(), isNotNull); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index b59115a3..670707a0 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -13,6 +13,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), equals( const GameState( @@ -21,6 +22,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ), ), ); @@ -35,6 +37,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ), isNotNull, ); @@ -52,6 +55,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -69,6 +73,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -86,6 +91,7 @@ void main() { multiplier: 0, rounds: 3, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); @@ -103,40 +109,13 @@ void main() { multiplier: 1, rounds: -1, bonusHistory: const [], + status: GameStatus.waiting, ), throwsAssertionError, ); }, ); - group('isGameOver', () { - test( - 'is true ' - 'when no rounds are left', () { - const gameState = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - expect(gameState.isGameOver, isTrue); - }); - - test( - 'is false ' - 'when one 1 round left', () { - const gameState = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 1, - bonusHistory: [], - ); - expect(gameState.isGameOver, isFalse); - }); - }); - group('copyWith', () { test( 'throws AssertionError ' @@ -148,6 +127,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); expect( () => gameState.copyWith(totalScore: gameState.totalScore - 1), @@ -166,6 +146,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); expect( gameState.copyWith(), @@ -184,6 +165,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.waiting, ); final otherGameState = GameState( totalScore: gameState.totalScore + 1, @@ -191,6 +173,7 @@ void main() { multiplier: gameState.multiplier + 1, rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], + status: GameStatus.playing, ); expect(gameState, isNot(equals(otherGameState))); @@ -201,6 +184,7 @@ void main() { multiplier: otherGameState.multiplier, rounds: otherGameState.rounds, bonusHistory: otherGameState.bonusHistory, + status: otherGameState.status, ), equals(otherGameState), ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index f9561b60..af262dbf 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -22,24 +22,19 @@ void main() { () => EmptyPinballTestGame(assets: assets), ); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - final bloc = _MockGameBloc(); - const state = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - assets: assets, - ); - group('FlipperController', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + group('onKeyEvent', () { final leftKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowLeft, @@ -65,6 +60,12 @@ void main() { 'moves upwards ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); @@ -79,6 +80,14 @@ void main() { flameBlocTester.testGameWidget( 'does nothing when is game over', setUp: (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + await game.ensureAdd(flipper); controller.onKeyEvent(event, {}); }, @@ -94,6 +103,12 @@ void main() { 'moves downwards ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); @@ -109,6 +124,12 @@ void main() { 'does nothing ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); @@ -135,6 +156,12 @@ void main() { 'moves upwards ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); @@ -150,6 +177,12 @@ void main() { 'moves downwards ' 'when ${event.logicalKey.keyLabel} is released', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ready(); await game.add(flipper); controller.onKeyEvent(event, {}); @@ -164,6 +197,14 @@ void main() { flameBlocTester.testGameWidget( 'does nothing when is game over', setUp: (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + await game.ensureAdd(flipper); controller.onKeyEvent(event, {}); }, diff --git a/test/game/components/controlled_plunger_test.dart b/test/game/components/controlled_plunger_test.dart index 4d9c9c74..f91b0c37 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -17,23 +17,18 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(EmptyPinballTestGame.new); - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () { - final bloc = _MockGameBloc(); - const state = GameState( - totalScore: 0, - roundScore: 0, - multiplier: 1, - rounds: 0, - bonusHistory: [], - ); - whenListen(bloc, Stream.value(state), initialState: state); - return bloc; - }, - ); - group('PlungerController', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + ); + group('onKeyEvent', () { final downKeys = UnmodifiableListView([ LogicalKeyboardKey.arrowDown, @@ -55,6 +50,12 @@ void main() { 'moves down ' 'when ${event.logicalKey.keyLabel} is pressed', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ensureAdd(plunger); controller.onKeyEvent(event, {}); @@ -70,6 +71,12 @@ void main() { 'when ${event.logicalKey.keyLabel} is released ' 'and plunger is below its starting position', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ensureAdd(plunger); plunger.body.setTransform(Vector2(0, 1), 0); controller.onKeyEvent(event, {}); @@ -85,6 +92,12 @@ void main() { 'does not move when ${event.logicalKey.keyLabel} is released ' 'and plunger is in its starting position', (game) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + await game.ensureAdd(plunger); controller.onKeyEvent(event, {}); @@ -98,6 +111,14 @@ void main() { flameBlocTester.testGameWidget( 'does nothing when is game over', setUp: (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial().copyWith( + status: GameStatus.gameOver, + ), + ); + await game.ensureAdd(plunger); controller.onKeyEvent(event, {}); }, diff --git a/test/game/components/game_flow_controller_test.dart b/test/game/components/game_bloc_status_listener_test.dart similarity index 80% rename from test/game/components/game_flow_controller_test.dart rename to test/game/components/game_bloc_status_listener_test.dart index 25e2ed50..39c81115 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -19,7 +19,7 @@ class _MockActiveOverlaysNotifier extends Mock class _MockPinballPlayer extends Mock implements PinballPlayer {} void main() { - group('GameFlowController', () { + group('GameBlocStatusListener', () { group('listenWhen', () { test('is true when the game over state has changed', () { final state = GameState( @@ -28,11 +28,12 @@ void main() { multiplier: 1, rounds: 0, bonusHistory: const [], + status: GameStatus.playing, ); final previous = GameState.initial(); expect( - GameFlowController(_MockPinballGame()).listenWhen(previous, state), + GameBlocStatusListener().listenWhen(previous, state), isTrue, ); }); @@ -42,7 +43,7 @@ void main() { late PinballGame game; late Backbox backbox; late CameraController cameraController; - late GameFlowController gameFlowController; + late GameBlocStatusListener gameFlowController; late PinballPlayer pinballPlayer; late ActiveOverlaysNotifier overlays; @@ -50,10 +51,12 @@ void main() { game = _MockPinballGame(); backbox = _MockBackbox(); cameraController = _MockCameraController(); - gameFlowController = GameFlowController(game); + gameFlowController = GameBlocStatusListener(); overlays = _MockActiveOverlaysNotifier(); pinballPlayer = _MockPinballPlayer(); + gameFlowController.mockGameRef(game); + when( () => backbox.initialsInput( score: any(named: 'score'), @@ -78,19 +81,19 @@ void main() { 'changes the backbox display and camera correctly ' 'when the game is over', () { - gameFlowController.onNewState( - GameState( - totalScore: 0, - roundScore: 10, - multiplier: 1, - rounds: 0, - bonusHistory: const [], - ), + final state = GameState( + totalScore: 0, + roundScore: 10, + multiplier: 1, + rounds: 0, + bonusHistory: const [], + status: GameStatus.gameOver, ); + gameFlowController.onNewState(state); verify( () => backbox.initialsInput( - score: 0, + score: state.displayScore, characterIconPath: any(named: 'characterIconPath'), onSubmit: any(named: 'onSubmit'), ), @@ -102,7 +105,9 @@ void main() { test( 'changes the backbox and camera correctly when it is not a game over', () { - gameFlowController.onNewState(GameState.initial()); + gameFlowController.onNewState( + GameState.initial().copyWith(status: GameStatus.playing), + ); verify(cameraController.focusOnGame).called(1); verify(() => overlays.remove(PinballGame.playButtonOverlay)) @@ -113,7 +118,9 @@ void main() { test( 'plays the background music on start', () { - gameFlowController.onNewState(GameState.initial()); + gameFlowController.onNewState( + GameState.initial().copyWith(status: GameStatus.playing), + ); verify(() => pinballPlayer.play(PinballAudio.backgroundMusic)) .called(1); diff --git a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart index b294d350..03c50041 100644 --- a/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart +++ b/test/game/components/multiballs/behaviors/multiballs_behavior_test.dart @@ -79,6 +79,7 @@ void main() { multiplier: 1, rounds: 0, bonusHistory: const [], + status: GameStatus.playing, ); expect( diff --git a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart index ca3c5921..ef39aad2 100644 --- a/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart +++ b/test/game/components/multipliers/behaviors/multipliers_behavior_test.dart @@ -60,6 +60,7 @@ void main() { roundScore: 10, multiplier: 2, rounds: 0, + status: GameStatus.playing, bonusHistory: const [], ); @@ -76,6 +77,7 @@ void main() { roundScore: 10, multiplier: 1, rounds: 0, + status: GameStatus.playing, bonusHistory: const [], ); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f1f3a4cb..cf70ad43 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -281,7 +281,7 @@ void main() { // TODO(ruimiguel): check why testGameWidget doesn't add any ball // to the game. Test needs to have no balls, so fortunately works. final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(false); + when(() => newState.status).thenReturn(GameStatus.playing); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); @@ -298,7 +298,7 @@ void main() { "doesn't listen when some balls are left", (game) async { final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(false); + when(() => newState.status).thenReturn(GameStatus.playing); await game.ready(); @@ -319,7 +319,7 @@ void main() { // TODO(ruimiguel): check why testGameWidget doesn't add any ball // to the game. Test needs to have no balls, so fortunately works. final newState = _MockGameState(); - when(() => newState.isGameOver).thenReturn(true); + when(() => newState.status).thenReturn(GameStatus.gameOver); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index 19c860da..f4054146 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -27,6 +27,7 @@ void main() { multiplier: 1, rounds: 1, bonusHistory: [], + status: GameStatus.playing, ); setUp(() async { diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index 049aba95..7078df77 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -18,6 +18,7 @@ void main() { multiplier: 1, rounds: 3, bonusHistory: [], + status: GameStatus.playing, ); setUp(() { diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 0e4acafc..18e94c09 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -23,6 +23,7 @@ void main() { multiplier: 1, rounds: 1, bonusHistory: [], + status: GameStatus.playing, ); setUp(() { @@ -54,9 +55,7 @@ void main() { final l10n = await AppLocalizations.delegate.load(const Locale('en')); stateController.add( - initialState.copyWith( - rounds: 0, - ), + initialState.copyWith(status: GameStatus.gameOver), ); await tester.pumpApp( diff --git a/test/start_game/bloc/start_game_bloc_test.dart b/test/start_game/bloc/start_game_bloc_test.dart index ac548a93..45460fe3 100644 --- a/test/start_game/bloc/start_game_bloc_test.dart +++ b/test/start_game/bloc/start_game_bloc_test.dart @@ -1,26 +1,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:pinball/game/game.dart'; import 'package:pinball/start_game/bloc/start_game_bloc.dart'; -class _MockPinballGame extends Mock implements PinballGame {} - -class _MockGameFlowController extends Mock implements GameFlowController {} - void main() { - late PinballGame pinballGame; - - setUp(() { - pinballGame = _MockPinballGame(); - - when( - () => pinballGame.gameFlowController, - ).thenReturn( - _MockGameFlowController(), - ); - }); - group('StartGameBloc', () { blocTest( 'on PlayTapped changes status to selectCharacter', diff --git a/test/start_game/widgets/start_game_listener_test.dart b/test/start_game/widgets/start_game_listener_test.dart index 566bb6f2..4e25796b 100644 --- a/test/start_game/widgets/start_game_listener_test.dart +++ b/test/start_game/widgets/start_game_listener_test.dart @@ -12,17 +12,14 @@ import '../../helpers/helpers.dart'; class _MockStartGameBloc extends Mock implements StartGameBloc {} -class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} - -class _MockPinballGame extends Mock implements PinballGame {} +class _MockGameBloc extends Mock implements GameBloc {} -class _MockGameFlowController extends Mock implements GameFlowController {} +class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {} class _MockPinballPlayer extends Mock implements PinballPlayer {} void main() { late StartGameBloc startGameBloc; - late PinballGame pinballGame; late PinballPlayer pinballPlayer; late CharacterThemeCubit characterThemeCubit; @@ -31,14 +28,25 @@ void main() { await mockFlameImages(); startGameBloc = _MockStartGameBloc(); - pinballGame = _MockPinballGame(); pinballPlayer = _MockPinballPlayer(); characterThemeCubit = _MockCharacterThemeCubit(); }); group('on selectCharacter status', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = _MockGameBloc(); + + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + testWidgets( - 'calls start on the game controller', + 'calls onGameStarted event', (tester) async { whenListen( startGameBloc, @@ -47,19 +55,16 @@ void main() { ), initialState: const StartGameState.initial(), ); - final gameController = _MockGameFlowController(); - when(() => pinballGame.gameFlowController) - .thenAnswer((_) => gameController); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), + gameBloc: gameBloc, startGameBloc: startGameBloc, ); - verify(gameController.start).called(1); + verify(() => gameBloc.add(const GameStarted())).called(1); }, ); @@ -78,15 +83,12 @@ void main() { Stream.value(const CharacterThemeState.initial()), initialState: const CharacterThemeState.initial(), ); - final gameController = _MockGameFlowController(); - when(() => pinballGame.gameFlowController) - .thenAnswer((_) => gameController); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), + gameBloc: gameBloc, startGameBloc: startGameBloc, characterThemeCubit: characterThemeCubit, ); @@ -113,9 +115,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -141,9 +142,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -173,9 +173,8 @@ void main() { ); await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -208,9 +207,8 @@ void main() { 'adds HowToPlayFinished event', (tester) async { await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, ); @@ -239,9 +237,8 @@ void main() { 'plays the I/O Pinball voice over audio', (tester) async { await tester.pumpApp( - StartGameListener( - game: pinballGame, - child: const SizedBox.shrink(), + const StartGameListener( + child: SizedBox.shrink(), ), startGameBloc: startGameBloc, pinballPlayer: pinballPlayer,