fix: `SpaceshipRamp` logic (#416)

* feat: added behaviors for arrow

* feat: arrow behaviors

* refactor: changed ramp behaviors to new flame bloc

* feat: reset and blinking on ramp

* fix: added behaviors to ramp and some tests

* test: coverage

* refactor: spaceship ramp sandbox

* chore: clean prints

* refactor: unnecessary test methods

* chore: removed unused files

* refactor: remove arrow blinking from this pr

* test: coverage

* refactor: ramp bonus listen when refactored

* refactor: moved FlameBlocProvider inside SpaceshipRamp

* refactor: moved FlameBlocProvider inside SpaceshipRamp and refactor tests

* chore: removed tests failures

* test: removed golden tests for spaceship

* chore: failure tests images

* test: refactor test

* test: coverage

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_reset_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update test/game/components/android_acres/behaviors/ramp_progress_behavior_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: flamebloclistenable

* Update packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* Update packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart

Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com>

* refactor: changed listenWhen conditions

* chore: formatting

* test: coverage

* test: multiblocprovider

* Update packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart

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

* Update packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart

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

* test: refactored tests

* refactor: max multiplier

* test: fixed error in test

* refactor: removed trailing commas

* test: used ensureAdd

* test: awaited zero duration to trigger next event loop

* test: triggered next event loop

* test: triggered next event loop

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

@ -82,6 +82,9 @@ class GameState extends Equatable {
/// The score displayed at the game.
int get displayScore => roundScore + totalScore;
/// The max multiplier in game.
bool get isMaxMultiplier => multiplier == 6;
GameState copyWith({
int? totalScore,
int? roundScore,

@ -16,41 +16,44 @@ class AndroidAcres extends Component {
AndroidAcres()
: super(
children: [
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
RampProgressBehavior(),
RampMultiplierBehavior(),
RampResetBehavior(),
],
),
SpaceshipRail(),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
CowBumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
FlameBlocProvider<AndroidSpaceshipCubit, AndroidSpaceshipState>(
create: AndroidSpaceshipCubit.new,
children: [
SpaceshipRamp(
children: [
RampShotBehavior(points: Points.fiveThousand),
RampBonusBehavior(points: Points.oneMillion),
],
),
SpaceshipRail(),
AndroidSpaceship(position: Vector2(-26.5, -28.5)),
AndroidAnimatronic(
children: [
ScoringContactBehavior(points: Points.twoHundredThousand),
],
)..initialPosition = Vector2(-26, -28.25),
AndroidBumper.a(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-25.2, 1.5),
AndroidBumper.b(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
],
)..initialPosition = Vector2(-32.9, -9.3),
AndroidBumper.cow(
children: [
ScoringContactBehavior(points: Points.twentyThousand),
BumperNoiseBehavior(),
CowBumperNoiseBehavior(),
],
)..initialPosition = Vector2(-20.7, -13),
AndroidSpaceshipBonusBehavior(),
],
),

@ -1,3 +1,6 @@
export 'android_spaceship_bonus_behavior.dart';
export 'ramp_bonus_behavior.dart';
export 'ramp_multiplier_behavior.dart';
export 'ramp_progress_behavior.dart';
export 'ramp_reset_behavior.dart';
export 'ramp_shot_behavior.dart';

@ -1,60 +1,40 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_bonus_behavior}
/// Increases the score when a [Ball] is shot 10 times into the [SpaceshipRamp].
/// {@endtemplate}
class RampBonusBehavior extends Component with ParentIsA<SpaceshipRamp> {
class RampBonusBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro ramp_bonus_behavior}
RampBonusBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampBonusBehavior].
///
/// This can be used for testing [RampBonusBehavior] in isolation.
@visibleForTesting
RampBonusBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (achievedOneMillionPoints) {
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
});
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
final hitsIncreased = previousState.hits < newState.hits;
final achievedOneMillionPoints = newState.hits % 10 == 0;
return hitsIncreased && achievedOneMillionPoints;
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
void onNewState(SpaceshipRampState state) {
parent!.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -60),
duration: 2,
),
);
}
}

@ -0,0 +1,27 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Increases the multiplier when a [Ball] is shot 5 times into the
/// [SpaceshipRamp].
class RampMultiplierBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
final hitsIncreased = previousState.hits < newState.hits;
final achievedFiveShots = newState.hits % 5 == 0;
final notMaxMultiplier =
!readBloc<GameBloc, GameState>().state.isMaxMultiplier;
return hitsIncreased & achievedFiveShots && notMaxMultiplier;
}
@override
void onNewState(SpaceshipRampState state) {
readBloc<GameBloc, GameState>().add(const MultiplierIncreased());
}
}

