fix: fix merge conflicts with imports from main

pull/14/head
RuiAlonso 4 years ago
commit ec300bec9c

@ -9,6 +9,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) { GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost); on<BallLost>(_onBallLost);
on<Scored>(_onScored); on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated);
} }
void _onBallLost(BallLost event, Emitter emit) { void _onBallLost(BallLost event, Emitter emit) {
@ -22,4 +23,15 @@ class GameBloc extends Bloc<GameEvent, GameState> {
emit(state.copyWith(score: state.score + event.points)); emit(state.copyWith(score: state.score + event.points));
} }
} }
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) {
emit(
state.copyWith(
bonusLetters: [
...state.bonusLetters,
event.letter,
],
),
);
}
} }

@ -24,3 +24,12 @@ class Scored extends GameEvent {
@override @override
List<Object?> get props => [points]; List<Object?> get props => [points];
} }
class BonusLetterActivated extends GameEvent {
const BonusLetterActivated(this.letter);
final String letter;
@override
List<Object?> get props => [letter];
}

@ -8,12 +8,14 @@ class GameState extends Equatable {
const GameState({ const GameState({
required this.score, required this.score,
required this.balls, required this.balls,
required this.bonusLetters,
}) : 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 [];
/// The current score of the game. /// The current score of the game.
final int score; final int score;
@ -23,6 +25,9 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over. /// When the number of balls is 0, the game is over.
final int balls; final int balls;
/// Active bonus letters.
final List<String> bonusLetters;
/// Determines when the game is over. /// Determines when the game is over.
bool get isGameOver => balls == 0; bool get isGameOver => balls == 0;
@ -32,6 +37,7 @@ class GameState extends Equatable {
GameState copyWith({ GameState copyWith({
int? score, int? score,
int? balls, int? balls,
List<String>? bonusLetters,
}) { }) {
assert( assert(
score == null || score >= this.score, score == null || score >= this.score,
@ -41,6 +47,7 @@ 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,
); );
} }
@ -48,5 +55,6 @@ class GameState extends Equatable {
List<Object?> get props => [ List<Object?> get props => [
score, score,
balls, balls,
bonusLetters,
]; ];
} }

@ -0,0 +1,22 @@
import 'package:pinball/game/game.dart';
/// Indicates a side of the board.
///
/// Usually used to position or mirror elements of a [PinballGame]; such as a
/// [Flipper].
enum BoardSide {
/// The left side of the board.
left,
/// The right side of the board.
right,
}
/// Utility methods for [BoardSide].
extension BoardSideX on BoardSide {
/// Whether this side is [BoardSide.left].
bool get isLeft => this == BoardSide.left;
/// Whether this side is [BoardSide.right].
bool get isRight => this == BoardSide.right;
}

@ -1,5 +1,7 @@
export 'anchor.dart'; export 'anchor.dart';
export 'ball.dart'; export 'ball.dart';
export 'board_side.dart';
export 'flipper.dart';
export 'pathway.dart'; export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'score_points.dart'; export 'score_points.dart';

@ -0,0 +1,241 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart';
/// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board.
///
/// [Flipper] can be controlled by the player in an arc motion.
/// {@endtemplate flipper}
class Flipper extends BodyComponent with KeyboardHandler {
/// {@macro flipper}
Flipper._({
required Vector2 position,
required this.side,
required List<LogicalKeyboardKey> keys,
}) : _position = position,
_keys = keys {
// TODO(alestiago): Use sprite instead of color when provided.
paint = Paint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
}
/// A left positioned [Flipper].
Flipper.left({
required Vector2 position,
}) : this._(
position: position,
side: BoardSide.left,
keys: [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
],
);
/// A right positioned [Flipper].
Flipper.right({
required Vector2 position,
}) : this._(
position: position,
side: BoardSide.right,
keys: [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
],
);
/// The width of the [Flipper].
static const width = 12.0;
/// The height of the [Flipper].
static const height = 2.8;
/// The speed required to move the [Flipper] to its highest position.
///
/// The higher the value, the faster the [Flipper] will move.
static const double _speed = 60;
/// Whether the [Flipper] is on the left or right side of the board.
///
/// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion,
/// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion.
final BoardSide side;
/// The initial position of the [Flipper] body.
final Vector2 _position;
/// The [LogicalKeyboardKey]s that will control the [Flipper].
///
/// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys;
/// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position.
void _moveDown() {
body.linearVelocity = Vector2(0, -_speed);
}
/// Applies upward linear velocity to the [Flipper], moving it to its highest
/// position.
void _moveUp() {
body.linearVelocity = Vector2(0, _speed);
}
List<FixtureDef> _createFixtureDefs() {
final fixtures = <FixtureDef>[];
final isLeft = side.isLeft;
final bigCircleShape = CircleShape()..radius = height / 2;
bigCircleShape.position.setValues(
isLeft
? -(width / 2) + bigCircleShape.radius
: (width / 2) - bigCircleShape.radius,
0,
);
final bigCircleFixtureDef = FixtureDef(bigCircleShape);
fixtures.add(bigCircleFixtureDef);
final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2;
smallCircleShape.position.setValues(
isLeft
? (width / 2) - smallCircleShape.radius
: -(width / 2) + smallCircleShape.radius,
0,
);
final smallCircleFixtureDef = FixtureDef(smallCircleShape);
fixtures.add(smallCircleFixtureDef);
final trapeziumVertices = isLeft
? [
Vector2(bigCircleShape.position.x, bigCircleShape.radius),
Vector2(smallCircleShape.position.x, smallCircleShape.radius),
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
Vector2(bigCircleShape.position.x, -bigCircleShape.radius),
]
: [
Vector2(smallCircleShape.position.x, smallCircleShape.radius),
Vector2(bigCircleShape.position.x, bigCircleShape.radius),
Vector2(bigCircleShape.position.x, -bigCircleShape.radius),
Vector2(smallCircleShape.position.x, -smallCircleShape.radius),
];
final trapezium = PolygonShape()..set(trapeziumVertices);
final trapeziumFixtureDef = FixtureDef(trapezium)
..density = 50.0 // TODO(alestiago): Use a proper density.
..friction = .1; // TODO(alestiago): Use a proper friction.
fixtures.add(trapeziumFixtureDef);
return fixtures;
}
@override
Body createBody() {
final bodyDef = BodyDef()
..gravityScale = 0
..type = BodyType.dynamic
..position = _position;
final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture);
return body;
}
// TODO(erickzanardo): Remove this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417
final Completer hasMounted = Completer<void>();
@override
void onMount() {
super.onMount();
hasMounted.complete();
}
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
// 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) {
_moveUp();
} else if (event is RawKeyUpEvent) {
_moveDown();
}
return true;
}
}
/// {@template flipper_anchor}
/// [Anchor] positioned at the end of a [Flipper].
///
/// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate}
class FlipperAnchor extends Anchor {
/// {@macro flipper_anchor}
FlipperAnchor({
required Flipper flipper,
}) : super(
position: Vector2(
flipper.side.isLeft
? flipper.body.position.x - Flipper.width / 2
: flipper.body.position.x + Flipper.width / 2,
flipper.body.position.y,
),
);
}
/// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [Anchor] to achieve an arc motion.
/// {@endtemplate}
class FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required Anchor anchor,
}) {
initialize(
flipper.body,
anchor.body,
anchor.body.position,
);
enableLimit = true;
final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2;
lowerAngle = upperAngle = angle;
}
/// The total angle of the arc motion.
static const _sweepingAngle = math.pi / 3.5;
/// Unlocks the [Flipper] from its resting position.
///
/// The [Flipper] is locked when initialized in order to force it to be at
/// its resting position.
// TODO(alestiago): consider refactor once the issue is solved:
// https://github.com/flame-engine/forge2d/issues/36
static void unlock(RevoluteJoint joint, BoardSide side) {
late final double upperLimit, lowerLimit;
switch (side) {
case BoardSide.left:
lowerLimit = -joint.lowerLimit;
upperLimit = joint.upperLimit;
break;
case BoardSide.right:
lowerLimit = joint.lowerLimit;
upperLimit = -joint.upperLimit;
}
joint.setLimits(lowerLimit, upperLimit);
}
}

