diff --git a/analysis_options.yaml b/analysis_options.yaml index 07aa1dab..44aef9ac 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1 @@ include: package:very_good_analysis/analysis_options.2.4.0.yaml -linter: - rules: - public_member_api_docs: false diff --git a/assets/images/components/flipper.png b/assets/images/components/flipper.png new file mode 100644 index 00000000..f63974c4 Binary files /dev/null and b/assets/images/components/flipper.png differ diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 7e3fdf17..cf6213e9 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.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 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:pinball/l10n/l10n.dart'; diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index c612b584..34fcc47a 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.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'; diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index f854c10d..b9987935 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -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'; diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index a2fefda8..7d3e3c91 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -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 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'); diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 8a35e046..2812a049 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + part of 'game_bloc.dart'; /// Defines bonuses that a player can gain during a PinballGame. diff --git a/lib/game/components/ball.dart b/lib/game/components/ball.dart index 2d9dddf0..cdd1296c 100644 --- a/lib/game/components/ball.dart +++ b/lib/game/components/ball.dart @@ -3,29 +3,36 @@ import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; +/// {@template ball} +/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the +/// [PinballGame]. +/// {@endtemplate} class Ball extends PositionBodyComponent with BlocComponent { + /// {@macro ball} Ball({ required Vector2 position, }) : _position = position, - super(size: ballSize); - - static final ballSize = Vector2.all(2); + super(size: Vector2.all(2)); + /// The initial position of the [Ball] body. final Vector2 _position; + /// Asset location of the sprite that renders with the [Ball]. + /// + /// Sprite is preloaded by [PinballGameAssetsX]. static const spritePath = 'components/ball.png'; @override Future onLoad() async { await super.onLoad(); final sprite = await gameRef.loadSprite(spritePath); - positionComponent = SpriteComponent(sprite: sprite, size: ballSize); + positionComponent = SpriteComponent(sprite: sprite, size: size); } @override Body createBody() { - final shape = CircleShape()..radius = ballSize.x / 2; + final shape = CircleShape()..radius = size.x / 2; final fixtureDef = FixtureDef(shape)..density = 1; @@ -37,6 +44,11 @@ class Ball extends PositionBodyComponent return world.createBody(bodyDef)..createFixture(fixtureDef); } + /// 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; diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index bd5f5437..89f60343 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -2,6 +2,7 @@ export 'anchor.dart'; export 'ball.dart'; export 'board_side.dart'; export 'flipper.dart'; +export 'pathway.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 index bd071b93..16754ed3 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -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 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 _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() { @@ -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(); @override diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart new file mode 100644 index 00000000..d41ce3da --- /dev/null +++ b/lib/game/components/pathway.dart @@ -0,0 +1,178 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:geometry/geometry.dart'; + +/// {@template pathway} +/// [Pathway] creates lines of various shapes. +/// +/// [BodyComponent]s such as a Ball can collide and move along a [Pathway]. +/// {@endtemplate} +class Pathway extends BodyComponent { + Pathway._({ + // TODO(ruialonso): remove color when assets added. + Color? color, + required Vector2 position, + required List> paths, + }) : _position = position, + _paths = paths { + paint = Paint() + ..color = color ?? const Color.fromARGB(0, 0, 0, 0) + ..style = PaintingStyle.stroke; + } + + /// Creates a uniform unidirectional (straight) [Pathway]. + /// + /// Does so with two [ChainShape] separated by a [width]. Placed + /// at a [position] between [start] and [end] points. Can + /// be rotated by a given [rotation] in radians. + /// + /// If [singleWall] is true, just one [ChainShape] is created. + factory Pathway.straight({ + Color? color, + required Vector2 position, + required Vector2 start, + required Vector2 end, + required double width, + double rotation = 0, + bool singleWall = false, + }) { + final paths = >[]; + + // TODO(ruialonso): Refactor repetitive logic + final firstWall = [ + start.clone(), + end.clone(), + ].map((vector) => vector..rotate(rotation)).toList(); + paths.add(firstWall); + + if (!singleWall) { + final secondWall = [ + start + Vector2(width, 0), + end + Vector2(width, 0), + ].map((vector) => vector..rotate(rotation)).toList(); + paths.add(secondWall); + } + + return Pathway._( + color: color, + position: position, + paths: paths, + ); + } + + /// Creates an arc [Pathway]. + /// + /// The [angle], in radians, specifies the size of the arc. For example, 2*pi + /// returns a complete circumference and minor angles a semi circumference. + /// + /// The center of the arc is placed at [position]. + /// + /// Does so with two [ChainShape] separated by a [width]. Which can be + /// rotated by a given [rotation] in radians. + /// + /// The outer radius is specified by [radius], whilst the inner one is + /// equivalent to [radius] - [width]. + /// + /// If [singleWall] is true, just one [ChainShape] is created. + factory Pathway.arc({ + Color? color, + required Vector2 position, + required double width, + required double radius, + required double angle, + double rotation = 0, + bool singleWall = false, + }) { + final paths = >[]; + + // TODO(ruialonso): Refactor repetitive logic + final outerWall = calculateArc( + center: position, + radius: radius, + angle: angle, + offsetAngle: rotation, + ); + paths.add(outerWall); + + if (!singleWall) { + final innerWall = calculateArc( + center: position, + radius: radius - width, + angle: angle, + offsetAngle: rotation, + ); + paths.add(innerWall); + } + + return Pathway._( + color: color, + position: position, + paths: paths, + ); + } + + /// Creates a bezier curve [Pathway]. + /// + /// Does so with two [ChainShape] separated by a [width]. Which can be + /// rotated by a given [rotation] in radians. + /// + /// First and last [controlPoints] set the beginning and end of the curve, + /// inner points between them set its final shape. + /// + /// If [singleWall] is true, just one [ChainShape] is created. + factory Pathway.bezierCurve({ + Color? color, + required Vector2 position, + required List controlPoints, + required double width, + double rotation = 0, + bool singleWall = false, + }) { + final paths = >[]; + + // TODO(ruialonso): Refactor repetitive logic + final firstWall = calculateBezierCurve(controlPoints: controlPoints) + .map((vector) => vector..rotate(rotation)) + .toList(); + paths.add(firstWall); + + if (!singleWall) { + final secondWall = calculateBezierCurve( + controlPoints: controlPoints + .map((vector) => vector + Vector2(width, -width)) + .toList(), + ).map((vector) => vector..rotate(rotation)).toList(); + paths.add(secondWall); + } + + return Pathway._( + color: color, + position: position, + paths: paths, + ); + } + + final Vector2 _position; + final List> _paths; + + @override + Body createBody() { + final bodyDef = BodyDef() + ..type = BodyType.static + ..position = _position; + + final body = world.createBody(bodyDef); + for (final path in _paths) { + final chain = ChainShape() + ..createChain( + path.map(gameRef.screenToWorld).toList(), + ); + final fixtureDef = FixtureDef(chain); + + body.createFixture(fixtureDef); + } + + return body; + } +} diff --git a/lib/game/components/wall.dart b/lib/game/components/wall.dart index b784b8cb..f5a15af5 100644 --- a/lib/game/components/wall.dart +++ b/lib/game/components/wall.dart @@ -6,13 +6,18 @@ import 'package:pinball/game/components/components.dart'; /// {@template wall} /// A continuos 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 @@ -39,6 +44,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), diff --git a/lib/game/game_assets.dart b/lib/game/game_assets.dart index 964aeda1..778e2bc2 100644 --- a/lib/game/game_assets.dart +++ b/lib/game/game_assets.dart @@ -6,6 +6,7 @@ extension PinballGameAssetsX on PinballGame { Future preLoadAssets() async { await Future.wait([ images.load(Ball.spritePath), + images.load(Flipper.spritePath), ]); } } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 308d8faf..33ea5eda 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,6 @@ -import 'dart:async'; +// ignore_for_file: public_member_api_docs +import 'dart:async'; import 'package:flame/input.dart'; import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 8a9a981c..a49ff0c1 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:flame/game.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/game/view/widgets/game_over_dialog.dart b/lib/game/view/widgets/game_over_dialog.dart index 586d6c56..9d1c61b0 100644 --- a/lib/game/view/widgets/game_over_dialog.dart +++ b/lib/game/view/widgets/game_over_dialog.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 diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 766b5e31..548a81a6 100644 --- a/lib/l10n/l10n.dart +++ b/lib/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 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/landing/view/landing_page.dart b/lib/landing/view/landing_page.dart index a688dee1..c705f084 100644 --- a/lib/landing/view/landing_page.dart +++ b/lib/landing/view/landing_page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: public_member_api_docs + import 'package:flutter/material.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/l10n/l10n.dart'; diff --git a/lib/theme/cubit/theme_cubit.dart b/lib/theme/cubit/theme_cubit.dart index 7ba79e59..94eba4a6 100644 --- a/lib/theme/cubit/theme_cubit.dart +++ b/lib/theme/cubit/theme_cubit.dart @@ -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'; diff --git a/lib/theme/cubit/theme_state.dart b/lib/theme/cubit/theme_state.dart index 13b3ea5f..078f5c84 100644 --- a/lib/theme/cubit/theme_state.dart +++ b/lib/theme/cubit/theme_state.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 { diff --git a/packages/geometry/.gitignore b/packages/geometry/.gitignore new file mode 100644 index 00000000..526da158 --- /dev/null +++ b/packages/geometry/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/packages/geometry/README.md b/packages/geometry/README.md new file mode 100644 index 00000000..f0841d82 --- /dev/null +++ b/packages/geometry/README.md @@ -0,0 +1,11 @@ +# geometry + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +Helper package to calculate points of lines, arcs and curves for the pathways of the ball. + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/packages/geometry/analysis_options.yaml b/packages/geometry/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/geometry/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/geometry/lib/geometry.dart b/packages/geometry/lib/geometry.dart new file mode 100644 index 00000000..2453ed05 --- /dev/null +++ b/packages/geometry/lib/geometry.dart @@ -0,0 +1,3 @@ +library geometry; + +export 'src/geometry.dart'; diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart new file mode 100644 index 00000000..dceb4e9e --- /dev/null +++ b/packages/geometry/lib/src/geometry.dart @@ -0,0 +1,107 @@ +import 'dart:math' as math; +import 'package:flame/extensions.dart'; + +/// Calculates all [Vector2]s of a circumference. +/// +/// A circumference can be achieved by specifying a [center] and a [radius]. +/// In addition, a semi-circle can be achieved by specifying its [angle] and an +/// [offsetAngle] (both in radians). +/// +/// The higher the [precision], the more [Vector2]s will be calculated; +/// achieving a more rounded arc. +/// +/// For more information read: https://en.wikipedia.org/wiki/Trigonometric_functions. +List calculateArc({ + required Vector2 center, + required double radius, + required double angle, + double offsetAngle = 0, + int precision = 100, +}) { + final stepAngle = angle / (precision - 1); + + final points = []; + for (var i = 0; i < precision; i++) { + final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); + final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); + + final point = Vector2(xCoord, yCoord); + points.add(point); + } + + return points; +} + +/// Calculates all [Vector2]s of a bezier curve. +/// +/// A bezier curve of [controlPoints] that say how to create this curve. +/// +/// First and last points specify the beginning and the end respectively +/// of the curve. The inner points specify the shape of the curve and +/// its turning points. +/// +/// The [step] must be between zero and one (inclusive), indicating the +/// precision to calculate the curve. +/// +/// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve +List calculateBezierCurve({ + required List controlPoints, + double step = 0.001, +}) { + assert( + 0 <= step && step <= 1, + 'Step ($step) must be in range 0 <= step <= 1', + ); + assert( + controlPoints.length >= 2, + 'At least 2 control points needed to create a bezier curve', + ); + + var t = 0.0; + final n = controlPoints.length - 1; + final points = []; + + do { + var xCoord = 0.0; + var yCoord = 0.0; + for (var i = 0; i <= n; i++) { + final point = controlPoints[i]; + + xCoord += + binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; + yCoord += + binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; + } + points.add(Vector2(xCoord, yCoord)); + + t = t + step; + } while (t <= 1); + + return points; +} + +/// Calculates the binomial coefficient of 'n' and 'k'. +/// +/// For more information read: https://en.wikipedia.org/wiki/Binomial_coefficient +num binomial(num n, num k) { + assert(0 <= k && k <= n, 'k ($k) and n ($n) must be in range 0 <= k <= n'); + + if (k == 0 || n == k) { + return 1; + } else { + return factorial(n) / (factorial(k) * factorial(n - k)); + } +} + +/// Calculate the factorial of 'n'. +/// +/// For more information read: https://en.wikipedia.org/wiki/Factorial +num factorial(num n) { + assert(n >= 0, 'Factorial is not defined for negative number n ($n)'); + + if (n == 0 || n == 1) { + return 1; + } else { + return n * factorial(n - 1); + } +} diff --git a/packages/geometry/pubspec.yaml b/packages/geometry/pubspec.yaml new file mode 100644 index 00000000..2678cdef --- /dev/null +++ b/packages/geometry/pubspec.yaml @@ -0,0 +1,19 @@ +name: geometry +description: Helper package to calculate points of lines, arcs and curves for the pathways of the ball +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dependencies: + flame: ^1.0.0 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^0.2.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart new file mode 100644 index 00000000..a3040a9c --- /dev/null +++ b/packages/geometry/test/src/geometry_test.dart @@ -0,0 +1,159 @@ +// 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'; + +class Binomial { + Binomial({required this.n, required this.k}); + + final num n; + final num k; +} + +void main() { + group('calculateArc', () { + test('returns by default 100 points as indicated by precision', () { + final points = calculateArc( + center: Vector2.zero(), + radius: 100, + angle: 90, + ); + expect(points.length, 100); + }); + + test('returns as many points as indicated by precision', () { + final points = calculateArc( + center: Vector2.zero(), + radius: 100, + angle: 90, + precision: 50, + ); + expect(points.length, 50); + }); + }); + + group('calculateBezierCurve', () { + test('fails if step not in range', () { + expect( + () => calculateBezierCurve( + controlPoints: [ + Vector2(0, 0), + Vector2(10, 10), + ], + step: 2, + ), + throwsAssertionError, + ); + }); + + test('fails if not enough control points', () { + expect( + () => calculateBezierCurve(controlPoints: [Vector2.zero()]), + throwsAssertionError, + ); + expect( + () => calculateBezierCurve(controlPoints: []), + throwsAssertionError, + ); + }); + + test('returns by default 1000 points as indicated by step', () { + final points = calculateBezierCurve( + controlPoints: [ + Vector2(0, 0), + Vector2(10, 10), + ], + ); + expect(points.length, 1000); + }); + + test('returns as many points as indicated by step', () { + final points = calculateBezierCurve( + controlPoints: [ + Vector2(0, 0), + Vector2(10, 10), + ], + step: 0.01, + ); + expect(points.length, 100); + }); + }); + + group('binomial', () { + test('fails if k is negative', () { + expect(() => binomial(1, -1), throwsAssertionError); + }); + + test('fails if n is negative', () { + expect(() => binomial(-1, 1), throwsAssertionError); + }); + + test('fails if n < k', () { + expect(() => binomial(1, 2), throwsAssertionError); + }); + + test('for a specific input gives a correct value', () { + final binomialInputsToExpected = { + Binomial(n: 0, k: 0): 1, + Binomial(n: 1, k: 0): 1, + Binomial(n: 1, k: 1): 1, + Binomial(n: 2, k: 0): 1, + Binomial(n: 2, k: 1): 2, + Binomial(n: 2, k: 2): 1, + Binomial(n: 3, k: 0): 1, + Binomial(n: 3, k: 1): 3, + Binomial(n: 3, k: 2): 3, + Binomial(n: 3, k: 3): 1, + Binomial(n: 4, k: 0): 1, + Binomial(n: 4, k: 1): 4, + Binomial(n: 4, k: 2): 6, + Binomial(n: 4, k: 3): 4, + Binomial(n: 4, k: 4): 1, + Binomial(n: 5, k: 0): 1, + Binomial(n: 5, k: 1): 5, + Binomial(n: 5, k: 2): 10, + Binomial(n: 5, k: 3): 10, + Binomial(n: 5, k: 4): 5, + Binomial(n: 5, k: 5): 1, + Binomial(n: 6, k: 0): 1, + Binomial(n: 6, k: 1): 6, + Binomial(n: 6, k: 2): 15, + Binomial(n: 6, k: 3): 20, + Binomial(n: 6, k: 4): 15, + Binomial(n: 6, k: 5): 6, + Binomial(n: 6, k: 6): 1, + }; + binomialInputsToExpected.forEach((input, value) { + expect(binomial(input.n, input.k), value); + }); + }); + }); + + group('factorial', () { + test('fails if negative number', () { + expect(() => factorial(-1), throwsAssertionError); + }); + + test('for a specific input gives a correct value', () { + final factorialInputsToExpected = { + 0: 1, + 1: 1, + 2: 2, + 3: 6, + 4: 24, + 5: 120, + 6: 720, + 7: 5040, + 8: 40320, + 9: 362880, + 10: 3628800, + 11: 39916800, + 12: 479001600, + 13: 6227020800, + }; + factorialInputsToExpected.forEach((input, expected) { + expect(factorial(input), expected); + }); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 861dae5b..4b375ff7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -198,6 +198,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + geometry: + dependency: "direct main" + description: + path: "packages/geometry" + relative: true + source: path + version: "1.0.0+1" glob: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8738f2bb..41b0d081 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,8 @@ dependencies: flutter_bloc: ^8.0.1 flutter_localizations: sdk: flutter + geometry: + path: packages/geometry intl: ^0.17.0 pinball_theme: path: packages/pinball_theme diff --git a/test/game/components/anchor_test.dart b/test/game/components/anchor_test.dart index 5cc37eca..a5aa7d2d 100644 --- a/test/game/components/anchor_test.dart +++ b/test/game/components/anchor_test.dart @@ -15,6 +15,7 @@ void main() { 'loads correctly', (game) async { final anchor = Anchor(position: Vector2.zero()); + await game.ready(); await game.ensureAdd(anchor); expect(game.contains(anchor), isTrue); @@ -25,6 +26,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 +39,7 @@ void main() { flameTester.test( 'is static', (game) async { + await game.ready(); final anchor = Anchor(position: Vector2.zero()); await game.ensureAdd(anchor); diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart new file mode 100644 index 00000000..036309b1 --- /dev/null +++ b/test/game/components/pathway_test.dart @@ -0,0 +1,268 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors +import 'dart:math' as math; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final flameTester = FlameTester(PinballGame.new); + + group('Pathway', () { + const width = 50.0; + + group('straight', () { + group('color', () { + 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), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + expect(pathway.paint, isNotNull); + expect( + pathway.paint.color, + equals(Color.fromARGB(0, 0, 0, 0)), + ); + }, + ); + + flameTester.test( + 'has a color when is specified', + (game) async { + await game.ready(); + const defaultColor = Colors.blue; + + final pathway = Pathway.straight( + color: defaultColor, + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + expect(pathway.paint, isNotNull); + expect(pathway.paint.color.value, equals(defaultColor.value)); + }, + ); + }); + + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + await game.ready(); + final position = Vector2.all(10); + final pathway = Pathway.straight( + position: position, + start: Vector2(10, 10), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + game.contains(pathway); + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + await game.ready(); + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixtures', () { + 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), + end: Vector2(20, 20), + width: width, + singleWall: true, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.fixtures.length, 1); + final fixture = pathway.body.fixtures[0]; + expect(fixture, isA()); + expect(fixture.shape.shapeType, equals(ShapeType.chain)); + }, + ); + + 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), + end: Vector2(20, 20), + width: width, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.fixtures.length, 2); + for (final fixture in pathway.body.fixtures) { + expect(fixture, isA()); + expect(fixture.shape.shapeType, equals(ShapeType.chain)); + } + }, + ); + }); + }); + + group('arc', () { + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final pathway = Pathway.arc( + position: Vector2.zero(), + width: width, + radius: 100, + angle: math.pi / 2, + ); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + await game.ready(); + final position = Vector2.all(10); + final pathway = Pathway.arc( + position: position, + width: width, + radius: 100, + angle: math.pi / 2, + ); + await game.ensureAdd(pathway); + + game.contains(pathway); + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + await game.ready(); + final pathway = Pathway.arc( + position: Vector2.zero(), + width: width, + radius: 100, + angle: math.pi / 2, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + + group('bezier curve', () { + final controlPoints = [ + Vector2(0, 0), + Vector2(50, 0), + Vector2(0, 50), + Vector2(50, 50), + ]; + + flameTester.test( + 'loads correctly', + (game) async { + await game.ready(); + final pathway = Pathway.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + width: width, + ); + await game.ensureAdd(pathway); + + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + await game.ready(); + final position = Vector2.all(10); + final pathway = Pathway.bezierCurve( + position: position, + controlPoints: controlPoints, + width: width, + ); + await game.ensureAdd(pathway); + + game.contains(pathway); + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + await game.ready(); + final pathway = Pathway.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + width: width, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + }); +} diff --git a/test/game/components/plunger_test.dart b/test/game/components/plunger_test.dart index 67e215fd..835bbc65 100644 --- a/test/game/components/plunger_test.dart +++ b/test/game/components/plunger_test.dart @@ -16,6 +16,7 @@ void main() { flameTester.test( 'loads correctly', (game) async { + await game.ready(); final plunger = Plunger(position: Vector2.zero()); await game.ensureAdd(plunger); diff --git a/test/game/components/wall_test.dart b/test/game/components/wall_test.dart index 8151055e..7f3c1c9c 100644 --- a/test/game/components/wall_test.dart +++ b/test/game/components/wall_test.dart @@ -37,6 +37,7 @@ void main() { flameTester.test( 'loads correctly', (game) async { + await game.ready(); final wall = Wall( start: Vector2.zero(), end: Vector2(100, 0),