@ -0,0 +1,34 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Changes arrow lit when a [Ball] is shot into the [SpaceshipRamp].
class RampProgressBehavior extends Component
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
return previousState.hits < newState.hits;
}
@override
void onNewState(SpaceshipRampState state) {
final gameBloc = readBloc<GameBloc, GameState>();
final spaceshipCubit = readBloc<SpaceshipRampCubit, SpaceshipRampState>();
final canProgress = !gameBloc.state.isMaxMultiplier ||
(gameBloc.state.isMaxMultiplier && !state.arrowFullyLit);
if (canProgress) {
spaceshipCubit.onProgressed();
}
if (spaceshipCubit.state.arrowFullyLit && !gameBloc.state.isMaxMultiplier) {
spaceshipCubit.onProgressed();
}
}
}

@ -0,0 +1,19 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// Reset [SpaceshipRamp] state when GameState.rounds changes.
class RampResetBehavior extends Component
with FlameBlocListenable<GameBloc, GameState> {
@override
bool listenWhen(GameState previousState, GameState newState) {
return previousState.rounds != newState.rounds;
}
@override
void onNewState(GameState state) {
readBloc<SpaceshipRampCubit, SpaceshipRampState>().onReset();
}
}

@ -1,64 +1,36 @@
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:pinball/game/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
/// {@template ramp_shot_behavior}
/// Increases the score when a [Ball] is shot into the [SpaceshipRamp].
/// {@endtemplate}
class RampShotBehavior extends Component
with ParentIsA<SpaceshipRamp>, FlameBlocReader<GameBloc, GameState> {
with FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro ramp_shot_behavior}
RampShotBehavior({
required Points points,
}) : _points = points,
super();
/// Creates a [RampShotBehavior].
///
/// This can be used for testing [RampShotBehavior] in isolation.
@visibleForTesting
RampShotBehavior.test({
required Points points,
required this.subscription,
}) : _points = points,
super();
final Points _points;
/// Subscription to [SpaceshipRampState] at [SpaceshipRamp].
@visibleForTesting
StreamSubscription? subscription;
@override
void onMount() {
super.onMount();
subscription = subscription ??
parent.bloc.stream.listen((state) {
final achievedOneMillionPoints = state.hits % 10 == 0;
if (!achievedOneMillionPoints) {
bloc.add(const MultiplierIncreased());
parent.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
});
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
return previousState.hits < newState.hits;
}
@override
void onRemove() {
subscription?.cancel();
super.onRemove();
void onNewState(SpaceshipRampState state) {
parent!.add(
ScoringBehavior(
points: _points,
position: Vector2(0, -45),
),
);
}
}

@ -16,7 +16,8 @@ class RampBallAscendingContactBehavior
if (other is! Ball) return;
if (other.body.linearVelocity.y < 0) {
parent.parent.bloc.onAscendingBallEntered();
readBloc<SpaceshipRampCubit, SpaceshipRampState>()
.onAscendingBallEntered();
}
}
}