@ -1,5 +1,5 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; 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

@ -1,15 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
class PinballGame extends Forge2DGame with FlameBloc { class PinballGame extends Forge2DGame
void spawnBall() { with FlameBloc, HasKeyboardHandlerComponents {
add(
Ball(position: ballStartingPosition),
);
}
// TODO(erickzanardo): Change to the plumber position // TODO(erickzanardo): Change to the plumber position
late final ballStartingPosition = screenToWorld( late final ballStartingPosition = screenToWorld(
Vector2( Vector2(
@ -19,17 +15,86 @@ class PinballGame extends Forge2DGame with FlameBloc {
) - ) -
Vector2(0, -20); Vector2(0, -20);
// TODO(alestiago): Change to the design position.
late final flippersPosition = ballStartingPosition - Vector2(0, 5);
@override
void onAttach() {
super.onAttach();
spawnBall();
}
void spawnBall() {
add(Ball(position: ballStartingPosition));
}
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
addContactCallback(BallScorePointsCallback()); addContactCallback(BallScorePointsCallback());
await add(BottomWall(this)); await add(BottomWall(this));
addContactCallback(BottomWallBallContactCallback()); addContactCallback(BottomWallBallContactCallback());
unawaited(_addFlippers());
} }
@override Future<void> _addFlippers() async {
void onAttach() { const spaceBetweenFlippers = 2;
super.onAttach(); final leftFlipper = Flipper.left(
spawnBall(); position: Vector2(
flippersPosition.x - (Flipper.width / 2) - (spaceBetweenFlippers / 2),
flippersPosition.y,
),
);
await add(leftFlipper);
final leftFlipperAnchor = FlipperAnchor(flipper: leftFlipper);
await add(leftFlipperAnchor);
final leftFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef(
flipper: leftFlipper,
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(
leftFlipper.hasMounted.future.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(
leftFlipperRevoluteJoint,
leftFlipper.side,
),
),
);
unawaited(
rightFlipper.hasMounted.future.whenComplete(
() => FlipperAnchorRevoluteJointDef.unlock(
rightFlipperRevoluteJoint,
rightFlipper.side,
),
),
);
} }
} }

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
/// {@template game_hud}
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
/// [GameState.balls].
/// {@endtemplate}
class GameHud extends StatelessWidget {
/// {@macro game_hud}
const GameHud({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.watch<GameBloc>().state;
return Container(
color: Colors.redAccent,
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${state.score}',
style: Theme.of(context).textTheme.headline3,
),
Wrap(
direction: Axis.vertical,
children: [
for (var i = 0; i < state.balls; i++)
const Padding(
padding: EdgeInsets.only(top: 6, right: 6),
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.black,
),
),
],
),
],
),
);
}
}

