chore: merged main

pull/39/head
alestiago 4 years ago
commit 7c6e528bb2

@ -14,6 +14,8 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<BonusLetterActivated>(_onBonusLetterActivated); on<BonusLetterActivated>(_onBonusLetterActivated);
} }
static const bonusWord = 'GOOGLE';
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
if (state.balls > 0) { if (state.balls > 0) {
emit(state.copyWith(balls: state.balls - 1)); emit(state.copyWith(balls: state.balls - 1));
@ -27,13 +29,25 @@ class GameBloc extends Bloc<GameEvent, GameState> {
} }
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) {
emit( final newBonusLetters = [
state.copyWith( ...state.activatedBonusLetters,
bonusLetters: [ event.letterIndex,
...state.bonusLetters, ];
event.letter,
], if (newBonusLetters.length == bonusWord.length) {
), emit(
); state.copyWith(
activatedBonusLetters: [],
bonusHistory: [
...state.bonusHistory,
GameBonus.word,
],
),
);
} else {
emit(
state.copyWith(activatedBonusLetters: newBonusLetters),
);
}
} }
} }

@ -34,10 +34,14 @@ class Scored extends GameEvent {
} }
class BonusLetterActivated extends GameEvent { class BonusLetterActivated extends GameEvent {
const BonusLetterActivated(this.letter); const BonusLetterActivated(this.letterIndex)
: assert(
letterIndex < GameBloc.bonusWord.length,
'Index must be smaller than the length of the word',
);
final String letter; final int letterIndex;
@override @override
List<Object?> get props => [letter]; List<Object?> get props => [letterIndex];
} }