@ -6,9 +6,17 @@ part 'spaceship_ramp_state.dart';
class SpaceshipRampCubit extends Cubit<SpaceshipRampState> {
SpaceshipRampCubit() : super(const SpaceshipRampState.initial());
void onAscendingBallEntered() {
void onAscendingBallEntered() => emit(state.copyWith(hits: state.hits + 1));
void onProgressed() {
final index = ArrowLightState.values.indexOf(state.lightState);
emit(
state.copyWith(hits: state.hits + 1),
state.copyWith(
lightState:
ArrowLightState.values[(index + 1) % ArrowLightState.values.length],
),
);
}
void onReset() => emit(const SpaceshipRampState.initial());
}

@ -1,22 +1,55 @@
// ignore_for_file: comment_references
part of 'spaceship_ramp_cubit.dart';
class SpaceshipRampState extends Equatable {
const SpaceshipRampState({
required this.hits,
required this.lightState,
}) : assert(hits >= 0, "Hits can't be negative");
const SpaceshipRampState.initial() : this(hits: 0);
const SpaceshipRampState.initial()
: this(
hits: 0,
lightState: ArrowLightState.inactive,
);
final int hits;
final ArrowLightState lightState;
bool get arrowFullyLit => lightState == ArrowLightState.active5;
SpaceshipRampState copyWith({
int? hits,
ArrowLightState? lightState,
}) {
return SpaceshipRampState(
hits: hits ?? this.hits,
lightState: lightState ?? this.lightState,
);
}
@override
List<Object?> get props => [hits];
List<Object?> get props => [hits, lightState];
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
enum ArrowLightState {
/// Arrow with no lights lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}

@ -1,6 +1,7 @@
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:pinball_components/gen/assets.gen.dart';
@ -19,30 +20,32 @@ class SpaceshipRamp extends Component {
Iterable<Component>? children,
}) : this._(
children: children,
bloc: SpaceshipRampCubit(),
);
SpaceshipRamp._({
Iterable<Component>? children,
required this.bloc,
}) : super(
children: [
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship,
rotation: math.pi,
)
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
SpaceshipRampBoardOpening()..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent(
current: bloc.state.hits,
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>(
create: SpaceshipRampCubit.new,
children: [
_SpaceshipRampOpening(
outsideLayer: Layer.spaceship,
outsidePriority: ZIndexes.ballOnSpaceship,
rotation: math.pi,
)
..initialPosition = Vector2(-13.7, -18.6)
..layer = Layer.spaceshipEntranceRamp,
_SpaceshipRampBackground(),
SpaceshipRampBoardOpening()
..initialPosition = Vector2(3.4, -39.5),
_SpaceshipRampForegroundRailing(),
SpaceshipRampBase()..initialPosition = Vector2(3.4, -42.5),
_SpaceshipRampBackgroundRailingSpriteComponent(),
SpaceshipRampArrowSpriteComponent(),
...?children,
],
),
...?children,
],
);
@ -51,16 +54,8 @@ class SpaceshipRamp extends Component {
/// This can be used for testing [SpaceshipRamp]'s behaviors in isolation.
@visibleForTesting
SpaceshipRamp.test({
required this.bloc,
}) : super();
final SpaceshipRampCubit bloc;
@override
void onRemove() {
bloc.close();
super.onRemove();
}
Iterable<Component>? children,
}) : super(children: children);
}
class _SpaceshipRampBackground extends BodyComponent
@ -167,82 +162,71 @@ class _SpaceshipRampBackgroundRampSpriteComponent extends SpriteComponent
/// Lights progressively whenever a [Ball] gets into [SpaceshipRamp].
/// {@endtemplate}
@visibleForTesting
class SpaceshipRampArrowSpriteComponent extends SpriteGroupComponent<int>
with HasGameRef, ParentIsA<SpaceshipRamp>, ZIndex {
class SpaceshipRampArrowSpriteComponent
extends SpriteGroupComponent<ArrowLightState>
with
HasGameRef,
ZIndex,
FlameBlocListenable<SpaceshipRampCubit, SpaceshipRampState> {
/// {@macro spaceship_ramp_arrow_sprite_component}
SpaceshipRampArrowSpriteComponent({
required int current,
}) : super(
SpaceshipRampArrowSpriteComponent()
: super(
anchor: Anchor.center,
position: Vector2(-3.9, -56.5),
current: current,
) {
zIndex = ZIndexes.spaceshipRampArrow;
}
@override
bool listenWhen(
SpaceshipRampState previousState,
SpaceshipRampState newState,
) {
return previousState.lightState != newState.lightState;
}
@override
void onNewState(SpaceshipRampState state) {
current = state.lightState;
}
@override
Future<void> onLoad() async {
await super.onLoad();
parent.bloc.stream.listen((state) {
current = state.hits % SpaceshipRampArrowSpriteState.values.length;
});
final sprites = <int, Sprite>{};
final sprites = <ArrowLightState, Sprite>{};
this.sprites = sprites;
for (final spriteState in SpaceshipRampArrowSpriteState.values) {
sprites[spriteState.index] = Sprite(
for (final spriteState in ArrowLightState.values) {
sprites[spriteState] = Sprite(
gameRef.images.fromCache(spriteState.path),
);
}
current = 0;
current = ArrowLightState.inactive;
size = sprites[current]!.originalSize / 10;
}
}
/// Indicates the state of the arrow on the [SpaceshipRamp].
@visibleForTesting
enum SpaceshipRampArrowSpriteState {
/// Arrow with no dashes lit up.
inactive,
/// Arrow with 1 light lit up.
active1,
/// Arrow with 2 lights lit up.
active2,
/// Arrow with 3 lights lit up.
active3,
/// Arrow with 4 lights lit up.
active4,
/// Arrow with all 5 lights lit up.
active5,
}
extension on SpaceshipRampArrowSpriteState {
extension on ArrowLightState {
String get path {
switch (this) {
case SpaceshipRampArrowSpriteState.inactive:
case ArrowLightState.inactive:
return Assets.images.android.ramp.arrow.inactive.keyName;
case SpaceshipRampArrowSpriteState.active1:
case ArrowLightState.active1:
return Assets.images.android.ramp.arrow.active1.keyName;
case SpaceshipRampArrowSpriteState.active2:
case ArrowLightState.active2:
return Assets.images.android.ramp.arrow.active2.keyName;
case SpaceshipRampArrowSpriteState.active3:
case ArrowLightState.active3:
return Assets.images.android.ramp.arrow.active3.keyName;
case SpaceshipRampArrowSpriteState.active4:
case ArrowLightState.active4:
return Assets.images.android.ramp.arrow.active4.keyName;
case SpaceshipRampArrowSpriteState.active5:
case ArrowLightState.active5:
return Assets.images.android.ramp.arrow.active5.keyName;
}
}
}
class SpaceshipRampBoardOpening extends BodyComponent
with Layered, ZIndex, InitialPosition, ParentIsA<SpaceshipRamp> {
with Layered, ZIndex, InitialPosition {
SpaceshipRampBoardOpening()
: super(
renderBody: false,

@ -44,9 +44,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
await super.onLoad();
camera.followVector2(Vector2(-12, -50));
await add(
_spaceshipRamp = SpaceshipRamp(),
);
_spaceshipRamp = SpaceshipRamp();
await add(_spaceshipRamp);
await traceAllBodies();
}
@ -57,7 +57,9 @@ class SpaceshipRampGame extends BallGame with KeyboardEvents {
) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
_spaceshipRamp.bloc.onAscendingBallEntered();
_spaceshipRamp
.readBloc<SpaceshipRampCubit, SpaceshipRampState>()
.onProgressed();
return KeyEventResult.handled;
}

@ -1,14 +1,47 @@
// ignore_for_file: cascade_invocations
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import '../../../../helpers/helpers.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
]);
}
Future<void> pump(
SpaceshipRamp children, {
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
children: [
ZCanvasComponent(children: [children]),
],
),
);
}
}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
@ -20,20 +53,8 @@ class _MockContact extends Mock implements Contact {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
final flameTester = FlameTester(_TestGame.new);
group(
'RampBallAscendingContactBehavior',
@ -67,16 +88,18 @@ void main() {
initialState: const SpaceshipRampState.initial(),
);
final parent = SpaceshipRampBoardOpening.test();
final opening = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
children: [opening],
);
when(() => body.linearVelocity).thenReturn(Vector2(0, -1));
await spaceshipRamp.add(parent);
await game.ensureAddAll([spaceshipRamp, ball]);
await parent.add(behavior);
await game.pump(
spaceshipRamp,
bloc: bloc,
);
await opening.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());
@ -95,16 +118,18 @@ void main() {
initialState: const SpaceshipRampState.initial(),
);
final parent = SpaceshipRampBoardOpening.test();
final opening = SpaceshipRampBoardOpening.test();
final spaceshipRamp = SpaceshipRamp.test(
bloc: bloc,
children: [opening],
);
when(() => body.linearVelocity).thenReturn(Vector2(0, 1));
await spaceshipRamp.add(parent);
await game.ensureAddAll([spaceshipRamp, ball]);
await parent.add(behavior);
await game.pump(
spaceshipRamp,
bloc: bloc,
);
await opening.ensureAdd(behavior);
behavior.beginContact(ball, _MockContact());

@ -15,9 +15,70 @@ void main() {
..onAscendingBallEntered()
..onAscendingBallEntered(),
expect: () => [
SpaceshipRampState(hits: 1),
SpaceshipRampState(hits: 2),
SpaceshipRampState(hits: 3),
isA<SpaceshipRampState>().having((state) => state.hits, 'hits', 1),
isA<SpaceshipRampState>().having((state) => state.hits, 'hits', 2),
isA<SpaceshipRampState>().having((state) => state.hits, 'hits', 3),
],
);
});
group('onProgressed', () {
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
'emits next arrow lightState',
build: SpaceshipRampCubit.new,
act: (bloc) => bloc
..onProgressed()
..onProgressed()
..onProgressed()
..onProgressed()
..onProgressed()
..onProgressed(),
expect: () => [
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.active1,
),
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.active2,
),
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.active3,
),
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.active4,
),
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.active5,
),
isA<SpaceshipRampState>().having(
(state) => state.lightState,
'lightState',
ArrowLightState.inactive,
),
],
);
});
group('onReset', () {
blocTest<SpaceshipRampCubit, SpaceshipRampState>(
'emits initial state',
build: SpaceshipRampCubit.new,
seed: () => SpaceshipRampState(
hits: 100,
lightState: ArrowLightState.active3,
),
act: (bloc) => bloc.onReset(),
expect: () => [
SpaceshipRampState.initial(),
],
);
});

