From 403d997f1737250487280ab49f387e8e3002748e Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 2 Mar 2022 20:11:00 +0000 Subject: [PATCH 01/14] refactor: renamed method parameter hasPoints to scorePoints (#8) --- lib/game/components/score_points.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/game/components/score_points.dart b/lib/game/components/score_points.dart index 02506c8c..c6474b16 100644 --- a/lib/game/components/score_points.dart +++ b/lib/game/components/score_points.dart @@ -17,10 +17,10 @@ class BallScorePointsCallback extends ContactCallback { @override void begin( Ball ball, - ScorePoints hasPoints, + ScorePoints scorePoints, Contact _, ) { - ball.gameRef.read().add(Scored(points: hasPoints.points)); + ball.gameRef.read().add(Scored(points: scorePoints.points)); } // TODO(alestiago): remove once this issue is closed. From 518e183ad89920a55bd47802381ba22b7b2704bb Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:18:25 -0300 Subject: [PATCH 02/14] feat: game over mechanics --- lib/game/components/ball.dart | 16 ++++ lib/game/components/components.dart | 1 + lib/game/components/wall.dart | 24 +++++ lib/game/game.dart | 2 +- lib/game/pinball_game.dart | 39 ++++++++ lib/game/view/pinball_game_page.dart | 13 ++- lib/game/view/pinball_game_view.dart | 33 +++++++ lib/game/view/view.dart | 2 + test/game/components/wall_test.dart | 81 ++++++++++++++++ test/game/pinball_game_test.dart | 106 ++++++++++++++++++++- test/game/view/pinball_game_page_test.dart | 42 +++++++- test/game/view/pinball_game_view_test.dart | 46 +++++++++ test/helpers/pump_app.dart | 14 ++- 13 files changed, 405 insertions(+), 14 deletions(-) create mode 100644 lib/game/components/wall.dart create mode 100644 lib/game/view/pinball_game_view.dart create mode 100644 lib/game/view/view.dart create mode 100644 test/game/components/wall_test.dart create mode 100644 test/game/view/pinball_game_view_test.dart diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 8ff24094..3aa978d3 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -6,6 +6,7 @@ import 'package:pinball/game/game.dart'; class Ball extends BodyComponent with BlocComponent { + Ball({ required Vector2 position, }) : _position = position { @@ -28,4 +29,19 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } + + @override + void onRemove() { + final bloc = gameRef.read(); + + final shouldBallrespwan = bloc.state.balls > 1; + + bloc.add(const BallLost()); + + if (shouldBallrespwan) { + gameRef.resetBall(); + } + + super.onRemove(); + } } diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index e3d2c6ce..52e460a5 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,2 +1,3 @@ export 'ball.dart'; export 'score_points.dart'; +export 'wall.dart'; diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart new file mode 100644 index 00000000..de93e1ce --- /dev/null +++ b/lib/game/components/wall.dart @@ -0,0 +1,24 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +class Wall extends BodyComponent { + Wall(this.start, this.end); + + final Vector2 start; + final Vector2 end; + + @override + Body createBody() { + final shape = EdgeShape()..set(start, end); + + final fixtureDef = FixtureDef(shape) + ..restitution = 0.0 + ..friction = 0.3; + + final bodyDef = BodyDef() + ..userData = this + ..position = Vector2.zero() + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/lib/game/game.dart b/lib/game/game.dart index e2e5361f..253dcc9f 100644 --- a/lib/game/game.dart +++ b/lib/game/game.dart @@ -1,4 +1,4 @@ export 'bloc/game_bloc.dart'; export 'components/components.dart'; export 'pinball_game.dart'; -export 'view/pinball_game_page.dart'; +export 'view/view.dart'; diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index c4977435..0c160316 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,10 +1,49 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/forge2d_game.dart'; import 'package:pinball/game/game.dart'; +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + ball.shouldRemove = true; + } + + @override + void end(_, __, ___) {} +} + class PinballGame extends Forge2DGame with FlameBloc { + void resetBall() { + add(Ball(position: ballStartingPosition)); + } + + late final ballStartingPosition = screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 2, + camera.viewport.effectiveSize.y - 20, + ), + ) - + Vector2( + 0, + -20, + ); + @override Future onLoad() async { + await super.onLoad(); addContactCallback(BallScorePointsCallback()); + + final topLeft = Vector2.zero(); + final bottomRight = screenToWorld(camera.viewport.effectiveSize); + final bottomLeft = Vector2(topLeft.x, bottomRight.y); + + await add( + Wall(bottomRight, bottomLeft), + ); + addContactCallback(BallWallContactCallback()); + + resetBall(); } } diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index cfe8d5bc..c4387f34 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,16 +1,23 @@ -import 'package:flame/game.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; class PinballGamePage extends StatelessWidget { const PinballGamePage({Key? key}) : super(key: key); static Route route() { - return MaterialPageRoute(builder: (_) => const PinballGamePage()); + return MaterialPageRoute( + builder: (_) { + return BlocProvider( + create: (_) => GameBloc(), + child: const PinballGamePage(), + ); + }, + ); } @override Widget build(BuildContext context) { - return GameWidget(game: PinballGame()); + return const PinballGameView(); } } diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart new file mode 100644 index 00000000..5fbc8ae8 --- /dev/null +++ b/lib/game/view/pinball_game_view.dart @@ -0,0 +1,33 @@ +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +class PinballGameView extends StatelessWidget { + const PinballGameView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.isGameOver) { + showDialog( + context: context, + builder: (_) { + return const Dialog( + child: SizedBox( + width: 200, + height: 200, + child: Center( + child: Text('Game Over'), + ), + ), + ); + }, + ); + } + }, + child: GameWidget(game: PinballGame()), + ); + } +} diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart new file mode 100644 index 00000000..66134941 --- /dev/null +++ b/lib/game/view/view.dart @@ -0,0 +1,2 @@ +export 'pinball_game_page.dart'; +export 'pinball_game_view.dart'; diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart new file mode 100644 index 00000000..e17537c0 --- /dev/null +++ b/test/game/components/wall_test.dart @@ -0,0 +1,81 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Wall', () { + final flameTester = FlameTester(PinballGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(game.contains(wall), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + game.contains(wall); + + expect(wall.body.position, Vector2.zero()); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(wall.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('first fixture', () { + flameTester.test( + 'exists', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + expect(wall.body.fixtures[0], isA()); + }, + ); + + flameTester.test( + 'has restitution equals 0', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + final fixture = wall.body.fixtures[0]; + expect(fixture.restitution, equals(0)); + }, + ); + + flameTester.test( + 'has friction', + (game) async { + final wall = Wall(Vector2.zero(), Vector2(100, 0)); + await game.ensureAdd(wall); + + final fixture = wall.body.fixtures[0]; + expect(fixture.friction, greaterThan(0)); + }, + ); + }); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 75a77aa9..a576252b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,9 +1,109 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/game.dart'; + +class MockPinballGame extends Mock implements PinballGame {} + +class MockWall extends Mock implements Wall {} + +class MockBall extends Mock implements Ball {} + +class MockContact extends Mock implements Contact {} + +class MockGameBloc extends Mock implements GameBloc {} void main() { + // TODO(alestiago): test if [PinballGame] registers + // [BallScorePointsCallback] once the following issue is resolved: + // https://github.com/flame-engine/flame/issues/1416 group('PinballGame', () { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 + group('BallWallContactCallback', () { + test('removes the ball on begin contact', () { + final game = MockPinballGame(); + final wall = MockWall(); + final ball = MockBall(); + + when(() => ball.gameRef).thenReturn(game); + + BallWallContactCallback() + // Remove once https://github.com/flame-engine/flame/pull/1415 + // is merged + ..end(MockBall(), MockWall(), MockContact()) + ..begin(ball, wall, MockContact()); + + verify(() => ball.shouldRemove = true).called(1); + }); + }); + + group('resetting a ball', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + FlameTester( + PinballGame.new, + pumpWidget: (gameWidget, tester) async { + await tester.pumpWidget( + BlocProvider.value( + value: gameBloc, + child: gameWidget, + ), + ); + }, + ) + ..widgetTest('adds BallLost to GameBloc', (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + verify(() => gameBloc.add(const BallLost())).called(1); + }) + ..widgetTest( + 'resets the ball if the game is not over', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await game.ready(); // Making sure that all additions are done + + expect( + game.children.whereType().length, + equals(1), + ); + }, + ) + ..widgetTest( + 'no ball is added on game over', + (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState(score: 10, balls: 1), + ); + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + expect( + game.children.whereType().length, + equals(0), + ); + }, + ); + }); }); } diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 955ce763..2e005773 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -1,4 +1,5 @@ -import 'package:flame/game.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -6,9 +7,42 @@ import '../../helpers/helpers.dart'; void main() { group('PinballGamePage', () { - testWidgets('renders single GameWidget with PinballGame', (tester) async { - await tester.pumpApp(const PinballGamePage()); - expect(find.byType(GameWidget), findsOneWidget); + testWidgets('renders PinballGameView', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGamePage(), gameBloc: gameBloc); + expect(find.byType(PinballGameView), findsOneWidget); + }); + + testWidgets('route returns a valid navigation route', (tester) async { + await tester.pumpApp( + Scaffold( + body: Builder( + builder: (context) { + return ElevatedButton( + onPressed: () { + Navigator.of(context).push(PinballGamePage.route()); + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Tap me')); + + // We can't use pumpAndSettle here because the page renders a Flame game + // which is an infinity animation, so it will timeout + await tester.pump(); // Runs the button action + await tester.pump(); // Runs the navigation + + expect(find.byType(PinballGamePage), findsOneWidget); }); }); } diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart new file mode 100644 index 00000000..c00db60f --- /dev/null +++ b/test/game/view/pinball_game_view_test.dart @@ -0,0 +1,46 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('PinballGameView', () { + testWidgets('renders', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + expect( + find.byWidgetPredicate((w) => w is GameWidget), + findsOneWidget, + ); + }); + + testWidgets( + 'renders a game over dialog when the user has lost', + (tester) async { + final gameBloc = MockGameBloc(); + const state = GameState(score: 0, balls: 0); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + await tester.pump(); + + expect( + find.text('Game Over'), + findsOneWidget, + ); + }, + ); + }); +} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 97ca4590..ed5d4b1c 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -6,15 +6,20 @@ // https://opensource.org/licenses/MIT. import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; +class MockGameBloc extends Mock implements GameBloc {} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { MockNavigator? navigator, + GameBloc? gameBloc, }) { return pumpWidget( MaterialApp( @@ -23,9 +28,12 @@ extension PumpApp on WidgetTester { GlobalMaterialLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - home: navigator != null - ? MockNavigatorProvider(navigator: navigator, child: widget) - : widget, + home: BlocProvider.value( + value: gameBloc ?? MockGameBloc(), + child: navigator != null + ? MockNavigatorProvider(navigator: navigator, child: widget) + : widget, + ), ), ); } From ad6afddf155125682c826c0fabe8108eda373b55 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:24:45 -0300 Subject: [PATCH 03/14] fix: rebasing issues --- lib/game/components/ball.dart | 1 - lib/game/pinball_game.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 3aa978d3..39257bb6 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -6,7 +6,6 @@ import 'package:pinball/game/game.dart'; class Ball extends BodyComponent with BlocComponent { - Ball({ required Vector2 position, }) : _position = position { diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 0c160316..2ee242da 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,7 +1,7 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame_bloc/flame_bloc.dart'; -import 'package:flame_forge2d/forge2d_game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; class BallWallContactCallback extends ContactCallback { From 8865c2f32e8ade17ca12eb8ce2b2c3c56f6f7c7f Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 2 Mar 2022 18:35:42 -0300 Subject: [PATCH 04/14] fix: lint --- test/game/pinball_game_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index a576252b..582fc11b 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,5 +1,4 @@ import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/game.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; From fbbd04d91b482552c1c9017ecfb0171579674ba2 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 09:16:45 -0300 Subject: [PATCH 05/14] Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- test/game/view/pinball_game_view_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart index c00db60f..4f4fe246 100644 --- a/test/game/view/pinball_game_view_test.dart +++ b/test/game/view/pinball_game_view_test.dart @@ -7,7 +7,7 @@ import '../../helpers/helpers.dart'; void main() { group('PinballGameView', () { - testWidgets('renders', (tester) async { + testWidgets('renders game', (tester) async { final gameBloc = MockGameBloc(); whenListen( gameBloc, From dba7761e01bf9dbcd881244a7ca14ccd12340007 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 14:49:48 -0300 Subject: [PATCH 06/14] feat: addressing PR comments --- lib/game/bloc/game_state.dart | 3 + lib/game/components/ball.dart | 11 +- lib/game/components/wall.dart | 42 +++++++- lib/game/pinball_game.dart | 31 +----- lib/game/view/pinball_game_view.dart | 10 +- lib/game/view/view.dart | 1 + lib/game/view/widgets/game_over_dialog.dart | 18 ++++ lib/game/view/widgets/widgets.dart | 1 + test/game/bloc/game_state_test.dart | 26 +++++ test/game/components/ball_test.dart | 70 +++++++++++++ test/game/components/wall_test.dart | 60 +++++++++-- test/game/pinball_game_test.dart | 105 +------------------- test/helpers/builders.dart | 17 ++++ test/helpers/helpers.dart | 2 + test/helpers/mocks.dart | 13 +++ test/helpers/pump_app.dart | 2 +- 16 files changed, 259 insertions(+), 153 deletions(-) create mode 100644 lib/game/view/widgets/game_over_dialog.dart create mode 100644 lib/game/view/widgets/widgets.dart create mode 100644 test/helpers/builders.dart create mode 100644 test/helpers/mocks.dart diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 235e264d..862edf96 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -26,6 +26,9 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; + /// Determines when player has only one chance left. + bool get isLastBall => balls == 1; + GameState copyWith({ int? score, int? balls, diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 39257bb6..fd5f5605 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -29,18 +29,15 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } - @override - void onRemove() { + void ballLost() { final bloc = gameRef.read(); - final shouldBallrespwan = bloc.state.balls > 1; + final shouldBallRespwan = bloc.state.balls > 1; bloc.add(const BallLost()); - if (shouldBallrespwan) { - gameRef.resetBall(); + if (shouldBallRespwan) { + gameRef.spawnBall(); } - - super.onRemove(); } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index de93e1ce..c80e5bb3 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -1,10 +1,50 @@ +// ignore_for_file: avoid_renaming_method_parameters + import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:pinball/game/components/components.dart'; + +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + if (wall.type == WallType.bottom) { + ball + ..ballLost() + ..shouldRemove = true; + } + } + + @override + void end(_, __, ___) {} +} + +enum WallType { + top, + bottom, + left, + right, +} class Wall extends BodyComponent { - Wall(this.start, this.end); + Wall({ + required this.type, + required this.start, + required this.end, + }); + + factory Wall.bottom(Forge2DGame game) { + final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); + final bottomLeft = Vector2(0, bottomRight.y); + + return Wall( + type: WallType.bottom, + start: bottomRight, + end: bottomLeft, + ); + } final Vector2 start; final Vector2 end; + final WallType type; @override Body createBody() { diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 2ee242da..3d586f12 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,21 +1,9 @@ -// ignore_for_file: avoid_renaming_method_parameters - import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; -class BallWallContactCallback extends ContactCallback { - @override - void begin(Ball ball, Wall wall, Contact contact) { - ball.shouldRemove = true; - } - - @override - void end(_, __, ___) {} -} - class PinballGame extends Forge2DGame with FlameBloc { - void resetBall() { + void spawnBall() { add(Ball(position: ballStartingPosition)); } @@ -25,25 +13,14 @@ class PinballGame extends Forge2DGame with FlameBloc { camera.viewport.effectiveSize.y - 20, ), ) - - Vector2( - 0, - -20, - ); + Vector2(0, -20); @override Future onLoad() async { - await super.onLoad(); + spawnBall(); addContactCallback(BallScorePointsCallback()); - final topLeft = Vector2.zero(); - final bottomRight = screenToWorld(camera.viewport.effectiveSize); - final bottomLeft = Vector2(topLeft.x, bottomRight.y); - - await add( - Wall(bottomRight, bottomLeft), - ); + await add(Wall.bottom(this)); addContactCallback(BallWallContactCallback()); - - resetBall(); } } diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart index 5fbc8ae8..44715c25 100644 --- a/lib/game/view/pinball_game_view.dart +++ b/lib/game/view/pinball_game_view.dart @@ -14,15 +14,7 @@ class PinballGameView extends StatelessWidget { showDialog( context: context, builder: (_) { - return const Dialog( - child: SizedBox( - width: 200, - height: 200, - child: Center( - child: Text('Game Over'), - ), - ), - ); + return const GameOverDialog(); }, ); } diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 66134941..39cdb071 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,2 +1,3 @@ export 'pinball_game_page.dart'; export 'pinball_game_view.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart new file mode 100644 index 00000000..586d6c56 --- /dev/null +++ b/lib/game/view/widgets/game_over_dialog.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class GameOverDialog extends StatelessWidget { + const GameOverDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: SizedBox( + width: 200, + height: 200, + child: Center( + child: Text('Game Over'), + ), + ), + ); + } +} diff --git a/lib/game/view/widgets/widgets.dart b/lib/game/view/widgets/widgets.dart new file mode 100644 index 00000000..9c457b1c --- /dev/null +++ b/lib/game/view/widgets/widgets.dart @@ -0,0 +1 @@ +export 'game_over_dialog.dart'; diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index f62bae67..c5131492 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -62,6 +62,32 @@ void main() { }); }); + group('isGameOver', () { + test( + 'is true ' + 'when there is only on ball left', + () { + const gameState = GameState( + balls: 1, + score: 0, + ); + expect(gameState.isLastBall, isTrue); + }, + ); + + test( + 'is false ' + 'when there are more balls left', + () { + const gameState = GameState( + balls: 2, + score: 0, + ); + expect(gameState.isLastBall, isFalse); + }, + ); + }); + group('copyWith', () { test( 'throws AssertionError ' diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index c4576c68..8521a80f 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -1,10 +1,14 @@ // ignore_for_file: cascade_invocations +import 'package:bloc_test/bloc_test.dart'; import 'package:flame_forge2d/flame_forge2d.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'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -79,5 +83,71 @@ void main() { }, ); }); + + group('resetting a ball', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final tester = flameBlocTester( + gameBlocBuilder: () { + return gameBloc; + }, + ); + + tester.widgetTest( + 'adds BallLost to GameBloc', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.ballLost(); + await tester.pump(); + + verify(() => gameBloc.add(const BallLost())).called(1); + }, + ); + + tester.widgetTest( + 'resets the ball if the game is not over', + (game, tester) async { + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await game.ready(); // Making sure that all additions are done + + expect( + game.children.whereType().length, + equals(1), + ); + }, + ); + + tester.widgetTest( + 'no ball is added on game over', + (game, tester) async { + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState(score: 10, balls: 1), + ); + await game.ready(); + + game.children.whereType().first.removeFromParent(); + await tester.pump(); + + expect( + game.children.whereType().length, + equals(0), + ); + }, + ); + }); }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index e17537c0..2c4b241a 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -3,18 +3,46 @@ import 'package:flame_forge2d/flame_forge2d.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'; + void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Wall', () { + group('BallWallContactCallback', () { + test( + 'removes the ball on begin contact when the wall is a bottom one', + () { + final game = MockPinballGame(); + final wall = MockWall(); + final ball = MockBall(); + + when(() => wall.type).thenReturn(WallType.bottom); + when(() => ball.gameRef).thenReturn(game); + + BallWallContactCallback() + // Remove once https://github.com/flame-engine/flame/pull/1415 + // is merged + ..end(MockBall(), MockWall(), MockContact()) + ..begin(ball, wall, MockContact()); + + verify(() => ball.shouldRemove = true).called(1); + }, + ); + }); final flameTester = FlameTester(PinballGame.new); flameTester.test( 'loads correctly', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.bottom, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(game.contains(wall), isTrue); @@ -25,7 +53,11 @@ void main() { flameTester.test( 'positions correctly', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); game.contains(wall); @@ -36,7 +68,11 @@ void main() { flameTester.test( 'is static', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(wall.body.bodyType, equals(BodyType.static)); @@ -48,7 +84,11 @@ void main() { flameTester.test( 'exists', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); expect(wall.body.fixtures[0], isA()); @@ -58,7 +98,11 @@ void main() { flameTester.test( 'has restitution equals 0', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; @@ -69,7 +113,11 @@ void main() { flameTester.test( 'has friction', (game) async { - final wall = Wall(Vector2.zero(), Vector2(100, 0)); + final wall = Wall( + type: WallType.top, + start: Vector2.zero(), + end: Vector2(100, 0), + ); await game.ensureAdd(wall); final fixture = wall.body.fixtures[0]; diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 582fc11b..75a77aa9 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,108 +1,9 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flame_test/flame_test.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockingjay/mockingjay.dart'; -import 'package:pinball/game/game.dart'; - -class MockPinballGame extends Mock implements PinballGame {} - -class MockWall extends Mock implements Wall {} - -class MockBall extends Mock implements Ball {} - -class MockContact extends Mock implements Contact {} - -class MockGameBloc extends Mock implements GameBloc {} void main() { - // TODO(alestiago): test if [PinballGame] registers - // [BallScorePointsCallback] once the following issue is resolved: - // https://github.com/flame-engine/flame/issues/1416 group('PinballGame', () { - group('BallWallContactCallback', () { - test('removes the ball on begin contact', () { - final game = MockPinballGame(); - final wall = MockWall(); - final ball = MockBall(); - - when(() => ball.gameRef).thenReturn(game); - - BallWallContactCallback() - // Remove once https://github.com/flame-engine/flame/pull/1415 - // is merged - ..end(MockBall(), MockWall(), MockContact()) - ..begin(ball, wall, MockContact()); - - verify(() => ball.shouldRemove = true).called(1); - }); - }); - - group('resetting a ball', () { - late GameBloc gameBloc; - - setUp(() { - gameBloc = MockGameBloc(); - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState.initial(), - ); - }); - - FlameTester( - PinballGame.new, - pumpWidget: (gameWidget, tester) async { - await tester.pumpWidget( - BlocProvider.value( - value: gameBloc, - child: gameWidget, - ), - ); - }, - ) - ..widgetTest('adds BallLost to GameBloc', (game, tester) async { - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await tester.pump(); - - verify(() => gameBloc.add(const BallLost())).called(1); - }) - ..widgetTest( - 'resets the ball if the game is not over', - (game, tester) async { - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await game.ready(); // Making sure that all additions are done - - expect( - game.children.whereType().length, - equals(1), - ); - }, - ) - ..widgetTest( - 'no ball is added on game over', - (game, tester) async { - whenListen( - gameBloc, - const Stream.empty(), - initialState: const GameState(score: 10, balls: 1), - ); - await game.ready(); - - game.children.whereType().first.removeFromParent(); - await tester.pump(); - - expect( - game.children.whereType().length, - equals(0), - ); - }, - ); - }); + // TODO(alestiago): test if [PinballGame] registers + // [BallScorePointsCallback] once the following issue is resolved: + // https://github.com/flame-engine/flame/issues/1416 }); } diff --git a/test/helpers/builders.dart b/test/helpers/builders.dart new file mode 100644 index 00000000..5ef98226 --- /dev/null +++ b/test/helpers/builders.dart @@ -0,0 +1,17 @@ +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +FlameTester flameBlocTester({required GameBloc Function() gameBlocBuilder}) { + return FlameTester( + PinballGame.new, + pumpWidget: (gameWidget, tester) async { + await tester.pumpWidget( + BlocProvider.value( + value: gameBlocBuilder(), + child: gameWidget, + ), + ); + }, + ); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 695f8309..97bc22be 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -5,4 +5,6 @@ // license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +export 'builders.dart'; +export 'mocks.dart'; export 'pump_app.dart'; diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart new file mode 100644 index 00000000..559ea87a --- /dev/null +++ b/test/helpers/mocks.dart @@ -0,0 +1,13 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pinball/game/game.dart'; + +class MockPinballGame extends Mock implements PinballGame {} + +class MockWall extends Mock implements Wall {} + +class MockBall extends Mock implements Ball {} + +class MockContact extends Mock implements Contact {} + +class MockGameBloc extends Mock implements GameBloc {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index ed5d4b1c..2c1efd9f 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -13,7 +13,7 @@ import 'package:mockingjay/mockingjay.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; -class MockGameBloc extends Mock implements GameBloc {} +import 'helpers.dart'; extension PumpApp on WidgetTester { Future pumpApp( From ca989eac8d875443096f26148ae457ef92bf1641 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 15:05:11 -0300 Subject: [PATCH 07/14] feat: addressing PR comments --- lib/game/view/pinball_game_page.dart | 22 +++++++++++ lib/game/view/pinball_game_view.dart | 25 ------------ lib/game/view/view.dart | 1 - test/game/view/pinball_game_page_test.dart | 39 ++++++++++++++++++ test/game/view/pinball_game_view_test.dart | 46 ---------------------- 5 files changed, 61 insertions(+), 72 deletions(-) delete mode 100644 lib/game/view/pinball_game_view.dart delete mode 100644 test/game/view/pinball_game_view_test.dart diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index c4387f34..28834907 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,3 +1,4 @@ +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; @@ -21,3 +22,24 @@ class PinballGamePage extends StatelessWidget { return const PinballGameView(); } } + +class PinballGameView extends StatelessWidget { + const PinballGameView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state.isGameOver) { + showDialog( + context: context, + builder: (_) { + return const GameOverDialog(); + }, + ); + } + }, + child: GameWidget(game: PinballGame()), + ); + } +} diff --git a/lib/game/view/pinball_game_view.dart b/lib/game/view/pinball_game_view.dart deleted file mode 100644 index 44715c25..00000000 --- a/lib/game/view/pinball_game_view.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flame/game.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pinball/game/game.dart'; - -class PinballGameView extends StatelessWidget { - const PinballGameView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.isGameOver) { - showDialog( - context: context, - builder: (_) { - return const GameOverDialog(); - }, - ); - } - }, - child: GameWidget(game: PinballGame()), - ); - } -} diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 39cdb071..53d3813a 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,3 +1,2 @@ export 'pinball_game_page.dart'; -export 'pinball_game_view.dart'; export 'widgets/widgets.dart'; diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 2e005773..be418c1d 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pinball/game/game.dart'; @@ -45,4 +46,42 @@ void main() { expect(find.byType(PinballGamePage), findsOneWidget); }); }); + + group('PinballGameView', () { + testWidgets('renders game', (tester) async { + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(const GameState.initial()), + initialState: const GameState.initial(), + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + expect( + find.byWidgetPredicate((w) => w is GameWidget), + findsOneWidget, + ); + }); + + testWidgets( + 'renders a game over dialog when the user has lost', + (tester) async { + final gameBloc = MockGameBloc(); + const state = GameState(score: 0, balls: 0); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); + await tester.pump(); + + expect( + find.text('Game Over'), + findsOneWidget, + ); + }, + ); + }); } diff --git a/test/game/view/pinball_game_view_test.dart b/test/game/view/pinball_game_view_test.dart deleted file mode 100644 index 4f4fe246..00000000 --- a/test/game/view/pinball_game_view_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flame/game.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball/game/game.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('PinballGameView', () { - testWidgets('renders game', (tester) async { - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(const GameState.initial()), - initialState: const GameState.initial(), - ); - - await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); - expect( - find.byWidgetPredicate((w) => w is GameWidget), - findsOneWidget, - ); - }); - - testWidgets( - 'renders a game over dialog when the user has lost', - (tester) async { - final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0); - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); - - await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc); - await tester.pump(); - - expect( - find.text('Game Over'), - findsOneWidget, - ); - }, - ); - }); -} From 174cbae1bf93eff63ddb32a43f568822b6cac515 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 15:11:40 -0300 Subject: [PATCH 08/14] Update lib/game/pinball_game.dart --- lib/game/pinball_game.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 3d586f12..934d958e 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -7,6 +7,7 @@ class PinballGame extends Forge2DGame with FlameBloc { add(Ball(position: ballStartingPosition)); } + // TODO(erickzanardo): Change to the plumber position late final ballStartingPosition = screenToWorld( Vector2( camera.viewport.effectiveSize.x / 2, From d5c84acd8eb5b4d299a58f902091120f9d7fed32 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 15:27:43 -0300 Subject: [PATCH 09/14] feat: making use of isLastBall on ball component --- lib/game/components/ball.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index fd5f5605..04d3cf6f 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -32,7 +32,7 @@ class Ball extends BodyComponent void ballLost() { final bloc = gameRef.read(); - final shouldBallRespwan = bloc.state.balls > 1; + final shouldBallRespwan = !bloc.state.isLastBall; bloc.add(const BallLost()); From adcc43d04a4ea31853daf1af733b2954d9d4f621 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 3 Mar 2022 17:00:08 -0300 Subject: [PATCH 10/14] Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- test/game/bloc/game_state_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index c5131492..59cc0d1d 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -62,10 +62,10 @@ void main() { }); }); - group('isGameOver', () { + group('isLastBall', () { test( 'is true ' - 'when there is only on ball left', + 'when there is only one ball left', () { const gameState = GameState( balls: 1, From 4ccd172b4fdb01772a735b3b7279ce6861ebaf11 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Thu, 3 Mar 2022 17:05:09 -0300 Subject: [PATCH 11/14] feat: pr suggestions --- lib/game/components/wall.dart | 38 ++++++++++++++--------------- test/game/components/wall_test.dart | 10 ++------ 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index c80e5bb3..bb527c3b 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,32 +3,16 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; -class BallWallContactCallback extends ContactCallback { - @override - void begin(Ball ball, Wall wall, Contact contact) { - if (wall.type == WallType.bottom) { - ball - ..ballLost() - ..shouldRemove = true; - } - } - - @override - void end(_, __, ___) {} -} - enum WallType { - top, - bottom, - left, - right, + fatal, + passive, } class Wall extends BodyComponent { Wall({ - required this.type, required this.start, required this.end, + this.type = WallType.passive, }); factory Wall.bottom(Forge2DGame game) { @@ -36,7 +20,7 @@ class Wall extends BodyComponent { final bottomLeft = Vector2(0, bottomRight.y); return Wall( - type: WallType.bottom, + type: WallType.fatal, start: bottomRight, end: bottomLeft, ); @@ -62,3 +46,17 @@ class Wall extends BodyComponent { return world.createBody(bodyDef)..createFixture(fixtureDef); } } + +class BallWallContactCallback extends ContactCallback { + @override + void begin(Ball ball, Wall wall, Contact contact) { + if (wall.type == WallType.fatal) { + ball + ..ballLost() + ..shouldRemove = true; + } + } + + @override + void end(_, __, ___) {} +} diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 2c4b241a..1ff5df95 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -14,13 +14,13 @@ void main() { group('Wall', () { group('BallWallContactCallback', () { test( - 'removes the ball on begin contact when the wall is a bottom one', + 'removes the ball on begin contact when the wall is a fatal one', () { final game = MockPinballGame(); final wall = MockWall(); final ball = MockBall(); - when(() => wall.type).thenReturn(WallType.bottom); + when(() => wall.type).thenReturn(WallType.fatal); when(() => ball.gameRef).thenReturn(game); BallWallContactCallback() @@ -39,7 +39,6 @@ void main() { 'loads correctly', (game) async { final wall = Wall( - type: WallType.bottom, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -54,7 +53,6 @@ void main() { 'positions correctly', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -69,7 +67,6 @@ void main() { 'is static', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -85,7 +82,6 @@ void main() { 'exists', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -99,7 +95,6 @@ void main() { 'has restitution equals 0', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); @@ -114,7 +109,6 @@ void main() { 'has friction', (game) async { final wall = Wall( - type: WallType.top, start: Vector2.zero(), end: Vector2(100, 0), ); From 9fc554927d67c2cd871ea917d0f44a6327c91837 Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 4 Mar 2022 09:03:41 -0300 Subject: [PATCH 12/14] Apply suggestions from code review Co-authored-by: Alejandro Santiago --- lib/game/bloc/game_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 862edf96..1a0568f7 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -26,7 +26,7 @@ class GameState extends Equatable { /// Determines when the game is over. bool get isGameOver => balls == 0; - /// Determines when player has only one chance left. + /// Determines when the player has only one ball left. bool get isLastBall => balls == 1; GameState copyWith({ From 5a2ee2c2ac3c5847e4383ecc18b6c70ef3f9773c Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Fri, 4 Mar 2022 09:25:09 -0300 Subject: [PATCH 13/14] feat: applying suggestions --- lib/game/components/ball.dart | 9 +++---- lib/game/components/wall.dart | 39 +++++++++++------------------ lib/game/pinball_game.dart | 4 +-- test/game/components/ball_test.dart | 2 +- test/game/components/wall_test.dart | 13 +++++----- test/helpers/mocks.dart | 2 ++ 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 04d3cf6f..e285b14b 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -29,13 +29,12 @@ class Ball extends BodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } - void ballLost() { - final bloc = gameRef.read(); + void lost() { + shouldRemove = true; - final shouldBallRespwan = !bloc.state.isLastBall; - - bloc.add(const BallLost()); + final bloc = gameRef.read()..add(const BallLost()); + final shouldBallRespwan = !bloc.state.isLastBall; if (shouldBallRespwan) { gameRef.spawnBall(); } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index bb527c3b..7bf58273 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -3,32 +3,14 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/components/components.dart'; -enum WallType { - fatal, - passive, -} - class Wall extends BodyComponent { Wall({ required this.start, required this.end, - this.type = WallType.passive, }); - factory Wall.bottom(Forge2DGame game) { - final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize); - final bottomLeft = Vector2(0, bottomRight.y); - - return Wall( - type: WallType.fatal, - start: bottomRight, - end: bottomLeft, - ); - } - final Vector2 start; final Vector2 end; - final WallType type; @override Body createBody() { @@ -47,14 +29,21 @@ class Wall extends BodyComponent { } } -class BallWallContactCallback extends ContactCallback { +class BottomWall extends Wall { + BottomWall(Forge2DGame game) + : super( + start: game.screenToWorld(game.camera.viewport.effectiveSize), + end: Vector2( + 0, + game.screenToWorld(game.camera.viewport.effectiveSize).y, + ), + ); +} + +class BottomWallBallContactCallback extends ContactCallback { @override - void begin(Ball ball, Wall wall, Contact contact) { - if (wall.type == WallType.fatal) { - ball - ..ballLost() - ..shouldRemove = true; - } + void begin(Ball ball, BottomWall wall, Contact contact) { + ball.lost(); } @override diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 934d958e..91e12854 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -21,7 +21,7 @@ class PinballGame extends Forge2DGame with FlameBloc { spawnBall(); addContactCallback(BallScorePointsCallback()); - await add(Wall.bottom(this)); - addContactCallback(BallWallContactCallback()); + await add(BottomWall(this)); + addContactCallback(BottomWallBallContactCallback()); } } diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 8521a80f..b32d16d5 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -107,7 +107,7 @@ void main() { (game, tester) async { await game.ready(); - game.children.whereType().first.ballLost(); + game.children.whereType().first.lost(); await tester.pump(); verify(() => gameBloc.add(const BallLost())).called(1); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 1ff5df95..8151055e 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -12,24 +12,23 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Wall', () { - group('BallWallContactCallback', () { + group('BottomWallBallContactCallback', () { test( - 'removes the ball on begin contact when the wall is a fatal one', + 'removes the ball on begin contact when the wall is a bottom one', () { final game = MockPinballGame(); - final wall = MockWall(); + final wall = MockBottomWall(); final ball = MockBall(); - when(() => wall.type).thenReturn(WallType.fatal); when(() => ball.gameRef).thenReturn(game); - BallWallContactCallback() + BottomWallBallContactCallback() // Remove once https://github.com/flame-engine/flame/pull/1415 // is merged - ..end(MockBall(), MockWall(), MockContact()) + ..end(MockBall(), MockBottomWall(), MockContact()) ..begin(ball, wall, MockContact()); - verify(() => ball.shouldRemove = true).called(1); + verify(ball.lost).called(1); }, ); }); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 559ea87a..b46e2c5c 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -6,6 +6,8 @@ class MockPinballGame extends Mock implements PinballGame {} class MockWall extends Mock implements Wall {} +class MockBottomWall extends Mock implements BottomWall {} + class MockBall extends Mock implements Ball {} class MockContact extends Mock implements Contact {} From 9891931ffe9b9c007bc02cf2c1699b732b236d63 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Fri, 4 Mar 2022 13:48:48 +0000 Subject: [PATCH 14/14] feat: anchor component (#12) * feat: implemented Anchor component * docs: included initialize example * docs: improved Anchor doc comment * chore: fixed white space difference --- lib/game/components/anchor.dart | 32 ++++++++++++++ lib/game/components/components.dart | 1 + test/game/components/anchor_test.dart | 60 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 lib/game/components/anchor.dart create mode 100644 test/game/components/anchor_test.dart diff --git a/lib/game/components/anchor.dart b/lib/game/components/anchor.dart new file mode 100644 index 00000000..0e78aa1c --- /dev/null +++ b/lib/game/components/anchor.dart @@ -0,0 +1,32 @@ +import 'package:flame_forge2d/flame_forge2d.dart'; + +/// {@template anchor} +/// Non visual [BodyComponent] used to hold a [BodyType.dynamic] in [Joint]s +/// with this [BodyType.static]. +/// +/// It is recommended to [_position] the anchor first and then use the body +/// position as the anchor point when initializing a [JointDef]. +/// +/// ```dart +/// initialize( +/// dynamicBody.body, +/// anchor.body, +/// anchor.body.position, +/// ); +/// ``` +/// {@endtemplate} +class Anchor extends BodyComponent { + /// {@macro anchor} + Anchor({ + required Vector2 position, + }) : _position = position; + + final Vector2 _position; + + @override + Body createBody() { + final bodyDef = BodyDef()..position = _position; + + return world.createBody(bodyDef); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 52e460a5..42c79ae6 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,3 +1,4 @@ +export 'anchor.dart'; export 'ball.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/test/game/components/anchor_test.dart b/test/game/components/anchor_test.dart new file mode 100644 index 00000000..5cc37eca --- /dev/null +++ b/test/game/components/anchor_test.dart @@ -0,0 +1,60 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Anchor', () { + final flameTester = FlameTester(PinballGame.new); + + flameTester.test( + 'loads correctly', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(game.contains(anchor), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final anchor = Anchor(position: position); + await game.ensureAdd(anchor); + game.contains(anchor); + + expect(anchor.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(anchor.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'has none', + (game) async { + final anchor = Anchor(position: Vector2.zero()); + await game.ensureAdd(anchor); + + expect(anchor.body.fixtures, isEmpty); + }, + ); + }); + }); +}