@ -2,6 +2,13 @@
part of 'game_bloc.dart'; part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame.
enum GameBonus {
/// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word
word,
}
/// {@template game_state} /// {@template game_state}
/// Represents the state of the pinball game. /// Represents the state of the pinball game.
/// {@endtemplate} /// {@endtemplate}
@ -10,14 +17,16 @@ class GameState extends Equatable {
const GameState({ const GameState({
required this.score, required this.score,
required this.balls, required this.balls,
required this.bonusLetters, required this.activatedBonusLetters,
required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative"); assert(balls >= 0, "Number of balls can't be negative");
const GameState.initial() const GameState.initial()
: score = 0, : score = 0,
balls = 3, balls = 3,
bonusLetters = const []; activatedBonusLetters = const [],
bonusHistory = const [];
/// The current score of the game. /// The current score of the game.
final int score; final int score;
@ -28,7 +37,11 @@ class GameState extends Equatable {
final int balls; final int balls;
/// Active bonus letters. /// Active bonus letters.
final List<String> bonusLetters; final List<int> activatedBonusLetters;
/// 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. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => balls == 0;
@ -39,7 +52,8 @@ class GameState extends Equatable {
GameState copyWith({ GameState copyWith({
int? score, int? score,
int? balls, int? balls,
List<String>? bonusLetters, List<int>? activatedBonusLetters,
List<GameBonus>? bonusHistory,
}) { }) {
assert( assert(
score == null || score >= this.score, score == null || score >= this.score,
@ -49,7 +63,9 @@ class GameState extends Equatable {
return GameState( return GameState(
score: score ?? this.score, score: score ?? this.score,
balls: balls ?? this.balls, balls: balls ?? this.balls,
bonusLetters: bonusLetters ?? this.bonusLetters, activatedBonusLetters:
activatedBonusLetters ?? this.activatedBonusLetters,
bonusHistory: bonusHistory ?? this.bonusHistory,
); );
} }
@ -57,6 +73,7 @@ class GameState extends Equatable {
List<Object?> get props => [ List<Object?> get props => [
score, score,
balls, balls,
bonusLetters, activatedBonusLetters,
bonusHistory,
]; ];
} }

@ -37,7 +37,7 @@ class Ball extends PositionBodyComponent<PinballGame, SpriteComponent> {
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
..position = _position ..position = Vector2(_position.x, _position.y + size.y)
..type = BodyType.dynamic; ..type = BodyType.dynamic;
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);

@ -1,12 +1,45 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart' show SpriteComponent; import 'package:flame/components.dart' show PositionComponent, SpriteComponent;
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
/// {@template flipper_group}
/// Loads a [Flipper.right] and a [Flipper.left].
/// {@endtemplate}
class FlipperGroup extends PositionComponent {
/// {@macro flipper_group}
FlipperGroup({
required Vector2 position,
required this.spacing,
}) : super(position: position);
/// The amount of space between the [Flipper.right] and [Flipper.left].
final double spacing;
@override
Future<void> onLoad() async {
final leftFlipper = Flipper.left(
position: Vector2(
position.x - (Flipper.width / 2) - (spacing / 2),
position.y,
),
);
await add(leftFlipper);
final rightFlipper = Flipper.right(
position: Vector2(
position.x + (Flipper.width / 2) + (spacing / 2),
position.y,
),
);
await add(rightFlipper);
}
}
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// A bat, typically found in pairs at the bottom of the board.
/// ///
@ -76,20 +109,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
/// [onKeyEvent] method listens to when one of these keys is pressed. /// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys; final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(
sprite: sprite,
size: size,
);
if (side == BoardSide.right) {
positionComponent?.flipHorizontally();
}
}
/// Applies downward linear velocity to the [Flipper], moving it to its /// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position. /// resting position.
void _moveDown() { void _moveDown() {
@ -102,15 +121,50 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
body.linearVelocity = Vector2(0, _speed); body.linearVelocity = Vector2(0, _speed);
} }
/// Loads the sprite that renders with the [Flipper].
Future<void> _loadSprite() async {
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(
sprite: sprite,
size: size,
);
if (side.isRight) {
positionComponent!.flipHorizontally();
}
}
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async {
final anchor = FlipperAnchor(flipper: this);
await add(anchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: this,
anchor: anchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final joint = world.createJoint(jointDef) as RevoluteJoint;
// FIXME(erickzanardo): when mounted the initial position is not fully
// reached.
unawaited(
mounted.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(joint, side),
),
);
}
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[]; final fixtures = <FixtureDef>[];
final isLeft = side.isLeft; final isLeft = side.isLeft;
final bigCircleShape = CircleShape()..radius = height / 2; final bigCircleShape = CircleShape()..radius = size.y / 2;
bigCircleShape.position.setValues( bigCircleShape.position.setValues(
isLeft isLeft
? -(width / 2) + bigCircleShape.radius ? -(size.x / 2) + bigCircleShape.radius
: (width / 2) - bigCircleShape.radius, : (size.x / 2) - bigCircleShape.radius,
0, 0,
); );
fixtures.add(FixtureDef(bigCircleShape)); fixtures.add(FixtureDef(bigCircleShape));
@ -118,8 +172,8 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
isLeft isLeft
? (width / 2) - smallCircleShape.radius ? (size.x / 2) - smallCircleShape.radius
: -(width / 2) + smallCircleShape.radius, : -(size.x / 2) + smallCircleShape.radius,
0, 0,
); );
fixtures.add(FixtureDef(smallCircleShape)); fixtures.add(FixtureDef(smallCircleShape));
@ -146,6 +200,15 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
return fixtures; return fixtures;
} }
@override
Future<void> onLoad() async {
await super.onLoad();
await Future.wait([
_loadSprite(),
_anchorToJoint(),
]);
}
@override @override
Body createBody() { Body createBody() {
final bodyDef = BodyDef() final bodyDef = BodyDef()
@ -159,17 +222,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler {
return body; return body;
} }
// TODO(erickzanardo): Remove this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417
// ignore: public_member_api_docs
final Completer hasMounted = Completer<void>();
@override
void onMount() {
super.onMount();
hasMounted.complete();
}
@override @override
bool onKeyEvent( bool onKeyEvent(
RawKeyEvent event, RawKeyEvent event,
@ -202,8 +254,8 @@ class FlipperAnchor extends Anchor {
}) : super( }) : super(
position: Vector2( position: Vector2(
flipper.side.isLeft flipper.side.isLeft
? flipper.body.position.x - Flipper.width / 2 ? flipper.body.position.x - flipper.size.x / 2
: flipper.body.position.x + Flipper.width / 2, : flipper.body.position.x + flipper.size.x / 2,
flipper.body.position.y, flipper.body.position.y,
), ),
); );
@ -216,7 +268,7 @@ class FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def} /// {@macro flipper_anchor_revolute_joint_def}
FlipperAnchorRevoluteJointDef({ FlipperAnchorRevoluteJointDef({
required Flipper flipper, required Flipper flipper,
required Anchor anchor, required FlipperAnchor anchor,
}) { }) {
initialize( initialize(
flipper.body, flipper.body,

@ -1,23 +1,32 @@
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart' show Anchor; import 'package:pinball/game/game.dart' show Anchor;
/// {@template plunger} /// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the /// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// playfield. /// playfield.
/// ///
/// [Plunger] ignores gravity so the player controls its downward [pull]. /// [Plunger] ignores gravity so the player controls its downward [_pull].
/// {@endtemplate} /// {@endtemplate}
class Plunger extends BodyComponent { class Plunger extends BodyComponent with KeyboardHandler {
/// {@macro plunger} /// {@macro plunger}
Plunger({required Vector2 position}) : _position = position; Plunger({
required Vector2 position,
required this.compressionDistance,
}) : _position = position;
/// The initial position of the [Plunger] body.
final Vector2 _position; final Vector2 _position;
/// Distance the plunger can lower.
final double compressionDistance;
@override @override
Body createBody() { Body createBody() {
final shape = PolygonShape()..setAsBoxXY(2.5, 1.5); final shape = PolygonShape()..setAsBoxXY(2, 0.75);
final fixtureDef = FixtureDef(shape); final fixtureDef = FixtureDef(shape)..density = 5;
final bodyDef = BodyDef() final bodyDef = BodyDef()
..userData = this ..userData = this
@ -29,18 +38,57 @@ class Plunger extends BodyComponent {
} }
/// Set a constant downward velocity on the [Plunger]. /// Set a constant downward velocity on the [Plunger].
void pull() { void _pull() {
body.linearVelocity = Vector2(0, -7); body.linearVelocity = Vector2(0, -3);
} }
/// Set an upward velocity on the [Plunger]. /// Set an upward velocity on the [Plunger].
/// ///
/// The velocity's magnitude depends on how far the [Plunger] has been pulled /// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [_position]. /// from its original [_position].
void release() { void _release() {
final velocity = (_position.y - body.position.y) * 9; final velocity = (_position.y - body.position.y) * 9;
body.linearVelocity = Vector2(0, velocity); body.linearVelocity = Vector2(0, velocity);
} }
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
final keys = [
LogicalKeyboardKey.space,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.keyS,
];
// TODO(alestiago): Check why false cancels the event for other components.
// Investigate why return is of type [bool] expected instead of a type
// [KeyEventResult].
if (!keys.contains(event.logicalKey)) return true;
if (event is RawKeyDownEvent) {
_pull();
} else if (event is RawKeyUpEvent) {
_release();
}
return true;
}
}
/// {@template plunger_anchor}
/// [Anchor] positioned below a [Plunger].
/// {@endtemplate}
class PlungerAnchor extends Anchor {
/// {@macro plunger_anchor}
PlungerAnchor({
required Plunger plunger,
}) : super(
position: Vector2(
plunger.body.position.x,
plunger.body.position.y - plunger.compressionDistance,
),
);
} }
/// {@template plunger_anchor_prismatic_joint_def} /// {@template plunger_anchor_prismatic_joint_def}
@ -54,11 +102,8 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def} /// {@macro plunger_anchor_prismatic_joint_def}
PlungerAnchorPrismaticJointDef({ PlungerAnchorPrismaticJointDef({
required Plunger plunger, required Plunger plunger,
required Anchor anchor, required PlungerAnchor anchor,
}) : assert( }) {
anchor.body.position.y < plunger.body.position.y,
'Anchor must be below the Plunger',
) {
initialize( initialize(
plunger.body, plunger.body,
anchor.body, anchor.body,
@ -67,6 +112,9 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
); );
enableLimit = true; enableLimit = true;
lowerTranslation = double.negativeInfinity; lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 50;
maxMotorForce = motorSpeed;
collideConnected = true; collideConnected = true;
} }
} }

@ -4,7 +4,7 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart'; import 'package:pinball/game/components/components.dart';
/// {@template wall} /// {@template wall}
/// A continuos generic and [BodyType.static] barrier that divides a game area. /// A continuous generic and [BodyType.static] barrier that divides a game area.
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Remove [Wall] for [Pathway.straight]. // TODO(alestiago): Remove [Wall] for [Pathway.straight].
class Wall extends BodyComponent { class Wall extends BodyComponent {
@ -25,7 +25,7 @@ class Wall extends BodyComponent {
final shape = EdgeShape()..set(start, end); final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape) final fixtureDef = FixtureDef(shape)
..restitution = 0.0 ..restitution = 0.1
..friction = 0.3; ..friction = 0.3;
final bodyDef = BodyDef() final bodyDef = BodyDef()
@ -37,6 +37,20 @@ class Wall extends BodyComponent {
} }
} }
/// Create top, left, and right [Wall]s for the game board.
List<Wall> createBoundaries(Forge2DGame game) {
final topLeft = Vector2.zero();
final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize);
final topRight = Vector2(bottomRight.x, topLeft.y);
final bottomLeft = Vector2(topLeft.x, bottomRight.y);
return [
Wall(start: topLeft, end: topRight),
Wall(start: topRight, end: bottomRight),
Wall(start: bottomLeft, end: topLeft),
];
}
/// {@template bottom_wall} /// {@template bottom_wall}
/// [Wall] located at the bottom of the board. /// [Wall] located at the bottom of the board.
/// ///

@ -13,17 +13,7 @@ class PinballGame extends Forge2DGame
final PinballTheme theme; final PinballTheme theme;
// TODO(erickzanardo): Change to the plumber position late final Plunger plunger;
late final ballStartingPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y - 20,
),
) -
Vector2(0, -20);
// TODO(alestiago): Change to the design position.
late final flippersPosition = ballStartingPosition - Vector2(0, 5);
@override @override
void onAttach() { void onAttach() {
@ -31,76 +21,85 @@ class PinballGame extends Forge2DGame
spawnBall(); spawnBall();
} }
void spawnBall() {
add(Ball(position: ballStartingPosition));
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
addContactCallback(BallScorePointsCallback()); _addContactCallbacks();
await add(BottomWall(this)); await _addGameBoundaries();
addContactCallback(BottomWallBallContactCallback()); unawaited(_addPlunger());
unawaited(_addFlippers()); // Corner wall above plunger so the ball deflects into the rest of the
} // board.
// TODO(allisonryan0002): remove once we have the launch track for the ball.
Future<void> _addFlippers() async { await add(
const spaceBetweenFlippers = 2; Wall(
final leftFlipper = Flipper.left( start: screenToWorld(
position: Vector2( Vector2(
flippersPosition.x - (Flipper.width / 2) - (spaceBetweenFlippers / 2), camera.viewport.effectiveSize.x,
flippersPosition.y, 100,
),
),
end: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x - 100,
0,
),
),
), ),
); );
await add(leftFlipper);
final leftFlipperAnchor = FlipperAnchor(flipper: leftFlipper); final flippersPosition = screenToWorld(
await add(leftFlipperAnchor); Vector2(
final leftFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef( camera.viewport.effectiveSize.x / 2,
flipper: leftFlipper, camera.viewport.effectiveSize.y / 1.1,
anchor: leftFlipperAnchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final leftFlipperRevoluteJoint =
world.createJoint(leftFlipperRevoluteJointDef) as RevoluteJoint;
final rightFlipper = Flipper.right(
position: Vector2(
flippersPosition.x + (Flipper.width / 2) + (spaceBetweenFlippers / 2),
flippersPosition.y,
), ),
); );
await add(rightFlipper);
final rightFlipperAnchor = FlipperAnchor(flipper: rightFlipper);
await add(rightFlipperAnchor);
final rightFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef(
flipper: rightFlipper,
anchor: rightFlipperAnchor,
);
// TODO(alestiago): Remove casting once the following is closed:
// https://github.com/flame-engine/forge2d/issues/36
final rightFlipperRevoluteJoint =
world.createJoint(rightFlipperRevoluteJointDef) as RevoluteJoint;
// TODO(erickzanardo): Clean this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417
// FIXME(erickzanardo): when mounted the initial position is not fully
// reached.
unawaited( unawaited(
leftFlipper.hasMounted.future.whenComplete( add(
() => FlipperAnchorRevoluteJointDef.unlock( FlipperGroup(
leftFlipperRevoluteJoint, position: flippersPosition,
leftFlipper.side, spacing: 2,
), ),
), ),
); );
unawaited( }
rightFlipper.hasMounted.future.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock( void spawnBall() {
rightFlipperRevoluteJoint, add(Ball(position: plunger.body.position));
rightFlipper.side, }
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback());
addContactCallback(BottomWallBallContactCallback());
}
Future<void> _addGameBoundaries() async {
await add(BottomWall(this));
createBoundaries(this).forEach(add);
}
Future<void> _addPlunger() async {
late PlungerAnchor plungerAnchor;
final compressionDistance = camera.viewport.effectiveSize.y / 12;
await add(
plunger = Plunger(
position: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 1.035,
camera.viewport.effectiveSize.y - compressionDistance,
),
), ),
compressionDistance: compressionDistance,
),
);
await add(plungerAnchor = PlungerAnchor(plunger: plunger));
world.createJoint(
PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: plungerAnchor,
), ),
); );
} }

