feat: game over mechanics

pull/9/head
Erick Zanardo 4 years ago
parent 403d997f17
commit 518e183ad8

@ -6,6 +6,7 @@ import 'package:pinball/game/game.dart';
class Ball extends BodyComponent<PinballGame>
with BlocComponent<GameBloc, GameState> {
Ball({
required Vector2 position,
}) : _position = position {
@ -28,4 +29,19 @@ class Ball extends BodyComponent<PinballGame>
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
@override
void onRemove() {
final bloc = gameRef.read<GameBloc>();
final shouldBallrespwan = bloc.state.balls > 1;
bloc.add(const BallLost());
if (shouldBallrespwan) {
gameRef.resetBall();
}
super.onRemove();
}
}

@ -1,2 +1,3 @@
export 'ball.dart';
export 'score_points.dart';
export '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);
}
}

@ -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';

@ -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<Ball, Wall> {
@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<void> 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();
}
}

@ -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<void>(builder: (_) => const PinballGamePage());
return MaterialPageRoute<void>(
builder: (_) {
return BlocProvider(
create: (_) => GameBloc(),
child: const PinballGamePage(),
);
},
);
}
@override
Widget build(BuildContext context) {
return GameWidget(game: PinballGame());
return const PinballGameView();
}
}

@ -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<GameBloc, GameState>(
listener: (context, state) {
if (state.isGameOver) {
showDialog<void>(
context: context,
builder: (_) {
return const Dialog(
child: SizedBox(
width: 200,
height: 200,
child: Center(
child: Text('Game Over'),
),
),
);
},
);
}
},
child: GameWidget<PinballGame>(game: PinballGame()),
);
}
}

@ -0,0 +1,2 @@
export 'pinball_game_page.dart';
export 'pinball_game_view.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<Fixture>());
},
);
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));
},
);
});
});
}

@ -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<GameState>.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<Ball>().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<Ball>().first.removeFromParent();
await game.ready(); // Making sure that all additions are done
expect(
game.children.whereType<Ball>().length,
equals(1),
);
},
)
..widgetTest(
'no ball is added on game over',
(game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState(score: 10, balls: 1),
);
await game.ready();
game.children.whereType<Ball>().first.removeFromParent();
await tester.pump();
expect(
game.children.whereType<Ball>().length,
equals(0),
);
},
);
});
});
}

@ -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<PinballGame>), 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<void>(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);
});
});
}

@ -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<PinballGame>),
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,
);
},
);
});
}

@ -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<void> 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,
),
),
);
}

Loading…
Cancel
Save