@ -7,9 +7,15 @@ void main() {
group('SpaceshipRampState', () {
test('supports value equality', () {
expect(
SpaceshipRampState(hits: 0),
SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
),
equals(
SpaceshipRampState(hits: 0),
SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
),
),
);
});
@ -17,19 +23,41 @@ void main() {
group('constructor', () {
test('can be instantiated', () {
expect(
SpaceshipRampState(hits: 0),
SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
),
isNotNull,
);
});
test(
'throws AssertionError '
'when hits is negative',
() {
expect(
() => SpaceshipRampState(
hits: -1,
lightState: ArrowLightState.inactive,
),
throwsAssertionError,
);
},
);
});
test(
'throws AssertionError '
'when hits is negative',
'arrowFullyLit returns true when lightState is last one',
() {
expect(
() => SpaceshipRampState(hits: -1),
throwsAssertionError,
SpaceshipRampState.initial().arrowFullyLit,
isFalse,
);
expect(
SpaceshipRampState.initial()
.copyWith(lightState: ArrowLightState.active5)
.arrowFullyLit,
isTrue,
);
},
);
@ -39,7 +67,10 @@ void main() {
'throws AssertionError '
'when hits is decreased',
() {
const rampState = SpaceshipRampState(hits: 0);
const rampState = SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
);
expect(
() => rampState.copyWith(hits: rampState.hits - 1),
throwsAssertionError,
@ -51,7 +82,10 @@ void main() {
'copies correctly '
'when no argument specified',
() {
const rampState = SpaceshipRampState(hits: 0);
const rampState = SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
);
expect(
rampState.copyWith(),
equals(rampState),
@ -63,12 +97,21 @@ void main() {
'copies correctly '
'when all arguments specified',
() {
const rampState = SpaceshipRampState(hits: 0);
final otherRampState = SpaceshipRampState(hits: rampState.hits + 1);
const rampState = SpaceshipRampState(
hits: 0,
lightState: ArrowLightState.inactive,
);
final otherRampState = SpaceshipRampState(
hits: rampState.hits + 1,
lightState: ArrowLightState.active1,
);
expect(rampState, isNot(equals(otherRampState)));
expect(
rampState.copyWith(hits: rampState.hits + 1),
rampState.copyWith(
hits: otherRampState.hits,
lightState: otherRampState.lightState,
),
equals(otherRampState),
);
},

@ -1,7 +1,10 @@
// ignore_for_file: cascade_invocations
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
@ -10,7 +13,38 @@ import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart';
import 'package:pinball_flame/pinball_flame.dart';
import '../../../helpers/helpers.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
]);
}
Future<void> pump(
SpaceshipRamp children, {
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
children: [
ZCanvasComponent(children: [children]),
],
),
);
}
}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
@ -22,264 +56,74 @@ class _MockManifold extends Mock implements Manifold {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final assets = [
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
];
final flameTester = FlameTester(() => TestGame(assets));
final flameTester = FlameTester(_TestGame.new);
group('SpaceshipRamp', () {
flameTester.test(
'loads correctly',
(game) async {
final spaceshipRamp = SpaceshipRamp();
await game.ensureAdd(spaceshipRamp);
expect(game.children, contains(spaceshipRamp));
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
);
final ramp = SpaceshipRamp.test();
await game.pump(ramp, bloc: bloc);
expect(game.descendants(), contains(ramp));
},
);
group('renders correctly', () {
const goldenFilePath = '../golden/spaceship_ramp/';
final centerForSpaceshipRamp = Vector2(-13, -55);
flameTester.testGameWidget(
'inactive sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.inactive,
);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}inactive.png'),
);
},
);
flameTester.testGameWidget(
'active1 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
ramp.bloc.onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active1,
);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}active1.png'),
);
},
);
flameTester.testGameWidget(
'active2 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active2,
);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}active2.png'),
);
},
);
group('adds', () {
flameTester.test('a FlameBlocProvider', (game) async {
final ramp = SpaceshipRamp();
await game.ensureAdd(ramp);
expect(
ramp.children
.whereType<
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>>()
.single,
isNotNull,
);
});
flameTester.testGameWidget(
'active3 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
flameTester.test(
'a SpaceshipRampBoardOpening',
(game) async {
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ensureAdd(ramp);
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active3,
);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}active3.png'),
game.descendants().whereType<SpaceshipRampBoardOpening>().length,
equals(1),
);
},
);
flameTester.testGameWidget(
'active4 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
flameTester.test(
'a SpaceshipRampArrowSpriteComponent',
(game) async {
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active4,
);
await game.ensureAdd(ramp);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}active4.png'),
);
},
);
flameTester.testGameWidget(
'active5 sprite',
setUp: (game, tester) async {
await game.images.loadAll(assets);
final ramp = SpaceshipRamp();
final canvas = ZCanvasComponent(children: [ramp]);
await game.ensureAdd(canvas);
ramp.bloc
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered()
..onAscendingBallEntered();
await game.ready();
await tester.pump();
final index = ramp.children
.whereType<SpaceshipRampArrowSpriteComponent>()
.first
.current;
expect(
SpaceshipRampArrowSpriteState.values[index!],
SpaceshipRampArrowSpriteState.active5,
);
game.camera.followVector2(centerForSpaceshipRamp);
},
verify: (game, tester) async {
await expectLater(
find.byGame<TestGame>(),
matchesGoldenFile('${goldenFilePath}active5.png'),
game
.descendants()
.whereType<SpaceshipRampArrowSpriteComponent>()
.length,
equals(1),
);
},
);
});
flameTester.test('closes bloc when removed', (game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: const SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final ramp = SpaceshipRamp.test(
bloc: bloc,
);
await game.ensureAdd(ramp);
game.remove(ramp);
await game.ready();
verify(bloc.close).called(1);
});
group('adds', () {
flameTester.test('new children', (game) async {
final component = Component();
final ramp = SpaceshipRamp(children: [component]);
await game.ensureAdd(ramp);
expect(ramp.children, contains(component));
expect(ramp.descendants(), contains(component));
});
});
});
@ -332,17 +176,19 @@ void main() {
});
flameTester.test('can be loaded', (game) async {
final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit());
final component = SpaceshipRampBoardOpening();
await game.ensureAdd(parent);
final parent = SpaceshipRamp.test();
await game.pump(parent, bloc: _MockSpaceshipRampCubit());
await parent.ensureAdd(component);
expect(parent.children, contains(component));
});
flameTester.test('adds a RampBallAscendingContactBehavior', (game) async {
final parent = SpaceshipRamp.test(bloc: _MockSpaceshipRampCubit());
final component = SpaceshipRampBoardOpening();
await game.ensureAdd(parent);
final parent = SpaceshipRamp.test();
await game.pump(parent, bloc: _MockSpaceshipRampCubit());
await parent.ensureAdd(component);
expect(
component.children.whereType<RampBallAscendingContactBehavior>().length,
@ -350,4 +196,36 @@ void main() {
);
});
});
group('SpaceshipRampArrowSpriteComponent', () {
flameTester.test(
'changes current state '
'when SpaceshipRampState changes lightState',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
final arrow = SpaceshipRampArrowSpriteComponent();
final ramp = SpaceshipRamp.test(children: [arrow]);
await game.pump(
ramp,
bloc: bloc,
);
expect(arrow.current, ArrowLightState.inactive);
streamController
.add(state.copyWith(lightState: ArrowLightState.active1));
await game.ready();
expect(arrow.current, ArrowLightState.active1);
},
);
});
}

