From 4524849c29f9ba32b856853eb5ea1787b72e285d Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Thu, 28 Apr 2022 19:46:38 +0200 Subject: [PATCH] feat: game bloc multiplier (#213) * feat: added events for multiplier * feat: added events for increment, apply and reset multiplier * feat: added multiplier to game bloc state * test: test for multiplier at game bloc * test: added multiplier to game state * refactor: multiplier always increased by 1 * refactor: add multiplier state on BallLost * refactor: added round to game state and changed gameover and ball lost logic * test: fixed tests for game bloc * refactor: multiplier max value 6 at game bloc * test: fixed tests with new game over logic * chore: properties renamed and removed unused * Update lib/game/bloc/game_event.dart Co-authored-by: Alejandro Santiago * fix: pubspec from main * refactor: pubspec from main * chore: removed unused import * feat: ball added event to game bloc * test: fixed test for ball added * feat: added BallAdded event on ball mounted * test: fixing tests for BallAdded * test: flamebloctester for ball added * test: refactored tests for pinballgame * refactor: BallAdded event on ball mounted * chore: removed unnecessary imports * test: refactor tests for pinball_game * refactor: use rounds instead of balls * refactor: changed BallLost event with RoundLost, and moved part of the logic to controlled_ball * test: tests for RoundLost * fix: fixed wrong tests for pinball_game * test: remove deleted balls property from GameState * chore: doc Co-authored-by: Alejandro Santiago Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/bloc/game_bloc.dart | 32 ++- lib/game/bloc/game_event.dart | 21 +- lib/game/bloc/game_state.dart | 28 ++- lib/game/components/controlled_ball.dart | 25 +- lib/game/pinball_game.dart | 20 +- lib/game/view/widgets/game_hud.dart | 2 +- .../view/widgets/round_count_display.dart | 10 +- pubspec.lock | 27 ++- test/game/bloc/game_bloc_test.dart | 199 ++++++++++++---- test/game/bloc/game_event_test.dart | 21 +- test/game/bloc/game_state_test.dart | 64 +++-- .../game/components/controlled_ball_test.dart | 27 ++- .../components/controlled_flipper_test.dart | 3 +- .../components/controlled_plunger_test.dart | 3 +- .../components/game_flow_controller_test.dart | 6 +- .../components/scoring_behavior_test.dart | 3 +- test/game/components/wall_test.dart | 4 +- test/game/pinball_game_test.dart | 218 +++++++++--------- test/game/view/widgets/game_hud_test.dart | 3 +- .../widgets/round_count_display_test.dart | 7 +- test/game/view/widgets/score_view_test.dart | 5 +- 21 files changed, 464 insertions(+), 264 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 4ba63092..49f40d1f 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -1,5 +1,5 @@ // ignore_for_file: public_member_api_docs - +import 'dart:math' as math; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -9,19 +9,41 @@ part 'game_state.dart'; class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { - on(_onBallLost); + on(_onRoundLost); on(_onScored); + on(_onIncreasedMultiplier); on(_onBonusActivated); on(_onSparkyTurboChargeActivated); } - void _onBallLost(BallLost event, Emitter emit) { - emit(state.copyWith(balls: state.balls - 1)); + void _onRoundLost(RoundLost event, Emitter emit) { + final score = state.score * state.multiplier; + final roundsLeft = math.max(state.rounds - 1, 0); + + emit( + state.copyWith( + score: score, + multiplier: 1, + rounds: roundsLeft, + ), + ); } void _onScored(Scored event, Emitter emit) { if (!state.isGameOver) { - emit(state.copyWith(score: state.score + event.points)); + emit( + state.copyWith(score: state.score + event.points), + ); + } + } + + void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) { + if (!state.isGameOver) { + 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 bbb89028..c81ce526 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable { const GameEvent(); } -/// {@template ball_lost_game_event} -/// Event added when a user drops a ball off the screen. +/// {@template round_lost_game_event} +/// Event added when a user drops all balls off the screen and loses a round. /// {@endtemplate} -class BallLost extends GameEvent { - /// {@macro ball_lost_game_event} - const BallLost(); +class RoundLost extends GameEvent { + /// {@macro round_lost_game_event} + const RoundLost(); @override List get props => []; @@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent { @override List get props => []; } + +/// {@template multiplier_increased_game_event} +/// Added when a multiplier is gained. +/// {@endtemplate} +class MultiplierIncreased extends GameEvent { + /// {@macro multiplier_increased_game_event} + const MultiplierIncreased(); + + @override + List get props => []; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 772bfea3..4ce9042d 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -27,34 +27,42 @@ class GameState extends Equatable { /// {@macro game_state} const GameState({ required this.score, - required this.balls, + required this.multiplier, + required this.rounds, required this.bonusHistory, }) : assert(score >= 0, "Score can't be negative"), - assert(balls >= 0, "Number of balls 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() : score = 0, - balls = 3, + multiplier = 1, + rounds = 3, bonusHistory = const []; /// The current score of the game. final int score; - /// The number of balls left in the game. + /// The current multiplier for the score. + final int multiplier; + + /// The number of rounds left in the game. /// - /// When the number of balls is 0, the game is over. - final int balls; + /// 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 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( @@ -64,7 +72,8 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, - balls: balls ?? this.balls, + multiplier: multiplier ?? this.multiplier, + rounds: rounds ?? this.rounds, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -72,7 +81,8 @@ class GameState extends Equatable { @override List get props => [ score, - balls, + multiplier, + rounds, bonusHistory, ]; } diff --git a/lib/game/components/controlled_ball.dart b/lib/game/components/controlled_ball.dart index f36cfef2..e76aabe1 100644 --- a/lib/game/components/controlled_ball.dart +++ b/lib/game/components/controlled_ball.dart @@ -1,5 +1,4 @@ import 'package:flame/components.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -8,12 +7,12 @@ import 'package:pinball_theme/pinball_theme.dart'; /// {@template controlled_ball} /// A [Ball] with a [BallController] attached. +/// +/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is +/// not over, a new [Ball] will be spawned. /// {@endtemplate} class ControlledBall extends Ball with Controls { /// A [Ball] that launches from the [Plunger]. - /// - /// When a launched [Ball] is lost, it will decrease the [GameState.balls] - /// count, and a new [Ball] is spawned. ControlledBall.launch({ required CharacterTheme characterTheme, }) : super(baseColor: characterTheme.ballColor) { @@ -24,8 +23,6 @@ class ControlledBall extends Ball with Controls { /// {@template bonus_ball} /// {@macro controlled_ball} - /// - /// When a bonus [Ball] is lost, the [GameState.balls] doesn't change. /// {@endtemplate} ControlledBall.bonus({ required CharacterTheme characterTheme, @@ -36,7 +33,7 @@ class ControlledBall extends Ball with Controls { /// [Ball] used in [DebugPinballGame]. ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { - controller = DebugBallController(this); + controller = BallController(this); priority = RenderPriority.ballOnBoard; } } @@ -74,15 +71,9 @@ class BallController extends ComponentController @override void onRemove() { super.onRemove(); - gameRef.read().add(const BallLost()); + final noBallsLeft = gameRef.descendants().whereType().isEmpty; + if (noBallsLeft) { + gameRef.read().add(const RoundLost()); + } } } - -/// {@macro ball_controller} -class DebugBallController extends BallController { - /// {@macro ball_controller} - DebugBallController(Ball component) : super(component); - - @override - void onRemove() {} -} diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 944274a4..c0d4445e 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -72,7 +72,7 @@ class PinballGame extends Forge2DGame } class _GameBallsController extends ComponentController - with BlocComponent, HasGameRef { + with BlocComponent { _GameBallsController(PinballGame game) : super(game); late final Plunger _plunger; @@ -80,9 +80,9 @@ class _GameBallsController extends ComponentController @override bool listenWhen(GameState? previousState, GameState newState) { final noBallsLeft = component.descendants().whereType().isEmpty; - final canBallRespawn = newState.balls > 0; + final notGameOver = !newState.isGameOver; - return noBallsLeft && canBallRespawn; + return noBallsLeft && notGameOver; } @override @@ -99,7 +99,7 @@ class _GameBallsController extends ComponentController void _spawnBall() { final ball = ControlledBall.launch( - characterTheme: gameRef.characterTheme, + characterTheme: component.characterTheme, )..initialPosition = Vector2( _plunger.body.position.x, _plunger.body.position.y - Ball.size.y, @@ -160,18 +160,6 @@ class DebugPinballGame extends PinballGame with FPSCounter, TapDetector { class _DebugGameBallsController extends _GameBallsController { _DebugGameBallsController(PinballGame game) : super(game); - - @override - bool listenWhen(GameState? previousState, GameState newState) { - final noBallsLeft = component - .descendants() - .whereType() - .where((ball) => ball.controller is! DebugBallController) - .isEmpty; - final canBallRespawn = newState.balls > 0; - - return noBallsLeft && canBallRespawn; - } } // TODO(wolfenrain): investigate this CI failure. diff --git a/lib/game/view/widgets/game_hud.dart b/lib/game/view/widgets/game_hud.dart index 3623e21f..9cfb2d67 100644 --- a/lib/game/view/widgets/game_hud.dart +++ b/lib/game/view/widgets/game_hud.dart @@ -7,7 +7,7 @@ import 'package:pinball/theme/app_colors.dart'; /// {@template game_hud} /// Overlay on the [PinballGame]. /// -/// Displays the current [GameState.score], [GameState.balls] and animates when +/// Displays the current [GameState.score], [GameState.rounds] and animates when /// the player gets a [GameBonus]. /// {@endtemplate} class GameHud extends StatefulWidget { diff --git a/lib/game/view/widgets/round_count_display.dart b/lib/game/view/widgets/round_count_display.dart index 98776764..30135cd2 100644 --- a/lib/game/view/widgets/round_count_display.dart +++ b/lib/game/view/widgets/round_count_display.dart @@ -14,9 +14,7 @@ class RoundCountDisplay extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - // TODO(arturplaczek): refactor when GameState handle balls and rounds and - // select state.rounds property instead of state.ball - final balls = context.select((GameBloc bloc) => bloc.state.balls); + final rounds = context.select((GameBloc bloc) => bloc.state.rounds); return Row( children: [ @@ -29,9 +27,9 @@ class RoundCountDisplay extends StatelessWidget { const SizedBox(width: 8), Row( children: [ - RoundIndicator(isActive: balls >= 1), - RoundIndicator(isActive: balls >= 2), - RoundIndicator(isActive: balls >= 3), + RoundIndicator(isActive: rounds >= 1), + RoundIndicator(isActive: rounds >= 2), + RoundIndicator(isActive: rounds >= 3), ], ), ], diff --git a/pubspec.lock b/pubspec.lock index 4a851209..9ee8ae6c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "39.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "2.8.0" args: dependency: transitive description: @@ -71,6 +71,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -316,7 +323,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.4" + version: "0.6.3" json_annotation: dependency: transitive description: @@ -351,7 +358,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.3" meta: dependency: transitive description: @@ -414,7 +421,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.0" path_provider: dependency: transitive description: @@ -587,7 +594,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.8.1" stack_trace: dependency: transitive description: @@ -629,21 +636,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.21.1" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.13" + version: "0.4.9" typed_data: dependency: transitive description: @@ -664,7 +671,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.1" very_good_analysis: dependency: "direct dev" description: diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 37e14f73..3711105e 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -4,27 +4,69 @@ 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 and empty score', () { final gameBloc = GameBloc(); expect(gameBloc.state.score, equals(0)); - expect(gameBloc.state.balls, equals(3)); + expect(gameBloc.state.rounds, equals(3)); }); - group('LostBall', () { + group('RoundLost', () { blocTest( - 'decreases number of balls', + 'decreases number of rounds ' + 'when there are already available rounds', build: GameBloc.new, act: (bloc) { - bloc.add(const BallLost()); + bloc.add(const RoundLost()); }, expect: () => [ const GameState( score: 0, - balls: 2, + multiplier: 1, + rounds: 2, bonusHistory: [], ), ], ); + + blocTest( + 'apply multiplier to score ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 15) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); + + blocTest( + 'resets multiplier ' + 'when round is lost', + build: GameBloc.new, + seed: () => const GameState( + score: 5, + multiplier: 3, + rounds: 2, + bonusHistory: [], + ), + act: (bloc) { + bloc.add(const RoundLost()); + }, + expect: () => [ + isA() + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.rounds, 'rounds', 1), + ], + ); }); group('Scored', () { @@ -36,16 +78,12 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState( - score: 2, - balls: 3, - bonusHistory: [], - ), - const GameState( - score: 5, - balls: 3, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 5) + ..having((state) => state.isGameOver, 'isGameOver', false), ], ); @@ -54,27 +92,85 @@ 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 RoundLost()); } bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState( - score: 0, - balls: 2, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 1, - bonusHistory: [], - ), - const GameState( - score: 0, - balls: 0, - bonusHistory: [], - ), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.rounds, 'rounds', 0) + ..having((state) => state.isGameOver, 'isGameOver', true), + ], + ); + }); + + group('MultiplierIncreased', () { + blocTest( + 'increases multiplier ' + 'when multiplier is below 6 and game is not over', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const MultiplierIncreased()) + ..add(const MultiplierIncreased()), + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 2) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 3) + ..having((state) => state.isGameOver, 'isGameOver', false), + ], + ); + + blocTest( + "doesn't increase multiplier " + 'when multiplier is 6 and game is not over', + build: GameBloc.new, + seed: () => const GameState( + score: 0, + multiplier: 6, + rounds: 3, + bonusHistory: [], + ), + act: (bloc) => bloc..add(const MultiplierIncreased()), + expect: () => const [], + ); + + blocTest( + "doesn't increase multiplier " + 'when game is over', + build: GameBloc.new, + act: (bloc) { + for (var i = 0; i < bloc.state.rounds; i++) { + bloc.add(const RoundLost()); + } + bloc.add(const MultiplierIncreased()); + }, + expect: () => [ + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', false), + isA() + ..having((state) => state.score, 'score', 0) + ..having((state) => state.multiplier, 'multiplier', 1) + ..having((state) => state.isGameOver, 'isGameOver', true), ], ); }); @@ -88,17 +184,19 @@ void main() { act: (bloc) => bloc ..add(const BonusActivated(GameBonus.googleWord)) ..add(const BonusActivated(GameBonus.dashNest)), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord], - ), - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.googleWord, GameBonus.dashNest], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord], + ), + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.googleWord, GameBonus.dashNest], + ), ], ); }, @@ -109,12 +207,13 @@ void main() { 'adds game bonus', build: GameBloc.new, act: (bloc) => bloc..add(const SparkyTurboChargeActivated()), - expect: () => const [ - GameState( - score: 0, - balls: 3, - bonusHistory: [GameBonus.sparkyTurboCharge], - ), + expect: () => [ + isA() + ..having( + (state) => state.bonusHistory, + 'bonusHistory', + [GameBonus.sparkyTurboCharge], + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index d7d587bd..6a39bd67 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -5,15 +5,15 @@ import 'package:pinball/game/game.dart'; void main() { group('GameEvent', () { - group('BallLost', () { + group('RoundLost', () { test('can be instantiated', () { - expect(const BallLost(), isNotNull); + expect(const RoundLost(), isNotNull); }); test('supports value equality', () { expect( - BallLost(), - equals(const BallLost()), + RoundLost(), + equals(const RoundLost()), ); }); }); @@ -41,6 +41,19 @@ void main() { }); }); + group('MultiplierIncreased', () { + test('can be instantiated', () { + expect(const MultiplierIncreased(), isNotNull); + }); + + test('supports value equality', () { + expect( + MultiplierIncreased(), + equals(const MultiplierIncreased()), + ); + }); + }); + group('BonusActivated', () { test('can be instantiated', () { expect(const BonusActivated(GameBonus.dashNest), isNotNull); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 8170346f..add25e05 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -9,13 +9,15 @@ void main() { expect( GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), equals( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), ), @@ -27,7 +29,8 @@ void main() { expect( const GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ), isNotNull, @@ -37,12 +40,13 @@ void main() { test( 'throws AssertionError ' - 'when balls are negative', + 'when score is negative', () { expect( () => GameState( - balls: -1, - score: 0, + score: -1, + multiplier: 1, + rounds: 3, bonusHistory: const [], ), throwsAssertionError, @@ -52,12 +56,29 @@ void main() { test( 'throws AssertionError ' - 'when score is negative', + 'when multiplier is less than 1', () { expect( () => GameState( - balls: 0, - score: -1, + score: 1, + multiplier: 0, + rounds: 3, + bonusHistory: const [], + ), + throwsAssertionError, + ); + }, + ); + + test( + 'throws AssertionError ' + 'when rounds is negative', + () { + expect( + () => GameState( + score: 1, + multiplier: 1, + rounds: -1, bonusHistory: const [], ), throwsAssertionError, @@ -68,10 +89,11 @@ void main() { group('isGameOver', () { test( 'is true ' - 'when no balls are left', () { + 'when no rounds are left', () { const gameState = GameState( - balls: 0, score: 0, + multiplier: 1, + rounds: 0, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -79,10 +101,11 @@ void main() { test( 'is false ' - 'when one 1 ball left', () { + 'when one 1 round left', () { const gameState = GameState( - balls: 1, score: 0, + multiplier: 1, + rounds: 1, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -95,8 +118,9 @@ void main() { 'when scored is decreased', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -111,8 +135,9 @@ void main() { 'when no argument specified', () { const gameState = GameState( - balls: 0, score: 2, + multiplier: 1, + rounds: 3, bonusHistory: [], ); expect( @@ -128,12 +153,14 @@ void main() { () { const gameState = GameState( score: 2, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, - balls: gameState.balls + 1, + multiplier: gameState.multiplier + 1, + rounds: gameState.rounds + 1, bonusHistory: const [GameBonus.googleWord], ); expect(gameState, isNot(equals(otherGameState))); @@ -141,7 +168,8 @@ void main() { expect( gameState.copyWith( score: otherGameState.score, - balls: otherGameState.balls, + multiplier: otherGameState.multiplier, + 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..c84ddaa7 100644 --- a/test/game/components/controlled_ball_test.dart +++ b/test/game/components/controlled_ball_test.dart @@ -53,16 +53,39 @@ void main() { }); flameBlocTester.testGameWidget( - 'lost adds BallLost to GameBloc', + "lost doesn't adds RoundLost to GameBloc " + 'when there are balls left', + setUp: (game, tester) async { + final controller = BallController(ball); + await ball.add(controller); + await game.ensureAdd(ball); + + final otherBall = Ball(baseColor: const Color(0xFF00FFFF)); + final otherController = BallController(otherBall); + await otherBall.add(otherController); + await game.ensureAdd(otherBall); + + controller.lost(); + await game.ready(); + }, + verify: (game, tester) async { + verifyNever(() => gameBloc.add(const RoundLost())); + }, + ); + + flameBlocTester.testGameWidget( + 'lost adds RoundLost to GameBloc ' + 'when there are no balls left', setUp: (game, tester) async { final controller = BallController(ball); await ball.add(controller); await game.ensureAdd(ball); controller.lost(); + await game.ready(); }, verify: (game, tester) async { - verify(() => gameBloc.add(const BallLost())).called(1); + verify(() => gameBloc.add(const RoundLost())).called(1); }, ); diff --git a/test/game/components/controlled_flipper_test.dart b/test/game/components/controlled_flipper_test.dart index 2f970254..36a8161b 100644 --- a/test/game/components/controlled_flipper_test.dart +++ b/test/game/components/controlled_flipper_test.dart @@ -25,7 +25,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, 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 eee2bcb0..a39bdef6 100644 --- a/test/game/components/controlled_plunger_test.dart +++ b/test/game/components/controlled_plunger_test.dart @@ -20,7 +20,8 @@ void main() { final bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 0, 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 3de04b90..ef93892c 100644 --- a/test/game/components/game_flow_controller_test.dart +++ b/test/game/components/game_flow_controller_test.dart @@ -15,7 +15,8 @@ void main() { test('is true when the game over state has changed', () { final state = GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ); @@ -66,7 +67,8 @@ void main() { gameFlowController.onNewState( GameState( score: 10, - balls: 0, + multiplier: 1, + rounds: 0, bonusHistory: const [], ), ); diff --git a/test/game/components/scoring_behavior_test.dart b/test/game/components/scoring_behavior_test.dart index d5e706b0..4fb07f40 100644 --- a/test/game/components/scoring_behavior_test.dart +++ b/test/game/components/scoring_behavior_test.dart @@ -43,7 +43,8 @@ void main() { bloc = MockGameBloc(); const state = GameState( score: 0, - balls: 0, + multiplier: 1, + rounds: 3, bonusHistory: [], ); whenListen(bloc, Stream.value(state), initialState: state); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 2905ab9a..16f7ce34 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -146,9 +146,9 @@ void main() { }, ); - flameTester.test( + flameBlocTester.testGameWidget( 'when ball is debug', - (game) async { + setUp: (game, tester) async { final ball = ControlledBall.debug(); final wall = BottomWall(); await game.ensureAddAll([ball, wall]); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f9e0eca4..3e90738d 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -13,28 +13,54 @@ import '../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final assets = [ - Assets.images.dash.bumper.main.active.keyName, - Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.androidBumper.a.lit.keyName, + Assets.images.androidBumper.a.dimmed.keyName, + Assets.images.androidBumper.b.lit.keyName, + Assets.images.androidBumper.b.dimmed.keyName, + Assets.images.backboard.backboardScores.keyName, + Assets.images.backboard.backboardGameOver.keyName, + Assets.images.backboard.display.keyName, + Assets.images.ball.ball.keyName, + Assets.images.ball.flameEffect.keyName, + Assets.images.baseboard.left.keyName, + Assets.images.baseboard.right.keyName, + Assets.images.boundary.bottom.keyName, + Assets.images.boundary.outer.keyName, + Assets.images.boundary.outerBottom.keyName, + Assets.images.chromeDino.mouth.keyName, + Assets.images.chromeDino.head.keyName, + Assets.images.dino.dinoLandTop.keyName, + Assets.images.dino.dinoLandBottom.keyName, + Assets.images.dash.animatronic.keyName, Assets.images.dash.bumper.a.active.keyName, Assets.images.dash.bumper.a.inactive.keyName, Assets.images.dash.bumper.b.active.keyName, Assets.images.dash.bumper.b.inactive.keyName, - Assets.images.dash.animatronic.keyName, + Assets.images.dash.bumper.main.active.keyName, + Assets.images.dash.bumper.main.inactive.keyName, + Assets.images.flipper.left.keyName, + Assets.images.flipper.right.keyName, + Assets.images.googleWord.letter1.keyName, + Assets.images.googleWord.letter2.keyName, + Assets.images.googleWord.letter3.keyName, + Assets.images.googleWord.letter4.keyName, + Assets.images.googleWord.letter5.keyName, + Assets.images.googleWord.letter6.keyName, + Assets.images.kicker.left.keyName, + Assets.images.kicker.right.keyName, + Assets.images.launchRamp.ramp.keyName, + Assets.images.launchRamp.foregroundRailing.keyName, + Assets.images.launchRamp.backgroundRailing.keyName, + Assets.images.plunger.plunger.keyName, + Assets.images.plunger.rocket.keyName, Assets.images.signpost.inactive.keyName, Assets.images.signpost.active1.keyName, Assets.images.signpost.active2.keyName, Assets.images.signpost.active3.keyName, - Assets.images.androidBumper.a.lit.keyName, - Assets.images.androidBumper.a.dimmed.keyName, - Assets.images.androidBumper.b.lit.keyName, - Assets.images.androidBumper.b.dimmed.keyName, - Assets.images.sparky.bumper.a.active.keyName, - Assets.images.sparky.bumper.a.inactive.keyName, - Assets.images.sparky.bumper.b.active.keyName, - Assets.images.sparky.bumper.b.inactive.keyName, - Assets.images.sparky.bumper.c.active.keyName, - Assets.images.sparky.bumper.c.inactive.keyName, - Assets.images.sparky.animatronic.keyName, + Assets.images.slingshot.upper.keyName, + Assets.images.slingshot.lower.keyName, + Assets.images.spaceship.saucer.keyName, + Assets.images.spaceship.bridge.keyName, Assets.images.spaceship.ramp.boardOpening.keyName, Assets.images.spaceship.ramp.railingForeground.keyName, Assets.images.spaceship.ramp.railingBackground.keyName, @@ -45,18 +71,26 @@ void main() { Assets.images.spaceship.ramp.arrow.active3.keyName, Assets.images.spaceship.ramp.arrow.active4.keyName, Assets.images.spaceship.ramp.arrow.active5.keyName, - Assets.images.baseboard.left.keyName, - Assets.images.baseboard.right.keyName, - Assets.images.flipper.left.keyName, - Assets.images.flipper.right.keyName, - Assets.images.boundary.outer.keyName, - Assets.images.boundary.outerBottom.keyName, - Assets.images.boundary.bottom.keyName, - Assets.images.slingshot.upper.keyName, - Assets.images.slingshot.lower.keyName, - Assets.images.dino.dinoLandTop.keyName, - Assets.images.dino.dinoLandBottom.keyName, + Assets.images.spaceship.rail.main.keyName, + Assets.images.spaceship.rail.foreground.keyName, + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.computer.top.keyName, + Assets.images.sparky.computer.base.keyName, + Assets.images.sparky.animatronic.keyName, + Assets.images.sparky.bumper.a.inactive.keyName, + Assets.images.sparky.bumper.a.active.keyName, + Assets.images.sparky.bumper.b.active.keyName, + Assets.images.sparky.bumper.b.inactive.keyName, + Assets.images.sparky.bumper.c.active.keyName, + Assets.images.sparky.bumper.c.inactive.keyName, ]; + final flameTester = FlameTester( () => PinballTestGame(assets: assets), ); @@ -72,7 +106,6 @@ void main() { 'has only one BottomWall', (game) async { await game.ready(); - expect( game.children.whereType().length, equals(1), @@ -91,34 +124,34 @@ void main() { }, ); - flameTester.test('has one Board', (game) async { - await game.ready(); - expect( - game.children.whereType().length, - equals(1), - ); - }); + flameTester.test( + 'has one Board', + (game) async { + await game.ready(); + expect( + game.children.whereType().length, + equals(1), + ); + }, + ); + + flameTester.test( + 'one GoogleWord', + (game) async { + await game.ready(); + expect(game.children.whereType().length, equals(1)); + }, + ); group('controller', () { - // TODO(alestiago): Write test to be controller agnostic. group('listenWhen', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = GameBloc(); - }); - - final flameBlocTester = FlameBlocTester( - gameBuilder: EmptyPinballTestGame.new, - blocBuilder: () => gameBloc, - // assets: assets, - ); - - flameBlocTester.testGameWidget( - 'listens when all balls are lost and there are more than 0 balls', + flameTester.testGameWidget( + 'listens when all balls are lost and there are more than 0 rounds', setUp: (game, tester) async { + // 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.balls).thenReturn(2); + when(() => newState.isGameOver).thenReturn(false); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); @@ -135,10 +168,10 @@ void main() { "doesn't listen when some balls are left", (game) async { final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + when(() => newState.isGameOver).thenReturn(false); expect( - game.descendants().whereType().length, + game.descendants().whereType().length, greaterThan(0), ); expect( @@ -148,19 +181,20 @@ void main() { }, ); - flameBlocTester.test( - "doesn't listen when no balls left", - (game) async { + flameTester.testGameWidget( + "doesn't listen when game is over", + setUp: (game, tester) async { + // 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.balls).thenReturn(0); - + when(() => newState.isGameOver).thenReturn(true); game.descendants().whereType().forEach( (ball) => ball.controller.lost(), ); await game.ready(); expect( - game.descendants().whereType().isEmpty, + game.descendants().whereType().isEmpty, isTrue, ); expect( @@ -177,14 +211,13 @@ void main() { flameTester.test( 'spawns a ball', (game) async { - await game.ready(); final previousBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); game.controller.onNewState(MockGameState()); await game.ready(); final currentBalls = - game.descendants().whereType().toList(); + game.descendants().whereType().toList(); expect( currentBalls.length, @@ -199,57 +232,26 @@ void main() { }); group('DebugPinballGame', () { - debugModeFlameTester.test('adds a ball on tap up', (game) async { - await game.ready(); - - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2.all(10)); - - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); - - final previousBalls = game.descendants().whereType().toList(); - - game.onTapUp(tapUpEvent); - await game.ready(); - - expect( - game.children.whereType().length, - equals(previousBalls.length + 1), - ); - }); - - group('controller', () { - late GameBloc gameBloc; + debugModeFlameTester.test( + 'adds a ball on tap up', + (game) async { + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.all(10)); - setUp(() { - gameBloc = GameBloc(); - }); - - final debugModeFlameBlocTester = - FlameBlocTester( - gameBuilder: DebugPinballTestGame.new, - blocBuilder: () => gameBloc, - assets: assets, - ); + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); - debugModeFlameBlocTester.testGameWidget( - 'ignores debug balls', - setUp: (game, tester) async { - final newState = MockGameState(); - when(() => newState.balls).thenReturn(1); + final previousBalls = + game.descendants().whereType().toList(); - await game.ready(); - game.children.removeWhere((component) => component is Ball); - await game.ready(); - await game.ensureAdd(ControlledBall.debug()); + game.onTapUp(tapUpEvent); + await game.ready(); - expect( - game.controller.listenWhen(MockGameState(), newState), - isTrue, - ); - }, - ); - }); + expect( + game.children.whereType().length, + equals(previousBalls.length + 1), + ); + }, + ); }); } diff --git a/test/game/view/widgets/game_hud_test.dart b/test/game/view/widgets/game_hud_test.dart index fe8bd092..d101d06e 100644 --- a/test/game/view/widgets/game_hud_test.dart +++ b/test/game/view/widgets/game_hud_test.dart @@ -28,7 +28,8 @@ void main() { const initialState = GameState( score: 1000, - balls: 2, + multiplier: 1, + rounds: 1, bonusHistory: [], ); diff --git a/test/game/view/widgets/round_count_display_test.dart b/test/game/view/widgets/round_count_display_test.dart index 8281ce83..dfa28869 100644 --- a/test/game/view/widgets/round_count_display_test.dart +++ b/test/game/view/widgets/round_count_display_test.dart @@ -11,7 +11,8 @@ void main() { late GameBloc gameBloc; const initialState = GameState( score: 0, - balls: 3, + multiplier: 1, + rounds: 3, bonusHistory: [], ); @@ -37,7 +38,7 @@ void main() { testWidgets('two active round indicator', (tester) async { final state = initialState.copyWith( - balls: 2, + rounds: 2, ); whenListen( gameBloc, @@ -68,7 +69,7 @@ void main() { testWidgets('one active round indicator', (tester) async { final state = initialState.copyWith( - balls: 1, + rounds: 1, ); whenListen( gameBloc, diff --git a/test/game/view/widgets/score_view_test.dart b/test/game/view/widgets/score_view_test.dart index 0d3af694..63f7d1c5 100644 --- a/test/game/view/widgets/score_view_test.dart +++ b/test/game/view/widgets/score_view_test.dart @@ -15,7 +15,8 @@ void main() { const score = 123456789; const initialState = GameState( score: score, - balls: 1, + multiplier: 1, + rounds: 1, bonusHistory: [], ); @@ -46,7 +47,7 @@ void main() { stateController.add( initialState.copyWith( - balls: 0, + rounds: 0, ), );