fix: fixed conflicts on merge pinballgame screen with main

pull/40/head
RuiAlonso 4 years ago
commit 23a4b25df3

@ -0,0 +1,3 @@
# Every request must be reviewed and accepted by:
* @erickzanardo @alestiago @RuiMiguel @allisonryan0002

@ -0,0 +1,18 @@
name: geometry
on:
push:
paths:
- "packages/geometry/**"
- ".github/workflows/geometry.yaml"
pull_request:
paths:
- "packages/geometry/**"
- ".github/workflows/geometry.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
working_directory: packages/geometry

@ -15,4 +15,5 @@ jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/pinball_theme
working_directory: packages/pinball_theme
coverage_excludes: "lib/src/generated/*.dart"

@ -1,4 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml
linter:
rules:
public_member_api_docs: false

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

@ -5,6 +5,8 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:pinball/l10n/l10n.dart';

@ -5,6 +5,8 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:developer';

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

@ -1,3 +1,5 @@
// ignore_for_file: public_member_api_docs
part of 'game_bloc.dart';
@immutable
@ -5,16 +7,22 @@ abstract class GameEvent extends Equatable {
const GameEvent();
}
/// {@template ball_lost_game_event}
/// Event added when a user drops a ball off the screen.
/// {@endtemplate}
class BallLost extends GameEvent {
/// {@macro ball_lost_game_event}
const BallLost();
@override
List<Object?> get props => [];
}
/// {@template scored_game_event}
/// Event added when a user increases their score.
/// {@endtemplate}
class Scored extends GameEvent {
/// {@macro scored_game_event}
const Scored({
required this.points,
}) : assert(points > 0, 'Points must be greater than 0');
@ -26,10 +34,14 @@ class Scored 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
List<Object?> get props => [letter];
List<Object?> get props => [letterIndex];
}

