diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 31aa0498..74685215 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -14,6 +14,8 @@ class GameBloc extends Bloc { on(_onBonusLetterActivated); } + static const bonusWord = 'GOOGLE'; + void _onBallLost(BallLost event, Emitter emit) { if (state.balls > 0) { emit(state.copyWith(balls: state.balls - 1)); @@ -27,13 +29,25 @@ class GameBloc extends Bloc { } 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), + ); + } } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index fa57cbff..0edc91ab 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -34,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 get props => [letter]; + List get props => [letterIndex]; } diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index f8456518..2812a049 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -2,6 +2,13 @@ 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} @@ -10,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; @@ -28,7 +37,11 @@ class GameState extends Equatable { final int balls; /// Active bonus letters. - final List bonusLetters; + final List activatedBonusLetters; + + /// Holds the history of all the [GameBonus]es earned by the player during a + /// PinballGame. + final List bonusHistory; /// Determines when the game is over. bool get isGameOver => balls == 0; @@ -39,7 +52,8 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, - List? bonusLetters, + List? activatedBonusLetters, + List? bonusHistory, }) { assert( score == null || score >= this.score, @@ -49,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, ); } @@ -57,6 +73,7 @@ class GameState extends Equatable { List get props => [ score, balls, - bonusLetters, + activatedBonusLetters, + bonusHistory, ]; } diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index d1721927..d5b05fa1 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -37,7 +37,7 @@ class Ball extends PositionBodyComponent { final bodyDef = BodyDef() ..userData = this - ..position = _position + ..position = Vector2(_position.x, _position.y + size.y) ..type = BodyType.dynamic; return world.createBody(bodyDef)..createFixture(fixtureDef); diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 5d9a84df..87248b64 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -1,12 +1,45 @@ import 'dart:async'; import 'dart:math' as math; -import 'package:flame/components.dart' show SpriteComponent; +import 'package:flame/components.dart' show PositionComponent, SpriteComponent; import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; +/// {@template flipper_group} +/// Loads a [Flipper.right] and a [Flipper.left]. +/// {@endtemplate} +class FlipperGroup extends PositionComponent { + /// {@macro flipper_group} + FlipperGroup({ + required Vector2 position, + required this.spacing, + }) : super(position: position); + + /// The amount of space between the [Flipper.right] and [Flipper.left]. + final double spacing; + + @override + Future onLoad() async { + final leftFlipper = Flipper.left( + position: Vector2( + position.x - (Flipper.width / 2) - (spacing / 2), + position.y, + ), + ); + await add(leftFlipper); + + final rightFlipper = Flipper.right( + position: Vector2( + position.x + (Flipper.width / 2) + (spacing / 2), + position.y, + ), + ); + await add(rightFlipper); + } +} + /// {@template flipper} /// A bat, typically found in pairs at the bottom of the board. /// @@ -76,20 +109,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler { /// [onKeyEvent] method listens to when one of these keys is pressed. final List _keys; - @override - Future 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() { @@ -102,15 +121,50 @@ class Flipper extends PositionBodyComponent with KeyboardHandler { body.linearVelocity = Vector2(0, _speed); } + /// Loads the sprite that renders with the [Flipper]. + Future _loadSprite() async { + final sprite = await gameRef.loadSprite(spritePath); + positionComponent = SpriteComponent( + sprite: sprite, + size: size, + ); + + if (side.isRight) { + positionComponent!.flipHorizontally(); + } + } + + /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. + Future _anchorToJoint() async { + final anchor = FlipperAnchor(flipper: this); + await add(anchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: this, + anchor: anchor, + ); + // TODO(alestiago): Remove casting once the following is closed: + // https://github.com/flame-engine/forge2d/issues/36 + final joint = world.createJoint(jointDef) as RevoluteJoint; + + // FIXME(erickzanardo): when mounted the initial position is not fully + // reached. + unawaited( + mounted.whenComplete( + () => FlipperAnchorRevoluteJointDef.unlock(joint, side), + ), + ); + } + List _createFixtureDefs() { final fixtures = []; final isLeft = side.isLeft; - final bigCircleShape = CircleShape()..radius = height / 2; + final bigCircleShape = CircleShape()..radius = size.y / 2; bigCircleShape.position.setValues( isLeft - ? -(width / 2) + bigCircleShape.radius - : (width / 2) - bigCircleShape.radius, + ? -(size.x / 2) + bigCircleShape.radius + : (size.x / 2) - bigCircleShape.radius, 0, ); fixtures.add(FixtureDef(bigCircleShape)); @@ -118,8 +172,8 @@ class Flipper extends PositionBodyComponent with KeyboardHandler { final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; smallCircleShape.position.setValues( isLeft - ? (width / 2) - smallCircleShape.radius - : -(width / 2) + smallCircleShape.radius, + ? (size.x / 2) - smallCircleShape.radius + : -(size.x / 2) + smallCircleShape.radius, 0, ); fixtures.add(FixtureDef(smallCircleShape)); @@ -146,6 +200,15 @@ class Flipper extends PositionBodyComponent with KeyboardHandler { return fixtures; } + @override + Future onLoad() async { + await super.onLoad(); + await Future.wait([ + _loadSprite(), + _anchorToJoint(), + ]); + } + @override Body createBody() { final bodyDef = BodyDef() @@ -159,17 +222,6 @@ class Flipper extends PositionBodyComponent with KeyboardHandler { return body; } - // TODO(erickzanardo): Remove this once the issue is solved: - // https://github.com/flame-engine/flame/issues/1417 - // ignore: public_member_api_docs - final Completer hasMounted = Completer(); - - @override - void onMount() { - super.onMount(); - hasMounted.complete(); - } - @override bool onKeyEvent( RawKeyEvent event, @@ -202,8 +254,8 @@ class FlipperAnchor extends Anchor { }) : super( position: Vector2( flipper.side.isLeft - ? flipper.body.position.x - Flipper.width / 2 - : flipper.body.position.x + Flipper.width / 2, + ? flipper.body.position.x - flipper.size.x / 2 + : flipper.body.position.x + flipper.size.x / 2, flipper.body.position.y, ), ); @@ -216,7 +268,7 @@ class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { /// {@macro flipper_anchor_revolute_joint_def} FlipperAnchorRevoluteJointDef({ required Flipper flipper, - required Anchor anchor, + required FlipperAnchor anchor, }) { initialize( flipper.body, diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index 364fc35e..2ec2f599 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -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 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; } } diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index c307aad4..c433365c 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -4,7 +4,7 @@ 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 { @@ -25,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() @@ -37,6 +37,20 @@ class Wall extends BodyComponent { } } +/// Create top, left, and right [Wall]s for the game board. +List 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. /// diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7d6fba41..fbb68e63 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -13,17 +13,7 @@ class PinballGame extends Forge2DGame final PinballTheme theme; - // 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); - - // TODO(alestiago): Change to the design position. - late final flippersPosition = ballStartingPosition - Vector2(0, 5); + late final Plunger plunger; @override void onAttach() { @@ -31,76 +21,85 @@ class PinballGame extends Forge2DGame spawnBall(); } - void spawnBall() { - add(Ball(position: ballStartingPosition)); - } - @override Future onLoad() async { - addContactCallback(BallScorePointsCallback()); + _addContactCallbacks(); - await add(BottomWall(this)); - addContactCallback(BottomWallBallContactCallback()); + await _addGameBoundaries(); + unawaited(_addPlunger()); - unawaited(_addFlippers()); - } - - Future _addFlippers() async { - const spaceBetweenFlippers = 2; - final leftFlipper = Flipper.left( - position: Vector2( - flippersPosition.x - (Flipper.width / 2) - (spaceBetweenFlippers / 2), - flippersPosition.y, + // 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, + ), + ), ), ); - 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, + + final flippersPosition = screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 2, + camera.viewport.effectiveSize.y / 1.1, ), ); - 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, + add( + FlipperGroup( + position: flippersPosition, + spacing: 2, ), ), ); - unawaited( - rightFlipper.hasMounted.future.whenComplete( - () => FlipperAnchorRevoluteJointDef.unlock( - rightFlipperRevoluteJoint, - rightFlipper.side, + } + + void spawnBall() { + add(Ball(position: plunger.body.position)); + } + + void _addContactCallbacks() { + addContactCallback(BallScorePointsCallback()); + addContactCallback(BottomWallBallContactCallback()); + } + + Future _addGameBoundaries() async { + await add(BottomWall(this)); + createBoundaries(this).forEach(add); + } + + Future _addPlunger() async { + late PlungerAnchor plungerAnchor; + final compressionDistance = camera.viewport.effectiveSize.y / 12; + + await add( + plunger = Plunger( + position: screenToWorld( + Vector2( + camera.viewport.effectiveSize.x / 1.035, + camera.viewport.effectiveSize.y - compressionDistance, + ), ), + compressionDistance: compressionDistance, + ), + ); + await add(plungerAnchor = PlungerAnchor(plunger: plunger)); + + world.createJoint( + PlungerAnchorPrismaticJointDef( + plunger: plunger, + anchor: plungerAnchor, ), ); } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index bd669397..ad1d6c55 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -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( + '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], ), ], ); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index 0e7a0f71..d6d2278b 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -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, + ); + }, + ); }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 7345d3bd..7b060984 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -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), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index b2d11cef..9f473e42 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -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 { @@ -133,7 +137,8 @@ void main() { initialState: const GameState( score: 10, balls: 1, - bonusLetters: [], + activatedBonusLetters: [], + bonusHistory: [], ), ); await game.ready(); diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 941090ea..070265bc 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -2,6 +2,7 @@ import 'dart:collection'; +import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/services.dart'; @@ -13,6 +14,105 @@ import '../../helpers/helpers.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGameTest.create); + + group('FlipperGroup', () { + flameTester.test( + 'loads correctly', + (game) async { + final flipperGroup = FlipperGroup( + position: Vector2.zero(), + spacing: 0, + ); + await game.ensureAdd(flipperGroup); + + expect(game.contains(flipperGroup), isTrue); + }, + ); + + group('constructor', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final flipperGroup = FlipperGroup( + position: position, + spacing: 0, + ); + await game.ensureAdd(flipperGroup); + + expect(flipperGroup.position, equals(position)); + }, + ); + }); + + group('children', () { + bool Function(Component) flipperSelector(BoardSide side) => + (component) => component is Flipper && component.side == side; + + flameTester.test( + 'has only one left Flipper', + (game) async { + final flipperGroup = FlipperGroup( + position: Vector2.zero(), + spacing: 0, + ); + await game.ensureAdd(flipperGroup); + + expect( + () => flipperGroup.children.singleWhere( + flipperSelector(BoardSide.left), + ), + returnsNormally, + ); + }, + ); + + flameTester.test( + 'has only one right Flipper', + (game) async { + final flipperGroup = FlipperGroup( + position: Vector2.zero(), + spacing: 0, + ); + await game.ensureAdd(flipperGroup); + + expect( + () => flipperGroup.children.singleWhere( + flipperSelector(BoardSide.right), + ), + returnsNormally, + ); + }, + ); + + flameTester.test( + 'spaced correctly', + (game) async { + final flipperGroup = FlipperGroup( + position: Vector2.zero(), + spacing: 2, + ); + await game.ready(); + await game.ensureAdd(flipperGroup); + + final leftFlipper = flipperGroup.children.singleWhere( + flipperSelector(BoardSide.left), + ) as Flipper; + final rightFlipper = flipperGroup.children.singleWhere( + flipperSelector(BoardSide.right), + ) as Flipper; + + expect( + leftFlipper.body.position.x + + leftFlipper.size.x + + flipperGroup.spacing, + equals(rightFlipper.body.position.x), + ); + }, + ); + }); + }); + group( 'Flipper', () { @@ -21,9 +121,11 @@ void main() { (game) async { final leftFlipper = Flipper.left(position: Vector2.zero()); final rightFlipper = Flipper.right(position: Vector2.zero()); + await game.ready(); await game.ensureAddAll([leftFlipper, rightFlipper]); expect(game.contains(leftFlipper), isTrue); + expect(game.contains(rightFlipper), isTrue); }, ); @@ -255,36 +357,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', () { diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 68d02160..02330b31 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -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'; @@ -13,11 +16,16 @@ void main() { final flameTester = FlameTester(PinballGameTest.create); group('Plunger', () { + const compressionDistance = 0.0; + flameTester.test( 'loads correctly', (game) async { await game.ready(); - final plunger = Plunger(position: Vector2.zero()); + final plunger = Plunger( + position: Vector2.zero(), + compressionDistance: compressionDistance, + ); await game.ensureAdd(plunger); expect(game.contains(plunger), isTrue); @@ -29,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); @@ -40,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)); @@ -50,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); @@ -58,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()); @@ -72,65 +92,128 @@ 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('PlungerAnchorPrismaticJointDef', () { - late Plunger plunger; - late Anchor anchor; + 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', () { + const compressionDistance = 10.0; final gameBloc = MockGameBloc(); + late Plunger plunger; setUp(() { whenListen( @@ -138,51 +221,21 @@ void main() { const Stream.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(gameBloc: gameBloc); - flameTester.test( - 'throws AssertionError ' - 'when anchor is above plunger', - (game) async { - final anchor = Anchor(position: Vector2(0, 1)); - await game.ensureAddAll([plunger, anchor]); - - expect( - () => PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ), - throwsAssertionError, - ); - }, - ); - - flameTester.test( - 'throws AssertionError ' - 'when anchor is in same position as plunger', - (game) async { - final anchor = Anchor(position: Vector2.zero()); - await game.ensureAddAll([plunger, anchor]); - - expect( - () => PlungerAnchorPrismaticJointDef( - plunger: plunger, - anchor: anchor, - ), - throwsAssertionError, - ); - }, - ); - group('initializes with', () { 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, @@ -196,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, @@ -211,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, @@ -226,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, @@ -241,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, @@ -254,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); + }, + ); + }); }); } diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 6d792ea4..e60046ad 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -77,7 +77,7 @@ void main() { ); }); - group('first fixture', () { + group('fixture', () { flameTester.test( 'exists', (game) async { @@ -92,7 +92,7 @@ void main() { ); flameTester.test( - 'has restitution equals 0', + 'has restitution', (game) async { final wall = Wall( start: Vector2.zero(), @@ -101,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)); }, ); diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index f1d6bb32..faa55d11 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -17,60 +17,81 @@ void main() { // 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', () { + bool Function(Component) componentSelector() => + (component) => component is T; + + 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( + componentSelector(), + ), + 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 Plunger', + (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + (component) => component is Plunger, + ), + returnsNormally, ); + }, + ); + + flameTester.test('has only one FlipperGroup', (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + (component) => component is FlipperGroup, + ), + returnsNormally, + ); + }); + }); - debugModeFlameTester.test('adds a ball on tap up', (game) async { - await game.ready(); + debugModeFlameTester.test('adds a ball on tap up', (game) async { + await game.ready(); - final eventPosition = MockEventPosition(); - when(() => eventPosition.game).thenReturn(Vector2.all(10)); + final eventPosition = MockEventPosition(); + when(() => eventPosition.game).thenReturn(Vector2.all(10)); - final tapUpEvent = MockTapUpInfo(); - when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); + final tapUpEvent = MockTapUpInfo(); + when(() => tapUpEvent.eventPosition).thenReturn(eventPosition); - game.onTapUp(tapUpEvent); - await game.ready(); + game.onTapUp(tapUpEvent); + await game.ready(); - expect( - game.children.whereType().length, - equals(1), - ); - }); - }); - }, - ); + expect( + game.children.whereType().length, + equals(1), + ); + }); }); } diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index e7334e41..536edbad 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -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( diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index 0e3ee963..9de36cde 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -84,7 +84,13 @@ 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),