feat: improved extra ball logic (#151)

pull/162/head
Alejandro Santiago 4 years ago committed by GitHub
parent 254c38d2a4
commit 655007b2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -33,7 +33,7 @@ abstract class ComponentController<T extends Component> extends Component {
/// Mixin that attaches a single [ComponentController] to a [Component]. /// Mixin that attaches a single [ComponentController] to a [Component].
mixin Controls<T extends ComponentController> on Component { mixin Controls<T extends ComponentController> on Component {
/// The [ComponentController] attached to this [Component]. /// The [ComponentController] attached to this [Component].
late final T controller; late T controller;
@override @override
@mustCallSuper @mustCallSuper

@ -19,9 +19,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
static const bonusWordScore = 10000; static const bonusWordScore = 10000;
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
if (state.balls > 0) { emit(state.copyWith(balls: state.balls - 1));
emit(state.copyWith(balls: state.balls - 1));
}
} }
void _onScored(Scored event, Emitter emit) { void _onScored(Scored event, Emitter emit) {
@ -36,7 +34,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
event.letterIndex, event.letterIndex,
]; ];
if (newBonusLetters.length == bonusWord.length) { final achievedBonus = newBonusLetters.length == bonusWord.length;
if (achievedBonus) {
emit( emit(
state.copyWith( state.copyWith(
activatedBonusLetters: [], activatedBonusLetters: [],
@ -55,15 +54,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onDashNestActivated(DashNestActivated event, Emitter emit) { void _onDashNestActivated(DashNestActivated event, Emitter emit) {
const nestsRequiredForBonus = 3;
final newNests = { final newNests = {
...state.activatedDashNests, ...state.activatedDashNests,
event.nestId, event.nestId,
}; };
if (newNests.length == nestsRequiredForBonus) {
final achievedBonus = newNests.length == 3;
if (achievedBonus) {
emit( emit(
state.copyWith( state.copyWith(
balls: state.balls + 1,
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [ bonusHistory: [
...state.bonusHistory, ...state.bonusHistory,

@ -5,11 +5,10 @@ part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame. /// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus { enum GameBonus {
/// Bonus achieved when the user activate all of the bonus /// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word /// letters on the board, forming the bonus word.
word, word,
/// Bonus achieved when the user activates all of the Dash /// Bonus achieved when the user activates all dash nest bumpers.
/// nests on the board, adding a new ball to the board.
dashNest, dashNest,
} }

@ -1,5 +1,4 @@
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/forge2d_game.dart'; import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/flame/flame.dart'; import 'package:pinball/flame/flame.dart';
@ -18,7 +17,7 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.launch({ ControlledBall.launch({
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = LaunchedBallController(this); controller = BallController(this);
} }
/// {@template bonus_ball} /// {@template bonus_ball}
@ -29,74 +28,43 @@ class ControlledBall extends Ball with Controls<BallController> {
ControlledBall.bonus({ ControlledBall.bonus({
required PinballTheme theme, required PinballTheme theme,
}) : super(baseColor: theme.characterTheme.ballColor) { }) : super(baseColor: theme.characterTheme.ballColor) {
controller = BonusBallController(this); controller = BallController(this);
} }
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BonusBallController(this); controller = DebugBallController(this);
} }
} }
/// {@template ball_controller} /// {@template ball_controller}
/// Controller attached to a [Ball] that handles its game related logic. /// Controller attached to a [Ball] that handles its game related logic.
/// {@endtemplate} /// {@endtemplate}
abstract class BallController extends ComponentController<Ball> { class BallController extends ComponentController<Ball>
with HasGameRef<PinballGame> {
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
/// Removes the [Ball] from a [PinballGame]. /// Removes the [Ball] from a [PinballGame].
/// ///
/// {@template ball_controller_lost}
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall]. /// a [BottomWall].
/// {@endtemplate}
void lost();
}
/// {@template bonus_ball_controller}
/// {@macro ball_controller}
///
/// A [BonusBallController] doesn't change the [GameState.balls] count.
/// {@endtemplate}
class BonusBallController extends BallController {
/// {@macro bonus_ball_controller}
BonusBallController(Ball<Forge2DGame> component) : super(component);
@override
void lost() { void lost() {
component.shouldRemove = true; component.shouldRemove = true;
} }
}
/// {@template launched_ball_controller}
/// {@macro ball_controller}
///
/// A [LaunchedBallController] changes the [GameState.balls] count.
/// {@endtemplate}
class LaunchedBallController extends BallController
with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro launched_ball_controller}
LaunchedBallController(Ball<Forge2DGame> ball) : super(ball);
@override @override
bool listenWhen(GameState? previousState, GameState newState) { void onRemove() {
return (previousState?.balls ?? 0) > newState.balls; super.onRemove();
gameRef.read<GameBloc>().add(const BallLost());
} }
}
@override /// {@macro ball_controller}
void onNewState(GameState state) { class DebugBallController extends BallController {
super.onNewState(state); /// {@macro ball_controller}
component.shouldRemove = true; DebugBallController(Ball<Forge2DGame> component) : super(component);
if (state.balls > 0) gameRef.spawnBall();
}
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// {@macro ball_controller_lost}
@override @override
void lost() { void onRemove() {}
gameRef.read<GameBloc>().add(const BallLost());
}
} }

@ -71,12 +71,12 @@ class BottomWall extends Wall {
} }
/// {@template bottom_wall_ball_contact_callback} /// {@template bottom_wall_ball_contact_callback}
/// Listens when a [Ball] falls into a [BottomWall]. /// Listens when a [ControlledBall] falls into a [BottomWall].
/// {@endtemplate} /// {@endtemplate}
class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> { class BottomWallBallContactCallback
extends ContactCallback<ControlledBall, BottomWall> {
@override @override
void begin(Ball ball, BottomWall wall, Contact contact) { void begin(ControlledBall ball, BottomWall wall, Contact contact) {
// TODO(alestiago): replace with .firstChild when available. ball.controller.lost();
ball.children.whereType<BallController>().first.lost();
} }
} }

@ -5,6 +5,7 @@ import 'package:flame/components.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/flame/flame.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
import 'package:pinball_audio/pinball_audio.dart'; import 'package:pinball_audio/pinball_audio.dart';
@ -12,29 +13,38 @@ import 'package:pinball_components/pinball_components.dart' hide Assets;
import 'package:pinball_theme/pinball_theme.dart' hide Assets; import 'package:pinball_theme/pinball_theme.dart' hide Assets;
class PinballGame extends Forge2DGame class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents { with
PinballGame({required this.theme, required this.audio}) { FlameBloc,
HasKeyboardHandlerComponents,
Controls<_GameBallsController> {
PinballGame({
required this.theme,
required this.audio,
}) {
images.prefix = ''; images.prefix = '';
controller = _GameBallsController(this);
} }
final PinballTheme theme; final PinballTheme theme;
final PinballAudio audio; final PinballAudio audio;
@override
void onAttach() {
super.onAttach();
spawnBall();
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
_addContactCallbacks(); _addContactCallbacks();
// Fix camera on the center of the board.
camera
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
await _addGameBoundaries(); await _addGameBoundaries();
unawaited(addFromBlueprint(Boundaries())); unawaited(addFromBlueprint(Boundaries()));
unawaited(addFromBlueprint(LaunchRamp())); unawaited(addFromBlueprint(LaunchRamp()));
unawaited(_addPlunger());
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
unawaited(add(Board())); unawaited(add(Board()));
unawaited(addFromBlueprint(DinoWalls())); unawaited(addFromBlueprint(DinoWalls()));
unawaited(_addBonusWord()); unawaited(_addBonusWord());
@ -52,10 +62,8 @@ class PinballGame extends Forge2DGame
), ),
); );
// Fix camera on the center of the board. controller.attachTo(plunger);
camera await super.onLoad();
..followVector2(Vector2(0, -7.8))
..zoom = size.y / 16;
} }
void _addContactCallbacks() { void _addContactCallbacks() {
@ -69,12 +77,6 @@ class PinballGame extends Forge2DGame
createBoundaries(this).forEach(add); createBoundaries(this).forEach(add);
} }
Future<void> _addPlunger() async {
final plunger = Plunger(compressionDistance: 29)
..initialPosition = Vector2(38, -19);
await add(plunger);
}
Future<void> _addBonusWord() async { Future<void> _addBonusWord() async {
await add( await add(
BonusWord( BonusWord(
@ -85,13 +87,49 @@ class PinballGame extends Forge2DGame
), ),
); );
} }
}
Future<void> spawnBall() async { class _GameBallsController extends ComponentController<PinballGame>
// TODO(alestiago): Remove once this logic is moved to controller. with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
_GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger;
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
_spawnBall();
}
@override
Future<void> onLoad() async {
await super.onLoad();
_spawnBall();
}
void _spawnBall() {
final ball = ControlledBall.launch( final ball = ControlledBall.launch(
theme: theme, theme: gameRef.theme,
)..initialPosition = Vector2(38, -19 + Ball.size.y); )..initialPosition = Vector2(
await add(ball); _plunger.body.position.x,
_plunger.body.position.y + Ball.size.y,
);
component.add(ball);
}
/// Attaches the controller to the plunger.
// TODO(alestiago): Remove this method and use onLoad instead.
// ignore: use_setters_to_change_properties
void attachTo(Plunger plunger) {
_plunger = plunger;
} }
} }
@ -102,7 +140,9 @@ class DebugPinballGame extends PinballGame with TapDetector {
}) : super( }) : super(
theme: theme, theme: theme,
audio: audio, audio: audio,
); ) {
controller = _DebugGameBallsController(this);
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
@ -134,3 +174,19 @@ class DebugPinballGame extends PinballGame with TapDetector {
); );
} }
} }
class _DebugGameBallsController extends _GameBallsController {
_DebugGameBallsController(PinballGame game) : super(game);
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component
.descendants()
.whereType<ControlledBall>()
.where((ball) => ball.controller is! DebugBallController)
.isEmpty;
final canBallRespawn = newState.balls > 0;
return noBallsLeft && canBallRespawn;
}
}