@ -3,6 +3,7 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame {
}
Future<void> pump(
SpaceshipRamp child, {
List<Component> children, {
required SpaceshipRampCubit bloc,
required GameBloc gameBloc,
}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
),
],
children: [
ZCanvasComponent(children: [child]),
ZCanvasComponent(children: children),
],
),
);
@ -54,20 +63,17 @@ class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion;
late GameBloc gameBloc;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
setUp(() {
gameBloc = _MockGameBloc();
});
group('RampBonusBehavior', () {
const shotPoints = Points.oneMillion;
final flameTester = FlameTester(_TestGame.new);
@ -75,22 +81,23 @@ void main() {
'when hits are multiples of 10 times adds a ScoringBehavior',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
initialState: state,
);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
[parent],
bloc: bloc,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
streamController.add(state.copyWith(hits: 10));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
@ -103,22 +110,23 @@ void main() {
"when hits are not multiple of 10 times doesn't add any ScoringBehavior",
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
initialState: state,
);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
final behavior = RampBonusBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
[parent],
bloc: bloc,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
streamController.add(state.copyWith(hits: 9));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
@ -126,38 +134,5 @@ void main() {
expect(scores.length, 0);
},
);
flameTester.test(
'closes subscription when removed',
(game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampBonusBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(bloc: bloc);
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

@ -0,0 +1,183 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName,
]);
}
Future<void> pump(
SpaceshipRamp child, {
required GameBloc gameBloc,
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
),
],
children: [
ZCanvasComponent(children: [child]),
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _FakeGameEvent extends Fake implements GameEvent {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampMultiplierBehavior', () {
late GameBloc gameBloc;
setUp(() {
registerFallbackValue(_FakeGameEvent());
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.test(
'adds MultiplierIncreased '
'when hits are multiples of 5 times and multiplier is less than 6',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 5,
),
);
when(() => gameBloc.add(any())).thenAnswer((_) async {});
final behavior = RampMultiplierBehavior();
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(hits: 5));
await Future<void>.delayed(Duration.zero);
verify(() => gameBloc.add(const MultiplierIncreased())).called(1);
},
);
flameTester.test(
"doesn't add MultiplierIncreased "
'when hits are multiples of 5 times but multiplier is 6',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 6,
),
);
final behavior = RampMultiplierBehavior();
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(hits: 5));
await Future<void>.delayed(Duration.zero);
verifyNever(() => gameBloc.add(const MultiplierIncreased()));
},
);
flameTester.test(
"doesn't add MultiplierIncreased "
"when hits aren't multiples of 5 times",
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 5,
),
);
final behavior = RampMultiplierBehavior();
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(hits: 1));
await game.ready();
verifyNever(() => gameBloc.add(const MultiplierIncreased()));
},
);
});
}

