feat: game bloc multiplier (#213)

* feat: added events for multiplier

* feat: added events for increment, apply and reset multiplier

* feat: added multiplier to game bloc state

* test: test for multiplier at game bloc

* test: added multiplier to game state

* refactor: multiplier always increased by 1

* refactor: add multiplier state on BallLost

* refactor: added round to game state and changed gameover and ball lost logic

* test: fixed tests for game bloc

* refactor: multiplier max value 6 at game bloc

* test: fixed tests with new game over logic

* chore: properties renamed and removed unused

* Update lib/game/bloc/game_event.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* fix: pubspec from main

* refactor: pubspec from main

* chore: removed unused import

* feat: ball added event to game bloc

* test: fixed test for ball added

* feat: added BallAdded event on ball mounted

* test: fixing tests for BallAdded

* test: flamebloctester for ball added

* test: refactored tests for pinballgame

* refactor: BallAdded event on ball mounted

* chore: removed unnecessary imports

* test: refactor tests for pinball_game

* refactor: use rounds instead of balls

* refactor: changed BallLost event with RoundLost, and moved part of the logic to controlled_ball

* test: tests for RoundLost

* fix: fixed wrong tests for pinball_game

* test: remove deleted balls property from GameState

* chore: doc

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>
pull/260/head
Rui Miguel Alonso 3 years ago committed by GitHub
parent 66270d103e
commit 4524849c29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'dart:math' as math;
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
@ -9,19 +9,41 @@ part 'game_state.dart';
class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost);
on<RoundLost>(_onRoundLost);
on<Scored>(_onScored);
on<MultiplierIncreased>(_onIncreasedMultiplier);
on<BonusActivated>(_onBonusActivated);
on<SparkyTurboChargeActivated>(_onSparkyTurboChargeActivated);
}
void _onBallLost(BallLost event, Emitter emit) {
emit(state.copyWith(balls: state.balls - 1));
void _onRoundLost(RoundLost event, Emitter emit) {
final score = state.score * state.multiplier;
final roundsLeft = math.max(state.rounds - 1, 0);
emit(
state.copyWith(
score: score,
multiplier: 1,
rounds: roundsLeft,
),
);
}
void _onScored(Scored event, Emitter emit) {
if (!state.isGameOver) {
emit(state.copyWith(score: state.score + event.points));
emit(
state.copyWith(score: state.score + event.points),
);
}
}
void _onIncreasedMultiplier(MultiplierIncreased event, Emitter emit) {
if (!state.isGameOver) {
emit(
state.copyWith(
multiplier: math.min(state.multiplier + 1, 6),
),
);
}
}

@ -7,12 +7,12 @@ abstract class GameEvent extends Equatable {
const GameEvent();
}
/// {@template ball_lost_game_event}
/// Event added when a user drops a ball off the screen.
/// {@template round_lost_game_event}
/// Event added when a user drops all balls off the screen and loses a round.
/// {@endtemplate}
class BallLost extends GameEvent {
/// {@macro ball_lost_game_event}
const BallLost();
class RoundLost extends GameEvent {
/// {@macro round_lost_game_event}
const RoundLost();
@override
List<Object?> get props => [];
@ -48,3 +48,14 @@ class SparkyTurboChargeActivated extends GameEvent {
@override
List<Object?> get props => [];
}
/// {@template multiplier_increased_game_event}
/// Added when a multiplier is gained.
/// {@endtemplate}
class MultiplierIncreased extends GameEvent {
/// {@macro multiplier_increased_game_event}
const MultiplierIncreased();
@override
List<Object?> get props => [];
}

@ -27,34 +27,42 @@ class GameState extends Equatable {
/// {@macro game_state}
const GameState({
required this.score,
required this.balls,
required this.multiplier,
required this.rounds,
required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls 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()
: score = 0,
balls = 3,
multiplier = 1,
rounds = 3,
bonusHistory = const [];
/// The current score of the game.
final int score;
/// The number of balls left in the game.
/// The current multiplier for the score.
final int multiplier;
/// The number of rounds left in the game.
///
/// When the number of balls is 0, the game is over.
final int balls;
/// When the number of rounds is 0, the game is over.
final int rounds;
/// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame.
final List<GameBonus> bonusHistory;
/// Determines when the game is over.
bool get isGameOver => balls == 0;
bool get isGameOver => rounds == 0;
GameState copyWith({
int? score,
int? multiplier,
int? balls,
int? rounds,
List<GameBonus>? bonusHistory,
}) {
assert(
@ -64,7 +72,8 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
multiplier: multiplier ?? this.multiplier,
rounds: rounds ?? this.rounds,
bonusHistory: bonusHistory ?? this.bonusHistory,
);
}
@ -72,7 +81,8 @@ class GameState extends Equatable {
@override
List<Object?> get props => [
score,
balls,
multiplier,
rounds,
bonusHistory,
];
}

@ -1,5 +1,4 @@
import 'package:flame/components.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
@ -8,12 +7,12 @@ import 'package:pinball_theme/pinball_theme.dart';
/// {@template controlled_ball}
/// A [Ball] with a [BallController] attached.
///
/// When a [Ball] is lost, if there aren't more [Ball]s in play and the game is
/// not over, a new [Ball] will be spawned.
/// {@endtemplate}
class ControlledBall extends Ball with Controls<BallController> {
/// A [Ball] that launches from the [Plunger].
///
/// When a launched [Ball] is lost, it will decrease the [GameState.balls]
/// count, and a new [Ball] is spawned.
ControlledBall.launch({
required CharacterTheme characterTheme,
}) : super(baseColor: characterTheme.ballColor) {
@ -24,8 +23,6 @@ class ControlledBall extends Ball with Controls<BallController> {
/// {@template bonus_ball}
/// {@macro controlled_ball}
///
/// When a bonus [Ball] is lost, the [GameState.balls] doesn't change.
/// {@endtemplate}
ControlledBall.bonus({
required CharacterTheme characterTheme,
@ -36,7 +33,7 @@ class ControlledBall extends Ball with Controls<BallController> {
/// [Ball] used in [DebugPinballGame].
ControlledBall.debug() : super(baseColor: const Color(0xFFFF0000)) {
controller = DebugBallController(this);
controller = BallController(this);
priority = RenderPriority.ballOnBoard;
}
}
@ -74,15 +71,9 @@ class BallController extends ComponentController<Ball>
@override
void onRemove() {
super.onRemove();
gameRef.read<GameBloc>().add(const BallLost());
final noBallsLeft = gameRef.descendants().whereType<Ball>().isEmpty;
if (noBallsLeft) {
gameRef.read<GameBloc>().add(const RoundLost());
}
}
/// {@macro ball_controller}
class DebugBallController extends BallController {
/// {@macro ball_controller}
DebugBallController(Ball<Forge2DGame> component) : super(component);
@override
void onRemove() {}
}

@ -72,7 +72,7 @@ class PinballGame extends Forge2DGame
}
class _GameBallsController extends ComponentController<PinballGame>
with BlocComponent<GameBloc, GameState>, HasGameRef<PinballGame> {
with BlocComponent<GameBloc, GameState> {
_GameBallsController(PinballGame game) : super(game);
late final Plunger _plunger;
@ -80,9 +80,9 @@ class _GameBallsController extends ComponentController<PinballGame>
@override
bool listenWhen(GameState? previousState, GameState newState) {
final noBallsLeft = component.descendants().whereType<Ball>().isEmpty;
final canBallRespawn = newState.balls > 0;
final notGameOver = !newState.isGameOver;
return noBallsLeft && canBallRespawn;
return noBallsLeft && notGameOver;
}
@override
@ -99,7 +99,7 @@ class _GameBallsController extends ComponentController<PinballGame>
void _spawnBall() {
final ball = ControlledBall.launch(
characterTheme: gameRef.characterTheme,
characterTheme: component.characterTheme,
)..initialPosition = Vector2(
_plunger.body.position.x,
_plunger.body.position.y - Ball.size.y,
@ -160,18 +160,6 @@ class DebugPinballGame extends PinballGame with FPSCounter, 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;
}
}
// TODO(wolfenrain): investigate this CI failure.

@ -7,7 +7,7 @@ import 'package:pinball/theme/app_colors.dart';
/// {@template game_hud}
/// Overlay on the [PinballGame].
///
/// Displays the current [GameState.score], [GameState.balls] and animates when
/// Displays the current [GameState.score], [GameState.rounds] and animates when
/// the player gets a [GameBonus].
/// {@endtemplate}
class GameHud extends StatefulWidget {

@ -14,9 +14,7 @@ class RoundCountDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
// TODO(arturplaczek): refactor when GameState handle balls and rounds and
// select state.rounds property instead of state.ball
final balls = context.select((GameBloc bloc) => bloc.state.balls);
final rounds = context.select((GameBloc bloc) => bloc.state.rounds);
return Row(
children: [
@ -29,9 +27,9 @@ class RoundCountDisplay extends StatelessWidget {
const SizedBox(width: 8),
Row(
children: [
RoundIndicator(isActive: balls >= 1),
RoundIndicator(isActive: balls >= 2),
RoundIndicator(isActive: balls >= 3),
RoundIndicator(isActive: rounds >= 1),
RoundIndicator(isActive: rounds >= 2),
RoundIndicator(isActive: rounds >= 3),
],
),
],

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "39.0.0"
version: "31.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "2.8.0"
args:
dependency: transitive
description:
@ -71,6 +71,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
clock:
dependency: transitive
description:
@ -316,7 +323,7 @@ packages:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.4"
version: "0.6.3"
json_annotation:
dependency: transitive
description:
@ -351,7 +358,7 @@ packages:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.1.3"
meta:
dependency: transitive
description:
@ -414,7 +421,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.1"
version: "1.8.0"
path_provider:
dependency: transitive
description:
@ -587,7 +594,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
version: "1.8.1"
stack_trace:
dependency: transitive
description:
@ -629,21 +636,21 @@ packages:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.21.1"
version: "1.19.5"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9"
version: "0.4.8"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.13"
version: "0.4.9"
typed_data:
dependency: transitive
description:
@ -664,7 +671,7 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
version: "2.1.1"
very_good_analysis:
dependency: "direct dev"
description:

@ -4,27 +4,69 @@ import 'package:pinball/game/game.dart';
void main() {
group('GameBloc', () {
test('initial state has 3 balls and empty score', () {
test('initial state has 3 rounds and empty score', () {
final gameBloc = GameBloc();
expect(gameBloc.state.score, equals(0));
expect(gameBloc.state.balls, equals(3));
expect(gameBloc.state.rounds, equals(3));
});
group('LostBall', () {
group('RoundLost', () {
blocTest<GameBloc, GameState>(
'decreases number of balls',
'decreases number of rounds '
'when there are already available rounds',
build: GameBloc.new,
act: (bloc) {
bloc.add(const BallLost());
bloc.add(const RoundLost());
},
expect: () => [
const GameState(
score: 0,
balls: 2,
multiplier: 1,
rounds: 2,
bonusHistory: [],
),
],
);
blocTest<GameBloc, GameState>(
'apply multiplier to score '
'when round is lost',
build: GameBloc.new,
seed: () => const GameState(
score: 5,
multiplier: 3,
rounds: 2,
bonusHistory: [],
),
act: (bloc) {
bloc.add(const RoundLost());
},
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 15)
..having((state) => state.rounds, 'rounds', 1),
],
);
blocTest<GameBloc, GameState>(
'resets multiplier '
'when round is lost',
build: GameBloc.new,
seed: () => const GameState(
score: 5,
multiplier: 3,
rounds: 2,
bonusHistory: [],
),
act: (bloc) {
bloc.add(const RoundLost());
},
expect: () => [
isA<GameState>()
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.rounds, 'rounds', 1),
],
);
});
group('Scored', () {
@ -36,16 +78,12 @@ void main() {
..add(const Scored(points: 2))
..add(const Scored(points: 3)),
expect: () => [
const GameState(
score: 2,
balls: 3,
bonusHistory: [],
),
const GameState(
score: 5,
balls: 3,
bonusHistory: [],
),
isA<GameState>()
..having((state) => state.score, 'score', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 5)
..having((state) => state.isGameOver, 'isGameOver', false),
],
);
@ -54,27 +92,85 @@ void main() {
'when game is over',
build: GameBloc.new,
act: (bloc) {
for (var i = 0; i < bloc.state.balls; i++) {
bloc.add(const BallLost());
for (var i = 0; i < bloc.state.rounds; i++) {
bloc.add(const RoundLost());
}
bloc.add(const Scored(points: 2));
},
expect: () => [
const GameState(
score: 0,
balls: 2,
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
bonusHistory: [],
),
const GameState(
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.rounds, 'rounds', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.rounds, 'rounds', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.rounds, 'rounds', 0)
..having((state) => state.isGameOver, 'isGameOver', true),
],
);
});
group('MultiplierIncreased', () {
blocTest<GameBloc, GameState>(
'increases multiplier '
'when multiplier is below 6 and game is not over',
build: GameBloc.new,
act: (bloc) => bloc
..add(const MultiplierIncreased())
..add(const MultiplierIncreased()),
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 2)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 3)
..having((state) => state.isGameOver, 'isGameOver', false),
],
);
blocTest<GameBloc, GameState>(
"doesn't increase multiplier "
'when multiplier is 6 and game is not over',
build: GameBloc.new,
seed: () => const GameState(
score: 0,
balls: 0,
multiplier: 6,
rounds: 3,
bonusHistory: [],
),
act: (bloc) => bloc..add(const MultiplierIncreased()),
expect: () => const <GameState>[],
);
blocTest<GameBloc, GameState>(
"doesn't increase multiplier "
'when game is over',
build: GameBloc.new,
act: (bloc) {
for (var i = 0; i < bloc.state.rounds; i++) {
bloc.add(const RoundLost());
}
bloc.add(const MultiplierIncreased());
},
expect: () => [
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', false),
isA<GameState>()
..having((state) => state.score, 'score', 0)
..having((state) => state.multiplier, 'multiplier', 1)
..having((state) => state.isGameOver, 'isGameOver', true),
],
);
});
@ -88,16 +184,18 @@ void main() {
act: (bloc) => bloc
..add(const BonusActivated(GameBonus.googleWord))
..add(const BonusActivated(GameBonus.dashNest)),
expect: () => const [
GameState(
score: 0,
balls: 3,
bonusHistory: [GameBonus.googleWord],
expect: () => [
isA<GameState>()
..having(
(state) => state.bonusHistory,
'bonusHistory',
[GameBonus.googleWord],
),
GameState(
score: 0,
balls: 3,
bonusHistory: [GameBonus.googleWord, GameBonus.dashNest],
isA<GameState>()
..having(
(state) => state.bonusHistory,
'bonusHistory',
[GameBonus.googleWord, GameBonus.dashNest],
),
],
);
@ -109,11 +207,12 @@ void main() {
'adds game bonus',
build: GameBloc.new,
act: (bloc) => bloc..add(const SparkyTurboChargeActivated()),
expect: () => const [
GameState(
score: 0,
balls: 3,
bonusHistory: [GameBonus.sparkyTurboCharge],
expect: () => [
isA<GameState>()
..having(
(state) => state.bonusHistory,
'bonusHistory',
[GameBonus.sparkyTurboCharge],
),
],
);

@ -5,15 +5,15 @@ import 'package:pinball/game/game.dart';
void main() {
group('GameEvent', () {
group('BallLost', () {
group('RoundLost', () {
test('can be instantiated', () {
expect(const BallLost(), isNotNull);
expect(const RoundLost(), isNotNull);
});
test('supports value equality', () {
expect(
BallLost(),
equals(const BallLost()),
RoundLost(),
equals(const RoundLost()),
);
});
});
@ -41,6 +41,19 @@ void main() {
});
});
group('MultiplierIncreased', () {
test('can be instantiated', () {
expect(const MultiplierIncreased(), isNotNull);
});
test('supports value equality', () {
expect(
MultiplierIncreased(),
equals(const MultiplierIncreased()),
);
});
});
group('BonusActivated', () {
test('can be instantiated', () {
expect(const BonusActivated(GameBonus.dashNest), isNotNull);

@ -9,13 +9,15 @@ void main() {
expect(
GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 3,
bonusHistory: const [],
),
equals(
const GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
),
),
@ -27,7 +29,8 @@ void main() {
expect(
const GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
),
isNotNull,
@ -37,12 +40,13 @@ void main() {
test(
'throws AssertionError '
'when balls are negative',
'when score is negative',
() {
expect(
() => GameState(
balls: -1,
score: 0,
score: -1,
multiplier: 1,
rounds: 3,
bonusHistory: const [],
),
throwsAssertionError,
@ -52,12 +56,29 @@ void main() {
test(
'throws AssertionError '
'when score is negative',
'when multiplier is less than 1',
() {
expect(
() => GameState(
balls: 0,
score: -1,
score: 1,
multiplier: 0,
rounds: 3,
bonusHistory: const [],
),
throwsAssertionError,
);
},
);
test(
'throws AssertionError '
'when rounds is negative',
() {
expect(
() => GameState(
score: 1,
multiplier: 1,
rounds: -1,
bonusHistory: const [],
),
throwsAssertionError,
@ -68,10 +89,11 @@ void main() {
group('isGameOver', () {
test(
'is true '
'when no balls are left', () {
'when no rounds are left', () {
const gameState = GameState(
balls: 0,
score: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
expect(gameState.isGameOver, isTrue);
@ -79,10 +101,11 @@ void main() {
test(
'is false '
'when one 1 ball left', () {
'when one 1 round left', () {
const gameState = GameState(
balls: 1,
score: 0,
multiplier: 1,
rounds: 1,
bonusHistory: [],
);
expect(gameState.isGameOver, isFalse);
@ -95,8 +118,9 @@ void main() {
'when scored is decreased',
() {
const gameState = GameState(
balls: 0,
score: 2,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
expect(
@ -111,8 +135,9 @@ void main() {
'when no argument specified',
() {
const gameState = GameState(
balls: 0,
score: 2,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
expect(
@ -128,12 +153,14 @@ void main() {
() {
const gameState = GameState(
score: 2,
balls: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
final otherGameState = GameState(
score: gameState.score + 1,
balls: gameState.balls + 1,
multiplier: gameState.multiplier + 1,
rounds: gameState.rounds + 1,
bonusHistory: const [GameBonus.googleWord],
);
expect(gameState, isNot(equals(otherGameState)));
@ -141,7 +168,8 @@ void main() {
expect(
gameState.copyWith(
score: otherGameState.score,
balls: otherGameState.balls,
multiplier: otherGameState.multiplier,
rounds: otherGameState.rounds,
bonusHistory: otherGameState.bonusHistory,
),
equals(otherGameState),

@ -53,16 +53,39 @@ void main() {
});
flameBlocTester.testGameWidget(
'lost adds BallLost to GameBloc',
"lost doesn't adds RoundLost to GameBloc "
'when there are balls left',
setUp: (game, tester) async {
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
final otherBall = Ball(baseColor: const Color(0xFF00FFFF));
final otherController = BallController(otherBall);
await otherBall.add(otherController);
await game.ensureAdd(otherBall);
controller.lost();
await game.ready();
},
verify: (game, tester) async {
verifyNever(() => gameBloc.add(const RoundLost()));
},
);
flameBlocTester.testGameWidget(
'lost adds RoundLost to GameBloc '
'when there are no balls left',
setUp: (game, tester) async {
final controller = BallController(ball);
await ball.add(controller);
await game.ensureAdd(ball);
controller.lost();
await game.ready();
},
verify: (game, tester) async {
verify(() => gameBloc.add(const BallLost())).called(1);
verify(() => gameBloc.add(const RoundLost())).called(1);
},
);

@ -25,7 +25,8 @@ void main() {
final bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);

@ -20,7 +20,8 @@ void main() {
final bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 0,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);

@ -15,7 +15,8 @@ void main() {
test('is true when the game over state has changed', () {
final state = GameState(
score: 10,
balls: 0,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
);
@ -66,7 +67,8 @@ void main() {
gameFlowController.onNewState(
GameState(
score: 10,
balls: 0,
multiplier: 1,
rounds: 0,
bonusHistory: const [],
),
);

@ -43,7 +43,8 @@ void main() {
bloc = MockGameBloc();
const state = GameState(
score: 0,
balls: 0,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
whenListen(bloc, Stream.value(state), initialState: state);

@ -146,9 +146,9 @@ void main() {
},
);
flameTester.test(
flameBlocTester.testGameWidget(
'when ball is debug',
(game) async {
setUp: (game, tester) async {
final ball = ControlledBall.debug();
final wall = BottomWall();
await game.ensureAddAll([ball, wall]);

@ -13,28 +13,54 @@ import '../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.androidBumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName,
Assets.images.backboard.backboardScores.keyName,
Assets.images.backboard.backboardGameOver.keyName,
Assets.images.backboard.display.keyName,
Assets.images.ball.ball.keyName,
Assets.images.ball.flameEffect.keyName,
Assets.images.baseboard.left.keyName,
Assets.images.baseboard.right.keyName,
Assets.images.boundary.bottom.keyName,
Assets.images.boundary.outer.keyName,
Assets.images.boundary.outerBottom.keyName,
Assets.images.chromeDino.mouth.keyName,
Assets.images.chromeDino.head.keyName,
Assets.images.dino.dinoLandTop.keyName,
Assets.images.dino.dinoLandBottom.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.dash.bumper.a.active.keyName,
Assets.images.dash.bumper.a.inactive.keyName,
Assets.images.dash.bumper.b.active.keyName,
Assets.images.dash.bumper.b.inactive.keyName,
Assets.images.dash.animatronic.keyName,
Assets.images.dash.bumper.main.active.keyName,
Assets.images.dash.bumper.main.inactive.keyName,
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
Assets.images.googleWord.letter1.keyName,
Assets.images.googleWord.letter2.keyName,
Assets.images.googleWord.letter3.keyName,
Assets.images.googleWord.letter4.keyName,
Assets.images.googleWord.letter5.keyName,
Assets.images.googleWord.letter6.keyName,
Assets.images.kicker.left.keyName,
Assets.images.kicker.right.keyName,
Assets.images.launchRamp.ramp.keyName,
Assets.images.launchRamp.foregroundRailing.keyName,
Assets.images.launchRamp.backgroundRailing.keyName,
Assets.images.plunger.plunger.keyName,
Assets.images.plunger.rocket.keyName,
Assets.images.signpost.inactive.keyName,
Assets.images.signpost.active1.keyName,
Assets.images.signpost.active2.keyName,
Assets.images.signpost.active3.keyName,
Assets.images.androidBumper.a.lit.keyName,
Assets.images.androidBumper.a.dimmed.keyName,
Assets.images.androidBumper.b.lit.keyName,
Assets.images.androidBumper.b.dimmed.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.slingshot.upper.keyName,
Assets.images.slingshot.lower.keyName,
Assets.images.spaceship.saucer.keyName,
Assets.images.spaceship.bridge.keyName,
Assets.images.spaceship.ramp.boardOpening.keyName,
Assets.images.spaceship.ramp.railingForeground.keyName,
Assets.images.spaceship.ramp.railingBackground.keyName,
@ -45,18 +71,26 @@ void main() {
Assets.images.spaceship.ramp.arrow.active3.keyName,
Assets.images.spaceship.ramp.arrow.active4.keyName,
Assets.images.spaceship.ramp.arrow.active5.keyName,
Assets.images.baseboard.left.keyName,
Assets.images.baseboard.right.keyName,
Assets.images.flipper.left.keyName,
Assets.images.flipper.right.keyName,
Assets.images.boundary.outer.keyName,
Assets.images.boundary.outerBottom.keyName,
Assets.images.boundary.bottom.keyName,
Assets.images.slingshot.upper.keyName,
Assets.images.slingshot.lower.keyName,
Assets.images.dino.dinoLandTop.keyName,
Assets.images.dino.dinoLandBottom.keyName,
Assets.images.spaceship.rail.main.keyName,
Assets.images.spaceship.rail.foreground.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.sparky.computer.top.keyName,
Assets.images.sparky.computer.base.keyName,
Assets.images.sparky.animatronic.keyName,
Assets.images.sparky.bumper.a.inactive.keyName,
Assets.images.sparky.bumper.a.active.keyName,
Assets.images.sparky.bumper.b.active.keyName,
Assets.images.sparky.bumper.b.inactive.keyName,
Assets.images.sparky.bumper.c.active.keyName,
Assets.images.sparky.bumper.c.inactive.keyName,
];
final flameTester = FlameTester(
() => PinballTestGame(assets: assets),
);
@ -72,7 +106,6 @@ void main() {
'has only one BottomWall',
(game) async {
await game.ready();
expect(
game.children.whereType<BottomWall>().length,
equals(1),
@ -91,34 +124,34 @@ void main() {
},
);
flameTester.test('has one Board', (game) async {
flameTester.test(
'has one Board',
(game) async {
await game.ready();
expect(
game.children.whereType<Board>().length,
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: EmptyPinballTestGame.new,
blocBuilder: () => gameBloc,
// assets: assets,
flameTester.test(
'one GoogleWord',
(game) async {
await game.ready();
expect(game.children.whereType<GoogleWord>().length, equals(1));
},
);
flameBlocTester.testGameWidget(
'listens when all balls are lost and there are more than 0 balls',
group('controller', () {
group('listenWhen', () {
flameTester.testGameWidget(
'listens when all balls are lost and there are more than 0 rounds',
setUp: (game, tester) async {
// 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.balls).thenReturn(2);
when(() => newState.isGameOver).thenReturn(false);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
@ -135,10 +168,10 @@ void main() {
"doesn't listen when some balls are left",
(game) async {
final newState = MockGameState();
when(() => newState.balls).thenReturn(1);
when(() => newState.isGameOver).thenReturn(false);
expect(
game.descendants().whereType<Ball>().length,
game.descendants().whereType<ControlledBall>().length,
greaterThan(0),
);
expect(
@ -148,19 +181,20 @@ void main() {
},
);
flameBlocTester.test(
"doesn't listen when no balls left",
(game) async {
flameTester.testGameWidget(
"doesn't listen when game is over",
setUp: (game, tester) async {
// 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.balls).thenReturn(0);
when(() => newState.isGameOver).thenReturn(true);
game.descendants().whereType<ControlledBall>().forEach(
(ball) => ball.controller.lost(),
);
await game.ready();
expect(
game.descendants().whereType<Ball>().isEmpty,
game.descendants().whereType<ControlledBall>().isEmpty,
isTrue,
);
expect(
@ -177,14 +211,13 @@ void main() {
flameTester.test(
'spawns a ball',
(game) async {
await game.ready();
final previousBalls =
game.descendants().whereType<Ball>().toList();
game.descendants().whereType<ControlledBall>().toList();
game.controller.onNewState(MockGameState());
await game.ready();
final currentBalls =
game.descendants().whereType<Ball>().toList();
game.descendants().whereType<ControlledBall>().toList();
expect(
currentBalls.length,
@ -199,57 +232,26 @@ void main() {
});
group('DebugPinballGame', () {
debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready();
debugModeFlameTester.test(
'adds a ball on tap up',
(game) async {
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.all(10));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
final previousBalls = game.descendants().whereType<Ball>().toList();
final previousBalls =
game.descendants().whereType<ControlledBall>().toList();
game.onTapUp(tapUpEvent);
await game.ready();
expect(
game.children.whereType<Ball>().length,
game.children.whereType<ControlledBall>().length,
equals(previousBalls.length + 1),
);
});
group('controller', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = GameBloc();
});
final debugModeFlameBlocTester =
FlameBlocTester<DebugPinballGame, GameBloc>(
gameBuilder: DebugPinballTestGame.new,
blocBuilder: () => gameBloc,
assets: assets,
);
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,
);
},
);
});
});
}

@ -28,7 +28,8 @@ void main() {
const initialState = GameState(
score: 1000,
balls: 2,
multiplier: 1,
rounds: 1,
bonusHistory: [],
);

@ -11,7 +11,8 @@ void main() {
late GameBloc gameBloc;
const initialState = GameState(
score: 0,
balls: 3,
multiplier: 1,
rounds: 3,
bonusHistory: [],
);
@ -37,7 +38,7 @@ void main() {
testWidgets('two active round indicator', (tester) async {
final state = initialState.copyWith(
balls: 2,
rounds: 2,
);
whenListen(
gameBloc,
@ -68,7 +69,7 @@ void main() {
testWidgets('one active round indicator', (tester) async {
final state = initialState.copyWith(
balls: 1,
rounds: 1,
);
whenListen(
gameBloc,

@ -15,7 +15,8 @@ void main() {
const score = 123456789;
const initialState = GameState(
score: score,
balls: 1,
multiplier: 1,
rounds: 1,
bonusHistory: [],
);
@ -46,7 +47,7 @@ void main() {
stateController.add(
initialState.copyWith(
balls: 0,
rounds: 0,
),
);

Loading…
Cancel
Save