diff --git a/lib/game/components/board_side.dart b/lib/game/components/board_side.dart new file mode 100644 index 00000000..611f70b8 --- /dev/null +++ b/lib/game/components/board_side.dart @@ -0,0 +1,22 @@ +import 'package:pinball/game/game.dart'; + +/// Indicates a side of the board. +/// +/// Usually used to position or mirror elements of a [PinballGame]; such as a +/// [Flipper]. +enum BoardSide { + /// The left side of the board. + left, + + /// The right side of the board. + right, +} + +/// Utility methods for [BoardSide]. +extension BoardSideX on BoardSide { + /// Whether this side is [BoardSide.left]. + bool get isLeft => this == BoardSide.left; + + /// Whether this side is [BoardSide.right]. + bool get isRight => this == BoardSide.right; +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 95134ec2..bd5f5437 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,5 +1,7 @@ export 'anchor.dart'; export 'ball.dart'; +export 'board_side.dart'; +export 'flipper.dart'; export 'plunger.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart new file mode 100644 index 00000000..bd071b93 --- /dev/null +++ b/lib/game/components/flipper.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pinball/game/game.dart'; + +/// {@template flipper} +/// A bat, typically found in pairs at the bottom of the board. +/// +/// [Flipper] can be controlled by the player in an arc motion. +/// {@endtemplate flipper} +class Flipper extends BodyComponent with KeyboardHandler { + /// {@macro flipper} + Flipper._({ + required Vector2 position, + required this.side, + required List keys, + }) : _position = position, + _keys = keys { + // TODO(alestiago): Use sprite instead of color when provided. + paint = Paint() + ..color = const Color(0xFF00FF00) + ..style = PaintingStyle.fill; + } + + /// A left positioned [Flipper]. + Flipper.left({ + required Vector2 position, + }) : this._( + position: position, + side: BoardSide.left, + keys: [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ], + ); + + /// A right positioned [Flipper]. + Flipper.right({ + required Vector2 position, + }) : this._( + position: position, + side: BoardSide.right, + keys: [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ], + ); + + /// The width of the [Flipper]. + static const width = 12.0; + + /// The height of the [Flipper]. + static const height = 2.8; + + /// The speed required to move the [Flipper] to its highest position. + /// + /// The higher the value, the faster the [Flipper] will move. + static const double _speed = 60; + + /// Whether the [Flipper] is on the left or right side of the board. + /// + /// A [Flipper] with [BoardSide.left] has a counter-clockwise arc motion, + /// whereas a [Flipper] with [BoardSide.right] has a clockwise arc motion. + final BoardSide side; + + /// The initial position of the [Flipper] body. + final Vector2 _position; + + /// The [LogicalKeyboardKey]s that will control the [Flipper]. + /// + /// [onKeyEvent] method listens to when one of these keys is pressed. + final List _keys; + + /// Applies downward linear velocity to the [Flipper], moving it to its + /// resting position. + void _moveDown() { + body.linearVelocity = Vector2(0, -_speed); + } + + /// Applies upward linear velocity to the [Flipper], moving it to its highest + /// position. + void _moveUp() { + body.linearVelocity = Vector2(0, _speed); + } + + List _createFixtureDefs() { + final fixtures = []; + final isLeft = side.isLeft; + + final bigCircleShape = CircleShape()..radius = height / 2; + bigCircleShape.position.setValues( + isLeft + ? -(width / 2) + bigCircleShape.radius + : (width / 2) - bigCircleShape.radius, + 0, + ); + final bigCircleFixtureDef = FixtureDef(bigCircleShape); + fixtures.add(bigCircleFixtureDef); + + final smallCircleShape = CircleShape()..radius = bigCircleShape.radius / 2; + smallCircleShape.position.setValues( + isLeft + ? (width / 2) - smallCircleShape.radius + : -(width / 2) + smallCircleShape.radius, + 0, + ); + final smallCircleFixtureDef = FixtureDef(smallCircleShape); + fixtures.add(smallCircleFixtureDef); + + final trapeziumVertices = isLeft + ? [ + Vector2(bigCircleShape.position.x, bigCircleShape.radius), + Vector2(smallCircleShape.position.x, smallCircleShape.radius), + Vector2(smallCircleShape.position.x, -smallCircleShape.radius), + Vector2(bigCircleShape.position.x, -bigCircleShape.radius), + ] + : [ + Vector2(smallCircleShape.position.x, smallCircleShape.radius), + Vector2(bigCircleShape.position.x, bigCircleShape.radius), + Vector2(bigCircleShape.position.x, -bigCircleShape.radius), + Vector2(smallCircleShape.position.x, -smallCircleShape.radius), + ]; + final trapezium = PolygonShape()..set(trapeziumVertices); + final trapeziumFixtureDef = FixtureDef(trapezium) + ..density = 50.0 // TODO(alestiago): Use a proper density. + ..friction = .1; // TODO(alestiago): Use a proper friction. + fixtures.add(trapeziumFixtureDef); + + return fixtures; + } + + @override + Body createBody() { + final bodyDef = BodyDef() + ..gravityScale = 0 + ..type = BodyType.dynamic + ..position = _position; + + final body = world.createBody(bodyDef); + _createFixtureDefs().forEach(body.createFixture); + + return body; + } + + // TODO(erickzanardo): Remove this once the issue is solved: + // https://github.com/flame-engine/flame/issues/1417 + final Completer hasMounted = Completer(); + + @override + void onMount() { + super.onMount(); + hasMounted.complete(); + } + + @override + bool onKeyEvent( + RawKeyEvent event, + Set keysPressed, + ) { + // TODO(alestiago): Check why false cancels the event for other components. + // Investigate why return is of type [bool] expected instead of a type + // [KeyEventResult]. + if (!_keys.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent) { + _moveUp(); + } else if (event is RawKeyUpEvent) { + _moveDown(); + } + + return true; + } +} + +/// {@template flipper_anchor} +/// [Anchor] positioned at the end of a [Flipper]. +/// +/// The end of a [Flipper] depends on its [Flipper.side]. +/// {@endtemplate} +class FlipperAnchor extends Anchor { + /// {@macro flipper_anchor} + FlipperAnchor({ + required Flipper flipper, + }) : super( + position: Vector2( + flipper.side.isLeft + ? flipper.body.position.x - Flipper.width / 2 + : flipper.body.position.x + Flipper.width / 2, + flipper.body.position.y, + ), + ); +} + +/// {@template flipper_anchor_revolute_joint_def} +/// Hinges one end of [Flipper] to a [Anchor] to achieve an arc motion. +/// {@endtemplate} +class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { + /// {@macro flipper_anchor_revolute_joint_def} + FlipperAnchorRevoluteJointDef({ + required Flipper flipper, + required Anchor anchor, + }) { + initialize( + flipper.body, + anchor.body, + anchor.body.position, + ); + enableLimit = true; + + final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2; + lowerAngle = upperAngle = angle; + } + + /// The total angle of the arc motion. + static const _sweepingAngle = math.pi / 3.5; + + /// Unlocks the [Flipper] from its resting position. + /// + /// The [Flipper] is locked when initialized in order to force it to be at + /// its resting position. + // TODO(alestiago): consider refactor once the issue is solved: + // https://github.com/flame-engine/forge2d/issues/36 + static void unlock(RevoluteJoint joint, BoardSide side) { + late final double upperLimit, lowerLimit; + switch (side) { + case BoardSide.left: + lowerLimit = -joint.lowerLimit; + upperLimit = joint.upperLimit; + break; + case BoardSide.right: + lowerLimit = joint.lowerLimit; + upperLimit = -joint.upperLimit; + } + + joint.setLimits(lowerLimit, upperLimit); + } +} diff --git a/lib/game/components/plunger.dart b/lib/game/components/plunger.dart index ed1ef36f..364fc35e 100644 --- a/lib/game/components/plunger.dart +++ b/lib/game/components/plunger.dart @@ -1,5 +1,5 @@ import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:pinball/game/game.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 diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7c701c09..308d8faf 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,16 +1,12 @@ import 'dart:async'; +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'; -class PinballGame extends Forge2DGame with FlameBloc { - void spawnBall() { - add( - Ball(position: ballStartingPosition), - ); - } - +class PinballGame extends Forge2DGame + with FlameBloc, HasKeyboardHandlerComponents { // TODO(erickzanardo): Change to the plumber position late final ballStartingPosition = screenToWorld( Vector2( @@ -20,17 +16,86 @@ class PinballGame extends Forge2DGame with FlameBloc { ) - Vector2(0, -20); + // TODO(alestiago): Change to the design position. + late final flippersPosition = ballStartingPosition - Vector2(0, 5); + + @override + void onAttach() { + super.onAttach(); + spawnBall(); + } + + void spawnBall() { + add(Ball(position: ballStartingPosition)); + } + @override Future onLoad() async { addContactCallback(BallScorePointsCallback()); await add(BottomWall(this)); addContactCallback(BottomWallBallContactCallback()); + + unawaited(_addFlippers()); } - @override - void onAttach() { - super.onAttach(); - spawnBall(); + Future _addFlippers() async { + const spaceBetweenFlippers = 2; + final leftFlipper = Flipper.left( + position: Vector2( + flippersPosition.x - (Flipper.width / 2) - (spaceBetweenFlippers / 2), + flippersPosition.y, + ), + ); + await add(leftFlipper); + final leftFlipperAnchor = FlipperAnchor(flipper: leftFlipper); + await add(leftFlipperAnchor); + final leftFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef( + flipper: leftFlipper, + anchor: leftFlipperAnchor, + ); + // TODO(alestiago): Remove casting once the following is closed: + // https://github.com/flame-engine/forge2d/issues/36 + final leftFlipperRevoluteJoint = + world.createJoint(leftFlipperRevoluteJointDef) as RevoluteJoint; + + final rightFlipper = Flipper.right( + position: Vector2( + flippersPosition.x + (Flipper.width / 2) + (spaceBetweenFlippers / 2), + flippersPosition.y, + ), + ); + await add(rightFlipper); + final rightFlipperAnchor = FlipperAnchor(flipper: rightFlipper); + await add(rightFlipperAnchor); + final rightFlipperRevoluteJointDef = FlipperAnchorRevoluteJointDef( + flipper: rightFlipper, + anchor: rightFlipperAnchor, + ); + // TODO(alestiago): Remove casting once the following is closed: + // https://github.com/flame-engine/forge2d/issues/36 + final rightFlipperRevoluteJoint = + world.createJoint(rightFlipperRevoluteJointDef) as RevoluteJoint; + + // TODO(erickzanardo): Clean this once the issue is solved: + // https://github.com/flame-engine/flame/issues/1417 + // FIXME(erickzanardo): when mounted the initial position is not fully + // reached. + unawaited( + leftFlipper.hasMounted.future.whenComplete( + () => FlipperAnchorRevoluteJointDef.unlock( + leftFlipperRevoluteJoint, + leftFlipper.side, + ), + ), + ); + unawaited( + rightFlipper.hasMounted.future.whenComplete( + () => FlipperAnchorRevoluteJointDef.unlock( + rightFlipperRevoluteJoint, + rightFlipper.side, + ), + ), + ); } } diff --git a/test/game/components/board_side_test.dart b/test/game/components/board_side_test.dart new file mode 100644 index 00000000..3d6d3fa1 --- /dev/null +++ b/test/game/components/board_side_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + group( + 'BoardSide', + () { + test('has two values', () { + expect(BoardSide.values.length, equals(2)); + }); + }, + ); + + group('BoardSideX', () { + test('isLeft is correct', () { + const side = BoardSide.left; + expect(side.isLeft, isTrue); + expect(side.isRight, isFalse); + }); + + test('isRight is correct', () { + const side = BoardSide.right; + expect(side.isLeft, isFalse); + expect(side.isRight, isTrue); + }); + }); +} diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart new file mode 100644 index 00000000..b9894d9a --- /dev/null +++ b/test/game/components/flipper_test.dart @@ -0,0 +1,401 @@ +// ignore_for_file: cascade_invocations + +import 'dart:collection'; + +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGame.new); + group( + 'Flipper', + () { + flameTester.test( + 'loads correctly', + (game) async { + final leftFlipper = Flipper.left(position: Vector2.zero()); + final rightFlipper = Flipper.right(position: Vector2.zero()); + await game.ensureAddAll([leftFlipper, rightFlipper]); + + expect(game.contains(leftFlipper), isTrue); + }, + ); + + group('constructor', () { + test('sets BoardSide', () { + final leftFlipper = Flipper.left(position: Vector2.zero()); + expect(leftFlipper.side, equals(leftFlipper.side)); + + final rightFlipper = Flipper.right(position: Vector2.zero()); + expect(rightFlipper.side, equals(rightFlipper.side)); + }); + }); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final flipper = Flipper.left(position: position); + await game.ensureAdd(flipper); + game.contains(flipper); + + expect(flipper.body.position, position); + }, + ); + + flameTester.test( + 'is dynamic', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + expect(flipper.body.bodyType, equals(BodyType.dynamic)); + }, + ); + + flameTester.test( + 'ignores gravity', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + expect(flipper.body.gravityScale, isZero); + }, + ); + + flameTester.test( + 'has greater mass than Ball', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + final ball = Ball(position: Vector2.zero()); + + await game.ensureAddAll([flipper, ball]); + + expect( + flipper.body.getMassData().mass, + greaterThan(ball.body.getMassData().mass), + ); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'has three', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + expect(flipper.body.fixtures.length, equals(3)); + }, + ); + + flameTester.test( + 'has density', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final fixtures = flipper.body.fixtures; + final density = fixtures.fold( + 0, + (sum, fixture) => sum + fixture.density, + ); + + expect(density, greaterThan(0)); + }, + ); + }); + + group('onKeyEvent', () { + final leftKeys = UnmodifiableListView([ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, + ]); + final rightKeys = UnmodifiableListView([ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, + ]); + + group('and Flipper is left', () { + late Flipper flipper; + + setUp(() { + flipper = Flipper.left(position: Vector2.zero()); + }); + + testRawKeyDownEvents(leftKeys, (event) { + flameTester.test( + 'moves upwards ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(leftKeys, (event) { + flameTester.test( + 'moves downwards ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isNegative); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(rightKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyDownEvents(rightKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + + group('and Flipper is right', () { + late Flipper flipper; + + setUp(() { + flipper = Flipper.right(position: Vector2.zero()); + }); + + testRawKeyDownEvents(rightKeys, (event) { + flameTester.test( + 'moves upwards ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isPositive); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(rightKeys, (event) { + flameTester.test( + 'moves downwards ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isNegative); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyUpEvents(leftKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is released', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + + testRawKeyDownEvents(leftKeys, (event) { + flameTester.test( + 'does nothing ' + 'when ${event.logicalKey.keyLabel} is pressed', + (game) async { + await game.ensureAdd(flipper); + flipper.onKeyEvent(event, {}); + + expect(flipper.body.linearVelocity.y, isZero); + expect(flipper.body.linearVelocity.x, isZero); + }, + ); + }); + }); + }); + }, + ); + + group( + 'FlipperAnchor', + () { + flameTester.test( + 'position is at the left of the left Flipper', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + expect(flipperAnchor.body.position.x, equals(-Flipper.width / 2)); + }, + ); + + flameTester.test( + 'position is at the right of the right Flipper', + (game) async { + final flipper = Flipper.right(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + expect(flipperAnchor.body.position.x, equals(Flipper.width / 2)); + }, + ); + }, + ); + + group('FlipperAnchorRevoluteJointDef', () { + group('initializes with', () { + flameTester.test( + 'limits enabled', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: flipper, + anchor: flipperAnchor, + ); + + expect(jointDef.enableLimit, isTrue); + }, + ); + + group('equal upper and lower limits', () { + flameTester.test( + 'when Flipper is left', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: flipper, + anchor: flipperAnchor, + ); + + expect(jointDef.lowerAngle, equals(jointDef.upperAngle)); + }, + ); + + flameTester.test( + 'when Flipper is right', + (game) async { + final flipper = Flipper.right(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: flipper, + anchor: flipperAnchor, + ); + + expect(jointDef.lowerAngle, equals(jointDef.upperAngle)); + }, + ); + }); + }); + + group( + 'unlocks', + () { + flameTester.test( + 'when Flipper is left', + (game) async { + final flipper = Flipper.left(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: flipper, + anchor: flipperAnchor, + ); + final joint = game.world.createJoint(jointDef) as RevoluteJoint; + + FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side); + + expect( + joint.upperLimit, + isNot(equals(joint.lowerLimit)), + ); + }, + ); + + flameTester.test( + 'when Flipper is right', + (game) async { + final flipper = Flipper.right(position: Vector2.zero()); + await game.ensureAdd(flipper); + + final flipperAnchor = FlipperAnchor(flipper: flipper); + await game.ensureAdd(flipperAnchor); + + final jointDef = FlipperAnchorRevoluteJointDef( + flipper: flipper, + anchor: flipperAnchor, + ); + final joint = game.world.createJoint(jointDef) as RevoluteJoint; + + FlipperAnchorRevoluteJointDef.unlock(joint, flipper.side); + + expect( + joint.upperLimit, + isNot(equals(joint.lowerLimit)), + ); + }, + ); + }, + ); + }); +} diff --git a/test/game/pinball_game_test.dart b/test/game/pinball_game_test.dart index 75a77aa9..4dc93b7f 100644 --- a/test/game/pinball_game_test.dart +++ b/test/game/pinball_game_test.dart @@ -1,9 +1,54 @@ +// ignore_for_file: cascade_invocations + +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; void main() { group('PinballGame', () { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGame.new); + // 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, + ); + }, + ); + + flameTester.test( + 'has only one right Flipper', + (game) async { + await game.ready(); + + expect( + () => game.children.singleWhere( + flipperSelector(BoardSide.right), + ), + returnsNormally, + ); + }, + ); + }); + }, + ); }); } diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 97bc22be..c2c1cd36 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -6,5 +6,6 @@ // https://opensource.org/licenses/MIT. export 'builders.dart'; +export 'key_testers.dart'; export 'mocks.dart'; export 'pump_app.dart'; diff --git a/test/helpers/key_testers.dart b/test/helpers/key_testers.dart new file mode 100644 index 00000000..04fed1da --- /dev/null +++ b/test/helpers/key_testers.dart @@ -0,0 +1,37 @@ +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers.dart'; + +@isTest +void testRawKeyUpEvents( + List keys, + Function(RawKeyUpEvent) test, +) { + for (final key in keys) { + test(_mockKeyUpEvent(key)); + } +} + +RawKeyUpEvent _mockKeyUpEvent(LogicalKeyboardKey key) { + final event = MockRawKeyUpEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} + +@isTest +void testRawKeyDownEvents( + List keys, + Function(RawKeyDownEvent) test, +) { + for (final key in keys) { + test(_mockKeyDownEvent(key)); + } +} + +RawKeyDownEvent _mockKeyDownEvent(LogicalKeyboardKey key) { + final event = MockRawKeyDownEvent(); + when(() => event.logicalKey).thenReturn(key); + return event; +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b46e2c5c..da9fd537 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -1,4 +1,6 @@ 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'; @@ -13,3 +15,17 @@ class MockBall extends Mock implements Ball {} class MockContact extends Mock implements Contact {} class MockGameBloc extends Mock implements GameBloc {} + +class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +} + +class MockRawKeyUpEvent extends Mock implements RawKeyUpEvent { + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return super.toString(); + } +}