@ -0,0 +1,309 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
]);
}
Future<void> pump(
SpaceshipRamp child, {
required GameBloc gameBloc,
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
),
],
children: [
ZCanvasComponent(children: [child]),
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _FakeGameEvent extends Fake implements GameEvent {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampProgressBehavior', () {
late GameBloc gameBloc;
setUp(() {
registerFallbackValue(_FakeGameEvent());
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.test(
'adds onProgressed '
'when hits and multiplier are less than 6',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 1,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(hits: 5));
await Future<void>.delayed(Duration.zero);
verify(bloc.onProgressed).called(1);
},
);
flameTester.test(
'adds onProgressed '
'when hits and multiplier are 6 but arrow is not fully lit',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 6,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(hits: 5));
await Future<void>.delayed(Duration.zero);
verify(bloc.onProgressed).called(1);
},
);
flameTester.test(
"doesn't add onProgressed "
'when hits and multiplier are 6 and arrow is fully lit',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 6,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(
state.copyWith(
hits: 5,
lightState: ArrowLightState.active5,
),
);
await Future<void>.delayed(Duration.zero);
verifyNever(bloc.onProgressed);
},
);
flameTester.test(
'adds onProgressed to dim arrow '
'when arrow is fully lit after hit and multiplier is less than 6',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 5,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(
state.copyWith(
hits: 5,
lightState: ArrowLightState.active5,
),
);
await Future<void>.delayed(Duration.zero);
verify(bloc.onProgressed).called(2);
},
);
flameTester.test(
"doesn't add onProgressed to dim arrow "
'when arrow is not fully lit after hit',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 5,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(
state.copyWith(
hits: 4,
lightState: ArrowLightState.active4,
),
);
await Future<void>.delayed(Duration.zero);
verify(bloc.onProgressed).called(1);
},
);
flameTester.test(
"doesn't add onProgressed to dim arrow "
'when multiplier is 6 after hit',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: state,
);
when(() => gameBloc.state).thenReturn(
GameState.initial().copyWith(
multiplier: 6,
),
);
final behavior = RampProgressBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(
state.copyWith(hits: 4),
);
await Future<void>.delayed(Duration.zero);
verify(bloc.onProgressed).called(1);
},
);
});
}