@ -56,7 +56,18 @@ class _PinballGameViewState extends State<PinballGameView> {
); );
} }
}, },
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game), child: GameWidget<PinballGame>(game: _game),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
),
],
),
); );
} }
} }

@ -1,2 +1,3 @@
export 'game_hud.dart';
export 'pinball_game_page.dart'; export 'pinball_game_page.dart';
export 'widgets/widgets.dart'; export 'widgets/widgets.dart';

@ -21,9 +21,9 @@ void main() {
} }
}, },
expect: () => [ expect: () => [
const GameState(score: 0, balls: 2), const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1), const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0), const GameState(score: 0, balls: 0, bonusLetters: []),
], ],
); );
}); });
@ -37,8 +37,8 @@ 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), const GameState(score: 2, balls: 3, bonusLetters: []),
const GameState(score: 5, balls: 3), const GameState(score: 5, balls: 3, bonusLetters: []),
], ],
); );
@ -53,9 +53,55 @@ void main() {
bloc.add(const Scored(points: 2)); bloc.add(const Scored(points: 2));
}, },
expect: () => [ expect: () => [
const GameState(score: 0, balls: 2), const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1), const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0), const GameState(score: 0, balls: 0, bonusLetters: []),
],
);
});
group('BonusLetterActivated', () {
blocTest<GameBloc, GameState>(
'adds the letter to the state',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('L'))
..add(const BonusLetterActivated('E')),
expect: () => [
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'],
),
], ],
); );
}); });

