refactor: made ball controller listen to bloc (#116)

* refactor: included BonusBallController

* feat: made LaunchedBallControler react to states

* refactor: removed isLastBall

* fix: solved tests

* fix: solved BonusLetterActivated test

* refactor: used ensureAdd
pull/121/head
Alejandro Santiago 4 years ago committed by GitHub
parent 79687c8ea3
commit 07ddb1f7b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -55,9 +55,6 @@ class GameState extends Equatable {
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => balls == 0;
/// Determines when the player has only one ball left.
bool get isLastBall => balls == 1;
/// Shortcut method to check if the given [i] /// Shortcut method to check if the given [i]
/// is activated. /// is activated.
bool isLetterActivated(int i) => activatedBonusLetters.contains(i); bool isLetterActivated(int i) => activatedBonusLetters.contains(i);

@ -1,4 +1,5 @@
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';
@ -28,19 +29,19 @@ 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 = BallController(this); controller = BonusBallController(this);
} }
/// [Ball] used in [DebugPinballGame]. /// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) { ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = BallController(this); controller = BonusBallController(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}
class BallController extends ComponentController<Ball> { abstract class BallController extends ComponentController<Ball> {
/// {@macro ball_controller} /// {@macro ball_controller}
BallController(Ball ball) : super(ball); BallController(Ball ball) : super(ball);
@ -50,30 +51,52 @@ class BallController extends ComponentController<Ball> {
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into /// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall]. /// a [BottomWall].
/// {@endtemplate} /// {@endtemplate}
@mustCallSuper 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} /// {@macro ball_controller}
///
/// A [LaunchedBallController] changes the [GameState.balls] count.
/// {@endtemplate}
class LaunchedBallController extends BallController class LaunchedBallController extends BallController
with HasGameRef<PinballGame> { with HasGameRef<PinballGame>, BlocComponent<GameBloc, GameState> {
/// {@macro ball_controller} /// {@macro launched_ball_controller}
LaunchedBallController(Ball<Forge2DGame> ball) : super(ball); LaunchedBallController(Ball<Forge2DGame> ball) : super(ball);
@override
bool listenWhen(GameState? previousState, GameState newState) {
return (previousState?.balls ?? 0) > newState.balls;
}
@override
void onNewState(GameState state) {
super.onNewState(state);
component.shouldRemove = true;
if (state.balls > 1) gameRef.spawnBall();
}
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if /// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left. /// any are left.
/// ///
/// {@macro ball_controller_lost} /// {@macro ball_controller_lost}
@override @override
void lost() { void lost() {
super.lost(); gameRef.read<GameBloc>().add(const BallLost());
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
// TODO(alestiago): Consider the use of onNewState instead.
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) gameRef.spawnBall();
} }
} }

@ -103,38 +103,6 @@ void main() {
}); });
}); });
group('isLastBall', () {
test(
'is true '
'when there is only one ball left',
() {
const gameState = GameState(
balls: 1,
score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLastBall, isTrue);
},
);
test(
'is false '
'when there are more balls left',
() {
const gameState = GameState(
balls: 2,
score: 0,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
);
expect(gameState.isLastBall, isFalse);
},
);
});
group('isLetterActivated', () { group('isLetterActivated', () {
test( test(
'is true when the letter is activated', 'is true when the letter is activated',

@ -196,7 +196,7 @@ void main() {
group('bonus letter activation', () { group('bonus letter activation', () {
late GameBloc gameBloc; late GameBloc gameBloc;
final tester = flameBlocTester( final tester = flameBlocTester<PinballGame>(
// TODO(alestiago): Use TestGame once BonusLetter has controller. // TODO(alestiago): Use TestGame once BonusLetter has controller.
game: PinballGameTest.create, game: PinballGameTest.create,
gameBloc: () => gameBloc, gameBloc: () => gameBloc,
@ -217,13 +217,8 @@ void main() {
await game.ready(); await game.ready();
final bonusLetter = game.descendants().whereType<BonusLetter>().first; final bonusLetter = game.descendants().whereType<BonusLetter>().first;
await game.add(bonusLetter);
await game.ready();
bonusLetter.activate(); bonusLetter.activate();
await game.ready(); await game.ready();
await tester.pump();
}, },
verify: (game, tester) async { verify: (game, tester) async {
verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1); verify(() => gameBloc.add(const BonusLetterActivated(0))).called(1);

@ -15,20 +15,26 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
group('BallController', () { group('BonusBallController', () {
late Ball ball; late Ball ball;
setUp(() { setUp(() {
ball = Ball(baseColor: const Color(0xFF00FFFF)); ball = Ball(baseColor: const Color(0xFF00FFFF));
}); });
test('can be instantiated', () {
expect(
BonusBallController(ball),
isA<BonusBallController>(),
);
});
flameTester.test( flameTester.test(
'lost removes ball', 'lost removes ball',
(game) async { (game) async {
await game.add(ball); await game.add(ball);
final controller = BallController(ball); final controller = BonusBallController(ball);
await ball.add(controller); await ball.ensureAdd(controller);
await game.ready();
controller.lost(); controller.lost();
await game.ready(); await game.ready();
@ -39,13 +45,20 @@ void main() {
}); });
group('LaunchedBallController', () { group('LaunchedBallController', () {
group('lost', () { test('can be instantiated', () {
late GameBloc gameBloc; expect(
LaunchedBallController(MockBall()),
isA<LaunchedBallController>(),
);
});
group('description', () {
late Ball ball; late Ball ball;
late GameBloc gameBloc;
setUp(() { setUp(() {
gameBloc = MockGameBloc();
ball = Ball(baseColor: const Color(0xFF00FFFF)); ball = Ball(baseColor: const Color(0xFF00FFFF));
gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
@ -59,81 +72,126 @@ void main() {
); );
tester.testGameWidget( tester.testGameWidget(
'removes ball', 'lost adds BallLost to GameBloc',
verify: (game, tester) async { setUp: (game, tester) async {
await game.add(ball);
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
await ball.add(controller); await ball.add(controller);
await game.ready(); await game.ensureAdd(ball);
controller.lost(); controller.lost();
await game.ready(); },
verify: (game, tester) async {
expect(game.contains(ball), isFalse); verify(() => gameBloc.add(const BallLost())).called(1);
}, },
); );
group('listenWhen', () {
tester.testGameWidget( tester.testGameWidget(
'adds BallLost to GameBloc', 'listens when a ball has been lost',
verify: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
await ball.add(controller); await ball.add(controller);
await game.add(ball); await game.ensureAdd(ball);
await game.ready(); },
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
controller.lost(); final previousState = MockGameState();
final newState = MockGameState();
when(() => previousState.balls).thenReturn(3);
when(() => newState.balls).thenReturn(2);
verify(() => gameBloc.add(const BallLost())).called(1); expect(controller.listenWhen(previousState, newState), isTrue);
}, },
); );
tester.testGameWidget( tester.testGameWidget(
'adds a new ball if the game is not over', 'does not listen when a ball has not been lost',
verify: (game, tester) async { setUp: (game, tester) async {
final controller = LaunchedBallController(ball); final controller = LaunchedBallController(ball);
await ball.add(controller); await ball.add(controller);
await game.add(ball); await game.ensureAdd(ball);
await game.ready(); },
verify: (game, tester) async {
final controller =
game.descendants().whereType<LaunchedBallController>().first;
final previousBalls = game.descendants().whereType<Ball>().length; final previousState = MockGameState();
controller.lost(); final newState = MockGameState();
await game.ready(); when(() => previousState.balls).thenReturn(3);
final currentBalls = game.descendants().whereType<Ball>().length; when(() => newState.balls).thenReturn(3);
expect(previousBalls, equals(currentBalls)); expect(controller.listenWhen(previousState, newState), isFalse);
}, },
); );
});
group('onNewState', () {
tester.testGameWidget( tester.testGameWidget(
'no ball is added on game over', '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 { verify: (game, tester) async {
whenListen( expect(game.contains(ball), isFalse);
gameBloc, },
const Stream<GameState>.empty(),
initialState: const GameState(
score: 10,
balls: 1,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [],
),
); );
final controller = BallController(ball);
tester.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 ball.add(controller);
await game.add(ball); await game.ensureAdd(ball);
final state = MockGameState();
when(() => state.balls).thenReturn(2);
final previousBalls = game.descendants().whereType<Ball>().toList();
controller.onNewState(state);
await game.ready(); await game.ready();
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect(currentBalls.length, equals(previousBalls.length));
},
);
tester.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(1);
final previousBalls = game.descendants().whereType<Ball>().toList(); final previousBalls = game.descendants().whereType<Ball>().toList();
controller.lost(); controller.onNewState(state);
await game.ready(); await game.ready();
final currentBalls = game.descendants().whereType<Ball>().length;
final currentBalls = game.descendants().whereType<Ball>();
expect(currentBalls.contains(ball), isFalse);
expect( expect(
currentBalls, currentBalls.length,
equals((previousBalls..remove(ball)).length), equals((previousBalls..remove(ball)).length),
); );
}, },
); );
}); });
}); });
});
} }

@ -1,6 +1,7 @@
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
class TestGame extends Forge2DGame { class TestGame extends Forge2DGame with FlameBloc {
TestGame() { TestGame() {
images.prefix = ''; images.prefix = '';
} }

Loading…
Cancel
Save