@ -0,0 +1,135 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_components/pinball_components.dart';
import 'package:pinball_flame/pinball_flame.dart';
class _TestGame extends Forge2DGame {
@override
Future<void> onLoad() async {
images.prefix = '';
await images.loadAll([
Assets.images.android.ramp.boardOpening.keyName,
Assets.images.android.ramp.railingForeground.keyName,
Assets.images.android.ramp.railingBackground.keyName,
Assets.images.android.ramp.main.keyName,
Assets.images.android.ramp.arrow.inactive.keyName,
Assets.images.android.ramp.arrow.active1.keyName,
Assets.images.android.ramp.arrow.active2.keyName,
Assets.images.android.ramp.arrow.active3.keyName,
Assets.images.android.ramp.arrow.active4.keyName,
Assets.images.android.ramp.arrow.active5.keyName,
Assets.images.android.rail.main.keyName,
Assets.images.android.rail.exit.keyName,
Assets.images.score.fiveThousand.keyName,
]);
}
Future<void> pump(
SpaceshipRamp child, {
required GameBloc gameBloc,
required SpaceshipRampCubit bloc,
}) async {
await ensureAdd(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
),
],
children: [
ZCanvasComponent(children: [child]),
],
),
);
}
}
class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampResetBehavior', () {
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
final flameTester = FlameTester(_TestGame.new);
flameTester.test(
'calls onReset when round lost',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = GameState.initial();
final streamController = StreamController<GameState>();
whenListen(
gameBloc,
streamController.stream,
initialState: state,
);
final behavior = RampResetBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController.add(state.copyWith(rounds: state.rounds - 1));
await Future<void>.delayed(Duration.zero);
verify(bloc.onReset).called(1);
},
);
flameTester.test(
"doesn't call onReset when round stays the same",
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = GameState.initial();
final streamController = StreamController<GameState>();
whenListen(
gameBloc,
streamController.stream,
initialState: state,
);
final behavior = RampResetBehavior();
final parent = SpaceshipRamp.test(
children: [behavior],
);
await game.pump(
parent,
gameBloc: gameBloc,
bloc: bloc,
);
streamController
.add(state.copyWith(roundScore: state.roundScore + 100));
await Future<void>.delayed(Duration.zero);
verifyNever(bloc.onReset);
},
);
});
}