@ -12,13 +12,10 @@ void main() {
group('LostBall', () { group('LostBall', () {
blocTest<GameBloc, GameState>( blocTest<GameBloc, GameState>(
"doesn't decrease ball " 'decreases number of balls',
'when no balls left',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) { act: (bloc) {
for (var i = 0; i <= bloc.state.balls; i++) { bloc.add(const BallLost());
bloc.add(const BallLost());
}
}, },
expect: () => [ expect: () => [
const GameState( const GameState(
@ -28,20 +25,6 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
], ],
); );
}); });
@ -230,7 +213,7 @@ void main() {
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 4,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],

@ -13,42 +13,12 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new);
group('BonusBallController', () { group('BallController', () {
late Ball ball;
setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF));
});
test('can be instantiated', () {
expect(
BonusBallController(ball),
isA<BonusBallController>(),
);
});
flameTester.test(
'lost removes ball',
(game) async {
await game.add(ball);
final controller = BonusBallController(ball);
await ball.ensureAdd(controller);
controller.lost();
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
group('LaunchedBallController', () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
LaunchedBallController(MockBall()), BallController(MockBall()),
isA<LaunchedBallController>(), isA<BallController>(),
); );
}); });
@ -74,7 +44,7 @@ void main() {
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc', 'lost adds BallLost to GameBloc',
setUp: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = BallController(ball);
await ball.add(controller); await ball.add(controller);
await game.ensureAdd(ball); await game.ensureAdd(ball);
@ -84,114 +54,6 @@ void main() {
verify(() => gameBloc.add(const BallLost())).called(1); verify(() => gameBloc.add(const BallLost())).called(1);
}, },
); );
group('listenWhen', () {
flameBlocTester.testGameWidget(
'listens when a ball has been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
expect(controller.listenWhen(previousState, newState), isTrue);
},
);
flameBlocTester.testGameWidget(
'does not listen when a ball has not been lost',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
},
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(3);
expect(controller.listenWhen(previousState, newState), isFalse);
},
);
});
group('onNewState', () {
flameBlocTester.testGameWidget(
'removes ball',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
controller.onNewState(state);
await game.ready();
},
verify: (game, tester) async {
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'spawns a new ball when the ball is not the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(1);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>().toList();
expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length));
},
);
flameBlocTester.testGameWidget(
'does not spawn a new ball is the last one',
setUp: (game, tester) async {
final controller = LaunchedBallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(0);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(
currentBalls.length,
equals((previousBalls..remove(ball)).length),
);
},
);
});
}); });
}); });
} }

