refactor: include `GameStatus` on `GameBloc` (#345)

pull/320/head
Alejandro Santiago 3 years ago committed by GitHub
parent 34d0e7d65a
commit 2ad0196e44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,16 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
on<GameOver>(_onGameOver);
on<GameStarted>(_onGameStarted);
}
void _onGameStarted(GameStarted _, Emitter emit) {
emit(state.copyWith(status: GameStatus.playing));
}
void _onGameOver(GameOver _, Emitter emit) {
emit(state.copyWith(status: GameStatus.gameOver));
}
void _onRoundLost(RoundLost event, Emitter emit) {
@ -26,12 +36,13 @@ class GameBloc extends Bloc<GameEvent, GameState> {
roundScore: 0,
multiplier: 1,
rounds: roundsLeft,
status: roundsLeft == 0 ? GameStatus.gameOver : state.status,
),
);
}
void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) {
if (state.status.isPlaying) {
emit(
state.copyWith(roundScore: state.roundScore + event.points),
);
@ -39,7 +50,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
}
void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) {
if (!state.isGameOver) {
if (state.status.isPlaying) {
emit(
state.copyWith(
multiplier: math.min(state.multiplier + 1, 6),

@ -59,3 +59,17 @@ class MultiplierIncreased extends GameEvent {
@override
List<Object?> get props => [];
}
class GameStarted extends GameEvent {
const GameStarted();
@override
List<Object?> get props => [];
}
class GameOver extends GameEvent {
const GameOver();
@override
List<Object?> get props => [];
}

@ -20,6 +20,17 @@ enum GameBonus {
androidSpaceship,
}
enum GameStatus {
waiting,
playing,
gameOver,
}
extension GameStatusX on GameStatus {
bool get isPlaying => this == GameStatus.playing;
bool get isGameOver => this == GameStatus.gameOver;
}
/// {@template game_state}
/// Represents the state of the pinball game.
/// {@endtemplate}
@ -31,13 +42,15 @@ class GameState extends Equatable {
required this.multiplier,
required this.rounds,
required this.bonusHistory,
required this.status,
}) : assert(totalScore >= 0, "TotalScore can't be negative"),
assert(roundScore >= 0, "Round score can't be negative"),
assert(multiplier > 0, 'Multiplier must be greater than zero'),
assert(rounds >= 0, "Number of rounds can't be negative");
const GameState.initial()
: totalScore = 0,
: status = GameStatus.waiting,
totalScore = 0,
roundScore = 0,
multiplier = 1,
rounds = 3,
@ -65,8 +78,7 @@ class GameState extends Equatable {
/// PinballGame.
final List<GameBonus> bonusHistory;
/// Determines when the game is over.
bool get isGameOver => rounds == 0;
final GameStatus status;
/// The score displayed at the game.
int get displayScore => roundScore + totalScore;
@ -78,6 +90,7 @@ class GameState extends Equatable {
int? balls,
int? rounds,
List<GameBonus>? bonusHistory,
GameStatus? status,
}) {
assert(
totalScore == null || totalScore >= this.totalScore,
@ -90,6 +103,7 @@ class GameState extends Equatable {
multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory,
status: status ?? this.status,
);
}
@ -100,5 +114,6 @@ class GameState extends Equatable {
multiplier,
rounds,
bonusHistory,
status,
];
}

@ -8,7 +8,7 @@ export 'controlled_plunger.dart';
export 'dino_desert/dino_desert.dart';
export 'drain.dart';
export 'flutter_forest/flutter_forest.dart';
export 'game_flow_controller.dart';
export 'game_bloc_status_listener.dart';
export 'google_word/google_word.dart';
export 'launcher.dart';
export 'multiballs/multiballs.dart';

@ -37,7 +37,7 @@ class FlipperController extends ComponentController<Flipper>
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (state?.isGameOver ?? false) return true;
if (state?.status.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {

@ -38,7 +38,7 @@ class PlungerController extends ComponentController<Plunger>
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (state?.isGameOver ?? false) return true;
if (state?.status.isGameOver ?? false) return true;
if (!_keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {

@ -0,0 +1,33 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
/// Listens to the [GameBloc] and updates the game accordingly.
class GameBlocStatusListener extends Component
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.status != newState.status;
}
@override
void onNewState(GameState state) {
switch (state.status) {
case GameStatus.waiting:
break;
case GameStatus.playing:
gameRef.player.play(PinballAudio.backgroundMusic);
gameRef.firstChild<CameraController>()?.focusOnGame();
gameRef.overlays.remove(PinballGame.playButtonOverlay);
break;
case GameStatus.gameOver:
gameRef.descendants().whereType<Backbox>().first.initialsInput(
score: state.displayScore,
characterIconPath: gameRef.characterTheme.leaderboardIcon.keyName,
);
gameRef.firstChild<CameraController>()!.focusOnGameOverBackbox();
break;
}
}
}

@ -1,46 +0,0 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template game_flow_controller}
/// A [Component] that controls the game over and game restart logic
/// {@endtemplate}
class GameFlowController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState> {
/// {@macro game_flow_controller}
GameFlowController(PinballGame component) : super(component);
@override
bool listenWhen(GameState? previousState, GameState newState) {
return previousState?.isGameOver != newState.isGameOver;
}
@override
void onNewState(GameState state) {
if (state.isGameOver) {
_initialsInput();
} else {
start();
}
}
/// Puts the game in the initials input state.
void _initialsInput() {
// TODO(erickzanardo): implement score submission and "navigate" to the
// next page
component.descendants().whereType<Backbox>().first.initialsInput(
score: state?.displayScore ?? 0,
characterIconPath: component.characterTheme.leaderboardIcon.keyName,
);
component.firstChild<CameraController>()!.focusOnGameOverBackbox();
}
/// Puts the game in the playing state.
void start() {
component.player.play(PinballAudio.backgroundMusic);
component.firstChild<CameraController>()?.focusOnGame();
component.overlays.remove(PinballGame.playButtonOverlay);
}
}

@ -42,11 +42,8 @@ class PinballGame extends PinballForge2DGame
final AppLocalizations l10n;
late final GameFlowController gameFlowController;
@override
Future<void> onLoad() async {
await add(gameFlowController = GameFlowController(this));
await add(CameraController(this));
final machine = [
@ -71,26 +68,29 @@ class PinballGame extends PinballForge2DGame
SparkyScorch(),
];
await add(
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
...machine,
...decals,
...characterAreas,
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
await addAll(
[
GameBlocStatusListener(),
CanvasComponent(
onSpritePainted: (paint) {
if (paint.filterQuality != FilterQuality.medium) {
paint.filterQuality = FilterQuality.medium;
}
},
children: [
ZCanvasComponent(
children: [
...machine,
...decals,
...characterAreas,
Drain(),
BottomGroup(),
Launcher(),
],
),
],
),
],
);
await super.onLoad();
@ -151,9 +151,7 @@ class _GameBallsController extends ComponentController<PinballGame>
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final notGameOver = !newState.isGameOver;
return noBallsLeft && notGameOver;
return noBallsLeft && newState.status.isPlaying;
}
@override

@ -113,7 +113,6 @@ class PinballGameLoadedView extends StatelessWidget {
final clampedMargin = leftMargin > 0 ? leftMargin : 0.0;
return StartGameListener(
game: game,
child: Stack(
children: [
Positioned.fill(

@ -26,7 +26,8 @@ class _GameHudState extends State<GameHud> {
@override
Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
final isGameOver =
context.select((GameBloc bloc) => bloc.state.status.isGameOver);
final height = _calculateHeight(context);

@ -13,7 +13,8 @@ class ScoreView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isGameOver = context.select((GameBloc bloc) => bloc.state.isGameOver);
final isGameOver =
context.select((GameBloc bloc) => bloc.state.status.isGameOver);
return Padding(
padding: const EdgeInsets.symmetric(

@ -17,13 +17,10 @@ class StartGameListener extends StatelessWidget {
const StartGameListener({
Key? key,
required Widget child,
required PinballGame game,
}) : _child = child,
_game = game,
super(key: key);
final Widget _child;
final PinballGame _game;
@override
Widget build(BuildContext context) {
@ -34,7 +31,7 @@ class StartGameListener extends StatelessWidget {
break;
case StartGameStatus.selectCharacter:
_onSelectCharacter(context);
_game.gameFlowController.start();
context.read<GameBloc>().add(const GameStarted());
break;
case StartGameStatus.howToPlay:
_onHowToPlay(context);

@ -57,6 +57,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.playing,
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;

@ -10,6 +10,34 @@ void main() {
expect(gameBloc.state.rounds, equals(3));
});
blocTest<GameBloc, GameState>(
'GameStarted starts the game',
build: GameBloc.new,
act: (bloc) => bloc.add(const GameStarted()),
expect: () => [
isA<GameState>()
..having(
(state) => state.status,
'status',
GameStatus.playing,
),
],
);
blocTest<GameBloc, GameState>(
'GameOver finishes the game',
build: GameBloc.new,
act: (bloc) => bloc.add(const GameOver()),
expect: () => [
isA<GameState>()
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
],
);
group('RoundLost', () {
blocTest<GameBloc, GameState>(
'decreases number of rounds '
@ -23,6 +51,24 @@ void main() {
],
);
blocTest<GameBloc, GameState>(
'sets game over when there are no more rounds',
build: GameBloc.new,
act: (bloc) {
bloc
..add(const RoundLost())
..add(const RoundLost())
..add(const RoundLost());
},
expect: () => [
isA<GameState>()..having((state) => state.rounds, 'rounds', 2),
isA<GameState>()..having((state) => state.rounds, 'rounds', 1),
isA<GameState>()
..having((state) => state.rounds, 'rounds', 0)
..having((state) => state.status, 'status', GameStatus.gameOver),
],
);
blocTest<GameBloc, GameState>(
'apply multiplier to roundScore and add it to totalScore '
'when round is lost',
@ -33,6 +79,7 @@ void main() {
multiplier: 3,
rounds: 2,
bonusHistory: [],
status: GameStatus.playing,
),
act: (bloc) {
bloc.add(const RoundLost());
@ -45,8 +92,7 @@ void main() {
);
blocTest<GameBloc, GameState>(
'resets multiplier '
'when round is lost',
'resets multiplier when round is lost',
build: GameBloc.new,
seed: () => const GameState(
totalScore: 10,
@ -54,6 +100,7 @@ void main() {
multiplier: 3,
rounds: 2,
bonusHistory: [],
status: GameStatus.playing,
),
act: (bloc) {
bloc.add(const RoundLost());
@ -66,25 +113,26 @@ void main() {
group('Scored', () {
blocTest<GameBloc, GameState>(
'increases score '
'when game is not over',
'increases score when playing',
build: GameBloc.new,
act: (bloc) => bloc
..add(const GameStarted())
..add(const Scored(points: 2))
..add(const Scored(points: 3)),
expect: () => [
isA<GameState>()
..having((state) => state.status, 'status', GameStatus.playing),
isA<GameState>()
..having((state) => state.roundScore, 'roundScore', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
..having((state) => state.status, 'status', GameStatus.playing),
isA<GameState>()
..having((state) => state.roundScore, 'roundScore', 5)
..having((state) => state.isGameOver, 'isGameOver', false),
..having((state) => state.status, 'status', GameStatus.playing),
],
);
blocTest<GameBloc, GameState>(
"doesn't increase score "
'when game is over',
"doesn't increase score when game is over",
build: GameBloc.new,
act: (bloc) {
for (var i = 0; i < bloc.state.rounds; i++) {
@ -96,15 +144,27 @@ void main() {
isA<GameState>()
..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
isA<GameState>()
..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
isA<GameState>()
..having((state) => state.roundScore, 'roundScore', 0)
..having((state) => state.rounds, 'rounds', 0)
..having((state) => state.isGameOver, 'isGameOver', true),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
],
);
});
@ -115,15 +175,26 @@ void main() {
'when multiplier is below 6 and game is not over',
build: GameBloc.new,
act: (bloc) => bloc
..add(const GameStarted())
..add(const MultiplierIncreased())
..add(const MultiplierIncreased()),
expect: () => [
isA<GameState>()
..having((state) => state.status, 'status', GameStatus.playing),
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 3)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
],
);
@ -137,6 +208,7 @@ void main() {
multiplier: 6,
rounds: 3,
bonusHistory: [],
status: GameStatus.playing,
),
act: (bloc) => bloc..add(const MultiplierIncreased()),
expect: () => const <GameState>[],
@ -147,21 +219,36 @@ void main() {
'when game is over',
build: GameBloc.new,
act: (bloc) {
bloc.add(const GameStarted());
for (var i = 0; i < bloc.state.rounds; i++) {
bloc.add(const RoundLost());
}
bloc.add(const MultiplierIncreased());
},
expect: () => [
isA<GameState>()
..having((state) => state.status, 'status', GameStatus.playing),
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', true),
..having(
(state) => state.status,
'status',
GameStatus.gameOver,
),
],
);
});

@ -72,6 +72,32 @@ void main() {
});
});
group('GameStarted', () {
test('can be instantiated', () {
expect(const GameStarted(), isNotNull);
});
test('supports value equality', () {
expect(
GameStarted(),
equals(const GameStarted()),
);
});
});
group('GameOver', () {
test('can be instantiated', () {
expect(const GameOver(), isNotNull);
});
test('supports value equality', () {
expect(
GameOver(),
equals(const GameOver()),
);
});
});
group('SparkyTurboChargeActivated', () {
test('can be instantiated', () {
expect(const SparkyTurboChargeActivated(), isNotNull);

@ -13,6 +13,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: const [],
status: GameStatus.waiting,
),
equals(
const GameState(
@ -21,6 +22,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.waiting,
),
),
);
@ -35,6 +37,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.waiting,
),
isNotNull,
);
@ -52,6 +55,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: const [],
status: GameStatus.waiting,
),
throwsAssertionError,
);
@ -69,6 +73,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: const [],
status: GameStatus.waiting,
),
throwsAssertionError,
);
@ -86,6 +91,7 @@ void main() {
multiplier: 0,
rounds: 3,
bonusHistory: const [],
status: GameStatus.waiting,
),
throwsAssertionError,
);
@ -103,40 +109,13 @@ void main() {
multiplier: 1,
rounds: -1,
bonusHistory: const [],
status: GameStatus.waiting,
),
throwsAssertionError,
);
},
);
group('isGameOver', () {
test(
'is true '
'when no rounds are left', () {
const gameState = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
expect(gameState.isGameOver, isTrue);
});
test(
'is false '
'when one 1 round left', () {
const gameState = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 1,
bonusHistory: [],
);
expect(gameState.isGameOver, isFalse);
});
});
group('copyWith', () {
test(
'throws AssertionError '
@ -148,6 +127,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.waiting,
);
expect(
() => gameState.copyWith(totalScore: gameState.totalScore - 1),
@ -166,6 +146,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.waiting,
);
expect(
gameState.copyWith(),
@ -184,6 +165,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.waiting,
);
final otherGameState = GameState(
totalScore: gameState.totalScore + 1,
@ -191,6 +173,7 @@ void main() {
multiplier: gameState.multiplier + 1,
rounds: gameState.rounds + 1,
bonusHistory: const [GameBonus.googleWord],
status: GameStatus.playing,
);
expect(gameState, isNot(equals(otherGameState)));
@ -201,6 +184,7 @@ void main() {
multiplier: otherGameState.multiplier,
rounds: otherGameState.rounds,
bonusHistory: otherGameState.bonusHistory,
status: otherGameState.status,
),
equals(otherGameState),
);

@ -22,24 +22,19 @@ void main() {
() => EmptyPinballTestGame(assets: assets),
);
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
final bloc = _MockGameBloc();
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
assets: assets,
);
group('FlipperController', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
group('onKeyEvent', () {
final leftKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowLeft,
@ -65,6 +60,12 @@ void main() {
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.add(flipper);
controller.onKeyEvent(event, {});
@ -79,6 +80,14 @@ void main() {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.gameOver,
),
);
await game.ensureAdd(flipper);
controller.onKeyEvent(event, {});
},
@ -94,6 +103,12 @@ void main() {
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.add(flipper);
controller.onKeyEvent(event, {});
@ -109,6 +124,12 @@ void main() {
'does nothing '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.add(flipper);
controller.onKeyEvent(event, {});
@ -135,6 +156,12 @@ void main() {
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.add(flipper);
controller.onKeyEvent(event, {});
@ -150,6 +177,12 @@ void main() {
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ready();
await game.add(flipper);
controller.onKeyEvent(event, {});
@ -164,6 +197,14 @@ void main() {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.gameOver,
),
);
await game.ensureAdd(flipper);
controller.onKeyEvent(event, {});
},

@ -17,23 +17,18 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(EmptyPinballTestGame.new);
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () {
final bloc = _MockGameBloc();
const state = GameState(
totalScore: 0,
roundScore: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);
return bloc;
},
);
group('PlungerController', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameBlocTester = FlameBlocTester<EmptyPinballTestGame, GameBloc>(
gameBuilder: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
);
group('onKeyEvent', () {
final downKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowDown,
@ -55,6 +50,12 @@ void main() {
'moves down '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
@ -70,6 +71,12 @@ void main() {
'when ${event.logicalKey.keyLabel} is released '
'and plunger is below its starting position',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, 1), 0);
controller.onKeyEvent(event, {});
@ -85,6 +92,12 @@ void main() {
'does not move when ${event.logicalKey.keyLabel} is released '
'and plunger is in its starting position',
(game) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
@ -98,6 +111,14 @@ void main() {
flameBlocTester.testGameWidget(
'does nothing when is game over',
setUp: (game, tester) async {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial().copyWith(
status: GameStatus.gameOver,
),
);
await game.ensureAdd(plunger);
controller.onKeyEvent(event, {});
},

@ -19,7 +19,7 @@ class _MockActiveOverlaysNotifier extends Mock
class _MockPinballPlayer extends Mock implements PinballPlayer {}
void main() {
group('GameFlowController', () {
group('GameBlocStatusListener', () {
group('listenWhen', () {
test('is true when the game over state has changed', () {
final state = GameState(
@ -28,11 +28,12 @@ void main() {
multiplier: 1,
rounds: 0,
bonusHistory: const [],
status: GameStatus.playing,
);
final previous = GameState.initial();
expect(
GameFlowController(_MockPinballGame()).listenWhen(previous, state),
GameBlocStatusListener().listenWhen(previous, state),
isTrue,
);
});
@ -42,7 +43,7 @@ void main() {
late PinballGame game;
late Backbox backbox;
late CameraController cameraController;
late GameFlowController gameFlowController;
late GameBlocStatusListener gameFlowController;
late PinballPlayer pinballPlayer;
late ActiveOverlaysNotifier overlays;
@ -50,10 +51,12 @@ void main() {
game = _MockPinballGame();
backbox = _MockBackbox();
cameraController = _MockCameraController();
gameFlowController = GameFlowController(game);
gameFlowController = GameBlocStatusListener();
overlays = _MockActiveOverlaysNotifier();
pinballPlayer = _MockPinballPlayer();
gameFlowController.mockGameRef(game);
when(
() => backbox.initialsInput(
score: any(named: 'score'),
@ -78,19 +81,19 @@ void main() {
'changes the backbox display and camera correctly '
'when the game is over',
() {
gameFlowController.onNewState(
GameState(
totalScore: 0,
roundScore: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
),
final state = GameState(
totalScore: 0,
roundScore: 10,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
status: GameStatus.gameOver,
);
gameFlowController.onNewState(state);
verify(
() => backbox.initialsInput(
score: 0,
score: state.displayScore,
characterIconPath: any(named: 'characterIconPath'),
onSubmit: any(named: 'onSubmit'),
),
@ -102,7 +105,9 @@ void main() {
test(
'changes the backbox and camera correctly when it is not a game over',
() {
gameFlowController.onNewState(GameState.initial());
gameFlowController.onNewState(
GameState.initial().copyWith(status: GameStatus.playing),
);
verify(cameraController.focusOnGame).called(1);
verify(() => overlays.remove(PinballGame.playButtonOverlay))
@ -113,7 +118,9 @@ void main() {
test(
'plays the background music on start',
() {
gameFlowController.onNewState(GameState.initial());
gameFlowController.onNewState(
GameState.initial().copyWith(status: GameStatus.playing),
);
verify(() => pinballPlayer.play(PinballAudio.backgroundMusic))
.called(1);

@ -79,6 +79,7 @@ void main() {
multiplier: 1,
rounds: 0,
bonusHistory: const [],
status: GameStatus.playing,
);
expect(

@ -60,6 +60,7 @@ void main() {
roundScore: 10,
multiplier: 2,
rounds: 0,
status: GameStatus.playing,
bonusHistory: const [],
);
@ -76,6 +77,7 @@ void main() {
roundScore: 10,
multiplier: 1,
rounds: 0,
status: GameStatus.playing,
bonusHistory: const [],
);

@ -281,7 +281,7 @@ void main() {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = _MockGameState();
when(() => newState.isGameOver).thenReturn(false);
when(() => newState.status).thenReturn(GameStatus.playing);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
@ -298,7 +298,7 @@ void main() {
"doesn't listen when some balls are left",
(game) async {
final newState = _MockGameState();
when(() => newState.isGameOver).thenReturn(false);
when(() => newState.status).thenReturn(GameStatus.playing);
await game.ready();
@ -319,7 +319,7 @@ void main() {
// TODO(ruimiguel): check why testGameWidget doesn't add any ball
// to the game. Test needs to have no balls, so fortunately works.
final newState = _MockGameState();
when(() => newState.isGameOver).thenReturn(true);
when(() => newState.status).thenReturn(GameStatus.gameOver);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);

@ -27,6 +27,7 @@ void main() {
multiplier: 1,
rounds: 1,
bonusHistory: [],
status: GameStatus.playing,
);
setUp(() async {

@ -18,6 +18,7 @@ void main() {
multiplier: 1,
rounds: 3,
bonusHistory: [],
status: GameStatus.playing,
);
setUp(() {

@ -23,6 +23,7 @@ void main() {
multiplier: 1,
rounds: 1,
bonusHistory: [],
status: GameStatus.playing,
);
setUp(() {
@ -54,9 +55,7 @@ void main() {
final l10n = await AppLocalizations.delegate.load(const Locale('en'));
stateController.add(
initialState.copyWith(
rounds: 0,
),
initialState.copyWith(status: GameStatus.gameOver),
);
await tester.pumpApp(

@ -1,26 +1,8 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/start_game/bloc/start_game_bloc.dart';
class _MockPinballGame extends Mock implements PinballGame {}
class _MockGameFlowController extends Mock implements GameFlowController {}
void main() {
late PinballGame pinballGame;
setUp(() {
pinballGame = _MockPinballGame();
when(
() => pinballGame.gameFlowController,
).thenReturn(
_MockGameFlowController(),
);
});
group('StartGameBloc', () {
blocTest<StartGameBloc, StartGameState>(
'on PlayTapped changes status to selectCharacter',

@ -12,17 +12,14 @@ import '../../helpers/helpers.dart';
class _MockStartGameBloc extends Mock implements StartGameBloc {}
class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
class _MockPinballGame extends Mock implements PinballGame {}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockGameFlowController extends Mock implements GameFlowController {}
class _MockCharacterThemeCubit extends Mock implements CharacterThemeCubit {}
class _MockPinballPlayer extends Mock implements PinballPlayer {}
void main() {
late StartGameBloc startGameBloc;
late PinballGame pinballGame;
late PinballPlayer pinballPlayer;
late CharacterThemeCubit characterThemeCubit;
@ -31,14 +28,25 @@ void main() {
await mockFlameImages();
startGameBloc = _MockStartGameBloc();
pinballGame = _MockPinballGame();
pinballPlayer = _MockPinballPlayer();
characterThemeCubit = _MockCharacterThemeCubit();
});
group('on selectCharacter status', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
});
testWidgets(
'calls start on the game controller',
'calls onGameStarted event',
(tester) async {
whenListen(
startGameBloc,
@ -47,19 +55,16 @@ void main() {
),
initialState: const StartGameState.initial(),
);
final gameController = _MockGameFlowController();
when(() => pinballGame.gameFlowController)
.thenAnswer((_) => gameController);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
);
verify(gameController.start).called(1);
verify(() => gameBloc.add(const GameStarted())).called(1);
},
);
@ -78,15 +83,12 @@ void main() {
Stream.value(const CharacterThemeState.initial()),
initialState: const CharacterThemeState.initial(),
);
final gameController = _MockGameFlowController();
when(() => pinballGame.gameFlowController)
.thenAnswer((_) => gameController);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
gameBloc: gameBloc,
startGameBloc: startGameBloc,
characterThemeCubit: characterThemeCubit,
);
@ -113,9 +115,8 @@ void main() {
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
@ -141,9 +142,8 @@ void main() {
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
@ -173,9 +173,8 @@ void main() {
);
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
@ -208,9 +207,8 @@ void main() {
'adds HowToPlayFinished event',
(tester) async {
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
startGameBloc: startGameBloc,
);
@ -239,9 +237,8 @@ void main() {
'plays the I/O Pinball voice over audio',
(tester) async {
await tester.pumpApp(
StartGameListener(
game: pinballGame,
child: const SizedBox.shrink(),
const StartGameListener(
child: SizedBox.shrink(),
),
startGameBloc: startGameBloc,
pinballPlayer: pinballPlayer,

Loading…
Cancel
Save