@ -3,6 +3,7 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
@ -36,14 +37,22 @@ class _TestGame extends Forge2DGame {
}
Future<void> pump(
SpaceshipRamp child, {
List<Component> children, {
required SpaceshipRampCubit bloc,
required GameBloc gameBloc,
}) async {
await ensureAdd(
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<GameBloc, GameState>.value(
value: gameBloc,
),
FlameBlocProvider<SpaceshipRampCubit, SpaceshipRampState>.value(
value: bloc,
),
],
children: [
ZCanvasComponent(children: [child]),
ZCanvasComponent(children: children),
],
),
);
@ -54,120 +63,47 @@ class _MockGameBloc extends Mock implements GameBloc {}
class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {}
class _MockStreamSubscription extends Mock
implements StreamSubscription<SpaceshipRampState> {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand;
late GameBloc gameBloc;
late GameBloc gameBloc;
setUp(() {
gameBloc = _MockGameBloc();
});
setUp(() {
gameBloc = _MockGameBloc();
});
group('RampShotBehavior', () {
const shotPoints = Points.fiveThousand;
final flameBlocTester = FlameTester(_TestGame.new);
final flameTester = FlameTester(_TestGame.new);
flameBlocTester.test(
'when hits are not multiple of 10 times '
'increases multiplier and adds a ScoringBehavior',
flameTester.test(
'adds a ScoringBehavior when hit',
(game) async {
final bloc = _MockSpaceshipRampCubit();
final state = SpaceshipRampState.initial();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState.initial(),
initialState: state,
);
final behavior = RampShotBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(bloc: bloc);
final behavior = RampShotBehavior(points: shotPoints);
final parent = SpaceshipRamp.test(children: [behavior]);
await game.pump(
parent,
[parent],
bloc: bloc,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 1));
streamController.add(state.copyWith(hits: state.hits + 1));
final scores = game.descendants().whereType<ScoringBehavior>();
await game.ready();
verify(() => gameBloc.add(MultiplierIncreased())).called(1);
expect(scores.length, 1);
},
);
flameBlocTester.test(
'when hits multiple of 10 times '
"doesn't increase multiplier, neither ScoringBehavior",
(game) async {
final bloc = _MockSpaceshipRampCubit();
final streamController = StreamController<SpaceshipRampState>();
whenListen(
bloc,
streamController.stream,
initialState: SpaceshipRampState(hits: 9),
);
final behavior = RampShotBehavior(
points: shotPoints,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
streamController.add(SpaceshipRampState(hits: 10));
final scores = game.children.whereType<ScoringBehavior>();
await game.ready();
verifyNever(() => gameBloc.add(MultiplierIncreased()));
expect(scores.length, 0);
},
);
flameBlocTester.test(
'closes subscription when removed',
(game) async {
final bloc = _MockSpaceshipRampCubit();
whenListen(
bloc,
const Stream<SpaceshipRampState>.empty(),
initialState: SpaceshipRampState.initial(),
);
when(bloc.close).thenAnswer((_) async {});
final subscription = _MockStreamSubscription();
when(subscription.cancel).thenAnswer((_) async {});
final behavior = RampShotBehavior.test(
points: shotPoints,
subscription: subscription,
);
final parent = SpaceshipRamp.test(
bloc: bloc,
);
await game.pump(
parent,
gameBloc: gameBloc,
);
await parent.ensureAdd(behavior);
parent.remove(behavior);
await game.ready();
verify(subscription.cancel).called(1);
},
);
});
}

Loading…
Cancel
Save