@ -40,5 +40,22 @@ void main() {
expect(() => Scored(points: 0), throwsAssertionError); expect(() => Scored(points: 0), throwsAssertionError);
}); });
}); });
group('BonusLetterActivated', () {
test('can be instantiated', () {
expect(const BonusLetterActivated('A'), isNotNull);
});
test('supports value equality', () {
expect(
BonusLetterActivated('A'),
equals(BonusLetterActivated('A')),
);
expect(
BonusLetterActivated('B'),
isNot(equals(BonusLetterActivated('A'))),
);
});
});
}); });
} }

@ -7,14 +7,27 @@ void main() {
group('GameState', () { group('GameState', () {
test('supports value equality', () { test('supports value equality', () {
expect( expect(
GameState(score: 0, balls: 0), GameState(
equals(const GameState(score: 0, balls: 0)), score: 0,
balls: 0,
bonusLetters: const [],
),
equals(
const GameState(
score: 0,
balls: 0,
bonusLetters: [],
),
),
); );
}); });
group('constructor', () { group('constructor', () {
test('can be instantiated', () { test('can be instantiated', () {
expect(const GameState(score: 0, balls: 0), isNotNull); expect(
const GameState(score: 0, balls: 0, bonusLetters: []),
isNotNull,
);
}); });
}); });
@ -23,7 +36,7 @@ void main() {
'when balls are negative', 'when balls are negative',
() { () {
expect( expect(
() => GameState(balls: -1, score: 0), () => GameState(balls: -1, score: 0, bonusLetters: const []),
throwsAssertionError, throwsAssertionError,
); );
}, },
@ -34,7 +47,7 @@ void main() {
'when score is negative', 'when score is negative',
() { () {
expect( expect(
() => GameState(balls: 0, score: -1), () => GameState(balls: 0, score: -1, bonusLetters: const []),
throwsAssertionError, throwsAssertionError,
); );
}, },
@ -47,6 +60,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 0, score: 0,
bonusLetters: [],
); );
expect(gameState.isGameOver, isTrue); expect(gameState.isGameOver, isTrue);
}); });
@ -57,6 +71,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
bonusLetters: [],
); );
expect(gameState.isGameOver, isFalse); expect(gameState.isGameOver, isFalse);
}); });
@ -70,6 +85,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 1, balls: 1,
score: 0, score: 0,
bonusLetters: [],
); );
expect(gameState.isLastBall, isTrue); expect(gameState.isLastBall, isTrue);
}, },
@ -82,6 +98,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 2, balls: 2,
score: 0, score: 0,
bonusLetters: [],
); );
expect(gameState.isLastBall, isFalse); expect(gameState.isLastBall, isFalse);
}, },
@ -96,6 +113,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
bonusLetters: [],
); );
expect( expect(
() => gameState.copyWith(score: gameState.score - 1), () => gameState.copyWith(score: gameState.score - 1),
@ -111,6 +129,7 @@ void main() {
const gameState = GameState( const gameState = GameState(
balls: 0, balls: 0,
score: 2, score: 2,
bonusLetters: [],
); );
expect( expect(
gameState.copyWith(), gameState.copyWith(),
@ -126,10 +145,12 @@ void main() {
const gameState = GameState( const gameState = GameState(
score: 2, score: 2,
balls: 0, balls: 0,
bonusLetters: [],
); );
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'],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -137,6 +158,7 @@ void main() {
gameState.copyWith( gameState.copyWith(
score: otherGameState.score, score: otherGameState.score,
balls: otherGameState.balls, balls: otherGameState.balls,
bonusLetters: otherGameState.bonusLetters,
), ),
equals(otherGameState), equals(otherGameState),
); );

@ -135,7 +135,11 @@ void main() {
whenListen( whenListen(
gameBloc, gameBloc,
const Stream<GameState>.empty(), const Stream<GameState>.empty(),
initialState: const GameState(score: 10, balls: 1), initialState: const GameState(
score: 10,
balls: 1,
bonusLetters: [],
),
); );
await game.ready(); await game.ready();

@ -0,0 +1,27 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group(
'BoardSide',
() {
test('has two values', () {
expect(BoardSide.values.length, equals(2));
});
},
);
group('BoardSideX', () {
test('isLeft is correct', () {
const side = BoardSide.left;
expect(side.isLeft, isTrue);
expect(side.isRight, isFalse);
});
test('isRight is correct', () {
const side = BoardSide.right;
expect(side.isLeft, isFalse);
expect(side.isRight, isTrue);
});
});
}

@ -0,0 +1,401 @@
// ignore_for_file: cascade_invocations
import 'dart:collection';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
group(
'Flipper',
() {
flameTester.test(
'loads correctly',
(game) async {
final leftFlipper = Flipper.left(position: Vector2.zero());
final rightFlipper = Flipper.right(position: Vector2.zero());
await game.ensureAddAll([leftFlipper, rightFlipper]);
expect(game.contains(leftFlipper), isTrue);
},
);
group('constructor', () {
test('sets BoardSide', () {
final leftFlipper = Flipper.left(position: Vector2.zero());
expect(leftFlipper.side, equals(leftFlipper.side));
final rightFlipper = Flipper.right(position: Vector2.zero());
expect(rightFlipper.side, equals(rightFlipper.side));
});
});
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final flipper = Flipper.left(position: position);
await game.ensureAdd(flipper);
game.contains(flipper);
expect(flipper.body.position, position);
},
);
flameTester.test(
'is dynamic',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
expect(flipper.body.bodyType, equals(BodyType.dynamic));
},
);
flameTester.test(
'ignores gravity',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
expect(flipper.body.gravityScale, isZero);
},
);
flameTester.test(
'has greater mass than Ball',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
final ball = Ball(position: Vector2.zero());
await game.ensureAddAll([flipper, ball]);
expect(
flipper.body.getMassData().mass,
greaterThan(ball.body.getMassData().mass),
);
},
);
});
group('fixtures', () {
flameTester.test(
'has three',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
expect(flipper.body.fixtures.length, equals(3));
},
);
flameTester.test(
'has density',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final fixtures = flipper.body.fixtures;
final density = fixtures.fold<double>(
0,
(sum, fixture) => sum + fixture.density,
);
expect(density, greaterThan(0));
},
);
});
group('onKeyEvent', () {
final leftKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
]);
final rightKeys = UnmodifiableListView([
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
]);
group('and Flipper is left', () {
late Flipper flipper;
setUp(() {
flipper = Flipper.left(position: Vector2.zero());
});
testRawKeyDownEvents(leftKeys, (event) {
flameTester.test(
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isPositive);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) {
flameTester.test(
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isNegative);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(rightKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyDownEvents(rightKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
});
group('and Flipper is right', () {
late Flipper flipper;
setUp(() {
flipper = Flipper.right(position: Vector2.zero());
});
testRawKeyDownEvents(rightKeys, (event) {
flameTester.test(
'moves upwards '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isPositive);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(rightKeys, (event) {
flameTester.test(
'moves downwards '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isNegative);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyUpEvents(leftKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is released',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
testRawKeyDownEvents(leftKeys, (event) {
flameTester.test(
'does nothing '
'when ${event.logicalKey.keyLabel} is pressed',
(game) async {
await game.ensureAdd(flipper);
flipper.onKeyEvent(event, {});
expect(flipper.body.linearVelocity.y, isZero);
expect(flipper.body.linearVelocity.x, isZero);
},
);
});
});
});
},
);
group(
'FlipperAnchor',
() {
flameTester.test(
'position is at the left of the left Flipper',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2));
},
);
flameTester.test(
'position is at the right of the right Flipper',
(game) async {
final flipper = Flipper.right(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(Flipper.width / 2));
},
);
},
);
group('FlipperAnchorRevoluteJointDef', () {
group('initializes with', () {
flameTester.test(
'limits enabled',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.enableLimit, isTrue);
},
);
group('equal upper and lower limits', () {
flameTester.test(
'when Flipper is left',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
},
);
flameTester.test(
'when Flipper is right',
(game) async {
final flipper = Flipper.right(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
expect(jointDef.lowerAngle, equals(jointDef.upperAngle));
},
);
});
});
group(
'unlocks',
() {
flameTester.test(
'when Flipper is left',
(game) async {
final flipper = Flipper.left(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
expect(
joint.upperLimit,
isNot(equals(joint.lowerLimit)),
);
},
);
flameTester.test(
'when Flipper is right',
(game) async {
final flipper = Flipper.right(position: Vector2.zero());
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
final jointDef = FlipperAnchorRevoluteJointDef(
flipper: flipper,
anchor: flipperAnchor,
);
final joint = game.world.createJoint(jointDef) as RevoluteJoint;
FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side);
expect(
joint.upperLimit,
isNot(equals(joint.lowerLimit)),
);
},
);
},
);
});
}