@ -1,7 +1,6 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -11,18 +10,6 @@ import 'package:pinball_components/pinball_components.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballGameTest.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
@ -92,7 +79,7 @@ void main() {
); );
flameBlocTester.testGameWidget( flameBlocTester.testGameWidget(
'listens when a Bonus.dashNest is added', 'listens when a Bonus.dashNest and a bonusBall is added',
verify: (game, tester) async { verify: (game, tester) async {
final flutterForest = FlutterForest(); final flutterForest = FlutterForest();
@ -103,6 +90,7 @@ void main() {
activatedDashNests: {}, activatedDashNests: {},
bonusHistory: [GameBonus.dashNest], bonusHistory: [GameBonus.dashNest],
); );
expect( expect(
flutterForest.controller flutterForest.controller
.listenWhen(const GameState.initial(), state), .listenWhen(const GameState.initial(), state),

@ -3,40 +3,15 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart'; import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(Forge2DGame.new); final flameTester = FlameTester(EmptyPinballGameTest.new);
group('Wall', () { group('Wall', () {
group('BottomWallBallContactCallback', () {
test(
'removes the ball on begin contact when the wall is a bottom one',
() {
final wall = MockBottomWall();
final ballController = MockBallController();
final ball = MockBall();
final componentSet = MockComponentSet();
when(() => componentSet.whereType<BallController>())
.thenReturn([ballController]);
when(() => ball.children).thenReturn(componentSet);
BottomWallBallContactCallback()
// Remove once https://github.com/flame-engine/flame/pull/1415
// is merged
..end(MockBall(), MockBottomWall(), MockContact())
..begin(ball, wall, MockContact());
verify(ballController.lost).called(1);
},
);
});
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
@ -123,4 +98,67 @@ void main() {
); );
}); });
}); });
group(
'BottomWall',
() {
group('removes ball on contact', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'when ball is launch',
setUp: (game, tester) async {
final ball = ControlledBall.launch(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameBlocTester.testGameWidget(
'when ball is bonus',
setUp: (game, tester) async {
final ball = ControlledBall.bonus(theme: game.theme);
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
flameTester.test(
'when ball is debug',
(game) async {
final ball = ControlledBall.debug();
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);
game.addContactCallback(BottomWallBallContactCallback());
beginContact(game, ball, wall);
await game.ready();
expect(game.contains(ball), isFalse);
},
);
});
},
);
} }

