From b73cddc1a3b2be92a9eb28d7fcb9273439f00ccd Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Fri, 11 Mar 2022 09:03:03 +0100 Subject: [PATCH 1/5] refactor: remove Flutter dep from geometry (#27) * fix: removed flutter dependency * test: fixed tests for assertions * test: check assertion with isA * ci: added geometry workflow file * refactor: changed flame dep to vector_math for vector2 * fix: changed import for vector to vector_math_64 * Update .github/workflows/geometry.yaml Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- .github/workflows/geometry.yaml | 18 ++++++++++++ packages/geometry/lib/src/geometry.dart | 3 +- packages/geometry/pubspec.yaml | 6 +--- packages/geometry/test/src/geometry_test.dart | 28 +++++++++++++------ 4 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/geometry.yaml diff --git a/.github/workflows/geometry.yaml b/.github/workflows/geometry.yaml new file mode 100644 index 00000000..8bf55107 --- /dev/null +++ b/.github/workflows/geometry.yaml @@ -0,0 +1,18 @@ +name: geometry + +on: + push: + paths: + - "packages/geometry/**" + - ".github/workflows/geometry.yaml" + + pull_request: + paths: + - "packages/geometry/**" + - ".github/workflows/geometry.yaml" + +jobs: + build: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1 + with: + working_directory: packages/geometry diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index dceb4e9e..6ada92e0 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; -import 'package:flame/extensions.dart'; + +import 'package:vector_math/vector_math_64.dart'; /// Calculates all [Vector2]s of a circumference. /// diff --git a/packages/geometry/pubspec.yaml b/packages/geometry/pubspec.yaml index 2678cdef..8fffb8b3 100644 --- a/packages/geometry/pubspec.yaml +++ b/packages/geometry/pubspec.yaml @@ -7,13 +7,9 @@ environment: sdk: ">=2.16.0 <3.0.0" dependencies: - flame: ^1.0.0 - flutter: - sdk: flutter + vector_math: ^2.1.1 dev_dependencies: - flutter_test: - sdk: flutter mocktail: ^0.2.0 test: ^1.19.2 very_good_analysis: ^2.4.0 diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index a3040a9c..2a5f9169 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -1,7 +1,8 @@ // ignore_for_file: prefer_const_constructors, cascade_invocations -import 'package:flame/extensions.dart'; -import 'package:flutter_test/flutter_test.dart'; + import 'package:geometry/geometry.dart'; +import 'package:test/test.dart'; +import 'package:vector_math/vector_math_64.dart'; class Binomial { Binomial({required this.n, required this.k}); @@ -42,18 +43,18 @@ void main() { ], step: 2, ), - throwsAssertionError, + throwsA(isA()), ); }); test('fails if not enough control points', () { expect( () => calculateBezierCurve(controlPoints: [Vector2.zero()]), - throwsAssertionError, + throwsA(isA()), ); expect( () => calculateBezierCurve(controlPoints: []), - throwsAssertionError, + throwsA(isA()), ); }); @@ -81,15 +82,24 @@ void main() { group('binomial', () { test('fails if k is negative', () { - expect(() => binomial(1, -1), throwsAssertionError); + expect( + () => binomial(1, -1), + throwsA(isA()), + ); }); test('fails if n is negative', () { - expect(() => binomial(-1, 1), throwsAssertionError); + expect( + () => binomial(-1, 1), + throwsA(isA()), + ); }); test('fails if n < k', () { - expect(() => binomial(1, 2), throwsAssertionError); + expect( + () => binomial(1, 2), + throwsA(isA()), + ); }); test('for a specific input gives a correct value', () { @@ -131,7 +141,7 @@ void main() { group('factorial', () { test('fails if negative number', () { - expect(() => factorial(-1), throwsAssertionError); + expect(() => factorial(-1), throwsA(isA())); }); test('for a specific input gives a correct value', () { From 7da7055957e05221b63ac3eef17725aff44db806 Mon Sep 17 00:00:00 2001 From: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Date: Fri, 11 Mar 2022 02:59:30 -0600 Subject: [PATCH 2/5] chore: rename pinball game test extension (#33) * chore: rename pinball game test extension * refactor: initial to create * docs: small change --- test/game/components/anchor_test.dart | 2 +- test/game/components/ball_test.dart | 2 +- test/game/components/flipper_test.dart | 2 +- test/game/components/pathway_test.dart | 2 +- test/game/components/plunger_test.dart | 2 +- test/game/components/wall_test.dart | 2 +- test/game/pinball_game_test.dart | 2 +- test/helpers/builders.dart | 13 +++---------- test/helpers/extensions.dart | 12 ++++++++++++ test/helpers/helpers.dart | 8 ++++---- 10 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 test/helpers/extensions.dart diff --git a/test/game/components/anchor_test.dart b/test/game/components/anchor_test.dart index e0cfd645..49721947 100644 --- a/test/game/components/anchor_test.dart +++ b/test/game/components/anchor_test.dart @@ -11,7 +11,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Anchor', () { - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); flameTester.test( 'loads correctly', diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 98e0d3fb..b2d11cef 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -13,7 +13,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Ball', () { - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); flameTester.test( 'loads correctly', diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 6537ee54..941090ea 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -12,7 +12,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); group( 'Flipper', () { diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 482f6d57..43af9b77 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -10,7 +10,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); group('Pathway', () { const width = 50.0; diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 17bc275e..68d02160 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -10,7 +10,7 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); group('Plunger', () { flameTester.test( diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index c19f20e8..6d792ea4 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -32,7 +32,7 @@ void main() { }, ); }); - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); flameTester.test( 'loads correctly', diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 34493cc7..3048308b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -10,7 +10,7 @@ import '../helpers/helpers.dart'; void main() { group('PinballGame', () { TestWidgetsFlutterBinding.ensureInitialized(); - final flameTester = FlameTester(PinballGameX.initial); + final flameTester = FlameTester(PinballGameTest.create); // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart index 8ae3c546..c77e55c5 100644 --- a/test/helpers/builders.dart +++ b/test/helpers/builders.dart @@ -1,13 +1,14 @@ import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; -import 'package:pinball_theme/pinball_theme.dart'; + +import 'helpers.dart'; FlameTester flameBlocTester({ required GameBloc gameBloc, }) { return FlameTester( - PinballGameX.initial, + PinballGameTest.create, pumpWidget: (gameWidget, tester) async { await tester.pumpWidget( BlocProvider.value( @@ -18,11 +19,3 @@ FlameTester flameBlocTester({ }, ); } - -extension PinballGameX on PinballGame { - static PinballGame initial() => PinballGame( - theme: const PinballTheme( - characterTheme: DashTheme(), - ), - ); -} diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart new file mode 100644 index 00000000..a976abd9 --- /dev/null +++ b/test/helpers/extensions.dart @@ -0,0 +1,12 @@ +import 'package:pinball/game/game.dart'; +import 'package:pinball_theme/pinball_theme.dart'; + +/// [PinballGame] extension to reduce boilerplate in tests. +extension PinballGameTest on PinballGame { + /// Create [PinballGame] with default [PinballTheme]. + static PinballGame create() => PinballGame( + theme: const PinballTheme( + characterTheme: DashTheme(), + ), + ); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index c2c1cd36..88b9c04d 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1,11 +1,11 @@ -// Copyright (c) 2021, Very Good Ventures -// https://verygood.ventures // +// Copyright (c) 2021, Very Good Ventures // Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. - +// https://verygood.ventures +// license that can be found in the LICENSE file or at export 'builders.dart'; +export 'extensions.dart'; export 'key_testers.dart'; export 'mocks.dart'; export 'pump_app.dart'; From e9d2d13bb3718dc5a2535c3e9d1365d1fee2cb77 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Fri, 11 Mar 2022 09:03:33 +0000 Subject: [PATCH 3/5] chore: removed unecessary end callback (#30) --- lib/game/components/score_points.dart | 5 ----- lib/game/components/wall.dart | 3 --- 2 files changed, 8 deletions(-) diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index c6474b16..b4ad1bdf 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -22,9 +22,4 @@ class BallScorePointsCallback extends ContactCallback { ) { ball.gameRef.read().add(Scored(points: scorePoints.points)); } - - // TODO(alestiago): remove once this issue is closed. - // https://github.com/flame-engine/flame/issues/1414 - @override - void end(Ball _, ScorePoints __, Contact ___) {} } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index f5a15af5..c307aad4 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -63,7 +63,4 @@ class BottomWallBallContactCallback extends ContactCallback { void begin(Ball ball, BottomWall wall, Contact contact) { ball.lost(); } - - @override - void end(_, __, ___) {} } From d0756e0b60058583cfa0d49ae7e22b7a62dc5d5b Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 11 Mar 2022 09:09:37 -0300 Subject: [PATCH 4/5] feat: adding ball spawning upon click on debug mode (#28) * feat: adding ball spawming upon click on debug mode * PR suggestions * fix: coverage * fix: rebase * feat: rebase fixes --- lib/game/components/ball.dart | 2 +- lib/game/pinball_game.dart | 9 +++++ lib/game/view/pinball_game_page.dart | 14 ++++++- test/game/pinball_game_test.dart | 20 +++++++++ test/game/view/pinball_game_page_test.dart | 47 +++++++++++++++++++++- test/helpers/extensions.dart | 10 +++++ test/helpers/mocks.dart | 5 +++ 7 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 20aa924e..d1721927 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -53,7 +53,7 @@ class Ball extends PositionBodyComponent { final bloc = gameRef.read()..add(const BallLost()); - final shouldBallRespwan = !bloc.state.isLastBall; + final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver; if (shouldBallRespwan) { gameRef.spawnBall(); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 5854422c..7d6fba41 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -105,3 +105,12 @@ class PinballGame extends Forge2DGame ); } } + +class DebugPinballGame extends PinballGame with TapDetector { + DebugPinballGame({required PinballTheme theme}) : super(theme: theme); + + @override + void onTapUp(TapUpInfo info) { + add(Ball(position: info.eventPosition.game)); + } +} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 95997832..4af2168e 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,6 +1,7 @@ // ignore_for_file: public_member_api_docs import 'package:flame/game.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; @@ -29,9 +30,15 @@ class PinballGamePage extends StatelessWidget { } class PinballGameView extends StatefulWidget { - const PinballGameView({Key? key, required this.theme}) : super(key: key); + const PinballGameView({ + Key? key, + required this.theme, + bool isDebugMode = kDebugMode, + }) : _isDebugMode = isDebugMode, + super(key: key); final PinballTheme theme; + final bool _isDebugMode; @override State createState() => _PinballGameViewState(); @@ -47,7 +54,10 @@ class _PinballGameViewState extends State { // TODO(erickzanardo): Revisit this when we start to have more assets // this could expose a Stream (maybe even a cubit?) so we could show the // the loading progress with some fancy widgets. - _game = PinballGame(theme: widget.theme)..preLoadAssets(); + _game = (widget._isDebugMode + ? DebugPinballGame(theme: widget.theme) + : PinballGame(theme: widget.theme)) + ..preLoadAssets(); } @override diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 3048308b..f1d6bb32 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -3,6 +3,7 @@ import 'package:flame/components.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import '../helpers/helpers.dart'; @@ -11,6 +12,7 @@ void main() { group('PinballGame', () { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); + final debugModeFlameTester = FlameTester(DebugPinballGameTest.create); // TODO(alestiago): test if [PinballGame] registers // [BallScorePointsCallback] once the following issue is resolved: @@ -49,6 +51,24 @@ void main() { ); }, ); + + 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); + + game.onTapUp(tapUpEvent); + await game.ready(); + + expect( + game.children.whereType().length, + equals(1), + ); + }); }); }, ); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index fcfbe149..0e3ee963 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer_const_constructors + import 'package:bloc_test/bloc_test.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; @@ -20,7 +22,7 @@ void main() { ); await tester.pumpApp( - const PinballGamePage(theme: theme), + PinballGamePage(theme: theme), gameBloc: gameBloc, ); expect(find.byType(PinballGameView), findsOneWidget); @@ -64,9 +66,10 @@ void main() { ); await tester.pumpApp( - const PinballGameView(theme: theme), + PinballGameView(theme: theme), gameBloc: gameBloc, ); + expect( find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, @@ -100,5 +103,45 @@ void main() { ); }, ); + + testWidgets('renders the real game when not in debug mode', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp( + const PinballGameView(theme: theme, isDebugMode: false), + gameBloc: gameBloc, + ); + expect( + find.byWidgetPredicate( + (w) => w is GameWidget && w.game is! DebugPinballGame, + ), + findsOneWidget, + ); + }); + + testWidgets('renders the debug game when on debug mode', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp( + const PinballGameView(theme: theme), + gameBloc: gameBloc, + ); + expect( + find.byWidgetPredicate( + (w) => w is GameWidget && w.game is DebugPinballGame, + ), + findsOneWidget, + ); + }); }); } diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index a976abd9..2a0a7e59 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -10,3 +10,13 @@ extension PinballGameTest on PinballGame { ), ); } + +/// [DebugPinballGame] extension to reduce boilerplate in tests. +extension DebugPinballGameTest on DebugPinballGame { + /// Create [PinballGame] with default [PinballTheme]. + static DebugPinballGame create() => DebugPinballGame( + theme: const PinballTheme( + characterTheme: DashTheme(), + ), + ); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 44e78afe..46886752 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,3 +1,4 @@ +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -32,3 +33,7 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { return super.toString(); } } + +class MockTapUpInfo extends Mock implements TapUpInfo {} + +class MockEventPosition extends Mock implements EventPosition {} From 07d16fbac00f370ec411e19b3b879ee596f81ab8 Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 11 Mar 2022 09:42:16 -0300 Subject: [PATCH 5/5] feat: adding bonus logic to the game bloc (#24) * feat: adding bonus logic to the game bloc * feat: PR suggestions * Apply suggestions from code review Co-authored-by: Alejandro Santiago * feat: pr suggestions * chore: main rebase * feat: pr suggestions * Apply suggestions from code review Co-authored-by: Alejandro Santiago * feat: pr suggestion * feat: pr suggestions * feat: pr suggestions * Apply suggestions from code review Co-authored-by: Alejandro Santiago * feat: pr suggestions Co-authored-by: Alejandro Santiago --- lib/game/bloc/game_bloc.dart | 30 +++-- lib/game/bloc/game_event.dart | 10 +- lib/game/bloc/game_state.dart | 29 ++++- test/game/bloc/game_bloc_test.dart | 129 ++++++++++++++++----- test/game/bloc/game_event_test.dart | 20 +++- test/game/bloc/game_state_test.dart | 54 ++++++--- test/game/components/ball_test.dart | 3 +- test/game/view/game_hud_test.dart | 7 +- test/game/view/pinball_game_page_test.dart | 8 +- 9 files changed, 224 insertions(+), 66 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 31aa0498..74685215 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -14,6 +14,8 @@ class GameBloc extends Bloc { on(_onBonusLetterActivated); } + static const bonusWord = 'GOOGLE'; + void _onBallLost(BallLost event, Emitter emit) { if (state.balls > 0) { emit(state.copyWith(balls: state.balls - 1)); @@ -27,13 +29,25 @@ class GameBloc extends Bloc { } void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { - emit( - state.copyWith( - bonusLetters: [ - ...state.bonusLetters, - event.letter, - ], - ), - ); + final newBonusLetters = [ + ...state.activatedBonusLetters, + event.letterIndex, + ]; + + if (newBonusLetters.length == bonusWord.length) { + emit( + state.copyWith( + activatedBonusLetters: [], + bonusHistory: [ + ...state.bonusHistory, + GameBonus.word, + ], + ), + ); + } else { + emit( + state.copyWith(activatedBonusLetters: newBonusLetters), + ); + } } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index fa57cbff..0edc91ab 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -34,10 +34,14 @@ class Scored extends GameEvent { } class BonusLetterActivated extends GameEvent { - const BonusLetterActivated(this.letter); + const BonusLetterActivated(this.letterIndex) + : assert( + letterIndex < GameBloc.bonusWord.length, + 'Index must be smaller than the length of the word', + ); - final String letter; + final int letterIndex; @override - List get props => [letter]; + List get props => [letterIndex]; } diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index f8456518..2812a049 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -2,6 +2,13 @@ part of 'game_bloc.dart'; +/// Defines bonuses that a player can gain during a PinballGame. +enum GameBonus { + /// Bonus achieved when the user activate all of the bonus + /// letters on the board, forming the bonus word + word, +} + /// {@template game_state} /// Represents the state of the pinball game. /// {@endtemplate} @@ -10,14 +17,16 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, - required this.bonusLetters, + required this.activatedBonusLetters, + required this.bonusHistory, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); const GameState.initial() : score = 0, balls = 3, - bonusLetters = const []; + activatedBonusLetters = const [], + bonusHistory = const []; /// The current score of the game. final int score; @@ -28,7 +37,11 @@ class GameState extends Equatable { final int balls; /// Active bonus letters. - final List bonusLetters; + final List activatedBonusLetters; + + /// 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; @@ -39,7 +52,8 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, - List? bonusLetters, + List? activatedBonusLetters, + List? bonusHistory, }) { assert( score == null || score >= this.score, @@ -49,7 +63,9 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, - bonusLetters: bonusLetters ?? this.bonusLetters, + activatedBonusLetters: + activatedBonusLetters ?? this.activatedBonusLetters, + bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -57,6 +73,7 @@ class GameState extends Equatable { List get props => [ score, balls, - bonusLetters, + activatedBonusLetters, + bonusHistory, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index bd669397..ad1d6c55 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,9 +21,24 @@ void main() { } }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetters: []), - const GameState(score: 0, balls: 1, bonusLetters: []), - const GameState(score: 0, balls: 0, bonusLetters: []), + const GameState( + score: 0, + balls: 2, + activatedBonusLetters: [], + bonusHistory: [], + ), + const GameState( + score: 0, + balls: 1, + activatedBonusLetters: [], + bonusHistory: [], + ), + const GameState( + score: 0, + balls: 0, + activatedBonusLetters: [], + bonusHistory: [], + ), ], ); }); @@ -37,8 +52,18 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState(score: 2, balls: 3, bonusLetters: []), - const GameState(score: 5, balls: 3, bonusLetters: []), + const GameState( + score: 2, + balls: 3, + activatedBonusLetters: [], + bonusHistory: [], + ), + const GameState( + score: 5, + balls: 3, + activatedBonusLetters: [], + bonusHistory: [], + ), ], ); @@ -53,9 +78,24 @@ void main() { bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetters: []), - const GameState(score: 0, balls: 1, bonusLetters: []), - const GameState(score: 0, balls: 0, bonusLetters: []), + const GameState( + score: 0, + balls: 2, + activatedBonusLetters: [], + bonusHistory: [], + ), + const GameState( + score: 0, + balls: 1, + activatedBonusLetters: [], + bonusHistory: [], + ), + const GameState( + score: 0, + balls: 0, + activatedBonusLetters: [], + bonusHistory: [], + ), ], ); }); @@ -65,42 +105,77 @@ void main() { 'adds the letter to the state', build: GameBloc.new, act: (bloc) => bloc - ..add(const BonusLetterActivated('G')) - ..add(const BonusLetterActivated('O')) - ..add(const BonusLetterActivated('O')) - ..add(const BonusLetterActivated('G')) - ..add(const BonusLetterActivated('L')) - ..add(const BonusLetterActivated('E')), - expect: () => [ - const GameState( + ..add(const BonusLetterActivated(0)) + ..add(const BonusLetterActivated(1)) + ..add(const BonusLetterActivated(2)), + expect: () => const [ + GameState( score: 0, balls: 3, - bonusLetters: ['G'], + activatedBonusLetters: [0], + bonusHistory: [], ), - const GameState( + GameState( score: 0, balls: 3, - bonusLetters: ['G', 'O'], + activatedBonusLetters: [0, 1], + bonusHistory: [], ), - const GameState( + GameState( score: 0, balls: 3, - bonusLetters: ['G', 'O', 'O'], + activatedBonusLetters: [0, 1, 2], + bonusHistory: [], ), - const GameState( + ], + ); + + blocTest( + 'adds the bonus when the bonusWord is completed', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const BonusLetterActivated(0)) + ..add(const BonusLetterActivated(1)) + ..add(const BonusLetterActivated(2)) + ..add(const BonusLetterActivated(3)) + ..add(const BonusLetterActivated(4)) + ..add(const BonusLetterActivated(5)), + expect: () => const [ + GameState( score: 0, balls: 3, - bonusLetters: ['G', 'O', 'O', 'G'], + activatedBonusLetters: [0], + bonusHistory: [], ), - const GameState( + GameState( score: 0, balls: 3, - bonusLetters: ['G', 'O', 'O', 'G', 'L'], + activatedBonusLetters: [0, 1], + bonusHistory: [], ), - const GameState( + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [0, 1, 2], + bonusHistory: [], + ), + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [0, 1, 2, 3], + bonusHistory: [], + ), + GameState( + score: 0, + balls: 3, + activatedBonusLetters: [0, 1, 2, 3, 4], + bonusHistory: [], + ), + GameState( score: 0, balls: 3, - bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'], + activatedBonusLetters: [], + bonusHistory: [GameBonus.word], ), ], ); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index 0e7a0f71..d6d2278b 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -43,19 +43,29 @@ void main() { group('BonusLetterActivated', () { test('can be instantiated', () { - expect(const BonusLetterActivated('A'), isNotNull); + expect(const BonusLetterActivated(0), isNotNull); }); test('supports value equality', () { expect( - BonusLetterActivated('A'), - equals(BonusLetterActivated('A')), + BonusLetterActivated(0), + equals(BonusLetterActivated(0)), ); expect( - BonusLetterActivated('B'), - isNot(equals(BonusLetterActivated('A'))), + BonusLetterActivated(0), + isNot(equals(BonusLetterActivated(1))), ); }); + + test( + 'throws assertion error if index is bigger than the word length', + () { + expect( + () => BonusLetterActivated(8), + throwsAssertionError, + ); + }, + ); }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 7345d3bd..7b060984 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -10,13 +10,15 @@ void main() { GameState( score: 0, balls: 0, - bonusLetters: const [], + activatedBonusLetters: const [], + bonusHistory: const [], ), equals( const GameState( score: 0, balls: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ), ), ); @@ -25,7 +27,12 @@ void main() { group('constructor', () { test('can be instantiated', () { expect( - const GameState(score: 0, balls: 0, bonusLetters: []), + const GameState( + score: 0, + balls: 0, + activatedBonusLetters: [], + bonusHistory: [], + ), isNotNull, ); }); @@ -36,7 +43,12 @@ void main() { 'when balls are negative', () { expect( - () => GameState(balls: -1, score: 0, bonusLetters: const []), + () => GameState( + balls: -1, + score: 0, + activatedBonusLetters: const [], + bonusHistory: const [], + ), throwsAssertionError, ); }, @@ -47,7 +59,12 @@ void main() { 'when score is negative', () { expect( - () => GameState(balls: 0, score: -1, bonusLetters: const []), + () => GameState( + balls: 0, + score: -1, + activatedBonusLetters: const [], + bonusHistory: const [], + ), throwsAssertionError, ); }, @@ -60,7 +77,8 @@ void main() { const gameState = GameState( balls: 0, score: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect(gameState.isGameOver, isTrue); }); @@ -71,7 +89,8 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect(gameState.isGameOver, isFalse); }); @@ -85,7 +104,8 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect(gameState.isLastBall, isTrue); }, @@ -98,7 +118,8 @@ void main() { const gameState = GameState( balls: 2, score: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect(gameState.isLastBall, isFalse); }, @@ -113,7 +134,8 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect( () => gameState.copyWith(score: gameState.score - 1), @@ -129,7 +151,8 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); expect( gameState.copyWith(), @@ -145,12 +168,14 @@ void main() { const gameState = GameState( score: 2, balls: 0, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, - bonusLetters: const ['A'], + activatedBonusLetters: const [0], + bonusHistory: const [GameBonus.word], ); expect(gameState, isNot(equals(otherGameState))); @@ -158,7 +183,8 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, - bonusLetters: otherGameState.bonusLetters, + activatedBonusLetters: otherGameState.activatedBonusLetters, + bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index b2d11cef..0b115055 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -133,7 +133,8 @@ void main() { initialState: const GameState( score: 10, balls: 1, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ), ); await game.ready(); diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index e7334e41..536edbad 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -9,7 +9,12 @@ import '../../helpers/helpers.dart'; void main() { group('GameHud', () { late GameBloc gameBloc; - const initialState = GameState(score: 10, balls: 2, bonusLetters: []); + const initialState = GameState( + score: 10, + balls: 2, + activatedBonusLetters: [], + bonusHistory: [], + ); void _mockState(GameState state) { whenListen( diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0e3ee963..9de36cde 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -84,7 +84,13 @@ void main() { 'renders a game over dialog when the user has lost', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0, bonusLetters: []); + const state = GameState( + score: 0, + balls: 0, + activatedBonusLetters: [], + bonusHistory: [], + ); + whenListen( gameBloc, Stream.value(state),