@ -1,5 +1,14 @@
// ignore_for_file: public_member_api_docs
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}
/// Represents the state of the pinball game.
/// {@endtemplate}
@ -8,14 +17,16 @@ class GameState extends Equatable {
const GameState({
required this.score,
required this.balls,
required this.bonusLetters,
required this.activatedBonusLetters,
required this.bonusHistory,
}) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative");
const GameState.initial()
: score = 0,
balls = 3,
bonusLetters = const [];
activatedBonusLetters = const [],
bonusHistory = const [];
/// The current score of the game.
final int score;
@ -26,7 +37,11 @@ class GameState extends Equatable {
final int balls;
/// 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.
bool get isGameOver => balls == 0;
@ -37,7 +52,8 @@ class GameState extends Equatable {
GameState copyWith({
int? score,
int? balls,
List<String>? bonusLetters,
List<int>? activatedBonusLetters,
List<GameBonus>? bonusHistory,
}) {
assert(
score == null || score >= this.score,
@ -47,7 +63,9 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
bonusLetters: bonusLetters ?? this.bonusLetters,
activatedBonusLetters:
activatedBonusLetters ?? this.activatedBonusLetters,
bonusHistory: bonusHistory ?? this.bonusHistory,
);
}
@ -55,6 +73,7 @@ class GameState extends Equatable {
List<Object?> get props => [
score,
balls,
bonusLetters,
activatedBonusLetters,
bonusHistory,
];
}

@ -1,40 +1,46 @@
import 'package:flame/components.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
class Ball extends PositionBodyComponent<PinballGame, SpriteComponent>
with BlocComponent<GameBloc, GameState> {
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame].
/// {@endtemplate}
class Ball extends PositionBodyComponent<PinballGame, SpriteComponent> {
/// {@macro ball}
Ball({
required Vector2 position,
int? maskBits,
}) : _position = position,
_maskBits = maskBits ?? Filter().maskBits,
super(size: ballSize);
static final ballSize = Vector2.all(2);
super(size: Vector2.all(2));
/// The initial position of the [Ball] body.
final Vector2 _position;
final int _maskBits;
/// Asset location of the sprite that renders with the [Ball].
///
/// Sprite is preloaded by [PinballGameAssetsX].
static const spritePath = 'components/ball.png';
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(sprite: sprite, size: ballSize);
final tint = gameRef.theme.characterTheme.ballColor.withOpacity(0.5);
positionComponent = SpriteComponent(sprite: sprite, size: size)..tint(tint);
}
@override
Body createBody() {
final shape = CircleShape()..radius = ballSize.x / 2;
final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef()
..userData = this
..position = _position
..position = Vector2(_position.x, _position.y + size.y)
..type = BodyType.dynamic;
final body = world.createBody(bodyDef);
@ -43,13 +49,18 @@ class Ball extends PositionBodyComponent<PinballGame, SpriteComponent>
return body;
}
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() {
shouldRemove = true;
final bloc = gameRef.read<GameBloc>()..add(const BallLost());
final shouldBallRespawn = !bloc.state.isLastBall;
if (shouldBallRespawn) {
final shouldBallRespwan = !bloc.state.isLastBall && !bloc.state.isGameOver;
if (shouldBallRespwan) {
gameRef.spawnBall();
}
}

@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart' show SpriteComponent;
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';
@ -12,19 +12,15 @@ import 'package:pinball/game/game.dart';
///
/// [Flipper] can be controlled by the player in an arc motion.
/// {@endtemplate flipper}
class Flipper extends BodyComponent with KeyboardHandler {
class Flipper extends PositionBodyComponent 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;
}
_keys = keys,
super(size: Vector2(width, height));
/// A left positioned [Flipper].
Flipper.left({
@ -50,6 +46,11 @@ class Flipper extends BodyComponent with KeyboardHandler {
],
);
/// Asset location of the sprite that renders with the [Flipper].
///
/// Sprite is preloaded by [PinballGameAssetsX].
static const spritePath = 'components/flipper.png';
/// The width of the [Flipper].
static const width = 12.0;
@ -75,6 +76,20 @@ class Flipper extends BodyComponent with KeyboardHandler {
/// [onKeyEvent] method listens to when one of these keys is pressed.
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
/// resting position.
void _moveDown() {
@ -148,6 +163,7 @@ class Flipper extends BodyComponent with KeyboardHandler {
// 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
@ -202,7 +218,7 @@ class FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def}
FlipperAnchorRevoluteJointDef({
required Flipper flipper,
required Anchor anchor,
required FlipperAnchor anchor,
}) {
initialize(
flipper.body,

@ -1,23 +1,32 @@
import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart' show Anchor;
/// {@template plunger}
/// [Plunger] serves as a spring, that shoots the ball on the right side of the
/// playfield.
///
/// [Plunger] ignores gravity so the player controls its downward [pull].
/// [Plunger] ignores gravity so the player controls its downward [_pull].
/// {@endtemplate}
class Plunger extends BodyComponent {
class Plunger extends BodyComponent with KeyboardHandler {
/// {@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;
/// Distance the plunger can lower.
final double compressionDistance;
@override
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()
..userData = this
@ -29,18 +38,57 @@ class Plunger extends BodyComponent {
}
/// Set a constant downward velocity on the [Plunger].
void pull() {
body.linearVelocity = Vector2(0, -7);
void _pull() {
body.linearVelocity = Vector2(0, -3);
}
/// Set an upward velocity on the [Plunger].
///
/// The velocity's magnitude depends on how far the [Plunger] has been pulled
/// from its original [_position].
void release() {
void _release() {
final velocity = (_position.y - body.position.y) * 9;
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}
@ -54,11 +102,8 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
/// {@macro plunger_anchor_prismatic_joint_def}
PlungerAnchorPrismaticJointDef({
required Plunger plunger,
required Anchor anchor,
}) : assert(
anchor.body.position.y < plunger.body.position.y,
'Anchor must be below the Plunger',
) {
required PlungerAnchor anchor,
}) {
initialize(
plunger.body,
anchor.body,
@ -67,6 +112,9 @@ class PlungerAnchorPrismaticJointDef extends PrismaticJointDef {
);
enableLimit = true;
lowerTranslation = double.negativeInfinity;
enableMotor = true;
motorSpeed = 50;
maxMotorForce = motorSpeed;
collideConnected = true;
}
}

@ -22,9 +22,4 @@ class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
) {
ball.gameRef.read<GameBloc>().add(Scored(points: scorePoints.points));
}
// TODO(alestiago): remove once this issue is closed.
// https://github.com/flame-engine/flame/issues/1414
@override
void end(Ball _, ScorePoints __, Contact ___) {}
}

@ -4,15 +4,20 @@ import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/components/components.dart';
/// {@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}
// TODO(alestiago): Remove [Wall] for [Pathway.straight].
class Wall extends BodyComponent {
/// {@macro wall}
Wall({
required this.start,
required this.end,
});
/// The [start] of the [Wall].
final Vector2 start;
/// The [end] of the [Wall].
final Vector2 end;
@override
@ -20,7 +25,7 @@ class Wall extends BodyComponent {
final shape = EdgeShape()..set(start, end);
final fixtureDef = FixtureDef(shape)
..restitution = 0.0
..restitution = 0.1
..friction = 0.3;
final bodyDef = BodyDef()
@ -32,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}
/// [Wall] located at the bottom of the board.
///
@ -39,6 +58,7 @@ class Wall extends BodyComponent {
/// [BottomWallBallContactCallback].
/// {@endtemplate}
class BottomWall extends Wall {
/// {@macro bottom_wall}
BottomWall(Forge2DGame game)
: super(
start: game.screenToWorld(game.camera.viewport.effectiveSize),
@ -57,7 +77,4 @@ class BottomWallBallContactCallback extends ContactCallback<Ball, BottomWall> {
void begin(Ball ball, BottomWall wall, Contact contact) {
ball.lost();
}
@override
void end(_, __, ___) {}
}

@ -6,6 +6,7 @@ extension PinballGameAssetsX on PinballGame {
Future<void> preLoadAssets() async {
await Future.wait([
images.load(Ball.spritePath),
images.load(Flipper.spritePath),
]);
}
}

@ -1,23 +1,20 @@
// ignore_for_file: public_member_api_docs
import 'dart:async';
import 'dart:ui';
import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGame extends Forge2DGame
with FlameBloc, HasKeyboardHandlerComponents, TapDetector {
// TODO(erickzanardo): Change to the plumber position
late final ballStartingPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y - 20,
),
) -
Vector2(0, -20);
with FlameBloc, HasKeyboardHandlerComponents {
PinballGame({required this.theme});
// TODO(alestiago): Change to the design position.
late final flippersPosition = ballStartingPosition - Vector2(0, 5);
final PinballTheme theme;
late final Plunger plunger;
late final launcherRampPosition = screenToWorld(
Vector2(
@ -48,23 +45,57 @@ class PinballGame extends Forge2DGame
spawnBall();
}
@override
Future<void> onLoad() async {
_addContactCallbacks();
await _addGameBoundaries();
unawaited(_addFlippers());
unawaited(_addPlunger());
unawaited(_addPaths());
// 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.
await add(
Wall(
start: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x,
100,
),
),
end: screenToWorld(
Vector2(
camera.viewport.effectiveSize.x - 100,
0,
),
),
),
);
}
void spawnBall() {
add(Ball(position: ballStartingPosition));
add(Ball(position: plunger.body.position));
}
@override
Future<void> onLoad() async {
void _addContactCallbacks() {
addContactCallback(BallScorePointsCallback());
await add(BottomWall(this));
addContactCallback(BottomWallBallContactCallback());
}
unawaited(_addFlippers());
unawaited(_addPaths());
Future<void> _addGameBoundaries() async {
await add(BottomWall(this));
createBoundaries(this).forEach(add);
}
Future<void> _addFlippers() async {
final flippersPosition = screenToWorld(
Vector2(
camera.viewport.effectiveSize.x / 2,
camera.viewport.effectiveSize.y / 1.1,
),
);
const spaceBetweenFlippers = 2;
final leftFlipper = Flipper.left(
position: Vector2(
@ -148,14 +179,38 @@ class PinballGame extends Forge2DGame
);
}
@override
void onTapDown(TapDownInfo info) {
super.onTapDown(info);
final tapPosition = info.eventPosition.game;
add(
Ball(
position: tapPosition,
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,
) -
Vector2(160, 0),
),
compressionDistance: compressionDistance,
),
);
await add(plungerAnchor = PlungerAnchor(plunger: plunger));
world.createJoint(
PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: plungerAnchor,
),
);
}
}
class DebugPinballGame extends PinballGame with TapDetector {
DebugPinballGame({required PinballTheme theme}) : super(theme: theme);
@override
void onTapUp(TapUpInfo info) {
add(Ball(position: info.eventPosition.game));
}
}

@ -1,17 +1,23 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/game.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget {
const PinballGamePage({Key? key}) : super(key: key);
const PinballGamePage({Key? key, required this.theme}) : super(key: key);
final PinballTheme theme;
static Route route() {
static Route route({required PinballTheme theme}) {
return MaterialPageRoute<void>(
builder: (_) {
return BlocProvider(
create: (_) => GameBloc(),
child: const PinballGamePage(),
child: PinballGamePage(theme: theme),
);
},
);
@ -19,12 +25,20 @@ class PinballGamePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const PinballGameView();
return PinballGameView(theme: theme);
}
}
class PinballGameView extends StatefulWidget {
const PinballGameView({Key? key}) : super(key: key);
const PinballGameView({
Key? key,
required this.theme,
bool isDebugMode = kDebugMode,
}) : _isDebugMode = isDebugMode,
super(key: key);
final PinballTheme theme;
final bool _isDebugMode;
@override
State<PinballGameView> createState() => _PinballGameViewState();
@ -40,7 +54,10 @@ class _PinballGameViewState extends State<PinballGameView> {
// TODO(erickzanardo): Revisit this when we start to have more assets
// this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets.
_game = PinballGame()..preLoadAssets();
_game = (widget._isDebugMode
? DebugPinballGame(theme: widget.theme)
: PinballGame(theme: widget.theme))
..preLoadAssets();
}
@override

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

@ -1,6 +1,11 @@
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
/// {@template game_over_dialog}
/// [Dialog] displayed when the [PinballGame] is over.
/// {@endtemplate}
class GameOverDialog extends StatelessWidget {
/// {@macro game_over_dialog}
const GameOverDialog({Key? key}) : super(key: key);
@override

@ -1 +1,2 @@
export 'game_hud.dart';
export 'game_over_dialog.dart';

@ -1,4 +1,15 @@
{
"@@locale": "en",
"play": "Play"
"play": "Play",
"@play": {
"description": "Text displayed on the landing page play button"
},
"start": "Start",
"@start": {
"description": "Text displayed on the character selection page start button"
},
"characterSelectionTitle": "Choose your character!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character selection page"
}
}

@ -1,4 +1,15 @@
{
"@@locale": "es",
"play": "Jugar"
"play": "Jugar",
"@play": {
"description": "Text displayed on the landing page play button"
},
"start": "Comienzo",
"@start": {
"description": "Text displayed on the character selection page start button"
},
"characterSelectionTitle": "¡Elige a tu personaje!",
"@characterSelectionTitle": {
"description": "Title text displayed on the character selection page"
}
}

@ -5,6 +5,8 @@
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// ignore_for_file: public_member_api_docs
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

@ -1,6 +1,8 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
class LandingPage extends StatelessWidget {
const LandingPage({Key? key}) : super(key: key);
@ -8,11 +10,12 @@ class LandingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: Center(
child: TextButton(
onPressed: () =>
Navigator.of(context).push<void>(PinballGamePage.route()),
Navigator.of(context).push<void>(CharacterSelectionPage.route()),
child: Text(l10n.play),
),
),

@ -1,3 +1,6 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:pinball_theme/pinball_theme.dart';

@ -1,3 +1,6 @@
// ignore_for_file: public_member_api_docs
// TODO(allisonryan0002): Document this section when the API is stable.
part of 'theme_cubit.dart';
class ThemeState extends Equatable {

@ -1 +1,2 @@
export 'cubit/theme_cubit.dart';
export 'view/view.dart';

@ -0,0 +1,130 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
class CharacterSelectionPage extends StatelessWidget {
const CharacterSelectionPage({Key? key}) : super(key: key);
static Route route() {
return MaterialPageRoute<void>(
builder: (_) => const CharacterSelectionPage(),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ThemeCubit(),
child: const CharacterSelectionView(),
);
}
}
class CharacterSelectionView extends StatelessWidget {
const CharacterSelectionView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Text(
l10n.characterSelectionTitle,
style: Theme.of(context).textTheme.headline3,
),
const SizedBox(height: 80),
const _CharacterSelectionGridView(),
const SizedBox(height: 20),
TextButton(
onPressed: () => Navigator.of(context).push<void>(
PinballGamePage.route(
theme: context.read<ThemeCubit>().state.theme,
),
),
child: Text(l10n.start),
),
],
),
),
);
}
}
class _CharacterSelectionGridView extends StatelessWidget {
const _CharacterSelectionGridView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
mainAxisSpacing: 20,
crossAxisSpacing: 20,
children: const [
CharacterImageButton(
DashTheme(),
key: Key('characterSelectionPage_dashButton'),
),
CharacterImageButton(
SparkyTheme(),
key: Key('characterSelectionPage_sparkyButton'),
),
CharacterImageButton(
AndroidTheme(),
key: Key('characterSelectionPage_androidButton'),
),
CharacterImageButton(
DinoTheme(),
key: Key('characterSelectionPage_dinoButton'),
),
],
),
);
}
}
// TODO(allisonryan0002): remove visibility when adding final UI.
@visibleForTesting
class CharacterImageButton extends StatelessWidget {
const CharacterImageButton(
this.characterTheme, {
Key? key,
}) : super(key: key);
final CharacterTheme characterTheme;
@override
Widget build(BuildContext context) {
final currentCharacterTheme = context.select<ThemeCubit, CharacterTheme>(
(cubit) => cubit.state.theme.characterTheme,
);
return GestureDetector(
onTap: () => context.read<ThemeCubit>().characterSelected(characterTheme),
child: DecoratedBox(
decoration: BoxDecoration(
color: (currentCharacterTheme == characterTheme)
? Colors.blue.withOpacity(0.5)
: null,
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: characterTheme.characterAsset.image(),
),
),
);
}
}

@ -0,0 +1 @@
export 'character_selection_page.dart';

@ -1,5 +1,6 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
import 'package:vector_math/vector_math_64.dart';
/// Calculates all [Vector2]s of a circumference.
///

@ -7,13 +7,9 @@ environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flame: ^1.0.0
flutter:
sdk: flutter
vector_math: ^2.1.1
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.2.0
test: ^1.19.2
very_good_analysis: ^2.4.0

@ -1,7 +1,8 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'package:flame/extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:geometry/geometry.dart';
import 'package:test/test.dart';
import 'package:vector_math/vector_math_64.dart';
class Binomial {
Binomial({required this.n, required this.k});
@ -42,18 +43,18 @@ void main() {
],
step: 2,
),
throwsAssertionError,
throwsA(isA<AssertionError>()),
);
});
test('fails if not enough control points', () {
expect(
() => calculateBezierCurve(controlPoints: [Vector2.zero()]),
throwsAssertionError,
throwsA(isA<AssertionError>()),
);
expect(
() => calculateBezierCurve(controlPoints: []),
throwsAssertionError,
throwsA(isA<AssertionError>()),
);
});
@ -81,15 +82,24 @@ void main() {
group('binomial', () {
test('fails if k is negative', () {
expect(() => binomial(1, -1), throwsAssertionError);
expect(
() => binomial(1, -1),
throwsA(isA<AssertionError>()),
);
});
test('fails if n is negative', () {
expect(() => binomial(-1, 1), throwsAssertionError);
expect(
() => binomial(-1, 1),
throwsA(isA<AssertionError>()),
);
});
test('fails if n < k', () {
expect(() => binomial(1, 2), throwsAssertionError);
expect(
() => binomial(1, 2),
throwsA(isA<AssertionError>()),
);
});
test('for a specific input gives a correct value', () {
@ -131,7 +141,7 @@ void main() {
group('factorial', () {
test('fails if negative number', () {
expect(() => factorial(-1), throwsAssertionError);
expect(() => factorial(-1), throwsA(isA<AssertionError>()));
});
test('for a specific input gives a correct value', () {

@ -1 +1,4 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml
include: package:very_good_analysis/analysis_options.2.4.0.yaml
analyzer:
exclude:
- lib/**/*.gen.dart

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

@ -1,4 +1,5 @@
library pinball_theme;
export 'src/generated/generated.dart';
export 'src/pinball_theme.dart';
export 'src/themes/themes.dart';

@ -0,0 +1,71 @@
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
import 'package:flutter/widgets.dart';
class $AssetsImagesGen {
const $AssetsImagesGen();
AssetGenImage get android => const AssetGenImage('assets/images/android.png');
AssetGenImage get dash => const AssetGenImage('assets/images/dash.png');
AssetGenImage get dino => const AssetGenImage('assets/images/dino.png');
AssetGenImage get sparky => const AssetGenImage('assets/images/sparky.png');
}
class Assets {
Assets._();
static const $AssetsImagesGen images = $AssetsImagesGen();
}
class AssetGenImage extends AssetImage {
const AssetGenImage(String assetName)
: super(assetName, package: 'pinball_theme');
Image image({
Key? key,
ImageFrameBuilder? frameBuilder,
ImageLoadingBuilder? loadingBuilder,
ImageErrorWidgetBuilder? errorBuilder,
String? semanticLabel,
bool excludeFromSemantics = false,
double? width,
double? height,
Color? color,
BlendMode? colorBlendMode,
BoxFit? fit,
AlignmentGeometry alignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect? centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
bool isAntiAlias = false,
FilterQuality filterQuality = FilterQuality.low,
}) {
return Image(
key: key,
image: this,
frameBuilder: frameBuilder,
loadingBuilder: loadingBuilder,
errorBuilder: errorBuilder,
semanticLabel: semanticLabel,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
color: color,
colorBlendMode: colorBlendMode,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
matchTextDirection: matchTextDirection,
gaplessPlayback: gaplessPlayback,
isAntiAlias: isAntiAlias,
filterQuality: filterQuality,
);
}
String get path => assetName;
}

@ -10,4 +10,7 @@ class AndroidTheme extends CharacterTheme {
@override
Color get ballColor => Colors.green;
@override
AssetGenImage get characterAsset => Assets.images.android;
}

@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template character_theme}
/// Base class for creating character themes.
@ -14,6 +15,9 @@ abstract class CharacterTheme extends Equatable {
/// Ball color for this theme.
Color get ballColor;
/// Asset for the theme character.
AssetGenImage get characterAsset;
@override
List<Object?> get props => [ballColor];
}

@ -10,4 +10,7 @@ class DashTheme extends CharacterTheme {
@override
Color get ballColor => Colors.blue;
@override
AssetGenImage get characterAsset => Assets.images.dash;
}

@ -10,4 +10,7 @@ class DinoTheme extends CharacterTheme {
@override
Color get ballColor => Colors.grey;
@override
AssetGenImage get characterAsset => Assets.images.dino;
}

@ -10,4 +10,7 @@ class SparkyTheme extends CharacterTheme {
@override
Color get ballColor => Colors.orange;
@override
AssetGenImage get characterAsset => Assets.images.sparky;
}

@ -14,4 +14,16 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
very_good_analysis: ^2.4.0
very_good_analysis: ^2.4.0
flutter:
uses-material-design: true
generate: true
assets:
- assets/images/
flutter_gen:
assets:
package_parameter_enabled: true
output: lib/src/generated/
line_length: 80

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(AndroidTheme().ballColor, equals(Colors.green));
});
test('characterAsset is correct', () {
expect(AndroidTheme().characterAsset, equals(Assets.images.android));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(DashTheme().ballColor, equals(Colors.blue));
});
test('characterAsset is correct', () {
expect(DashTheme().characterAsset, equals(Assets.images.dash));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(DinoTheme().ballColor, equals(Colors.grey));
});
test('characterAsset is correct', () {
expect(DinoTheme().characterAsset, equals(Assets.images.dino));
});
});
}

@ -17,5 +17,9 @@ void main() {
test('ballColor is correct', () {
expect(SparkyTheme().ballColor, equals(Colors.orange));
});
test('characterAsset is correct', () {
expect(SparkyTheme().characterAsset, equals(Assets.images.sparky));
});
});
}

@ -21,9 +21,24 @@ void main() {
}
},
expect: () => [
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
const GameState(
score: 0,
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: 3)),
expect: () => [
const GameState(score: 2, balls: 3, bonusLetters: []),
const GameState(score: 5, balls: 3, bonusLetters: []),
const GameState(
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));
},
expect: () => [
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
const GameState(
score: 0,
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',
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(
..add(const BonusLetterActivated(0))
..add(const BonusLetterActivated(1))
..add(const BonusLetterActivated(2)),
expect: () => const [
GameState(
score: 0,
balls: 3,
bonusLetters: ['G'],
activatedBonusLetters: [0],
bonusHistory: [],
),
const GameState(
GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O'],
activatedBonusLetters: [0, 1],
bonusHistory: [],
),
const GameState(
GameState(
score: 0,
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,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G'],
activatedBonusLetters: [0],
bonusHistory: [],
),
const GameState(
GameState(
score: 0,
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,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'],
activatedBonusLetters: [],
bonusHistory: [GameBonus.word],
),
],
);

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

@ -5,16 +5,19 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Anchor', () {
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {
final anchor = Anchor(position: Vector2.zero());
await game.ready();
await game.ensureAdd(anchor);
expect(game.contains(anchor), isTrue);
@ -25,6 +28,7 @@ void main() {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final anchor = Anchor(position: position);
await game.ensureAdd(anchor);
@ -37,6 +41,7 @@ void main() {
flameTester.test(
'is static',
(game) async {
await game.ready();
final anchor = Anchor(position: Vector2.zero());
await game.ensureAdd(anchor);

@ -13,7 +13,7 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Ball', () {
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
@ -34,7 +34,11 @@ void main() {
await game.ensureAdd(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(
'exists',
(game) async {
@ -96,10 +100,9 @@ void main() {
});
group('resetting a ball', () {
late GameBloc gameBloc;
final gameBloc = MockGameBloc();
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
@ -107,11 +110,7 @@ void main() {
);
});
final tester = flameBlocTester(
gameBlocBuilder: () {
return gameBloc;
},
);
final tester = flameBlocTester(gameBloc: gameBloc);
tester.widgetTest(
'adds BallLost to GameBloc',
@ -149,7 +148,8 @@ void main() {
initialState: const GameState(
score: 10,
balls: 1,
bonusLetters: [],
activatedBonusLetters: [],
bonusHistory: [],
),
);
await game.ready();

@ -5,6 +5,8 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
class FakeRampArea extends RampArea {
FakeRampArea({
required Vector2 position,
@ -56,7 +58,7 @@ void main() {
group('RampArea', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',

@ -12,7 +12,7 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
group(
'Flipper',
() {
@ -255,36 +255,33 @@ void main() {
},
);
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);
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);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2));
},
);
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);
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);
final flipperAnchor = FlipperAnchor(flipper: flipper);
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('initializes with', () {

@ -12,7 +12,7 @@ class MockJetpackRampArea extends Mock implements JetpackRampArea {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
group('JetpackRamp', () {
group('body', () {

@ -6,9 +6,11 @@ import 'package:flutter/material.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);
final flameTester = FlameTester(PinballGameTest.create);
group('Pathway', () {
const width = 50.0;
@ -18,6 +20,7 @@ void main() {
flameTester.test(
'has transparent color by default when no color is specified',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
@ -38,6 +41,7 @@ void main() {
flameTester.test(
'has a color when is specified',
(game) async {
await game.ready();
const defaultColor = Colors.blue;
final pathway = Pathway.straight(
@ -59,6 +63,7 @@ void main() {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
@ -75,6 +80,7 @@ void main() {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.straight(
position: position,
@ -92,6 +98,7 @@ void main() {
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
@ -109,6 +116,7 @@ void main() {
flameTester.test(
'has only one ChainShape when singleWall is true',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
@ -128,6 +136,7 @@ void main() {
flameTester.test(
'has two ChainShape when singleWall is false (default)',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
@ -196,6 +205,7 @@ void main() {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
@ -212,6 +222,7 @@ void main() {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.arc(
position: position,
@ -229,6 +240,7 @@ void main() {
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
@ -254,6 +266,7 @@ void main() {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,
@ -269,6 +282,7 @@ void main() {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.bezierCurve(
position: position,
@ -285,6 +299,7 @@ void main() {
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,

@ -1,8 +1,11 @@
// ignore_for_file: cascade_invocations
import 'dart:collection';
import 'package:bloc_test/bloc_test.dart';
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';
@ -10,13 +13,19 @@ import '../../helpers/helpers.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
group('Plunger', () {
const compressionDistance = 0.0;
flameTester.test(
'loads correctly',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ready();
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(game.contains(plunger), isTrue);
@ -28,7 +37,10 @@ void main() {
'positions correctly',
(game) async {
final position = Vector2.all(10);
final plunger = Plunger(position: position);
final plunger = Plunger(
position: position,
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
game.contains(plunger);
@ -39,7 +51,10 @@ void main() {
flameTester.test(
'is dynamic',
(game) async {
final plunger = Plunger(position: Vector2.zero());
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.bodyType, equals(BodyType.dynamic));
@ -49,7 +64,10 @@ void main() {
flameTester.test(
'ignores gravity',
(game) async {
final plunger = Plunger(position: Vector2.zero());
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.gravityScale, isZero);
@ -57,11 +75,14 @@ void main() {
);
});
group('first fixture', () {
group('fixture', () {
flameTester.test(
'exists',
(game) async {
final plunger = Plunger(position: Vector2.zero());
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
expect(plunger.body.fixtures[0], isA<Fixture>());
@ -71,121 +92,150 @@ void main() {
flameTester.test(
'shape is a polygon',
(game) async {
final plunger = Plunger(position: Vector2.zero());
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
final fixture = plunger.body.fixtures[0];
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(
'does not set a linear velocity '
'when plunger is in starting position',
'has density',
(game) async {
final plunger = Plunger(position: Vector2.zero());
final plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
await game.ensureAdd(plunger);
plunger.release();
expect(plunger.body.linearVelocity.y, isZero);
expect(plunger.body.linearVelocity.x, isZero);
final fixture = plunger.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
});
flameTester.test(
'sets a positive linear velocity '
'when plunger is below starting position',
(game) async {
final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger);
group('onKeyEvent', () {
final keys = UnmodifiableListView([
LogicalKeyboardKey.space,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.keyS,
]);
plunger.body.setTransform(Vector2(0, -1), 0);
plunger.release();
late Plunger plunger;
expect(plunger.body.linearVelocity.y, isPositive);
expect(plunger.body.linearVelocity.x, isZero);
},
);
setUp(() {
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('PlungerAnchor', () {
const compressionDistance = 10.0;
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', () {
late GameBloc gameBloc;
const compressionDistance = 10.0;
final gameBloc = MockGameBloc();
late Plunger plunger;
late Anchor anchor;
setUp(() {
gameBloc = MockGameBloc();
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState.initial(),
);
plunger = Plunger(position: Vector2.zero());
anchor = Anchor(position: Vector2(0, -1));
plunger = Plunger(
position: Vector2.zero(),
compressionDistance: compressionDistance,
);
});
final flameTester = flameBlocTester(
gameBlocBuilder: () {
return 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,
);
},
);
final flameTester = flameBlocTester(gameBloc: gameBloc);
group('initializes with', () {
flameTester.test(
'plunger body as bodyA',
(game) async {
await game.ensureAddAll([plunger, anchor]);
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
@ -199,7 +249,9 @@ void main() {
flameTester.test(
'anchor body as bodyB',
(game) async {
await game.ensureAddAll([plunger, anchor]);
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
@ -214,7 +266,9 @@ void main() {
flameTester.test(
'limits enabled',
(game) async {
await game.ensureAddAll([plunger, anchor]);
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
@ -229,7 +283,9 @@ void main() {
flameTester.test(
'lower translation limit as negative infinity',
(game) async {
await game.ensureAddAll([plunger, anchor]);
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
@ -244,7 +300,9 @@ void main() {
flameTester.test(
'connected body collison enabled',
(game) async {
await game.ensureAddAll([plunger, anchor]);
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
@ -257,46 +315,51 @@ void main() {
);
});
flameTester.widgetTest(
'plunger cannot go below anchor',
(game, tester) async {
await game.ensureAddAll([plunger, anchor]);
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
flameTester.widgetTest(
'plunger cannot go below 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.
anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1));
// Giving anchor a shape for the plunger to collide with.
anchor.body.createFixtureFromShape(PolygonShape()..setAsBoxXY(2, 1));
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
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(
'plunger cannot excessively exceed starting position',
(game, tester) async {
await game.ensureAddAll([plunger, anchor]);
testRawKeyUpEvents([LogicalKeyboardKey.space], (event) {
flameTester.widgetTest(
'plunger cannot excessively exceed starting position',
(game, tester) async {
await game.ensureAdd(plunger);
final anchor = PlungerAnchor(plunger: plunger);
await game.ensureAdd(anchor);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
final jointDef = PlungerAnchorPrismaticJointDef(
plunger: plunger,
anchor: anchor,
);
game.world.createJoint(jointDef);
plunger.pull();
await tester.pump(const Duration(seconds: 1));
plunger.body.setTransform(Vector2(0, -1), 0);
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);
},
);
});
});
}

@ -12,7 +12,7 @@ class MockSparkyRampArea extends Mock implements SparkyRampArea {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
group('SparkyRamp', () {
group('body', () {

@ -32,11 +32,12 @@ void main() {
},
);
});
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final wall = Wall(
start: Vector2.zero(),
end: Vector2(100, 0),
@ -76,7 +77,7 @@ void main() {
);
});
group('first fixture', () {
group('fixture', () {
flameTester.test(
'exists',
(game) async {
@ -91,7 +92,7 @@ void main() {
);
flameTester.test(
'has restitution equals 0',
'has restitution',
(game) async {
final wall = Wall(
start: Vector2.zero(),
@ -100,7 +101,7 @@ void main() {
await game.ensureAdd(wall);
final fixture = wall.body.fixtures[0];
expect(fixture.restitution, equals(0));
expect(fixture.restitution, greaterThan(0));
},
);

@ -3,84 +3,148 @@
import 'package:flame/components.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
import '../helpers/helpers.dart';
void main() {
group('PinballGame', () {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
final flameTester = FlameTester(PinballGameTest.create);
final debugModeFlameTester = FlameTester(DebugPinballGameTest.create);
// TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved:
// 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,
);
},
);
group('components', () {
group('Walls', () {
flameTester.test(
'has three Walls',
(game) async {
await game.ready();
final walls = game.children
.where(
(component) => component is Wall && component is! BottomWall,
)
.toList();
// TODO(allisonryan0002): expect 3 when launch track is added and
// temporary wall is removed.
expect(walls.length, 4);
},
);
flameTester.test(
'has only one BottomWall',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is BottomWall,
),
returnsNormally,
);
},
);
});
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,
);
},
);
});
flameTester.test(
'has only one right Flipper',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
flipperSelector(BoardSide.right),
),
returnsNormally,
);
},
flameTester.test(
'Plunger has only one Plunger',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
(component) => component is Plunger,
),
returnsNormally,
);
});
group('Paths', () {
bool Function(Component) rampSelector<T>() =>
(component) => component is T;
flameTester.test(
'has only one JetpackRamp',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
rampSelector<JetpackRamp>(),
),
returnsNormally,
);
},
},
);
});
group('Paths', () {
bool Function(Component) rampSelector<T>() =>
(component) => component is T;
flameTester.test(
'has only one JetpackRamp',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
rampSelector<JetpackRamp>(),
),
returnsNormally,
);
},
);
flameTester.test(
'has only one SparkyRamp',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
rampSelector<SparkyRamp>(),
),
returnsNormally,
);
},
flameTester.test(
'has only one SparkyRamp',
(game) async {
await game.ready();
expect(
() => game.children.singleWhere(
rampSelector<SparkyRamp>(),
),
returnsNormally,
);
});
},
);
},
);
});
debugModeFlameTester.test('adds a ball on tap up', (game) async {
await game.ready();
final eventPosition = MockEventPosition();
when(() => eventPosition.game).thenReturn(Vector2.all(10));
final tapUpEvent = MockTapUpInfo();
when(() => tapUpEvent.eventPosition).thenReturn(eventPosition);
game.onTapUp(tapUpEvent);
await game.ready();
expect(
game.children.whereType<Ball>().length,
equals(1),
);
});
});
}

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

@ -1,12 +1,17 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
const theme = PinballTheme(characterTheme: DashTheme());
group('PinballGamePage', () {
testWidgets('renders PinballGameView', (tester) async {
final gameBloc = MockGameBloc();
@ -16,7 +21,10 @@ void main() {
initialState: const GameState.initial(),
);
await tester.pumpApp(const PinballGamePage(), gameBloc: gameBloc);
await tester.pumpApp(
PinballGamePage(theme: theme),
gameBloc: gameBloc,
);
expect(find.byType(PinballGameView), findsOneWidget);
});
@ -27,7 +35,8 @@ void main() {
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context).push<void>(PinballGamePage.route());
Navigator.of(context)
.push<void>(PinballGamePage.route(theme: theme));
},
child: const Text('Tap me'),
);
@ -56,7 +65,11 @@ void main() {
initialState: const GameState.initial(),
);
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
await tester.pumpApp(
PinballGameView(theme: theme),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
@ -71,14 +84,23 @@ void main() {
'renders a game over dialog when the user has lost',
(tester) async {
final gameBloc = MockGameBloc();
const state = GameState(score: 0, balls: 0, bonusLetters: []);
const state = GameState(
score: 0,
balls: 0,
activatedBonusLetters: [],
bonusHistory: [],
);
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
await tester.pumpApp(const PinballGameView(), gameBloc: gameBloc);
await tester.pumpApp(
const PinballGameView(theme: theme),
gameBloc: gameBloc,
);
await tester.pump();
expect(
@ -87,5 +109,45 @@ void main() {
);
},
);
testWidgets('renders the real game when not in debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme, isDebugMode: false),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('renders the debug game when on debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
});
}

@ -2,15 +2,17 @@ import 'package:flame_test/flame_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
import 'helpers.dart';
FlameTester<PinballGame> flameBlocTester({
required GameBloc Function() gameBlocBuilder,
required GameBloc gameBloc,
}) {
return FlameTester<PinballGame>(
PinballGame.new,
PinballGameTest.create,
pumpWidget: (gameWidget, tester) async {
await tester.pumpWidget(
BlocProvider.value(
value: gameBlocBuilder(),
value: gameBloc,
child: gameWidget,
),
);

@ -0,0 +1,22 @@
import 'package:pinball/game/game.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// [PinballGame] extension to reduce boilerplate in tests.
extension PinballGameTest on PinballGame {
/// Create [PinballGame] with default [PinballTheme].
static PinballGame create() => PinballGame(
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
}
/// [DebugPinballGame] extension to reduce boilerplate in tests.
extension DebugPinballGameTest on DebugPinballGame {
/// Create [PinballGame] with default [PinballTheme].
static DebugPinballGame create() => DebugPinballGame(
theme: const PinballTheme(
characterTheme: DashTheme(),
),
);
}

@ -1,11 +1,11 @@
// Copyright (c) 2021, Very Good Ventures
// https://verygood.ventures
//
// Copyright (c) 2021, Very Good Ventures
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// https://verygood.ventures
// license that can be found in the LICENSE file or at
export 'builders.dart';
export 'extensions.dart';
export 'key_testers.dart';
export 'mocks.dart';
export 'pump_app.dart';

@ -1,8 +1,10 @@
import 'package:flame/input.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:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart';
class MockPinballGame extends Mock implements PinballGame {}
@ -16,6 +18,8 @@ class MockContact extends Mock implements Contact {}
class MockGameBloc extends Mock implements GameBloc {}
class MockThemeCubit extends Mock implements ThemeCubit {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
@ -29,3 +33,7 @@ class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent {
return super.toString();
}
}
class MockTapUpInfo extends Mock implements TapUpInfo {}
class MockEventPosition extends Mock implements EventPosition {}

@ -12,6 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockingjay/mockingjay.dart';
import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart';
import 'package:pinball/theme/theme.dart';
import 'helpers.dart';
@ -20,17 +21,25 @@ extension PumpApp on WidgetTester {
Widget widget, {
MockNavigator? navigator,
GameBloc? gameBloc,
ThemeCubit? themeCubit,
}) {
return pumpWidget(
MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
MultiBlocProvider(
providers: [
BlocProvider.value(
value: themeCubit ?? MockThemeCubit(),
),
BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
),
],
supportedLocales: AppLocalizations.supportedLocales,
home: BlocProvider.value(
value: gameBloc ?? MockGameBloc(),
child: navigator != null
child: MaterialApp(
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: navigator != null
? MockNavigatorProvider(navigator: navigator, child: widget)
: widget,
),

@ -12,7 +12,7 @@ void main() {
expect(find.byType(TextButton), findsOneWidget);
});
testWidgets('tapping on TextButton navigates to PinballGamePage',
testWidgets('tapping on TextButton navigates to CharacterSelectionPage',
(tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});

@ -0,0 +1,110 @@
// 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:mockingjay/mockingjay.dart';
import 'package:pinball/theme/theme.dart';
import 'package:pinball_theme/pinball_theme.dart';
import '../../helpers/helpers.dart';
void main() {
late ThemeCubit themeCubit;
setUp(() {
themeCubit = MockThemeCubit();
whenListen(
themeCubit,
const Stream<ThemeState>.empty(),
initialState: const ThemeState.initial(),
);
});
group('CharacterSelectionPage', () {
testWidgets('renders CharacterSelectionView', (tester) async {
await tester.pumpApp(
CharacterSelectionPage(),
themeCubit: themeCubit,
);
expect(find.byType(CharacterSelectionView), findsOneWidget);
});
testWidgets('route returns a valid navigation route', (tester) async {
await tester.pumpApp(
Scaffold(
body: Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
Navigator.of(context)
.push<void>(CharacterSelectionPage.route());
},
child: Text('Tap me'),
);
},
),
),
themeCubit: themeCubit,
);
await tester.tap(find.text('Tap me'));
await tester.pumpAndSettle();
expect(find.byType(CharacterSelectionPage), findsOneWidget);
});
});
group('CharacterSelectionView', () {
testWidgets('renders correctly', (tester) async {
const titleText = 'Choose your character!';
await tester.pumpApp(
CharacterSelectionView(),
themeCubit: themeCubit,
);
expect(find.text(titleText), findsOneWidget);
expect(find.byType(CharacterImageButton), findsNWidgets(4));
expect(find.byType(TextButton), findsOneWidget);
});
testWidgets('calls characterSelected when a character image is tapped',
(tester) async {
const sparkyButtonKey = Key('characterSelectionPage_sparkyButton');
await tester.pumpApp(
CharacterSelectionView(),
themeCubit: themeCubit,
);
await tester.tap(find.byKey(sparkyButtonKey));
verify(() => themeCubit.characterSelected(SparkyTheme())).called(1);
});
testWidgets('navigates to PinballGamePage when start is tapped',
(tester) async {
final navigator = MockNavigator();
when(() => navigator.push<void>(any())).thenAnswer((_) async {});
await tester.pumpApp(
CharacterSelectionView(),
themeCubit: themeCubit,
navigator: navigator,
);
await tester.ensureVisible(find.byType(TextButton));
await tester.tap(find.byType(TextButton));
verify(() => navigator.push<void>(any())).called(1);
});
});
testWidgets('CharacterImageButton renders correctly', (tester) async {
await tester.pumpApp(
CharacterImageButton(DashTheme()),
themeCubit: themeCubit,
);
expect(find.byType(Image), findsOneWidget);
});
}
Loading…
Cancel
Save