@ -1,9 +1,54 @@
// ignore_for_file: cascade_invocations
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() { void main() {
group('PinballGame', () { group('PinballGame', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
// 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(
'components',
() {
group('Flippers', () {
bool Function(Component) flipperSelector(BoardSide side) =>
(component) => component is Flipper && component.side == side;
flameTester.test(
'has only one left Flipper',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
flipperSelector(BoardSide.left),
),
returnsNormally,
);
},
);
flameTester.test(
'has only one right Flipper',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
flipperSelector(BoardSide.right),
),
returnsNormally,
);
},
);
});
},
);
}); });
} }

@ -0,0 +1,79 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('GameHud', () {
late GameBloc gameBloc;
const initialState = GameState(score: 10, balls: 2, bonusLetters: []);
void _mockState(GameState state) {
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
}
Future<void> _pumpHud(WidgetTester tester) async {
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
}
setUp(() {
gameBloc = MockGameBloc();
_mockState(initialState);
});
testWidgets(
'renders the current score',
(tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
},
);
testWidgets(
'renders the current ball number',
(tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
},
);
testWidgets('updates the score', (tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
_mockState(initialState.copyWith(score: 20));
await tester.pump();
expect(find.text('20'), findsOneWidget);
});
testWidgets('updates the ball number', (tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
_mockState(initialState.copyWith(balls: 1));
await tester.pump();
expect(
find.byType(CircleAvatar),
findsNWidgets(1),
);
});
});
}