@ -1,6 +1,7 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'package:flame/components.dart'; import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
@ -10,11 +11,11 @@ import 'package:pinball_components/pinball_components.dart';
import '../helpers/helpers.dart'; import '../helpers/helpers.dart';
void main() { void main() {
group('PinballGame', () { TestWidgetsFlutterBinding.ensureInitialized();
TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.new);
final flameTester = FlameTester(PinballGameTest.new); final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.new);
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved: // [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416 // https://github.com/flame-engine/flame/issues/1416
@ -60,8 +61,106 @@ void main() {
equals(1), equals(1),
); );
}); });
group('controller', () {
// TODO(alestiago): Write test to be controller agnostic.
group('listenWhen', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final flameBlocTester = FlameBlocTester<PinballGame, GameBloc>(
gameBuilder: EmptyPinballGameTest.new,
blocBuilder: () => gameBloc,
);
flameBlocTester.testGameWidget(
'listens when all balls are lost and there are more than 0 balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(2);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
);
flameTester.test(
"doesn't listen when some balls are left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
expect(
game.descendants().whereType<Ball>().length,
greaterThan(0),
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
flameBlocTester.test(
"doesn't listen when no balls left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(0);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.descendants().whereType<Ball>().isEmpty,
isTrue,
);
expect(
game.controller.listenWhen(MockGameState(), newState),
isFalse,
);
},
);
});
group(
'onNewState',
() {
flameTester.test(
'spawns a ball',
(game) async {
await game.ready();
final previousBalls =
game.descendants().whereType<Ball>().toList();
game.controller.onNewState(MockGameState());
await game.ready();
final currentBalls =
game.descendants().whereType<Ball>().toList();
expect(
currentBalls.length,
equals(previousBalls.length + 1),
);
},
);
},
);
});
}); });
});
group('DebugPinballGame', () {
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready(); await game.ready();
@ -71,12 +170,46 @@ void main() {
final tapUpEvent = MockTapUpInfo(); final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
game.onTapUp(tapUpEvent); game.onTapUp(tapUpEvent);
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<Ball>().length, game.children.whereType<Ball>().length,
equals(1), equals(previousBalls.length + 1),
);
});
group('controller', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballGameTest.new,
blocBuilder: () => gameBloc,
);
debugModeFlameBlocTester.testGameWidget(
'ignores debug balls',
setUp: (game, tester) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
await game.ready();
game.children.removeWhere((component) => component is Ball);
await game.ready();
await game.ensureAdd(ControlledBall.debug());
expect(
game.controller.listenWhen(MockGameState(), newState),
isTrue,
);
},
); );
}); });
}); });

@ -1,3 +1,5 @@
// ignore_for_file: must_call_super
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';

@ -0,0 +1,13 @@
import 'package:flame_forge2d/flame_forge2d.dart';
void beginContact(Forge2DGame game, BodyComponent bodyA, BodyComponent bodyB) {
assert(
bodyA.body.fixtures.isNotEmpty && bodyB.body.fixtures.isNotEmpty,
'Bodies require fixtures to contact each other.',
);
final fixtureA = bodyA.body.fixtures.first;
final fixtureB = bodyB.body.fixtures.first;
final contact = Contact.init(fixtureA, 0, fixtureB, 0);
game.world.contactManager.contactListener?.beginContact(contact);
}

@ -7,6 +7,7 @@
export 'builders.dart'; export 'builders.dart';
export 'extensions.dart'; export 'extensions.dart';
export 'fakes.dart'; export 'fakes.dart';
export 'forge2d.dart';
export 'key_testers.dart'; export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'navigator.dart'; export 'navigator.dart';

@ -21,6 +21,8 @@ class MockBody extends Mock implements Body {}
class MockBall extends Mock implements Ball {} class MockBall extends Mock implements Ball {}
class MockControlledBall extends Mock implements ControlledBall {}
class MockBallController extends Mock implements BallController {} class MockBallController extends Mock implements BallController {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}

Loading…
Cancel
Save