@ -21,9 +21,24 @@ void main() {
} }
}, },
expect: () => [ expect: () => [
const GameState(score: 0, balls: 2, bonusLetters: []), const GameState(
const GameState(score: 0, balls: 1, bonusLetters: []), score: 0,
const GameState(score: 0, balls: 0, bonusLetters: []), balls: 2,
activatedBonusLetters: [],
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
bonusHistory: [],
),
], ],
); );
}); });
@ -37,8 +52,18 @@ void main() {
..add(const Scored(points: 2)) ..add(const Scored(points: 2))
..add(const Scored(points: 3)), ..add(const Scored(points: 3)),
expect: () => [ expect: () => [
const GameState(score: 2, balls: 3, bonusLetters: []), const GameState(
const GameState(score: 5, balls: 3, bonusLetters: []), score: 2,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
),
const GameState(
score: 5,
balls: 3,
activatedBonusLetters: [],
bonusHistory: [],
),
], ],
); );
@ -53,9 +78,24 @@ void main() {
bloc.add(const Scored(points: 2)); bloc.add(const Scored(points: 2));
}, },
expect: () => [ expect: () => [
const GameState(score: 0, balls: 2, bonusLetters: []), const GameState(
const GameState(score: 0, balls: 1, bonusLetters: []), score: 0,
const GameState(score: 0, balls: 0, bonusLetters: []), balls: 2,
activatedBonusLetters: [],
bonusHistory: [],
),
const GameState(
score: 0,
balls: 1,
activatedBonusLetters: [],
bonusHistory: [],
),
const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
bonusHistory: [],
),
], ],
); );
}); });
@ -65,42 +105,77 @@ void main() {
'adds the letter to the state', 'adds the letter to the state',
build: GameBloc.new, build: GameBloc.new,
act: (bloc) => bloc act: (bloc) => bloc
..add(const BonusLetterActivated('G')) ..add(const BonusLetterActivated(0))
..add(const BonusLetterActivated('O')) ..add(const BonusLetterActivated(1))
..add(const BonusLetterActivated('O')) ..add(const BonusLetterActivated(2)),
..add(const BonusLetterActivated('G')) expect: () => const [
..add(const BonusLetterActivated('L')) GameState(
..add(const BonusLetterActivated('E')),
expect: () => [
const GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G'], activatedBonusLetters: [0],
bonusHistory: [],
), ),
const GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G', 'O'], activatedBonusLetters: [0, 1],
bonusHistory: [],
), ),
const GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G', 'O', 'O'], activatedBonusLetters: [0, 1, 2],
bonusHistory: [],
), ),
const GameState( ],
);
blocTest<GameBloc, GameState>(
'adds the bonus when the bonusWord is completed',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated(0))
..add(const BonusLetterActivated(1))
..add(const BonusLetterActivated(2))
..add(const BonusLetterActivated(3))
..add(const BonusLetterActivated(4))
..add(const BonusLetterActivated(5)),
expect: () => const [
GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G', 'O', 'O', 'G'], activatedBonusLetters: [0],
bonusHistory: [],
), ),
const GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L'], activatedBonusLetters: [0, 1],
bonusHistory: [],
), ),
const GameState( GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2],
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2, 3],
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [0, 1, 2, 3, 4],
bonusHistory: [],
),
GameState(
score: 0, score: 0,
balls: 3, balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'], activatedBonusLetters: [],
bonusHistory: [GameBonus.word],
), ),
], ],
); );