@ -48,7 +48,7 @@ void main() {
}); });
group('PinballGameView', () { group('PinballGameView', () {
testWidgets('renders game', (tester) async { testWidgets('renders game and a hud', (tester) async {
final gameBloc = MockGameBloc(); final gameBloc = MockGameBloc();
whenListen( whenListen(
gameBloc, gameBloc,
@ -61,13 +61,17 @@ void main() {
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>), find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget, findsOneWidget,
); );
expect(
find.byType(GameHud),
findsOneWidget,
);
}); });
testWidgets( testWidgets(
'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); const state = GameState(score: 0, balls: 0, bonusLetters: []);
whenListen( whenListen(
gameBloc, gameBloc,
Stream.value(state), Stream.value(state),

@ -6,5 +6,6 @@
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
export 'builders.dart'; export 'builders.dart';
export 'key_testers.dart';
export 'mocks.dart'; export 'mocks.dart';
export 'pump_app.dart'; export 'pump_app.dart';

@ -0,0 +1,37 @@
import 'package:flutter/services.dart';
import 'package:meta/meta.dart';
import 'package:mocktail/mocktail.dart';
import 'helpers.dart';
@isTest
void testRawKeyUpEvents(
List<LogicalKeyboardKey> keys,
Function(RawKeyUpEvent) test,
) {
for (final key in keys) {
test(_mockKeyUpEvent(key));
}
}
RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) {
final event = MockRawKeyUpEvent();
when(() => event.logicalKey).thenReturn(key);
return event;
}
@isTest
void testRawKeyDownEvents(
List<LogicalKeyboardKey> keys,
Function(RawKeyDownEvent) test,
) {
for (final key in keys) {
test(_mockKeyDownEvent(key));
}
}
RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) {
final event = MockRawKeyDownEvent();
when(() => event.logicalKey).thenReturn(key);
return event;
}

@ -1,4 +1,6 @@
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -13,3 +15,17 @@ class MockBall extends Mock implements Ball {}
class MockContact extends Mock implements Contact {} class MockContact extends Mock implements Contact {}
class MockGameBloc extends Mock implements GameBloc {} class MockGameBloc extends Mock implements GameBloc {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}
class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return super.toString();
}
}

Loading…
Cancel
Save