From 0ac9cb3140ba52cd1cd5f80eb7dab67ab83cab90 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Mon, 9 May 2022 19:24:57 +0100 Subject: [PATCH] fix: replaying resets game state (#441) * feat: added replay functionality * feat: resetting google word bonus * Merge remote-tracking branch 'origin' into feat/replay-functionality * test: tested Replay overlay * docs: fixed typo * test: renamed test Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- .../behaviors/ball_spawning_behavior.dart | 3 +- lib/game/bloc/game_bloc.dart | 2 +- .../displays/game_over_info_display.dart | 2 +- .../components/game_bloc_status_listener.dart | 11 ++++++ .../behaviors/google_word_bonus_behavior.dart | 2 +- lib/game/pinball_game.dart | 7 ++-- lib/game/view/pinball_game_page.dart | 35 ++++++++++--------- .../view/widgets/replay_button_overlay.dart | 2 ++ .../google_word_animating_behavior.dart | 2 +- .../google_word/cubit/google_word_cubit.dart | 2 +- .../google_word_animating_behavior_test.dart | 4 +-- .../cubit/google_word_cubit_test.dart | 4 +-- .../game_bloc_status_listener_test.dart | 20 +++++++++++ .../google_word_bonus_behavior_test.dart | 4 +-- test/game/view/pinball_game_page_test.dart | 17 +++++++++ .../widgets/replay_button_overlay_test.dart | 25 ++++++++++++- 16 files changed, 111 insertions(+), 31 deletions(-) diff --git a/lib/game/behaviors/ball_spawning_behavior.dart b/lib/game/behaviors/ball_spawning_behavior.dart index 8995c16b..8fc56905 100644 --- a/lib/game/behaviors/ball_spawning_behavior.dart +++ b/lib/game/behaviors/ball_spawning_behavior.dart @@ -13,7 +13,8 @@ class BallSpawningBehavior extends Component bool listenWhen(GameState? previousState, GameState newState) { if (!newState.status.isPlaying) return false; - final startedGame = previousState?.status.isWaiting ?? true; + final startedGame = (previousState?.status.isWaiting ?? true) || + (previousState?.status.isGameOver ?? true); final lostRound = (previousState?.rounds ?? newState.rounds + 1) > newState.rounds; return startedGame || lostRound; diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index c63bf514..feea7304 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -19,7 +19,7 @@ class GameBloc extends Bloc { static const _maxScore = 9999999999; void _onGameStarted(GameStarted _, Emitter emit) { - emit(state.copyWith(status: GameStatus.playing)); + emit(const GameState.initial().copyWith(status: GameStatus.playing)); } void _onGameOver(GameOver _, Emitter emit) { diff --git a/lib/game/components/backbox/displays/game_over_info_display.dart b/lib/game/components/backbox/displays/game_over_info_display.dart index 0ac160ee..2db7e20b 100644 --- a/lib/game/components/backbox/displays/game_over_info_display.dart +++ b/lib/game/components/backbox/displays/game_over_info_display.dart @@ -66,7 +66,7 @@ class GameOverInfoDisplay extends Component with HasGameRef { @override Future onLoad() async { await super.onLoad(); - gameRef.overlays.add(PinballGame.playButtonOverlay); + gameRef.overlays.add(PinballGame.replayButtonOverlay); } } diff --git a/lib/game/components/game_bloc_status_listener.dart b/lib/game/components/game_bloc_status_listener.dart index 359db070..c463cd94 100644 --- a/lib/game/components/game_bloc_status_listener.dart +++ b/lib/game/components/game_bloc_status_listener.dart @@ -22,6 +22,7 @@ class GameBlocStatusListener extends Component break; case GameStatus.playing: readProvider().play(PinballAudio.backgroundMusic); + _resetBonuses(); gameRef .descendants() .whereType() @@ -32,6 +33,7 @@ class GameBlocStatusListener extends Component .forEach(_addPlungerBehaviors); gameRef.overlays.remove(PinballGame.playButtonOverlay); + gameRef.overlays.remove(PinballGame.replayButtonOverlay); break; case GameStatus.gameOver: readProvider().play(PinballAudio.gameOverVoiceOver); @@ -54,6 +56,15 @@ class GameBlocStatusListener extends Component } } + void _resetBonuses() { + gameRef + .descendants() + .whereType>() + .single + .bloc + .onReset(); + } + void _addPlungerBehaviors(Plunger plunger) { final platformHelper = readProvider(); const pullingStrength = 7.0; diff --git a/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart index ed19f495..2313e921 100644 --- a/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart +++ b/lib/game/components/google_gallery/behaviors/google_word_bonus_behavior.dart @@ -17,7 +17,7 @@ class GoogleWordBonusBehavior extends Component { onNewState: (state) { readBloc() .add(const BonusActivated(GameBonus.googleWord)); - readBloc().onBonusAwarded(); + readBloc().onReset(); add(BonusBallSpawningBehavior()); add(GoogleWordAnimatingBehavior()); }, diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2250a8fa..c102eb0b 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -38,10 +38,13 @@ class PinballGame extends PinballForge2DGame images.prefix = ''; } - /// Identifier of the play button overlay + /// Identifier of the play button overlay. static const playButtonOverlay = 'play_button'; - /// Identifier of the mobile controls overlay + /// Identifier of the replay button overlay. + static const replayButtonOverlay = 'replay_button'; + + /// Identifier of the mobile controls overlay. static const mobileControlsOverlay = 'mobile_controls'; @override diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index efc11996..06fde72d 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -100,22 +100,25 @@ class PinballGameLoadedView extends StatelessWidget { focusNode: game.focusNode, initialActiveOverlays: const [PinballGame.playButtonOverlay], overlayBuilderMap: { - PinballGame.playButtonOverlay: (context, game) { - return const Positioned( - bottom: 20, - right: 0, - left: 0, - child: PlayButtonOverlay(), - ); - }, - PinballGame.mobileControlsOverlay: (context, game) { - return Positioned( - bottom: 0, - left: 0, - right: 0, - child: MobileControls(game: game), - ); - }, + PinballGame.playButtonOverlay: (_, game) => const Positioned( + bottom: 20, + right: 0, + left: 0, + child: PlayButtonOverlay(), + ), + PinballGame.mobileControlsOverlay: (_, game) => Positioned( + bottom: 0, + left: 0, + right: 0, + child: MobileControls(game: game), + ), + PinballGame.replayButtonOverlay: (context, game) => + const Positioned( + bottom: 20, + right: 0, + left: 0, + child: ReplayButtonOverlay(), + ) }, ), ), diff --git a/lib/game/view/widgets/replay_button_overlay.dart b/lib/game/view/widgets/replay_button_overlay.dart index c0b2a67d..806f6ed7 100644 --- a/lib/game/view/widgets/replay_button_overlay.dart +++ b/lib/game/view/widgets/replay_button_overlay.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/start_game/start_game.dart'; import 'package:pinball_ui/pinball_ui.dart'; @@ -18,6 +19,7 @@ class ReplayButtonOverlay extends StatelessWidget { return PinballButton( text: l10n.replay, onTap: () { + context.read().add(const GameStarted()); context.read().add(const ReplayTapped()); }, ); diff --git a/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart b/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart index c16d4a2e..2119c2f8 100644 --- a/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart +++ b/packages/pinball_components/lib/src/components/google_word/behaviors/google_word_animating_behavior.dart @@ -17,7 +17,7 @@ class GoogleWordAnimatingBehavior extends TimerComponent _blinks++; } else { timer.stop(); - bloc.onAnimationFinished(); + bloc.onReset(); shouldRemove = true; } } diff --git a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart index 8a8b976d..cd69fc9d 100644 --- a/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart +++ b/packages/pinball_components/lib/src/components/google_word/cubit/google_word_cubit.dart @@ -68,7 +68,7 @@ class GoogleWordCubit extends Cubit { ); } - void onAnimationFinished() { + void onReset() { emit(GoogleWordState.initial()); _lastLitLetter = 0; } diff --git a/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart b/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart index 7224aeed..6275678c 100644 --- a/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart +++ b/packages/pinball_components/test/src/components/google_word/behaviors/google_word_animating_behavior_test.dart @@ -41,7 +41,7 @@ void main() { ); flameTester.testGameWidget( - 'calls onAnimationFinished and removes itself ' + 'calls onReset and removes itself ' 'after all blinks complete', setUp: (game, tester) async { final behavior = GoogleWordAnimatingBehavior(); @@ -53,7 +53,7 @@ void main() { } await game.ready(); - verify(bloc.onAnimationFinished).called(1); + verify(bloc.onReset).called(1); expect( game.descendants().whereType().isEmpty, isTrue, diff --git a/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart index b5000387..152b5f96 100644 --- a/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart +++ b/packages/pinball_components/test/src/components/google_word/cubit/google_word_cubit_test.dart @@ -62,9 +62,9 @@ void main() { ); blocTest( - 'onAnimationFinished emits initial state', + 'onReset emits initial state', build: GoogleWordCubit.new, - act: (bloc) => bloc.onAnimationFinished(), + act: (bloc) => bloc.onReset(), expect: () => [GoogleWordState.initial()], ); }, diff --git a/test/game/components/game_bloc_status_listener_test.dart b/test/game/components/game_bloc_status_listener_test.dart index 3519dbbd..874f901c 100644 --- a/test/game/components/game_bloc_status_listener_test.dart +++ b/test/game/components/game_bloc_status_listener_test.dart @@ -37,6 +37,7 @@ class _TestGame extends Forge2DGame with HasTappables { Iterable children, { PinballAudioPlayer? pinballAudioPlayer, PlatformHelper? platformHelper, + GoogleWordCubit? googleWordBloc, }) async { return ensureAdd( FlameMultiBlocProvider( @@ -47,6 +48,9 @@ class _TestGame extends Forge2DGame with HasTappables { FlameBlocProvider.value( value: CharacterThemeCubit(), ), + FlameBlocProvider.value( + value: googleWordBloc ?? GoogleWordCubit(), + ), ], children: [ MultiFlameProvider( @@ -80,6 +84,8 @@ class _MockPlatformHelper extends Mock implements PlatformHelper {} class _MockPlungerCubit extends Mock implements PlungerCubit {} +class _MockGoogleWordCubit extends Mock implements GoogleWordCubit {} + class _MockAppLocalizations extends Mock implements AppLocalizations { @override String get score => ''; @@ -332,6 +338,20 @@ void main() { }, ); + flameTester.test( + 'resets the GoogleWordCubit', + (game) async { + final googleWordBloc = _MockGoogleWordCubit(); + final component = GameBlocStatusListener(); + await game.pump([component], googleWordBloc: googleWordBloc); + + expect(state.status, equals(GameStatus.playing)); + component.onNewState(state); + + verify(googleWordBloc.onReset).called(1); + }, + ); + flameTester.test( 'adds FlipperKeyControllingBehavior to Flippers', (game) async { diff --git a/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart index acabe4c7..17726156 100644 --- a/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart +++ b/test/game/components/google_gallery/behaviors/google_word_bonus_behavior_test.dart @@ -75,7 +75,7 @@ void main() { flameTester.testGameWidget( 'adds GameBonus.googleWord to the game when all letters ' - 'in google word are activated and calls onBonusAwarded', + 'in google word are activated and calls onReset', setUp: (game, tester) async { final behavior = GoogleWordBonusBehavior(); final parent = GoogleGallery.test(); @@ -114,7 +114,7 @@ void main() { verify( () => gameBloc.add(const BonusActivated(GameBonus.googleWord)), ).called(1); - verify(googleWordBloc.onBonusAwarded).called(1); + verify(googleWordBloc.onReset).called(1); }, ); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index a0a0f22c..669117ed 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -307,6 +307,23 @@ void main() { expect(find.byType(MobileControls), findsOneWidget); }); + testWidgets( + 'ReplayButtonOverlay when the overlay is added', + (tester) async { + await tester.pumpApp( + PinballGameView(game), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + game.overlays.add(PinballGame.replayButtonOverlay); + + await tester.pump(); + + expect(find.byType(ReplayButtonOverlay), findsOneWidget); + }, + ); + group('info icon', () { testWidgets('renders on game over', (tester) async { final gameState = GameState.initial().copyWith( diff --git a/test/game/view/widgets/replay_button_overlay_test.dart b/test/game/view/widgets/replay_button_overlay_test.dart index 1497031a..5c3e4884 100644 --- a/test/game/view/widgets/replay_button_overlay_test.dart +++ b/test/game/view/widgets/replay_button_overlay_test.dart @@ -8,24 +8,32 @@ import '../../../helpers/helpers.dart'; class _MockStartGameBloc extends Mock implements StartGameBloc {} +class _MockGameBloc extends Mock implements GameBloc {} + void main() { group('ReplayButtonOverlay', () { late StartGameBloc startGameBloc; + late _MockGameBloc gameBloc; setUp(() async { await mockFlameImages(); startGameBloc = _MockStartGameBloc(); + gameBloc = _MockGameBloc(); whenListen( startGameBloc, Stream.value(const StartGameState.initial()), initialState: const StartGameState.initial(), ); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); }); testWidgets('renders correctly', (tester) async { await tester.pumpApp(const ReplayButtonOverlay()); - expect(find.text('Replay'), findsOneWidget); }); @@ -33,6 +41,7 @@ void main() { (tester) async { await tester.pumpApp( const ReplayButtonOverlay(), + gameBloc: gameBloc, startGameBloc: startGameBloc, ); @@ -41,5 +50,19 @@ void main() { verify(() => startGameBloc.add(const ReplayTapped())).called(1); }); + + testWidgets('adds GameStarted event to GameBloc when tapped', + (tester) async { + await tester.pumpApp( + const ReplayButtonOverlay(), + gameBloc: gameBloc, + startGameBloc: startGameBloc, + ); + + await tester.tap(find.text('Replay')); + await tester.pump(); + + verify(() => gameBloc.add(const GameStarted())).called(1); + }); }); }