@ -43,19 +43,29 @@ void main() {
group('BonusLetterActivated', () { group('BonusLetterActivated', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const BonusLetterActivated('A'), isNotNull); expect(const BonusLetterActivated(0), isNotNull);
}); });
test('supports value equality', () { test('supports value equality', () {
expect( expect(
BonusLetterActivated('A'), BonusLetterActivated(0),
equals(BonusLetterActivated('A')), equals(BonusLetterActivated(0)),
); );
expect( expect(
BonusLetterActivated('B'), BonusLetterActivated(0),
isNot(equals(BonusLetterActivated('A'))), isNot(equals(BonusLetterActivated(1))),
); );
}); });
test(
'throws assertion error if index is bigger than the word length',
() {
expect(
() => BonusLetterActivated(8),
throwsAssertionError,
);
},
);
}); });
}); });
} }

@ -10,13 +10,15 @@ void main() {
GameState( GameState(
score: 0, score: 0,
balls: 0, balls: 0,
bonusLetters: const [], activatedBonusLetters: const [],
bonusHistory: const [],
), ),
equals( equals(
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
), ),
), ),
); );
@ -25,7 +27,12 @@ void main() {
group('constructor', () { group('constructor', () {
test('can be instantiated', () { test('can be instantiated', () {
expect( expect(
const GameState(score: 0, balls: 0, bonusLetters: []), const GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
bonusHistory: [],
),
isNotNull, isNotNull,
); );
}); });
@ -36,7 +43,12 @@ void main() {
'when balls are negative', 'when balls are negative',
() { () {
expect( expect(
() => GameState(balls: -1, score: 0, bonusLetters: const []), () => GameState(
balls: -1,
score: 0,
activatedBonusLetters: const [],
bonusHistory: const [],
),
throwsAssertionError, throwsAssertionError,
); );
}, },
@ -47,7 +59,12 @@ void main() {
'when score is negative', 'when score is negative',
() { () {
expect( expect(
() => GameState(balls: 0, score: -1, bonusLetters: const []), () => GameState(
balls: 0,
score: -1,
activatedBonusLetters: const [],
bonusHistory: const [],
),
throwsAssertionError, throwsAssertionError,
); );
}, },
@ -60,7 +77,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 0, score: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect(gameState.isGameOver, isTrue); expect(gameState.isGameOver, isTrue);
}); });
@ -71,7 +89,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect(gameState.isGameOver, isFalse); expect(gameState.isGameOver, isFalse);
}); });
@ -85,7 +104,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect(gameState.isLastBall, isTrue); expect(gameState.isLastBall, isTrue);
}, },
@ -98,7 +118,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 2, balls: 2,
score: 0, score: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect(gameState.isLastBall, isFalse); expect(gameState.isLastBall, isFalse);
}, },
@ -113,7 +134,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect( expect(
() => gameState.copyWith(score: gameState.score - 1), () => gameState.copyWith(score: gameState.score - 1),
@ -129,7 +151,8 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
expect( expect(
gameState.copyWith(), gameState.copyWith(),
@ -145,12 +168,14 @@ void main() {
const gameState = GameState( const gameState = GameState(
score: 2, score: 2,
balls: 0, balls: 0,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
); );
final otherGameState = GameState( final otherGameState = GameState(
score: gameState.score + 1, score: gameState.score + 1,
balls: gameState.balls + 1, balls: gameState.balls + 1,
bonusLetters: const ['A'], activatedBonusLetters: const [0],
bonusHistory: const [GameBonus.word],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -158,7 +183,8 @@ void main() {
gameState.copyWith( gameState.copyWith(
score: otherGameState.score, score: otherGameState.score,
balls: otherGameState.balls, balls: otherGameState.balls,
bonusLetters: otherGameState.bonusLetters, activatedBonusLetters: otherGameState.activatedBonusLetters,
bonusHistory: otherGameState.bonusHistory,
), ),
equals(otherGameState), equals(otherGameState),
); );

@ -34,7 +34,11 @@ void main() {
await game.ensureAdd(ball); await game.ensureAdd(ball);
game.contains(ball); game.contains(ball);
expect(ball.body.position, position); final expectedPosition = Vector2(
position.x,
position.y + ball.size.y,
);
expect(ball.body.position, equals(expectedPosition));
}, },
); );
@ -49,7 +53,7 @@ void main() {
); );
}); });
group('first fixture', () { group('fixture', () {
flameTester.test( flameTester.test(
'exists', 'exists',
(game) async { (game) async {
@ -133,7 +137,8 @@ void main() {
initialState: const GameState( initialState: const GameState(
score: 10, score: 10,
balls: 1, balls: 1,
bonusLetters: [], activatedBonusLetters: [],
bonusHistory: [],
), ),
); );
await game.ready(); await game.ready();

@ -2,6 +2,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -13,6 +14,105 @@ import '../../helpers/helpers.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
group('FlipperGroup', () {
flameTester.test(
'loads correctly',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(game.contains(flipperGroup), isTrue);
},
);
group('constructor', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final flipperGroup = FlipperGroup(
position: position,
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(flipperGroup.position, equals(position));
},
);
});
group('children', () {
bool Function(Component) flipperSelector(BoardSide side) =>
(component) => component is Flipper && component.side == side;
flameTester.test(
'has only one left Flipper',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(
() => flipperGroup.children.singleWhere(
flipperSelector(BoardSide.left),
),
returnsNormally,
);
},
);
flameTester.test(
'has only one right Flipper',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 0,
);
await game.ensureAdd(flipperGroup);
expect(
() => flipperGroup.children.singleWhere(
flipperSelector(BoardSide.right),
),
returnsNormally,
);
},
);
flameTester.test(
'spaced correctly',
(game) async {
final flipperGroup = FlipperGroup(
position: Vector2.zero(),
spacing: 2,
);
await game.ready();
await game.ensureAdd(flipperGroup);
final leftFlipper = flipperGroup.children.singleWhere(
flipperSelector(BoardSide.left),
) as Flipper;
final rightFlipper = flipperGroup.children.singleWhere(
flipperSelector(BoardSide.right),
) as Flipper;
expect(
leftFlipper.body.position.x +
leftFlipper.size.x +
flipperGroup.spacing,
equals(rightFlipper.body.position.x),
);
},
);
});
});
group( group(
'Flipper', 'Flipper',
() { () {
@ -21,9 +121,11 @@ void main() {
(game) async { (game) async {
final leftFlipper = Flipper.left(position: Vector2.zero()); final leftFlipper = Flipper.left(position: Vector2.zero());
final rightFlipper = Flipper.right(position: Vector2.zero()); final rightFlipper = Flipper.right(position: Vector2.zero());
await game.ready();
await game.ensureAddAll([leftFlipper, rightFlipper]); await game.ensureAddAll([leftFlipper, rightFlipper]);
expect(game.contains(leftFlipper), isTrue); expect(game.contains(leftFlipper), isTrue);
expect(game.contains(rightFlipper), isTrue);
}, },
); );
@ -255,36 +357,33 @@ void main() {
}, },
); );
group( group('FlipperAnchor', () {
'FlipperAnchor', flameTester.test(
() { 'position is at the left of the left Flipper',
flameTester.test( (game) async {
'position is at the left of the left Flipper', final flipper = Flipper.left(position: Vector2.zero());
(game) async { await game.ensureAdd(flipper);
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor); await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2));
}, },
); );
flameTester.test( flameTester.test(
'position is at the right of the right Flipper', 'position is at the right of the right Flipper',
(game) async { (game) async {
final flipper = Flipper.right(position: Vector2.zero()); final flipper = Flipper.right(position: Vector2.zero());
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper); final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor); await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); expect(flipperAnchor.body.position.x, equals(Flipper.width / 2));
}, },
); );
}, });
);
group('FlipperAnchorRevoluteJointDef', () { group('FlipperAnchorRevoluteJointDef', () {
group('initializes with', () { group('initializes with', () {

@ -1,8 +1,11 @@
// ignore_for_file: cascade_invocations // ignore_for_file: cascade_invocations
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart'; import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -13,11 +16,16 @@ void main() {
final flameTester = FlameTester(PinballGameTest.create); final flameTester = FlameTester(PinballGameTest.create);
group('Plunger', () { group('Plunger', () {
const compressionDistance = 0.0;
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready(); await game.ready();
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
expect(game.contains(plunger), isTrue); expect(game.contains(plunger), isTrue);
@ -29,7 +37,10 @@ void main() {
'positions correctly', 'positions correctly',
(game) async { (game) async {
final position = Vector2.all(10); final position = Vector2.all(10);
final plunger = Plunger(position: position); final plunger = Plunger(
position: position,
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
game.contains(plunger); game.contains(plunger);
@ -40,7 +51,10 @@ void main() {
flameTester.test( flameTester.test(
'is dynamic', 'is dynamic',
(game) async { (game) async {
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
expect(plunger.body.bodyType, equals(BodyType.dynamic)); expect(plunger.body.bodyType, equals(BodyType.dynamic));
@ -50,7 +64,10 @@ void main() {
flameTester.test( flameTester.test(
'ignores gravity', 'ignores gravity',
(game) async { (game) async {
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
expect(plunger.body.gravityScale, isZero); expect(plunger.body.gravityScale, isZero);
@ -58,11 +75,14 @@ void main() {
); );
}); });
group('first fixture', () { group('fixture', () {
flameTester.test( flameTester.test(
'exists', 'exists',
(game) async { (game) async {
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
expect(plunger.body.fixtures[0], isA<Fixture>()); expect(plunger.body.fixtures[0], isA<Fixture>());
@ -72,65 +92,128 @@ void main() {
flameTester.test( flameTester.test(
'shape is a polygon', 'shape is a polygon',
(game) async { (game) async {
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0]; final fixture = plunger.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.polygon)); expect(fixture.shape.shapeType, equals(ShapeType.polygon));
}, },
); );
});
flameTester.test(
'pull sets a negative linear velocity',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
plunger.pull();
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
group('release', () {
flameTester.test( flameTester.test(
'does not set a linear velocity ' 'has density',
'when plunger is in starting position',
(game) async { (game) async {
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger); await game.ensureAdd(plunger);
plunger.release(); final fixture = plunger.body.fixtures[0];
expect(fixture.density, greaterThan(0));
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
}, },
); );
});
flameTester.test( group('onKeyEvent', () {
'sets a positive linear velocity ' final keys = UnmodifiableListView([
'when plunger is below starting position', LogicalKeyboardKey.space,
(game) async { LogicalKeyboardKey.arrowDown,
final plunger = Plunger(position: Vector2.zero()); LogicalKeyboardKey.keyS,
await game.ensureAdd(plunger); ]);
plunger.body.setTransform(Vector2(0, -1), 0); late Plunger plunger;
plunger.release();
expect(plunger.body.linearVelocity.y, isPositive); setUp(() {
expect(plunger.body.linearVelocity.x, isZero); plunger = Plunger(
}, position: Vector2.zero(),
); compressionDistance: compressionDistance,
);
});
testRawKeyUpEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test(
'moves upwards when $keyLabel is released '
'and plunger is below its starting position',
(game) async {
await game.ensureAdd(plunger);
plunger.body.setTransform(Vector2(0, -1), 0);
plunger.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test(
'does not move when $keyLabel is released '
'and plunger is in its starting position',
(game) async {
await game.ensureAdd(plunger);
plunger.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
testRawKeyDownEvents(keys, (event) {
final keyLabel = (event.logicalKey != LogicalKeyboardKey.space)
? event.logicalKey.keyLabel
: 'Space';
flameTester.test(
'moves downwards when $keyLabel is pressed',
(game) async {
await game.ensureAdd(plunger);
plunger.onKeyEvent(event, {});
expect(plunger.body.linearVelocity.y, isNegative);
expect(plunger.body.linearVelocity.x, isZero);
},
);
});
}); });
}); });
group('PlungerAnchorPrismaticJointDef', () { group('PlungerAnchor', () {
late Plunger plunger; const compressionDistance = 10.0;
late Anchor anchor;
flameTester.test(
'position is a compression distance below the Plunger',
(game) async {
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final plungerAnchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(plungerAnchor);
expect(
plungerAnchor.body.position.y,
equals(plunger.body.position.y - compressionDistance),
);
},
);
});
group('PlungerAnchorPrismaticJointDef', () {
const compressionDistance = 10.0;
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
late Plunger plunger;
setUp(() { setUp(() {
whenListen( whenListen(
@ -138,51 +221,21 @@ void main() {
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
initialState: const GameState.initial(), initialState: const GameState.initial(),
); );
plunger = Plunger(position: Vector2.zero()); plunger = Plunger(
anchor = Anchor(position: Vector2(0, -1)); position: Vector2.zero(),
compressionDistance: compressionDistance,
);
}); });
final flameTester = flameBlocTester(gameBloc: gameBloc); final flameTester = flameBlocTester(gameBloc: gameBloc);
flameTester.test(
'throws AssertionError '
'when anchor is above plunger',
(game) async {
final anchor = Anchor(position: Vector2(0, 1));
await game.ensureAddAll([plunger, anchor]);
expect(
() => PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
),
throwsAssertionError,
);
},
);
flameTester.test(
'throws AssertionError '
'when anchor is in same position as plunger',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ensureAddAll([plunger, anchor]);
expect(
() => PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
),
throwsAssertionError,
);
},
);
group('initializes with', () { group('initializes with', () {
flameTester.test( flameTester.test(
'plunger body as bodyA', 'plunger body as bodyA',
(game) async { (game) async {
await game.ensureAddAll([plunger, anchor]); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
@ -196,7 +249,9 @@ void main() {
flameTester.test( flameTester.test(
'anchor body as bodyB', 'anchor body as bodyB',
(game) async { (game) async {
await game.ensureAddAll([plunger, anchor]); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
@ -211,7 +266,9 @@ void main() {
flameTester.test( flameTester.test(
'limits enabled', 'limits enabled',
(game) async { (game) async {
await game.ensureAddAll([plunger, anchor]); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
@ -226,7 +283,9 @@ void main() {
flameTester.test( flameTester.test(
'lower translation limit as negative infinity', 'lower translation limit as negative infinity',
(game) async { (game) async {
await game.ensureAddAll([plunger, anchor]); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
@ -241,7 +300,9 @@ void main() {
flameTester.test( flameTester.test(
'connected body collison enabled', 'connected body collison enabled',
(game) async { (game) async {
await game.ensureAddAll([plunger, anchor]); await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
@ -254,46 +315,51 @@ void main() {
); );
}); });
flameTester.widgetTest( testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
'plunger cannot go below anchor', flameTester.widgetTest(
(game, tester) async { 'plunger cannot go below anchor',
await game.ensureAddAll([plunger, anchor]); (game, tester) async {
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
// Giving anchor a shape for the plunger to collide with. // Giving anchor a shape for the plunger to collide with.
anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1)); anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1));
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(jointDef);
plunger.pull(); await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(plunger.body.position.y > anchor.body.position.y, isTrue); expect(plunger.body.position.y > anchor.body.position.y, isTrue);
}, },
); );
});
flameTester.widgetTest( testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
'plunger cannot excessively exceed starting position', flameTester.widgetTest(
(game, tester) async { 'plunger cannot excessively exceed starting position',
await game.ensureAddAll([plunger, anchor]); (game, tester) async {
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef( final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger, plunger: plunger,
anchor: anchor, anchor: anchor,
); );
game.world.createJoint(jointDef); game.world.createJoint(jointDef);
plunger.pull(); plunger.body.setTransform(Vector2(0, -1), 0);
await tester.pump(const Duration(seconds: 1));
plunger.release(); await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(plunger.body.position.y < 1, isTrue); expect(plunger.body.position.y < 1, isTrue);
}, },
); );
});
}); });
} }

@ -77,7 +77,7 @@ void main() {
); );
}); });
group('first fixture', () { group('fixture', () {
flameTester.test( flameTester.test(
'exists', 'exists',
(game) async { (game) async {
@ -92,7 +92,7 @@ void main() {
); );
flameTester.test( flameTester.test(
'has restitution equals 0', 'has restitution',
(game) async { (game) async {
final wall = Wall( final wall = Wall(
start: Vector2.zero(), start: Vector2.zero(),
@ -101,7 +101,7 @@ void main() {
await game.ensureAdd(wall); await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0]; final fixture = wall.body.fixtures[0];
expect(fixture.restitution, equals(0)); expect(fixture.restitution, greaterThan(0));
}, },
); );

@ -17,60 +17,81 @@ void main() {
// TODO(alestiago): test if [PinballGame] registers // TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved: // [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416 // https://github.com/flame-engine/flame/issues/1416
group( group('components', () {
'components', bool Function(Component) componentSelector<T>() =>
() { (component) => component is T;
group('Flippers', () {
bool Function(Component) flipperSelector(BoardSide side) => flameTester.test(
(component) => component is Flipper && component.side == side; 'has three Walls',
(game) async {
flameTester.test( await game.ready();
'has only one left Flipper', final walls = game.children
(game) async { .where(
await game.ready(); (component) => component is Wall && component is! BottomWall,
)
expect( .toList();
() => game.children.singleWhere( // TODO(allisonryan0002): expect 3 when launch track is added and
flipperSelector(BoardSide.left), // temporary wall is removed.
), expect(walls.length, 4);
returnsNormally, },
); );
},
flameTester.test(
'has only one BottomWall',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
componentSelector<BottomWall>(),
),
returnsNormally,
); );
},
);
flameTester.test( flameTester.test(
'has only one right Flipper', 'has only one Plunger',
(game) async { (game) async {
await game.ready(); await game.ready();
expect( expect(
() => game.children.singleWhere( () => game.children.singleWhere(
flipperSelector(BoardSide.right), (component) => component is Plunger,
), ),
returnsNormally, returnsNormally,
);
},
); );
},
);
flameTester.test('has only one FlipperGroup', (game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is FlipperGroup,
),
returnsNormally,
);
});
});
debugModeFlameTester.test('adds a ball on tap up', (game) async { debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready(); await game.ready();
final eventPosition = MockEventPosition(); final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.all(10)); when(() => eventPosition.game).thenReturn(Vector2.all(10));
final tapUpEvent = MockTapUpInfo(); final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent); game.onTapUp(tapUpEvent);
await game.ready(); await game.ready();
expect( expect(
game.children.whereType<Ball>().length, game.children.whereType<Ball>().length,
equals(1), equals(1),
); );
}); });
});
},
);
}); });
} }

@ -9,7 +9,12 @@ import '../../helpers/helpers.dart';
void main() { void main() {
group('GameHud', () { group('GameHud', () {
late GameBloc gameBloc; late GameBloc gameBloc;
const initialState = GameState(score: 10, balls: 2, bonusLetters: []); const initialState = GameState(
score: 10,
balls: 2,
activatedBonusLetters: [],
bonusHistory: [],
);
void _mockState(GameState state) { void _mockState(GameState state) {
whenListen( whenListen(

@ -84,7 +84,13 @@ void main() {
'renders a game over dialog when the user has lost', 'renders a game over dialog when the user has lost',
(tester) async { (tester) async {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
const state = GameState(score: 0, balls: 0, bonusLetters: []); const state = GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
bonusHistory: [],
);
whenListen( whenListen(
gameBloc, gameBloc,
Stream.value(state), Stream.value(state),

Loading…
Cancel
Save