From 4e2f56a70c372e6606d84e863303a8499248dafa Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 12:48:24 +0100 Subject: [PATCH 01/37] feat: created maths package for calculate curves --- packages/maths/.gitignore | 7 +++ packages/maths/README.md | 11 ++++ packages/maths/analysis_options.yaml | 1 + packages/maths/lib/maths.dart | 3 + packages/maths/lib/src/maths.dart | 81 +++++++++++++++++++++++++ packages/maths/pubspec.yaml | 15 +++++ packages/maths/test/src/maths_test.dart | 11 ++++ 7 files changed, 129 insertions(+) create mode 100644 packages/maths/.gitignore create mode 100644 packages/maths/README.md create mode 100644 packages/maths/analysis_options.yaml create mode 100644 packages/maths/lib/maths.dart create mode 100644 packages/maths/lib/src/maths.dart create mode 100644 packages/maths/pubspec.yaml create mode 100644 packages/maths/test/src/maths_test.dart diff --git a/packages/maths/.gitignore b/packages/maths/.gitignore new file mode 100644 index 00000000..526da158 --- /dev/null +++ b/packages/maths/.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/maths/README.md b/packages/maths/README.md new file mode 100644 index 00000000..cbc4cca1 --- /dev/null +++ b/packages/maths/README.md @@ -0,0 +1,11 @@ +# maths + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +A Very Good Project created by Very Good CLI. + +[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/maths/analysis_options.yaml b/packages/maths/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/maths/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/maths/lib/maths.dart b/packages/maths/lib/maths.dart new file mode 100644 index 00000000..b340388b --- /dev/null +++ b/packages/maths/lib/maths.dart @@ -0,0 +1,3 @@ +library maths; + +export 'src/maths.dart'; diff --git a/packages/maths/lib/src/maths.dart b/packages/maths/lib/src/maths.dart new file mode 100644 index 00000000..ea447db5 --- /dev/null +++ b/packages/maths/lib/src/maths.dart @@ -0,0 +1,81 @@ +import 'dart:math' as math; +import 'package:flame/extensions.dart'; + +/// Method to calculate all points (with a required precision amount of them) +/// of a circumference based on angle, offsetAngle and radius +List calculateArc({ + required Vector2 center, + required double radius, + required double angle, + double offsetAngle = 0, + int precision = 100, +}) { + final stepAngle = radians(angle / precision); + final stepOffset = radians(offsetAngle); + + final points = []; + for (var i = 0; i <= precision; i++) { + final xCoord = center.x + radius * math.cos((stepAngle * i) + stepOffset); + final yCoord = center.y - radius * math.sin((stepAngle * i) + stepOffset); + + final point = Vector2(xCoord, yCoord); + points.add(point); + } + + return points; +} + +/// Method that calculates all points of a bezier curve of degree 'g' and +/// n=g-1 control points and range 0<=t<=1 +/// https://en.wikipedia.org/wiki/B%C3%A9zier_curve +List calculateBezierCurve({ + required List controlPoints, + double step = 0.001, +}) { + assert( + controlPoints.length >= 2, + 'At least 2 control points 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; +} + +/// Method to calculate the binomial coefficient of 'n' and 'k' +/// https://en.wikipedia.org/wiki/Binomial_coefficient +num _binomial(num n, num k) { + assert(0 <= k && k <= n, 'Range 0<=k<=n'); + if (k == 0 || n == k) { + return 1; + } else { + return _factorial(n) / (_factorial(k) * _factorial(n - k)); + } +} + +/// Method to calculate the factorial of some number 'n' +/// https://en.wikipedia.org/wiki/Factorial +num _factorial(num n) { + if (n == 1) { + return 1; + } else { + return n * _factorial(n - 1); + } +} diff --git a/packages/maths/pubspec.yaml b/packages/maths/pubspec.yaml new file mode 100644 index 00000000..3b4b863c --- /dev/null +++ b/packages/maths/pubspec.yaml @@ -0,0 +1,15 @@ +name: maths +description: A Very Good Project created by Very Good CLI. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: ">=2.16.0 <3.0.0" + +dev_dependencies: + coverage: ^1.1.0 + mocktail: ^0.2.0 + test: ^1.19.2 + very_good_analysis: ^2.4.0 +dependencies: + flame: ^1.0.0 diff --git a/packages/maths/test/src/maths_test.dart b/packages/maths/test/src/maths_test.dart new file mode 100644 index 00000000..dceedab8 --- /dev/null +++ b/packages/maths/test/src/maths_test.dart @@ -0,0 +1,11 @@ +// ignore_for_file: prefer_const_constructors +import 'package:maths/maths.dart'; +import 'package:test/test.dart'; + +void main() { + group('Maths', () { + test('can be instantiated', () { + expect(Maths(), isNotNull); + }); + }); +} From 09464347ef2af67d9676e95a67f04096e83227d1 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 12:49:16 +0100 Subject: [PATCH 02/37] feat: added maths local dependency --- pubspec.lock | 7 +++++++ pubspec.yaml | 2 ++ 2 files changed, 9 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index e218776d..db3b239a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -261,6 +261,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + maths: + dependency: "direct main" + description: + path: "/Users/ruialonso/dev/flutter/googleIO22/pinball/packages/maths" + relative: false + source: path + version: "1.0.0+1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5d708073..7b9c9c19 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: flutter_localizations: sdk: flutter intl: ^0.17.0 + maths: + path: packages/maths dev_dependencies: bloc_test: ^9.0.2 From b0e8a3be143c27f1d022a65e14858e8c99206331 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 12:49:48 +0100 Subject: [PATCH 03/37] feat: created path component to paint pathways for the ball --- lib/game/components/components.dart | 1 + lib/game/components/path.dart | 141 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 lib/game/components/path.dart diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 95134ec2..81e8cad2 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,5 +1,6 @@ export 'anchor.dart'; export 'ball.dart'; +export 'path.dart'; export 'plunger.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/lib/game/components/path.dart b/lib/game/components/path.dart new file mode 100644 index 00000000..0d7105d7 --- /dev/null +++ b/lib/game/components/path.dart @@ -0,0 +1,141 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; +import 'package:maths/maths.dart'; + +class Path extends BodyComponent { + Path._({ + Color? color, + required Vector2 position, + required List> paths, + }) : _position = position, + _paths = paths { + if (color != null) { + paint = Paint() + ..color = color + ..style = PaintingStyle.stroke; + } + } + + factory Path.straight({ + Color? color, + required Vector2 position, + required Vector2 start, + required Vector2 end, + required double pathWidth, + double rotation = 0, + bool onlyOneWall = false, + }) { + final paths = >[]; + final wall1 = [ + start.clone(), + end.clone(), + ]; + paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); + + if (!onlyOneWall) { + final wall2 = [ + start + Vector2(pathWidth, 0), + end + Vector2(pathWidth, 0), + ]; + paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); + } + + return Path._( + color: color, + position: position, + paths: paths, + ); + } + + factory Path.arc({ + Color? color, + required Vector2 position, + required double pathWidth, + required double radius, + required double angle, + double rotation = 0, + bool onlyOneWall = false, + }) { + final paths = >[]; + + final wall1 = calculateArc( + center: position, + radius: radius, + angle: angle, + offsetAngle: rotation, + ); + paths.add(wall1); + + if (!onlyOneWall) { + final minRadius = radius - pathWidth; + + final wall2 = calculateArc( + center: position, + radius: minRadius, + angle: angle, + offsetAngle: rotation, + ); + paths.add(wall2); + } + + return Path._( + color: color, + position: position, + paths: paths, + ); + } + + factory Path.bezierCurve({ + Color? color, + required Vector2 position, + required List controlPoints, + required double pathWidth, + double rotation = 0, + bool onlyOneWall = false, + }) { + final paths = >[]; + + final wall1 = calculateBezierCurve(controlPoints: controlPoints); + paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); + + var wall2 = []; + if (!onlyOneWall) { + wall2 = calculateBezierCurve( + controlPoints: controlPoints + .map((e) => e + Vector2(pathWidth, -pathWidth)) + .toList(), + ); + paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); + } + + return Path._( + 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((e) => gameRef.screenToWorld(e)).toList(), + ); + final fixtureDef = FixtureDef(chain); + body.createFixture(fixtureDef); + } + + return body; + } +} From 53f24643b7a8c94dc82b725ff2d17bb511f11e0d Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 12:50:12 +0100 Subject: [PATCH 04/37] test: tests for Path component --- test/game/components/path_test.dart | 249 ++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 test/game/components/path_test.dart diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart new file mode 100644 index 00000000..440070b6 --- /dev/null +++ b/test/game/components/path_test.dart @@ -0,0 +1,249 @@ +// ignore_for_file: cascade_invocations + +import 'dart:math'; +import 'dart:ui'; + +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('Path', () { + const pathWidth = 50.0; + + group('straight', () { + group('color', () { + flameTester.test( + 'has white color by default if not specified', + (game) async { + final path = Path.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + expect(game.contains(path), isTrue); + expect(path.paint, isNotNull); + expect(path.paint.color, equals(Colors.white)); + }, + ); + flameTester.test( + 'has a color if set', + (game) async { + const defaultColor = Colors.blue; + + final path = Path.straight( + color: defaultColor, + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + expect(game.contains(path), isTrue); + expect(path.paint, isNotNull); + expect(path.paint.color.value, equals(defaultColor.value)); + }, + ); + }); + + flameTester.test( + 'loads correctly', + (game) async { + final path = Path.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + expect(game.contains(path), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final path = Path.straight( + position: position, + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + game.contains(path); + + expect(path.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final path = Path.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + + expect(path.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'exists only one ChainShape if just one wall', + (game) async { + final path = Path.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + onlyOneWall: true, + ); + await game.ensureAdd(path); + + expect(path.body.fixtures.length, 1); + final fixture = path.body.fixtures[0]; + expect(fixture, isA()); + expect(fixture.shape.shapeType, equals(ShapeType.chain)); + }, + ); + + flameTester.test( + 'exists two ChainShape if there is by default two walls', + (game) async { + final path = Path.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + + expect(path.body.fixtures.length, 2); + for (var fixture in path.body.fixtures) { + expect(fixture, isA()); + expect(fixture.shape.shapeType, equals(ShapeType.chain)); + } + }, + ); + }); + }); + + group('arc', () { + flameTester.test( + 'loads correctly', + (game) async { + final path = Path.arc( + position: Vector2.zero(), + pathWidth: pathWidth, + radius: 100, + angle: 90, + ); + await game.ensureAdd(path); + expect(game.contains(path), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final path = Path.arc( + position: position, + pathWidth: pathWidth, + radius: 100, + angle: 90, + ); + await game.ensureAdd(path); + game.contains(path); + + expect(path.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final path = Path.arc( + position: Vector2.zero(), + pathWidth: pathWidth, + radius: 100, + angle: 90, + ); + await game.ensureAdd(path); + + expect(path.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 { + final path = Path.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + expect(game.contains(path), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final path = Path.bezierCurve( + position: position, + controlPoints: controlPoints, + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + game.contains(path); + + expect(path.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final path = Path.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + pathWidth: pathWidth, + ); + await game.ensureAdd(path); + + expect(path.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + }); +} From c693e9a10910c0dc8151077406da3c1d2ce9ca07 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 12:52:58 +0100 Subject: [PATCH 05/37] chore: analysis errors fixed on tests --- test/game/components/path_test.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart index 440070b6..a97ba866 100644 --- a/test/game/components/path_test.dart +++ b/test/game/components/path_test.dart @@ -1,8 +1,4 @@ // ignore_for_file: cascade_invocations - -import 'dart:math'; -import 'dart:ui'; - import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; @@ -133,7 +129,7 @@ void main() { await game.ensureAdd(path); expect(path.body.fixtures.length, 2); - for (var fixture in path.body.fixtures) { + for (final fixture in path.body.fixtures) { expect(fixture, isA()); expect(fixture.shape.shapeType, equals(ShapeType.chain)); } From aeb289fce66e5d68dcb33033d3600e9f674b0575 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 13:04:28 +0100 Subject: [PATCH 06/37] fix: changed paths area default color to transparent --- lib/game/components/path.dart | 8 +++----- test/game/components/path_test.dart | 9 ++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/game/components/path.dart b/lib/game/components/path.dart index 0d7105d7..66dad004 100644 --- a/lib/game/components/path.dart +++ b/lib/game/components/path.dart @@ -10,11 +10,9 @@ class Path extends BodyComponent { required List> paths, }) : _position = position, _paths = paths { - if (color != null) { - paint = Paint() - ..color = color - ..style = PaintingStyle.stroke; - } + paint = Paint() + ..color = color ?? const Color.fromARGB(0, 0, 0, 0) + ..style = PaintingStyle.stroke; } factory Path.straight({ diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart index a97ba866..9564c21c 100644 --- a/test/game/components/path_test.dart +++ b/test/game/components/path_test.dart @@ -1,4 +1,4 @@ -// ignore_for_file: cascade_invocations +// ignore_for_file: cascade_invocations, prefer_const_constructors import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; @@ -15,7 +15,7 @@ void main() { group('straight', () { group('color', () { flameTester.test( - 'has white color by default if not specified', + 'has transparent color by default if not specified', (game) async { final path = Path.straight( position: Vector2.zero(), @@ -26,7 +26,10 @@ void main() { await game.ensureAdd(path); expect(game.contains(path), isTrue); expect(path.paint, isNotNull); - expect(path.paint.color, equals(Colors.white)); + expect( + path.paint.color, + equals(Color.fromARGB(0, 0, 0, 0)), + ); }, ); flameTester.test( From 9ccf85c2ef3bc060f4ac998c361a70807dabaace Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 13:27:13 +0100 Subject: [PATCH 07/37] doc: documented Path component --- lib/game/components/path.dart | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/game/components/path.dart b/lib/game/components/path.dart index 66dad004..69b16b20 100644 --- a/lib/game/components/path.dart +++ b/lib/game/components/path.dart @@ -3,6 +3,10 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:maths/maths.dart'; +/// {@template path} +/// [Path] creates different shapes that sets the pathways that ball can follow +/// or collide to like walls. +/// {@endtemplate} class Path extends BodyComponent { Path._({ Color? color, @@ -15,6 +19,12 @@ class Path extends BodyComponent { ..style = PaintingStyle.stroke; } + /// {@macro path} + /// [Path.straight] creates a straight path for the ball given a [position] + /// for the body, between a [start] and [end] points. + /// It creates two [ChainShape] separated by a [pathWidth]. If [onlyOneWall] + /// is true, just one [ChainShape] is created (like a wall instead of a path) + /// The path could be rotated by [rotation] in degrees. factory Path.straight({ Color? color, required Vector2 position, @@ -46,6 +56,15 @@ class Path extends BodyComponent { ); } + /// {@macro path} + /// [Path.straight] creates an arc path for the ball given a [position] + /// for the body, a [radius] for the circumference and an [angle] to specify + /// the size of the semi circumference. + /// It creates two [ChainShape] separated by a [pathWidth], like a circular + /// crown. The specified [radius] is for the outer arc, the inner one will + /// have a radius of radius-pathWidth. + /// If [onlyOneWall] is true, just one [ChainShape] is created. + /// The path could be rotated by [rotation] in degrees. factory Path.arc({ Color? color, required Vector2 position, @@ -84,6 +103,14 @@ class Path extends BodyComponent { ); } + /// {@macro path} + /// [Path.straight] creates a bezier curve path for the ball given a + /// [position] for the body, with control point specified by [controlPoints]. + /// First and last points set the beginning and end of the curve, all the + /// inner points between them set the bezier curve final shape. + /// It creates two [ChainShape] separated by a [pathWidth]. If [onlyOneWall] + /// is true, just one [ChainShape] is created (like a wall instead of a path) + /// The path could be rotated by [rotation] in degrees. factory Path.bezierCurve({ Color? color, required Vector2 position, From 8f6bad7a6f7cfc73ba1cbc5bba7984cff4292903 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 14:12:33 +0100 Subject: [PATCH 08/37] test: tests for maths methods --- packages/maths/lib/src/maths.dart | 18 +++-- packages/maths/pubspec.yaml | 10 ++- packages/maths/test/src/maths_test.dart | 87 +++++++++++++++++++++++-- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/packages/maths/lib/src/maths.dart b/packages/maths/lib/src/maths.dart index ea447db5..bdb31d43 100644 --- a/packages/maths/lib/src/maths.dart +++ b/packages/maths/lib/src/maths.dart @@ -3,6 +3,7 @@ import 'package:flame/extensions.dart'; /// Method to calculate all points (with a required precision amount of them) /// of a circumference based on angle, offsetAngle and radius +/// https://en.wikipedia.org/wiki/Trigonometric_functions List calculateArc({ required Vector2 center, required double radius, @@ -47,9 +48,9 @@ List calculateBezierCurve({ final point = controlPoints[i]; xCoord += - _binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x; + 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; + binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y; } points.add(Vector2(xCoord, yCoord)); @@ -61,21 +62,24 @@ List calculateBezierCurve({ /// Method to calculate the binomial coefficient of 'n' and 'k' /// https://en.wikipedia.org/wiki/Binomial_coefficient -num _binomial(num n, num k) { +num binomial(num n, num k) { assert(0 <= k && k <= n, 'Range 0<=k<=n'); if (k == 0 || n == k) { return 1; } else { - return _factorial(n) / (_factorial(k) * _factorial(n - k)); + return factorial(n) / (factorial(k) * factorial(n - k)); } } /// Method to calculate the factorial of some number 'n' /// https://en.wikipedia.org/wiki/Factorial -num _factorial(num n) { - if (n == 1) { +num factorial(num n) { + assert(0 <= n, 'Non negative n'); + if (n == 0) { + return 1; + } else if (n == 1) { return 1; } else { - return n * _factorial(n - 1); + return n * factorial(n - 1); } } diff --git a/packages/maths/pubspec.yaml b/packages/maths/pubspec.yaml index 3b4b863c..b037f4a4 100644 --- a/packages/maths/pubspec.yaml +++ b/packages/maths/pubspec.yaml @@ -6,10 +6,14 @@ publish_to: none environment: sdk: ">=2.16.0 <3.0.0" +dependencies: + flame: ^1.0.0 + flutter: + sdk: flutter + dev_dependencies: - coverage: ^1.1.0 + flutter_test: + sdk: flutter mocktail: ^0.2.0 test: ^1.19.2 very_good_analysis: ^2.4.0 -dependencies: - flame: ^1.0.0 diff --git a/packages/maths/test/src/maths_test.dart b/packages/maths/test/src/maths_test.dart index dceedab8..8faa57cf 100644 --- a/packages/maths/test/src/maths_test.dart +++ b/packages/maths/test/src/maths_test.dart @@ -1,11 +1,90 @@ -// ignore_for_file: prefer_const_constructors +// ignore_for_file: prefer_const_constructors, cascade_invocations +import 'package:flutter_test/flutter_test.dart'; import 'package:maths/maths.dart'; -import 'package:test/test.dart'; + +class Binomial { + Binomial({required this.n, required this.k}); + + final num n; + final num k; +} void main() { group('Maths', () { - test('can be instantiated', () { - expect(Maths(), isNotNull); + group('calculateArc', () {}); + group('calculateBezierCurve', () {}); + + 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); + }); + }); }); }); } From 71033266caa9817a1adc6df50279e494966d9093 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 14:25:21 +0100 Subject: [PATCH 09/37] fix: fixed precision value for calculateArc method --- packages/maths/lib/src/maths.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/maths/lib/src/maths.dart b/packages/maths/lib/src/maths.dart index bdb31d43..e37f8132 100644 --- a/packages/maths/lib/src/maths.dart +++ b/packages/maths/lib/src/maths.dart @@ -11,11 +11,11 @@ List calculateArc({ double offsetAngle = 0, int precision = 100, }) { - final stepAngle = radians(angle / precision); + final stepAngle = radians(angle / (precision - 1)); final stepOffset = radians(offsetAngle); final points = []; - for (var i = 0; i <= precision; i++) { + for (var i = 0; i < precision; i++) { final xCoord = center.x + radius * math.cos((stepAngle * i) + stepOffset); final yCoord = center.y - radius * math.sin((stepAngle * i) + stepOffset); From 53a22f1f1de320eb12ee014303f40a2cd4fc65b6 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 14:32:36 +0100 Subject: [PATCH 10/37] fix: added assert to maths --- packages/maths/lib/src/maths.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/maths/lib/src/maths.dart b/packages/maths/lib/src/maths.dart index e37f8132..d0912418 100644 --- a/packages/maths/lib/src/maths.dart +++ b/packages/maths/lib/src/maths.dart @@ -33,6 +33,7 @@ List calculateBezierCurve({ required List controlPoints, double step = 0.001, }) { + assert(0 <= step && step <= 1, 'Range 0<=step<=1'); assert( controlPoints.length >= 2, 'At least 2 control points to create a bezier curve', From ad8499d173783b8c7883b201ac7801cf7c336de2 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 14:33:01 +0100 Subject: [PATCH 11/37] test: coverage for maths methods --- packages/maths/test/src/maths_test.dart | 67 ++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/maths/test/src/maths_test.dart b/packages/maths/test/src/maths_test.dart index 8faa57cf..2e164989 100644 --- a/packages/maths/test/src/maths_test.dart +++ b/packages/maths/test/src/maths_test.dart @@ -1,4 +1,5 @@ // ignore_for_file: prefer_const_constructors, cascade_invocations +import 'package:flame/extensions.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:maths/maths.dart'; @@ -11,8 +12,70 @@ class Binomial { void main() { group('Maths', () { - group('calculateArc', () {}); - group('calculateBezierCurve', () {}); + group('calculateArc', () { + test('it returns by default 100 points as indicated by precision', () { + final points = calculateArc( + center: Vector2.zero(), + radius: 100, + angle: 90, + ); + expect(points.length, 100); + }); + test('it 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('it returns by default 1000 points as indicated by step', () { + final points = calculateBezierCurve( + controlPoints: [ + Vector2(0, 0), + Vector2(10, 10), + ], + ); + expect(points.length, 1000); + }); + + test('it 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', () { From d6f7bb853d3b7a0c31d4f4fd509554805c199614 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 16:19:18 +0100 Subject: [PATCH 12/37] test: names for tests --- test/game/components/path_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart index 9564c21c..73a7bcfb 100644 --- a/test/game/components/path_test.dart +++ b/test/game/components/path_test.dart @@ -109,7 +109,7 @@ void main() { start: Vector2(10, 10), end: Vector2(20, 20), pathWidth: pathWidth, - onlyOneWall: true, + singleWall: true, ); await game.ensureAdd(path); From 8901bcec70cebd819fc11e0d6165108c402b218b Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 16:20:01 +0100 Subject: [PATCH 13/37] chore: var names and lambda method for identity element --- lib/game/components/path.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/game/components/path.dart b/lib/game/components/path.dart index 69b16b20..d5263024 100644 --- a/lib/game/components/path.dart +++ b/lib/game/components/path.dart @@ -32,7 +32,7 @@ class Path extends BodyComponent { required Vector2 end, required double pathWidth, double rotation = 0, - bool onlyOneWall = false, + bool singleWall = false, }) { final paths = >[]; final wall1 = [ @@ -41,7 +41,7 @@ class Path extends BodyComponent { ]; paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); - if (!onlyOneWall) { + if (!singleWall) { final wall2 = [ start + Vector2(pathWidth, 0), end + Vector2(pathWidth, 0), @@ -72,7 +72,7 @@ class Path extends BodyComponent { required double radius, required double angle, double rotation = 0, - bool onlyOneWall = false, + bool singleWall = false, }) { final paths = >[]; @@ -84,7 +84,7 @@ class Path extends BodyComponent { ); paths.add(wall1); - if (!onlyOneWall) { + if (!singleWall) { final minRadius = radius - pathWidth; final wall2 = calculateArc( @@ -117,7 +117,7 @@ class Path extends BodyComponent { required List controlPoints, required double pathWidth, double rotation = 0, - bool onlyOneWall = false, + bool singleWall = false, }) { final paths = >[]; @@ -125,7 +125,7 @@ class Path extends BodyComponent { paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); var wall2 = []; - if (!onlyOneWall) { + if (!singleWall) { wall2 = calculateBezierCurve( controlPoints: controlPoints .map((e) => e + Vector2(pathWidth, -pathWidth)) @@ -155,7 +155,7 @@ class Path extends BodyComponent { for (final path in _paths) { final chain = ChainShape() ..createChain( - path.map((e) => gameRef.screenToWorld(e)).toList(), + path.map(gameRef.screenToWorld).toList(), ); final fixtureDef = FixtureDef(chain); body.createFixture(fixtureDef); From 20c81533638e1e8b2a833ca079fd6ca52fdd4bfa Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 16:23:21 +0100 Subject: [PATCH 14/37] chore: var and test names not saved --- lib/game/components/path.dart | 6 +++--- packages/maths/test/src/maths_test.dart | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/game/components/path.dart b/lib/game/components/path.dart index d5263024..6f5c786e 100644 --- a/lib/game/components/path.dart +++ b/lib/game/components/path.dart @@ -22,7 +22,7 @@ class Path extends BodyComponent { /// {@macro path} /// [Path.straight] creates a straight path for the ball given a [position] /// for the body, between a [start] and [end] points. - /// It creates two [ChainShape] separated by a [pathWidth]. If [onlyOneWall] + /// It creates two [ChainShape] separated by a [pathWidth]. If [singleWall] /// is true, just one [ChainShape] is created (like a wall instead of a path) /// The path could be rotated by [rotation] in degrees. factory Path.straight({ @@ -63,7 +63,7 @@ class Path extends BodyComponent { /// It creates two [ChainShape] separated by a [pathWidth], like a circular /// crown. The specified [radius] is for the outer arc, the inner one will /// have a radius of radius-pathWidth. - /// If [onlyOneWall] is true, just one [ChainShape] is created. + /// If [singleWall] is true, just one [ChainShape] is created. /// The path could be rotated by [rotation] in degrees. factory Path.arc({ Color? color, @@ -108,7 +108,7 @@ class Path extends BodyComponent { /// [position] for the body, with control point specified by [controlPoints]. /// First and last points set the beginning and end of the curve, all the /// inner points between them set the bezier curve final shape. - /// It creates two [ChainShape] separated by a [pathWidth]. If [onlyOneWall] + /// It creates two [ChainShape] separated by a [pathWidth]. If [singleWall] /// is true, just one [ChainShape] is created (like a wall instead of a path) /// The path could be rotated by [rotation] in degrees. factory Path.bezierCurve({ diff --git a/packages/maths/test/src/maths_test.dart b/packages/maths/test/src/maths_test.dart index 2e164989..eaaf9367 100644 --- a/packages/maths/test/src/maths_test.dart +++ b/packages/maths/test/src/maths_test.dart @@ -13,7 +13,7 @@ class Binomial { void main() { group('Maths', () { group('calculateArc', () { - test('it returns by default 100 points as indicated by precision', () { + test('returns by default 100 points as indicated by precision', () { final points = calculateArc( center: Vector2.zero(), radius: 100, @@ -21,7 +21,7 @@ void main() { ); expect(points.length, 100); }); - test('it returns as many points as indicated by precision', () { + test('returns as many points as indicated by precision', () { final points = calculateArc( center: Vector2.zero(), radius: 100, @@ -55,7 +55,7 @@ void main() { ); }); - test('it returns by default 1000 points as indicated by step', () { + test('returns by default 1000 points as indicated by step', () { final points = calculateBezierCurve( controlPoints: [ Vector2(0, 0), @@ -65,7 +65,7 @@ void main() { expect(points.length, 1000); }); - test('it returns as many points as indicated by step', () { + test('returns as many points as indicated by step', () { final points = calculateBezierCurve( controlPoints: [ Vector2(0, 0), From 7e43e0042e748da82f85208084eb238693ebe40a Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 7 Mar 2022 16:40:01 +0100 Subject: [PATCH 15/37] refactor: changed name of Path to Pathway --- lib/game/components/components.dart | 2 +- .../components/{path.dart => pathway.dart} | 70 +++++----- test/game/components/path_test.dart | 126 +++++++++--------- 3 files changed, 100 insertions(+), 98 deletions(-) rename lib/game/components/{path.dart => pathway.dart} (66%) diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 81e8cad2..a5989fd5 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,6 +1,6 @@ export 'anchor.dart'; export 'ball.dart'; -export 'path.dart'; +export 'pathway.dart'; export 'plunger.dart'; export 'score_points.dart'; export 'wall.dart'; diff --git a/lib/game/components/path.dart b/lib/game/components/pathway.dart similarity index 66% rename from lib/game/components/path.dart rename to lib/game/components/pathway.dart index 6f5c786e..75656e9e 100644 --- a/lib/game/components/path.dart +++ b/lib/game/components/pathway.dart @@ -3,12 +3,12 @@ import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; import 'package:maths/maths.dart'; -/// {@template path} -/// [Path] creates different shapes that sets the pathways that ball can follow -/// or collide to like walls. +/// {@template pathway} +/// [Pathway] creates different shapes that sets the pathwayways that ball +/// can follow or collide to like walls. /// {@endtemplate} -class Path extends BodyComponent { - Path._({ +class Pathway extends BodyComponent { + Pathway._({ Color? color, required Vector2 position, required List> paths, @@ -19,18 +19,19 @@ class Path extends BodyComponent { ..style = PaintingStyle.stroke; } - /// {@macro path} - /// [Path.straight] creates a straight path for the ball given a [position] - /// for the body, between a [start] and [end] points. - /// It creates two [ChainShape] separated by a [pathWidth]. If [singleWall] - /// is true, just one [ChainShape] is created (like a wall instead of a path) - /// The path could be rotated by [rotation] in degrees. - factory Path.straight({ + /// {@macro pathway} + /// [Pathway.straight] creates a straight pathway for the ball given + /// a [position] for the body, between a [start] and [end] points. + /// It creates two [ChainShape] separated by a [pathwayWidth]. If [singleWall] + /// is true, just one [ChainShape] is created + /// (like a wall instead of a pathway) + /// The pathway could be rotated by [rotation] in degrees. + factory Pathway.straight({ Color? color, required Vector2 position, required Vector2 start, required Vector2 end, - required double pathWidth, + required double pathwayWidth, double rotation = 0, bool singleWall = false, }) { @@ -43,32 +44,32 @@ class Path extends BodyComponent { if (!singleWall) { final wall2 = [ - start + Vector2(pathWidth, 0), - end + Vector2(pathWidth, 0), + start + Vector2(pathwayWidth, 0), + end + Vector2(pathwayWidth, 0), ]; paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); } - return Path._( + return Pathway._( color: color, position: position, paths: paths, ); } - /// {@macro path} - /// [Path.straight] creates an arc path for the ball given a [position] + /// {@macro pathway} + /// [Pathway.straight] creates an arc pathway for the ball given a [position] /// for the body, a [radius] for the circumference and an [angle] to specify /// the size of the semi circumference. - /// It creates two [ChainShape] separated by a [pathWidth], like a circular + /// It creates two [ChainShape] separated by a [pathwayWidth], like a circular /// crown. The specified [radius] is for the outer arc, the inner one will - /// have a radius of radius-pathWidth. + /// have a radius of radius-pathwayWidth. /// If [singleWall] is true, just one [ChainShape] is created. - /// The path could be rotated by [rotation] in degrees. - factory Path.arc({ + /// The pathway could be rotated by [rotation] in degrees. + factory Pathway.arc({ Color? color, required Vector2 position, - required double pathWidth, + required double pathwayWidth, required double radius, required double angle, double rotation = 0, @@ -85,7 +86,7 @@ class Path extends BodyComponent { paths.add(wall1); if (!singleWall) { - final minRadius = radius - pathWidth; + final minRadius = radius - pathwayWidth; final wall2 = calculateArc( center: position, @@ -96,26 +97,27 @@ class Path extends BodyComponent { paths.add(wall2); } - return Path._( + return Pathway._( color: color, position: position, paths: paths, ); } - /// {@macro path} - /// [Path.straight] creates a bezier curve path for the ball given a + /// {@macro pathway} + /// [Pathway.straight] creates a bezier curve pathway for the ball given a /// [position] for the body, with control point specified by [controlPoints]. /// First and last points set the beginning and end of the curve, all the /// inner points between them set the bezier curve final shape. - /// It creates two [ChainShape] separated by a [pathWidth]. If [singleWall] - /// is true, just one [ChainShape] is created (like a wall instead of a path) - /// The path could be rotated by [rotation] in degrees. - factory Path.bezierCurve({ + /// It creates two [ChainShape] separated by a [pathwayWidth]. If [singleWall] + /// is true, just one [ChainShape] is created + /// (like a wall instead of a pathway) + /// The pathway could be rotated by [rotation] in degrees. + factory Pathway.bezierCurve({ Color? color, required Vector2 position, required List controlPoints, - required double pathWidth, + required double pathwayWidth, double rotation = 0, bool singleWall = false, }) { @@ -128,13 +130,13 @@ class Path extends BodyComponent { if (!singleWall) { wall2 = calculateBezierCurve( controlPoints: controlPoints - .map((e) => e + Vector2(pathWidth, -pathWidth)) + .map((e) => e + Vector2(pathwayWidth, -pathwayWidth)) .toList(), ); paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); } - return Path._( + return Pathway._( color: color, position: position, paths: paths, diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart index 73a7bcfb..e1c10455 100644 --- a/test/game/components/path_test.dart +++ b/test/game/components/path_test.dart @@ -9,25 +9,25 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final flameTester = FlameTester(PinballGame.new); - group('Path', () { - const pathWidth = 50.0; + group('Pathway', () { + const pathwayWidth = 50.0; group('straight', () { group('color', () { flameTester.test( 'has transparent color by default if not specified', (game) async { - final path = Path.straight( + final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - expect(game.contains(path), isTrue); - expect(path.paint, isNotNull); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); + expect(pathway.paint, isNotNull); expect( - path.paint.color, + pathway.paint.color, equals(Color.fromARGB(0, 0, 0, 0)), ); }, @@ -37,17 +37,17 @@ void main() { (game) async { const defaultColor = Colors.blue; - final path = Path.straight( + final pathway = Pathway.straight( color: defaultColor, position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - expect(game.contains(path), isTrue); - expect(path.paint, isNotNull); - expect(path.paint.color.value, equals(defaultColor.value)); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); + expect(pathway.paint, isNotNull); + expect(pathway.paint.color.value, equals(defaultColor.value)); }, ); }); @@ -55,14 +55,14 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final path = Path.straight( + final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - expect(game.contains(path), isTrue); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -71,31 +71,31 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final path = Path.straight( + final pathway = Pathway.straight( position: position, start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - game.contains(path); + await game.ensureAdd(pathway); + game.contains(pathway); - expect(path.body.position, position); + expect(pathway.body.position, position); }, ); flameTester.test( 'is static', (game) async { - final path = Path.straight( + final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); + await game.ensureAdd(pathway); - expect(path.body.bodyType, equals(BodyType.static)); + expect(pathway.body.bodyType, equals(BodyType.static)); }, ); }); @@ -104,17 +104,17 @@ void main() { flameTester.test( 'exists only one ChainShape if just one wall', (game) async { - final path = Path.straight( + final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, singleWall: true, ); - await game.ensureAdd(path); + await game.ensureAdd(pathway); - expect(path.body.fixtures.length, 1); - final fixture = path.body.fixtures[0]; + expect(pathway.body.fixtures.length, 1); + final fixture = pathway.body.fixtures[0]; expect(fixture, isA()); expect(fixture.shape.shapeType, equals(ShapeType.chain)); }, @@ -123,16 +123,16 @@ void main() { flameTester.test( 'exists two ChainShape if there is by default two walls', (game) async { - final path = Path.straight( + final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); + await game.ensureAdd(pathway); - expect(path.body.fixtures.length, 2); - for (final fixture in path.body.fixtures) { + expect(pathway.body.fixtures.length, 2); + for (final fixture in pathway.body.fixtures) { expect(fixture, isA()); expect(fixture.shape.shapeType, equals(ShapeType.chain)); } @@ -145,14 +145,14 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final path = Path.arc( + final pathway = Pathway.arc( position: Vector2.zero(), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, radius: 100, angle: 90, ); - await game.ensureAdd(path); - expect(game.contains(path), isTrue); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -161,31 +161,31 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final path = Path.arc( + final pathway = Pathway.arc( position: position, - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, radius: 100, angle: 90, ); - await game.ensureAdd(path); - game.contains(path); + await game.ensureAdd(pathway); + game.contains(pathway); - expect(path.body.position, position); + expect(pathway.body.position, position); }, ); flameTester.test( 'is static', (game) async { - final path = Path.arc( + final pathway = Pathway.arc( position: Vector2.zero(), - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, radius: 100, angle: 90, ); - await game.ensureAdd(path); + await game.ensureAdd(pathway); - expect(path.body.bodyType, equals(BodyType.static)); + expect(pathway.body.bodyType, equals(BodyType.static)); }, ); }); @@ -202,13 +202,13 @@ void main() { flameTester.test( 'loads correctly', (game) async { - final path = Path.bezierCurve( + final pathway = Pathway.bezierCurve( position: Vector2.zero(), controlPoints: controlPoints, - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - expect(game.contains(path), isTrue); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -217,29 +217,29 @@ void main() { 'positions correctly', (game) async { final position = Vector2.all(10); - final path = Path.bezierCurve( + final pathway = Pathway.bezierCurve( position: position, controlPoints: controlPoints, - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); - game.contains(path); + await game.ensureAdd(pathway); + game.contains(pathway); - expect(path.body.position, position); + expect(pathway.body.position, position); }, ); flameTester.test( 'is static', (game) async { - final path = Path.bezierCurve( + final pathway = Pathway.bezierCurve( position: Vector2.zero(), controlPoints: controlPoints, - pathWidth: pathWidth, + pathwayWidth: pathwayWidth, ); - await game.ensureAdd(path); + await game.ensureAdd(pathway); - expect(path.body.bodyType, equals(BodyType.static)); + expect(pathway.body.bodyType, equals(BodyType.static)); }, ); }); From 631688f821998de9aa63d1f794dc06a8264e71dd Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Tue, 8 Mar 2022 11:26:57 +0100 Subject: [PATCH 16/37] refactor: path changed to pathway, as well as tests --- test/game/components/path_test.dart | 248 ---------------------------- 1 file changed, 248 deletions(-) delete mode 100644 test/game/components/path_test.dart diff --git a/test/game/components/path_test.dart b/test/game/components/path_test.dart deleted file mode 100644 index e1c10455..00000000 --- a/test/game/components/path_test.dart +++ /dev/null @@ -1,248 +0,0 @@ -// ignore_for_file: cascade_invocations, prefer_const_constructors -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 pathwayWidth = 50.0; - - group('straight', () { - group('color', () { - flameTester.test( - 'has transparent color by default if not specified', - (game) async { - final pathway = Pathway.straight( - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - 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 if set', - (game) async { - const defaultColor = Colors.blue; - - final pathway = Pathway.straight( - color: defaultColor, - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - 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 { - final pathway = Pathway.straight( - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - expect(game.contains(pathway), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final position = Vector2.all(10); - final pathway = Pathway.straight( - position: position, - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - game.contains(pathway); - - expect(pathway.body.position, position); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final pathway = Pathway.straight( - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - - expect(pathway.body.bodyType, equals(BodyType.static)); - }, - ); - }); - - group('fixtures', () { - flameTester.test( - 'exists only one ChainShape if just one wall', - (game) async { - final pathway = Pathway.straight( - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - 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( - 'exists two ChainShape if there is by default two walls', - (game) async { - final pathway = Pathway.straight( - position: Vector2.zero(), - start: Vector2(10, 10), - end: Vector2(20, 20), - pathwayWidth: pathwayWidth, - ); - 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 { - final pathway = Pathway.arc( - position: Vector2.zero(), - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, - ); - await game.ensureAdd(pathway); - expect(game.contains(pathway), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final position = Vector2.all(10); - final pathway = Pathway.arc( - position: position, - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, - ); - await game.ensureAdd(pathway); - game.contains(pathway); - - expect(pathway.body.position, position); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final pathway = Pathway.arc( - position: Vector2.zero(), - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, - ); - 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 { - final pathway = Pathway.bezierCurve( - position: Vector2.zero(), - controlPoints: controlPoints, - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - expect(game.contains(pathway), isTrue); - }, - ); - - group('body', () { - flameTester.test( - 'positions correctly', - (game) async { - final position = Vector2.all(10); - final pathway = Pathway.bezierCurve( - position: position, - controlPoints: controlPoints, - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - game.contains(pathway); - - expect(pathway.body.position, position); - }, - ); - - flameTester.test( - 'is static', - (game) async { - final pathway = Pathway.bezierCurve( - position: Vector2.zero(), - controlPoints: controlPoints, - pathwayWidth: pathwayWidth, - ); - await game.ensureAdd(pathway); - - expect(pathway.body.bodyType, equals(BodyType.static)); - }, - ); - }); - }); - }); -} From 4faf703648317810914a2238d0f9404565cc4a41 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Tue, 8 Mar 2022 11:28:18 +0100 Subject: [PATCH 17/37] chore: applied changes from GH comments, doc descriptions, var names, asserts, etc --- lib/game/components/pathway.dart | 59 +++--- test/game/components/pathway_test.dart | 248 +++++++++++++++++++++++++ 2 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 test/game/components/pathway_test.dart diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 75656e9e..13595cc9 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -4,11 +4,12 @@ import 'package:flutter/material.dart'; import 'package:maths/maths.dart'; /// {@template pathway} -/// [Pathway] creates different shapes that sets the pathwayways that ball -/// can follow or collide to like walls. +/// [Pathway] creates lines of various shapes that the [Ball] can collide +/// with and move along. /// {@endtemplate} class Pathway extends BodyComponent { Pathway._({ + // TODO(ruialonso): remove color when assets added. Color? color, required Vector2 position, required List> paths, @@ -20,10 +21,11 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.straight] creates a straight pathway for the ball given - /// a [position] for the body, between a [start] and [end] points. - /// It creates two [ChainShape] separated by a [pathwayWidth]. If [singleWall] - /// is true, just one [ChainShape] is created + /// [Pathway.straight] creates a straight pathway for the ball. + /// + /// given a [position] for the body, between a [start] and [end] points. + /// It creates two [ChainShape] separated by a [pathwayWidth]. + /// If [singleWall] is true, just one [ChainShape] is created /// (like a wall instead of a pathway) /// The pathway could be rotated by [rotation] in degrees. factory Pathway.straight({ @@ -39,15 +41,15 @@ class Pathway extends BodyComponent { final wall1 = [ start.clone(), end.clone(), - ]; - paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); + ].map((vector) => vector..rotate(radians(rotation))).toList(); + paths.add(wall1); if (!singleWall) { final wall2 = [ start + Vector2(pathwayWidth, 0), end + Vector2(pathwayWidth, 0), - ]; - paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); + ].map((vector) => vector..rotate(radians(rotation))).toList(); + paths.add(wall2); } return Pathway._( @@ -58,9 +60,11 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.straight] creates an arc pathway for the ball given a [position] - /// for the body, a [radius] for the circumference and an [angle] to specify - /// the size of the semi circumference. + /// [Pathway.arc] creates an arc pathway for the ball. + /// + /// The arc is created given a [position] for the body, a [radius] for the + /// circumference and an [angle] to specify the size of it (360 will return + /// a completed circumference and minor angles a semi circumference ). /// It creates two [ChainShape] separated by a [pathwayWidth], like a circular /// crown. The specified [radius] is for the outer arc, the inner one will /// have a radius of radius-pathwayWidth. @@ -86,11 +90,9 @@ class Pathway extends BodyComponent { paths.add(wall1); if (!singleWall) { - final minRadius = radius - pathwayWidth; - final wall2 = calculateArc( center: position, - radius: minRadius, + radius: radius - pathwayWidth, angle: angle, offsetAngle: rotation, ); @@ -105,12 +107,14 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.straight] creates a bezier curve pathway for the ball given a - /// [position] for the body, with control point specified by [controlPoints]. + /// [Pathway.bezierCurve] creates a bezier curve pathway for the ball. + /// + /// The curve is created given a [position] for the body, and + /// with a list of control points specified by [controlPoints]. /// First and last points set the beginning and end of the curve, all the /// inner points between them set the bezier curve final shape. - /// It creates two [ChainShape] separated by a [pathwayWidth]. If [singleWall] - /// is true, just one [ChainShape] is created + /// It creates two [ChainShape] separated by a [pathwayWidth]. + /// If [singleWall] is true, just one [ChainShape] is created /// (like a wall instead of a pathway) /// The pathway could be rotated by [rotation] in degrees. factory Pathway.bezierCurve({ @@ -123,17 +127,18 @@ class Pathway extends BodyComponent { }) { final paths = >[]; - final wall1 = calculateBezierCurve(controlPoints: controlPoints); - paths.add(wall1.map((e) => e..rotate(radians(rotation))).toList()); + final wall1 = calculateBezierCurve(controlPoints: controlPoints) + .map((vector) => vector..rotate(radians(rotation))) + .toList(); + paths.add(wall1); - var wall2 = []; if (!singleWall) { - wall2 = calculateBezierCurve( + final wall2 = calculateBezierCurve( controlPoints: controlPoints - .map((e) => e + Vector2(pathwayWidth, -pathwayWidth)) + .map((vector) => vector + Vector2(pathwayWidth, -pathwayWidth)) .toList(), - ); - paths.add(wall2.map((e) => e..rotate(radians(rotation))).toList()); + ).map((vector) => vector..rotate(radians(rotation))).toList(); + paths.add(wall2); } return Pathway._( diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart new file mode 100644 index 00000000..e1c10455 --- /dev/null +++ b/test/game/components/pathway_test.dart @@ -0,0 +1,248 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors +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 pathwayWidth = 50.0; + + group('straight', () { + group('color', () { + flameTester.test( + 'has transparent color by default if not specified', + (game) async { + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + 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 if set', + (game) async { + const defaultColor = Colors.blue; + + final pathway = Pathway.straight( + color: defaultColor, + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + 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 { + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final pathway = Pathway.straight( + position: position, + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + game.contains(pathway); + + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + + group('fixtures', () { + flameTester.test( + 'exists only one ChainShape if just one wall', + (game) async { + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + 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( + 'exists two ChainShape if there is by default two walls', + (game) async { + final pathway = Pathway.straight( + position: Vector2.zero(), + start: Vector2(10, 10), + end: Vector2(20, 20), + pathwayWidth: pathwayWidth, + ); + 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 { + final pathway = Pathway.arc( + position: Vector2.zero(), + pathwayWidth: pathwayWidth, + radius: 100, + angle: 90, + ); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final pathway = Pathway.arc( + position: position, + pathwayWidth: pathwayWidth, + radius: 100, + angle: 90, + ); + await game.ensureAdd(pathway); + game.contains(pathway); + + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final pathway = Pathway.arc( + position: Vector2.zero(), + pathwayWidth: pathwayWidth, + radius: 100, + angle: 90, + ); + 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 { + final pathway = Pathway.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); + }, + ); + + group('body', () { + flameTester.test( + 'positions correctly', + (game) async { + final position = Vector2.all(10); + final pathway = Pathway.bezierCurve( + position: position, + controlPoints: controlPoints, + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + game.contains(pathway); + + expect(pathway.body.position, position); + }, + ); + + flameTester.test( + 'is static', + (game) async { + final pathway = Pathway.bezierCurve( + position: Vector2.zero(), + controlPoints: controlPoints, + pathwayWidth: pathwayWidth, + ); + await game.ensureAdd(pathway); + + expect(pathway.body.bodyType, equals(BodyType.static)); + }, + ); + }); + }); + }); +} From 4290cb5a8d517c9ecafb9f8d8c5fbf34f46c8b6c Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Tue, 8 Mar 2022 11:37:08 +0100 Subject: [PATCH 18/37] refactor: changed math package to geometry --- lib/game/components/pathway.dart | 8 ++-- packages/{maths => geometry}/.gitignore | 0 packages/{maths => geometry}/README.md | 0 .../{maths => geometry}/analysis_options.yaml | 0 packages/geometry/lib/geometry.dart | 3 ++ .../lib/src/geometry.dart} | 46 +++++++++++++------ packages/{maths => geometry}/pubspec.yaml | 2 +- .../test/src/geometry_test.dart} | 9 +++- packages/maths/lib/maths.dart | 3 -- pubspec.lock | 14 +++--- pubspec.yaml | 4 +- 11 files changed, 56 insertions(+), 33 deletions(-) rename packages/{maths => geometry}/.gitignore (100%) rename packages/{maths => geometry}/README.md (100%) rename packages/{maths => geometry}/analysis_options.yaml (100%) create mode 100644 packages/geometry/lib/geometry.dart rename packages/{maths/lib/src/maths.dart => geometry/lib/src/geometry.dart} (53%) rename packages/{maths => geometry}/pubspec.yaml (95%) rename packages/{maths/test/src/maths_test.dart => geometry/test/src/geometry_test.dart} (98%) delete mode 100644 packages/maths/lib/maths.dart diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 13595cc9..1fc48af1 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -1,7 +1,7 @@ import 'package:flame/extensions.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/material.dart'; -import 'package:maths/maths.dart'; +import 'package:geometry/geometry.dart'; /// {@template pathway} /// [Pathway] creates lines of various shapes that the [Ball] can collide @@ -21,7 +21,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.straight] creates a straight pathway for the ball. + /// [Pathway.straight] creates a straight pathway for the [Ball]. /// /// given a [position] for the body, between a [start] and [end] points. /// It creates two [ChainShape] separated by a [pathwayWidth]. @@ -60,7 +60,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.arc] creates an arc pathway for the ball. + /// [Pathway.arc] creates an arc pathway for the [Ball]. /// /// The arc is created given a [position] for the body, a [radius] for the /// circumference and an [angle] to specify the size of it (360 will return @@ -107,7 +107,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.bezierCurve] creates a bezier curve pathway for the ball. + /// [Pathway.bezierCurve] creates a bezier curve pathway for the [Ball]. /// /// The curve is created given a [position] for the body, and /// with a list of control points specified by [controlPoints]. diff --git a/packages/maths/.gitignore b/packages/geometry/.gitignore similarity index 100% rename from packages/maths/.gitignore rename to packages/geometry/.gitignore diff --git a/packages/maths/README.md b/packages/geometry/README.md similarity index 100% rename from packages/maths/README.md rename to packages/geometry/README.md diff --git a/packages/maths/analysis_options.yaml b/packages/geometry/analysis_options.yaml similarity index 100% rename from packages/maths/analysis_options.yaml rename to packages/geometry/analysis_options.yaml 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/maths/lib/src/maths.dart b/packages/geometry/lib/src/geometry.dart similarity index 53% rename from packages/maths/lib/src/maths.dart rename to packages/geometry/lib/src/geometry.dart index d0912418..c992e5a2 100644 --- a/packages/maths/lib/src/maths.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -1,9 +1,15 @@ import 'dart:math' as math; import 'package:flame/extensions.dart'; -/// Method to calculate all points (with a required precision amount of them) -/// of a circumference based on angle, offsetAngle and radius -/// https://en.wikipedia.org/wiki/Trigonometric_functions +/// Calculates all [Vector2]s of a circumference. +/// +/// Circumference is created from a [center] and a [radius] +/// Also semi circumference could be created, specifying its [angle] in degrees +/// and the offset start angle [offsetAngle] for this semi circumference. +/// 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, @@ -26,18 +32,28 @@ List calculateArc({ return points; } -/// Method that calculates all points of a bezier curve of degree 'g' and -/// n=g-1 control points and range 0<=t<=1 -/// https://en.wikipedia.org/wiki/B%C3%A9zier_curve +/// 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 in range 0<=step<=1 and indicates 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, 'Range 0<=step<=1'); + assert( + 0 <= step && step <= 1, + 'Step ($step) must be in range 0 <= step <= 1', + ); assert( controlPoints.length >= 2, - 'At least 2 control points to create a bezier curve', + 'At least 2 control points needed to create a bezier curve', ); + var t = 0.0; final n = controlPoints.length - 1; final points = []; @@ -62,9 +78,10 @@ List calculateBezierCurve({ } /// Method to calculate the binomial coefficient of 'n' and 'k' -/// https://en.wikipedia.org/wiki/Binomial_coefficient +/// For more information read: https://en.wikipedia.org/wiki/Binomial_coefficient num binomial(num n, num k) { - assert(0 <= k && k <= n, 'Range 0<=k<=n'); + 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 { @@ -73,12 +90,11 @@ num binomial(num n, num k) { } /// Method to calculate the factorial of some number 'n' -/// https://en.wikipedia.org/wiki/Factorial +/// For more information read: https://en.wikipedia.org/wiki/Factorial num factorial(num n) { - assert(0 <= n, 'Non negative n'); - if (n == 0) { - return 1; - } else if (n == 1) { + 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/maths/pubspec.yaml b/packages/geometry/pubspec.yaml similarity index 95% rename from packages/maths/pubspec.yaml rename to packages/geometry/pubspec.yaml index b037f4a4..99c09cd0 100644 --- a/packages/maths/pubspec.yaml +++ b/packages/geometry/pubspec.yaml @@ -1,4 +1,4 @@ -name: maths +name: geometry description: A Very Good Project created by Very Good CLI. version: 1.0.0+1 publish_to: none diff --git a/packages/maths/test/src/maths_test.dart b/packages/geometry/test/src/geometry_test.dart similarity index 98% rename from packages/maths/test/src/maths_test.dart rename to packages/geometry/test/src/geometry_test.dart index eaaf9367..6ecf66c8 100644 --- a/packages/maths/test/src/maths_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -1,7 +1,7 @@ // ignore_for_file: prefer_const_constructors, cascade_invocations import 'package:flame/extensions.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:maths/maths.dart'; +import 'package:geometry/geometry.dart'; class Binomial { Binomial({required this.n, required this.k}); @@ -31,6 +31,7 @@ void main() { expect(points.length, 50); }); }); + group('calculateBezierCurve', () { test('fails if step not in range', () { expect( @@ -44,6 +45,7 @@ void main() { throwsAssertionError, ); }); + test('fails if not enough control points', () { expect( () => calculateBezierCurve(controlPoints: [Vector2.zero()]), @@ -81,12 +83,15 @@ void main() { 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, @@ -123,10 +128,12 @@ void main() { }); }); }); + group('factorial', () { test('fails if negative number', () { expect(() => factorial(-1), throwsAssertionError); }); + test('for a specific input gives a correct value', () { final factorialInputsToExpected = { 0: 1, diff --git a/packages/maths/lib/maths.dart b/packages/maths/lib/maths.dart deleted file mode 100644 index b340388b..00000000 --- a/packages/maths/lib/maths.dart +++ /dev/null @@ -1,3 +0,0 @@ -library maths; - -export 'src/maths.dart'; diff --git a/pubspec.lock b/pubspec.lock index db3b239a..5dee0c71 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: @@ -261,13 +268,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" - maths: - dependency: "direct main" - description: - path: "/Users/ruialonso/dev/flutter/googleIO22/pinball/packages/maths" - relative: false - source: path - version: "1.0.0+1" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7b9c9c19..1d0c8ada 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,9 +17,9 @@ dependencies: flutter_bloc: ^8.0.1 flutter_localizations: sdk: flutter + geometry: + path: packages/geometry intl: ^0.17.0 - maths: - path: packages/maths dev_dependencies: bloc_test: ^9.0.2 From 178876b4fc8f5124f5370d8ca7a2844f311bbb44 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Tue, 8 Mar 2022 11:40:07 +0100 Subject: [PATCH 19/37] refactor: changed geometry package description --- packages/geometry/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/geometry/pubspec.yaml b/packages/geometry/pubspec.yaml index 99c09cd0..2678cdef 100644 --- a/packages/geometry/pubspec.yaml +++ b/packages/geometry/pubspec.yaml @@ -1,5 +1,5 @@ name: geometry -description: A Very Good Project created by Very Good CLI. +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 From 6b3158b019cff4e9426c8a0325638843d0b1a58b Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Tue, 8 Mar 2022 11:41:45 +0100 Subject: [PATCH 20/37] chore: removed unscope Ball refs in doc --- lib/game/components/pathway.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 1fc48af1..d6582a91 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart'; /// {@template pathway} -/// [Pathway] creates lines of various shapes that the [Ball] can collide +/// [Pathway] creates lines of various shapes that the ball can collide /// with and move along. /// {@endtemplate} class Pathway extends BodyComponent { @@ -21,7 +21,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.straight] creates a straight pathway for the [Ball]. + /// [Pathway.straight] creates a straight pathway for the ball. /// /// given a [position] for the body, between a [start] and [end] points. /// It creates two [ChainShape] separated by a [pathwayWidth]. @@ -60,7 +60,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.arc] creates an arc pathway for the [Ball]. + /// [Pathway.arc] creates an arc pathway for the ball. /// /// The arc is created given a [position] for the body, a [radius] for the /// circumference and an [angle] to specify the size of it (360 will return @@ -107,7 +107,7 @@ class Pathway extends BodyComponent { } /// {@macro pathway} - /// [Pathway.bezierCurve] creates a bezier curve pathway for the [Ball]. + /// [Pathway.bezierCurve] creates a bezier curve pathway for the ball. /// /// The curve is created given a [position] for the body, and /// with a list of control points specified by [controlPoints]. From 0780266e2ac1d2605ad4e5086de3a543cbdce0b7 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 17:57:04 -0300 Subject: [PATCH 21/37] feat: adding deploy site --- firebase.json | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase.json b/firebase.json index 0d25a779..80e2ae69 100644 --- a/firebase.json +++ b/firebase.json @@ -1,6 +1,7 @@ { "hosting": { "public": "build/web", + "site": "ashehwkdkdjruejdnensjsjdne", "ignore": [ "firebase.json", "**/.*", From df2ce91b063daab7cad9ec75fd726626372de4ff Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 19:23:10 -0300 Subject: [PATCH 22/37] feat: adding bonus letter feature state management --- lib/game/bloc/game_bloc.dart | 12 +++++ lib/game/bloc/game_event.dart | 9 ++++ lib/game/bloc/game_state.dart | 10 +++- test/game/bloc/game_bloc_test.dart | 62 +++++++++++++++++++--- test/game/bloc/game_event_test.dart | 17 ++++++ test/game/bloc/game_state_test.dart | 29 ++++++++-- test/game/components/ball_test.dart | 2 +- test/game/view/pinball_game_page_test.dart | 2 +- 8 files changed, 127 insertions(+), 16 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 71c527a8..3cfc521f 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -9,6 +9,7 @@ class GameBloc extends Bloc { GameBloc() : super(const GameState.initial()) { on(_onBallLost); on(_onScored); + on(_onBonusLetterActivated); } void _onBallLost(BallLost event, Emitter emit) { @@ -22,4 +23,15 @@ class GameBloc extends Bloc { emit(state.copyWith(score: state.score + event.points)); } } + + void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { + emit( + state.copyWith( + bonusLetter: [ + ...state.bonusLetter, + event.letter, + ], + ), + ); + } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index 88ef265b..417f6322 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -24,3 +24,12 @@ class Scored extends GameEvent { @override List get props => [points]; } + +class BonusLetterActivated extends GameEvent { + const BonusLetterActivated(this.letter); + + final String letter; + + @override + List get props => [letter]; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 1a0568f7..8454bab7 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -8,12 +8,14 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, + required this.bonusLetter, }) : 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; + balls = 3, + bonusLetter = const []; /// The current score of the game. final int score; @@ -23,6 +25,9 @@ class GameState extends Equatable { /// When the number of balls is 0, the game is over. final int balls; + /// Active bonus letters + final List bonusLetter; + /// Determines when the game is over. bool get isGameOver => balls == 0; @@ -32,6 +37,7 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, + List? bonusLetter, }) { assert( score == null || score >= this.score, @@ -41,6 +47,7 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, + bonusLetter: bonusLetter ?? this.bonusLetter, ); } @@ -48,5 +55,6 @@ class GameState extends Equatable { List get props => [ score, balls, + bonusLetter, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 2676a286..3dc5dda7 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,9 +21,9 @@ void main() { } }, expect: () => [ - const GameState(score: 0, balls: 2), - const GameState(score: 0, balls: 1), - const GameState(score: 0, balls: 0), + const GameState(score: 0, balls: 2, bonusLetter: []), + const GameState(score: 0, balls: 1, bonusLetter: []), + const GameState(score: 0, balls: 0, bonusLetter: []), ], ); }); @@ -37,8 +37,8 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState(score: 2, balls: 3), - const GameState(score: 5, balls: 3), + const GameState(score: 2, balls: 3, bonusLetter: []), + const GameState(score: 5, balls: 3, bonusLetter: []), ], ); @@ -53,9 +53,55 @@ void main() { bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState(score: 0, balls: 2), - const GameState(score: 0, balls: 1), - const GameState(score: 0, balls: 0), + const GameState(score: 0, balls: 2, bonusLetter: []), + const GameState(score: 0, balls: 1, bonusLetter: []), + const GameState(score: 0, balls: 0, bonusLetter: []), + ], + ); + }); + + group('BonusLetterActivated', () { + blocTest( + 'adds the letter to the state', + build: GameBloc.new, + act: (bloc) => bloc + ..add(const BonusLetterActivated('G')) + ..add(const BonusLetterActivated('O')) + ..add(const BonusLetterActivated('O')) + ..add(const BonusLetterActivated('G')) + ..add(const BonusLetterActivated('L')) + ..add(const BonusLetterActivated('E')), + expect: () => [ + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G', 'L'], + ), + const GameState( + score: 0, + balls: 3, + bonusLetter: ['G', 'O', 'O', 'G', 'L', 'E'], + ), ], ); }); diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index e839ab56..0e7a0f71 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -40,5 +40,22 @@ void main() { expect(() => Scored(points: 0), throwsAssertionError); }); }); + + group('BonusLetterActivated', () { + test('can be instantiated', () { + expect(const BonusLetterActivated('A'), isNotNull); + }); + + test('supports value equality', () { + expect( + BonusLetterActivated('A'), + equals(BonusLetterActivated('A')), + ); + expect( + BonusLetterActivated('B'), + isNot(equals(BonusLetterActivated('A'))), + ); + }); + }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 59cc0d1d..e50acbcd 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -7,14 +7,24 @@ void main() { group('GameState', () { test('supports value equality', () { expect( - GameState(score: 0, balls: 0), - equals(const GameState(score: 0, balls: 0)), + GameState( + score: 0, + balls: 0, + bonusLetter: const [], + ), + equals( + const GameState( + score: 0, + balls: 0, + bonusLetter: [], + ), + ), ); }); group('constructor', () { test('can be instantiated', () { - expect(const GameState(score: 0, balls: 0), isNotNull); + expect(const GameState(score: 0, balls: 0, bonusLetter: []), isNotNull); }); }); @@ -23,7 +33,7 @@ void main() { 'when balls are negative', () { expect( - () => GameState(balls: -1, score: 0), + () => GameState(balls: -1, score: 0, bonusLetter: const []), throwsAssertionError, ); }, @@ -34,7 +44,7 @@ void main() { 'when score is negative', () { expect( - () => GameState(balls: 0, score: -1), + () => GameState(balls: 0, score: -1, bonusLetter: const []), throwsAssertionError, ); }, @@ -47,6 +57,7 @@ void main() { const gameState = GameState( balls: 0, score: 0, + bonusLetter: [], ); expect(gameState.isGameOver, isTrue); }); @@ -57,6 +68,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, + bonusLetter: [], ); expect(gameState.isGameOver, isFalse); }); @@ -70,6 +82,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, + bonusLetter: [], ); expect(gameState.isLastBall, isTrue); }, @@ -82,6 +95,7 @@ void main() { const gameState = GameState( balls: 2, score: 0, + bonusLetter: [], ); expect(gameState.isLastBall, isFalse); }, @@ -96,6 +110,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, + bonusLetter: [], ); expect( () => gameState.copyWith(score: gameState.score - 1), @@ -111,6 +126,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, + bonusLetter: [], ); expect( gameState.copyWith(), @@ -126,10 +142,12 @@ void main() { const gameState = GameState( score: 2, balls: 0, + bonusLetter: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, + bonusLetter: const ['A'], ); expect(gameState, isNot(equals(otherGameState))); @@ -137,6 +155,7 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, + bonusLetter: otherGameState.bonusLetter, ), equals(otherGameState), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 7ac3ceff..9885b310 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -135,7 +135,7 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState(score: 10, balls: 1), + initialState: const GameState(score: 10, balls: 1, bonusLetter: []), ); await game.ready(); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index be418c1d..eacee734 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -67,7 +67,7 @@ void main() { 'renders a game over dialog when the user has lost', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0); + const state = GameState(score: 0, balls: 0, bonusLetter: []); whenListen( gameBloc, Stream.value(state), From de25974f8c9b50bc31fd416f28a08bab0445a98a Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 9 Mar 2022 09:48:09 -0300 Subject: [PATCH 23/37] Update lib/game/bloc/game_state.dart Co-authored-by: Alejandro Santiago --- lib/game/bloc/game_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 8454bab7..09207b86 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -25,7 +25,7 @@ class GameState extends Equatable { /// When the number of balls is 0, the game is over. final int balls; - /// Active bonus letters + /// Active bonus letters. final List bonusLetter; /// Determines when the game is over. From e4cd4342c0c2bc40724cfed8deac93caf3abffa0 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 09:53:52 -0300 Subject: [PATCH 24/37] feat: pr suggestions --- lib/game/bloc/game_bloc.dart | 4 +-- lib/game/bloc/game_state.dart | 12 ++++----- test/game/bloc/game_bloc_test.dart | 28 +++++++++---------- test/game/bloc/game_state_test.dart | 31 ++++++++++++---------- test/game/components/ball_test.dart | 6 ++++- test/game/view/pinball_game_page_test.dart | 2 +- 6 files changed, 45 insertions(+), 38 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 3cfc521f..3b5c16b0 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -27,8 +27,8 @@ class GameBloc extends Bloc { void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) { emit( state.copyWith( - bonusLetter: [ - ...state.bonusLetter, + bonusLetters: [ + ...state.bonusLetters, event.letter, ], ), diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index 09207b86..8a5ab298 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -8,14 +8,14 @@ class GameState extends Equatable { const GameState({ required this.score, required this.balls, - required this.bonusLetter, + required this.bonusLetters, }) : 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, - bonusLetter = const []; + bonusLetters = const []; /// The current score of the game. final int score; @@ -26,7 +26,7 @@ class GameState extends Equatable { final int balls; /// Active bonus letters. - final List bonusLetter; + final List bonusLetters; /// Determines when the game is over. bool get isGameOver => balls == 0; @@ -37,7 +37,7 @@ class GameState extends Equatable { GameState copyWith({ int? score, int? balls, - List? bonusLetter, + List? bonusLetters, }) { assert( score == null || score >= this.score, @@ -47,7 +47,7 @@ class GameState extends Equatable { return GameState( score: score ?? this.score, balls: balls ?? this.balls, - bonusLetter: bonusLetter ?? this.bonusLetter, + bonusLetters: bonusLetters ?? this.bonusLetters, ); } @@ -55,6 +55,6 @@ class GameState extends Equatable { List get props => [ score, balls, - bonusLetter, + bonusLetters, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 3dc5dda7..bd669397 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -21,9 +21,9 @@ void main() { } }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetter: []), - const GameState(score: 0, balls: 1, bonusLetter: []), - const GameState(score: 0, balls: 0, bonusLetter: []), + const GameState(score: 0, balls: 2, bonusLetters: []), + const GameState(score: 0, balls: 1, bonusLetters: []), + const GameState(score: 0, balls: 0, bonusLetters: []), ], ); }); @@ -37,8 +37,8 @@ void main() { ..add(const Scored(points: 2)) ..add(const Scored(points: 3)), expect: () => [ - const GameState(score: 2, balls: 3, bonusLetter: []), - const GameState(score: 5, balls: 3, bonusLetter: []), + const GameState(score: 2, balls: 3, bonusLetters: []), + const GameState(score: 5, balls: 3, bonusLetters: []), ], ); @@ -53,9 +53,9 @@ void main() { bloc.add(const Scored(points: 2)); }, expect: () => [ - const GameState(score: 0, balls: 2, bonusLetter: []), - const GameState(score: 0, balls: 1, bonusLetter: []), - const GameState(score: 0, balls: 0, bonusLetter: []), + const GameState(score: 0, balls: 2, bonusLetters: []), + const GameState(score: 0, balls: 1, bonusLetters: []), + const GameState(score: 0, balls: 0, bonusLetters: []), ], ); }); @@ -75,32 +75,32 @@ void main() { const GameState( score: 0, balls: 3, - bonusLetter: ['G'], + bonusLetters: ['G'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O'], + bonusLetters: ['G', 'O'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O'], + bonusLetters: ['G', 'O', 'O'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G'], + bonusLetters: ['G', 'O', 'O', 'G'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G', 'L'], + bonusLetters: ['G', 'O', 'O', 'G', 'L'], ), const GameState( score: 0, balls: 3, - bonusLetter: ['G', 'O', 'O', 'G', 'L', 'E'], + bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'], ), ], ); diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index e50acbcd..7345d3bd 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -10,13 +10,13 @@ void main() { GameState( score: 0, balls: 0, - bonusLetter: const [], + bonusLetters: const [], ), equals( const GameState( score: 0, balls: 0, - bonusLetter: [], + bonusLetters: [], ), ), ); @@ -24,7 +24,10 @@ void main() { group('constructor', () { test('can be instantiated', () { - expect(const GameState(score: 0, balls: 0, bonusLetter: []), isNotNull); + expect( + const GameState(score: 0, balls: 0, bonusLetters: []), + isNotNull, + ); }); }); @@ -33,7 +36,7 @@ void main() { 'when balls are negative', () { expect( - () => GameState(balls: -1, score: 0, bonusLetter: const []), + () => GameState(balls: -1, score: 0, bonusLetters: const []), throwsAssertionError, ); }, @@ -44,7 +47,7 @@ void main() { 'when score is negative', () { expect( - () => GameState(balls: 0, score: -1, bonusLetter: const []), + () => GameState(balls: 0, score: -1, bonusLetters: const []), throwsAssertionError, ); }, @@ -57,7 +60,7 @@ void main() { const gameState = GameState( balls: 0, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isGameOver, isTrue); }); @@ -68,7 +71,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isGameOver, isFalse); }); @@ -82,7 +85,7 @@ void main() { const gameState = GameState( balls: 1, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isLastBall, isTrue); }, @@ -95,7 +98,7 @@ void main() { const gameState = GameState( balls: 2, score: 0, - bonusLetter: [], + bonusLetters: [], ); expect(gameState.isLastBall, isFalse); }, @@ -110,7 +113,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetter: [], + bonusLetters: [], ); expect( () => gameState.copyWith(score: gameState.score - 1), @@ -126,7 +129,7 @@ void main() { const gameState = GameState( balls: 0, score: 2, - bonusLetter: [], + bonusLetters: [], ); expect( gameState.copyWith(), @@ -142,12 +145,12 @@ void main() { const gameState = GameState( score: 2, balls: 0, - bonusLetter: [], + bonusLetters: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, - bonusLetter: const ['A'], + bonusLetters: const ['A'], ); expect(gameState, isNot(equals(otherGameState))); @@ -155,7 +158,7 @@ void main() { gameState.copyWith( score: otherGameState.score, balls: otherGameState.balls, - bonusLetter: otherGameState.bonusLetter, + bonusLetters: otherGameState.bonusLetters, ), equals(otherGameState), ); diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 9885b310..bd2cbcfc 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -135,7 +135,11 @@ void main() { whenListen( gameBloc, const Stream.empty(), - initialState: const GameState(score: 10, balls: 1, bonusLetter: []), + initialState: const GameState( + score: 10, + balls: 1, + bonusLetters: [], + ), ); await game.ready(); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index eacee734..d578a1db 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -67,7 +67,7 @@ void main() { 'renders a game over dialog when the user has lost', (tester) async { final gameBloc = MockGameBloc(); - const state = GameState(score: 0, balls: 0, bonusLetter: []); + const state = GameState(score: 0, balls: 0, bonusLetters: []); whenListen( gameBloc, Stream.value(state), From a3dc9d09cff15dfb97409f099981eca355585c5d Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 8 Mar 2022 18:45:51 -0300 Subject: [PATCH 25/37] feat: adding game hud --- lib/game/view/game_hud.dart | 40 ++++++++++++++++++++++ lib/game/view/pinball_game_page.dart | 13 ++++++- lib/game/view/view.dart | 1 + test/game/view/game_hud_test.dart | 32 +++++++++++++++++ test/game/view/pinball_game_page_test.dart | 6 +++- 5 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 lib/game/view/game_hud.dart create mode 100644 test/game/view/game_hud_test.dart diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart new file mode 100644 index 00000000..b694e812 --- /dev/null +++ b/lib/game/view/game_hud.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:pinball/game/game.dart'; + +class GameHud extends StatelessWidget { + const GameHud({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.watch().state; + + return Container( + color: Colors.redAccent, + width: 200, + height: 100, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${state.score}', + style: Theme.of(context).textTheme.headline3, + ), + Column( + children: [ + for (var i = 0; i < state.balls; i++) + const Padding( + padding: EdgeInsets.only(top: 6), + child: CircleAvatar( + radius: 8, + backgroundColor: Colors.black, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/game/view/pinball_game_page.dart b/lib/game/view/pinball_game_page.dart index 02f5b34c..8a9a981c 100644 --- a/lib/game/view/pinball_game_page.dart +++ b/lib/game/view/pinball_game_page.dart @@ -56,7 +56,18 @@ class _PinballGameViewState extends State { ); } }, - child: GameWidget(game: _game), + child: Stack( + children: [ + Positioned.fill( + child: GameWidget(game: _game), + ), + const Positioned( + top: 8, + left: 8, + child: GameHud(), + ), + ], + ), ); } } diff --git a/lib/game/view/view.dart b/lib/game/view/view.dart index 53d3813a..26b700d3 100644 --- a/lib/game/view/view.dart +++ b/lib/game/view/view.dart @@ -1,2 +1,3 @@ +export 'game_hud.dart'; export 'pinball_game_page.dart'; export 'widgets/widgets.dart'; diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart new file mode 100644 index 00000000..40079a2f --- /dev/null +++ b/test/game/view/game_hud_test.dart @@ -0,0 +1,32 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball/game/game.dart'; +import '../../helpers/helpers.dart'; + +void main() { + group('GameHud', () { + testWidgets( + 'renders the current score and balls', + (tester) async { + final state = GameState(score: 10, balls: 2); + final gameBloc = MockGameBloc(); + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + + expect(find.text('10'), findsOneWidget); + expect(find.byType(CircleAvatar), findsNWidgets(2)); + }, + ); + }); +} diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index d578a1db..746dc2c7 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -48,7 +48,7 @@ void main() { }); group('PinballGameView', () { - testWidgets('renders game', (tester) async { + testWidgets('renders game and a hud', (tester) async { final gameBloc = MockGameBloc(); whenListen( gameBloc, @@ -61,6 +61,10 @@ void main() { find.byWidgetPredicate((w) => w is GameWidget), findsOneWidget, ); + expect( + find.byType(GameHud), + findsOneWidget, + ); }); testWidgets( From f9f109ba5aeadfd8618439187b8ea3bad8a88b5f Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 9 Mar 2022 10:02:50 -0300 Subject: [PATCH 26/37] Apply suggestions from code review Co-authored-by: Alejandro Santiago --- lib/game/view/game_hud.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index b694e812..beff3391 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -2,7 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; +/// {@template game_hud} +/// Overlay of a [PinballGame] that displays the current [GameState.score] and +/// [GameState.balls]. +/// {@endtemplate} class GameHud extends StatelessWidget { + /// {@macro game_hud} const GameHud({Key? key}) : super(key: key); @override From 38b8a28ffc661563374f37f64e6dc0012475304f Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:18:54 -0300 Subject: [PATCH 27/37] feat: pr suggestions --- lib/game/view/game_hud.dart | 5 ++- test/game/view/game_hud_test.dart | 75 +++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index beff3391..7bf1e8c3 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -26,11 +26,12 @@ class GameHud extends StatelessWidget { '${state.score}', style: Theme.of(context).textTheme.headline3, ), - Column( + Wrap( + direction: Axis.vertical, children: [ for (var i = 0; i < state.balls; i++) const Padding( - padding: EdgeInsets.only(top: 6), + padding: EdgeInsets.only(top: 6, right: 6), child: CircleAvatar( radius: 8, backgroundColor: Colors.black, diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 40079a2f..27a423ed 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -8,25 +8,72 @@ import '../../helpers/helpers.dart'; void main() { group('GameHud', () { + late GameBloc gameBloc; + const initialState = GameState(score: 10, balls: 2); + + void _mockState(GameState state) { + whenListen( + gameBloc, + Stream.value(state), + initialState: state, + ); + } + + Future _pumpHud(WidgetTester tester) async { + await tester.pumpApp( + GameHud(), + gameBloc: gameBloc, + ); + } + + setUp(() { + gameBloc = MockGameBloc(); + _mockState(initialState); + }); + testWidgets( - 'renders the current score and balls', + 'renders the current score', (tester) async { - final state = GameState(score: 10, balls: 2); - final gameBloc = MockGameBloc(); - whenListen( - gameBloc, - Stream.value(state), - initialState: state, - ); + await _pumpHud(tester); + expect(find.text(initialState.score.toString()), findsOneWidget); + }, + ); - await tester.pumpApp( - GameHud(), - gameBloc: gameBloc, + testWidgets( + 'renders the current ball number', + (tester) async { + await _pumpHud(tester); + expect( + find.byType(CircleAvatar), + findsNWidgets(initialState.balls), ); - - expect(find.text('10'), findsOneWidget); - expect(find.byType(CircleAvatar), findsNWidgets(2)); }, ); + + testWidgets('updates the score', (tester) async { + await _pumpHud(tester); + expect(find.text(initialState.score.toString()), findsOneWidget); + + _mockState(initialState.copyWith(score: 20)); + + await tester.pump(); + expect(find.text('20'), findsOneWidget); + }); + + testWidgets('updates the ball number', (tester) async { + await _pumpHud(tester); + expect( + find.byType(CircleAvatar), + findsNWidgets(initialState.balls), + ); + + _mockState(initialState.copyWith(balls: 1)); + + await tester.pump(); + expect( + find.byType(CircleAvatar), + findsNWidgets(1), + ); + }); }); } From 884d8b36b07974dff833f37cc669721d58a26671 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:22:17 -0300 Subject: [PATCH 28/37] fix: lint --- lib/game/view/game_hud.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/game/view/game_hud.dart b/lib/game/view/game_hud.dart index 7bf1e8c3..00eedd2b 100644 --- a/lib/game/view/game_hud.dart +++ b/lib/game/view/game_hud.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pinball/game/game.dart'; /// {@template game_hud} -/// Overlay of a [PinballGame] that displays the current [GameState.score] and +/// Overlay of a [PinballGame] that displays the current [GameState.score] and /// [GameState.balls]. /// {@endtemplate} class GameHud extends StatelessWidget { From 84282fe83a67e95eaf5a3b3cb4a66d14192bc7c1 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Mar 2022 10:51:25 -0300 Subject: [PATCH 29/37] fix: test from rebase --- test/game/view/game_hud_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 27a423ed..e7334e41 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -9,7 +9,7 @@ import '../../helpers/helpers.dart'; void main() { group('GameHud', () { late GameBloc gameBloc; - const initialState = GameState(score: 10, balls: 2); + const initialState = GameState(score: 10, balls: 2, bonusLetters: []); void _mockState(GameState state) { whenListen( From bbab659b85613f8a11519317dd11549bbcb50137 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 17:36:15 +0100 Subject: [PATCH 30/37] refactor: changed docs and use angles in radians instead of degrees --- lib/game/components/pathway.dart | 81 +++++++++++++------------ lib/game/pinball_game.dart | 1 - packages/geometry/lib/src/geometry.dart | 20 +++--- test/game/components/pathway_test.dart | 61 ++++++++++--------- 4 files changed, 86 insertions(+), 77 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index d6582a91..3404f2d2 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -4,8 +4,9 @@ import 'package:flutter/material.dart'; import 'package:geometry/geometry.dart'; /// {@template pathway} -/// [Pathway] creates lines of various shapes that the ball can collide -/// with and move along. +/// [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._({ @@ -20,20 +21,19 @@ class Pathway extends BodyComponent { ..style = PaintingStyle.stroke; } - /// {@macro pathway} - /// [Pathway.straight] creates a straight pathway for the ball. + /// Creates a uniform unidirectional (straight) [Pathway]. /// - /// given a [position] for the body, between a [start] and [end] points. - /// It creates two [ChainShape] separated by a [pathwayWidth]. - /// If [singleWall] is true, just one [ChainShape] is created - /// (like a wall instead of a pathway) - /// The pathway could be rotated by [rotation] in degrees. + /// 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 pathwayWidth, + required double width, double rotation = 0, bool singleWall = false, }) { @@ -41,14 +41,14 @@ class Pathway extends BodyComponent { final wall1 = [ start.clone(), end.clone(), - ].map((vector) => vector..rotate(radians(rotation))).toList(); + ].map((vector) => vector..rotate(rotation)).toList(); paths.add(wall1); if (!singleWall) { final wall2 = [ - start + Vector2(pathwayWidth, 0), - end + Vector2(pathwayWidth, 0), - ].map((vector) => vector..rotate(radians(rotation))).toList(); + start + Vector2(width, 0), + end + Vector2(width, 0), + ].map((vector) => vector..rotate(rotation)).toList(); paths.add(wall2); } @@ -59,21 +59,24 @@ class Pathway extends BodyComponent { ); } - /// {@macro pathway} - /// [Pathway.arc] creates an arc pathway for the ball. + /// 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]. /// - /// The arc is created given a [position] for the body, a [radius] for the - /// circumference and an [angle] to specify the size of it (360 will return - /// a completed circumference and minor angles a semi circumference ). - /// It creates two [ChainShape] separated by a [pathwayWidth], like a circular - /// crown. The specified [radius] is for the outer arc, the inner one will - /// have a radius of radius-pathwayWidth. /// If [singleWall] is true, just one [ChainShape] is created. - /// The pathway could be rotated by [rotation] in degrees. factory Pathway.arc({ Color? color, required Vector2 position, - required double pathwayWidth, + required double width, required double radius, required double angle, double rotation = 0, @@ -92,7 +95,7 @@ class Pathway extends BodyComponent { if (!singleWall) { final wall2 = calculateArc( center: position, - radius: radius - pathwayWidth, + radius: radius - width, angle: angle, offsetAngle: rotation, ); @@ -106,38 +109,36 @@ class Pathway extends BodyComponent { ); } - /// {@macro pathway} - /// [Pathway.bezierCurve] creates a bezier curve pathway for the ball. + /// Creates a bezier curve [Pathway]. /// - /// The curve is created given a [position] for the body, and - /// with a list of control points specified by [controlPoints]. - /// First and last points set the beginning and end of the curve, all the - /// inner points between them set the bezier curve final shape. - /// It creates two [ChainShape] separated by a [pathwayWidth]. - /// If [singleWall] is true, just one [ChainShape] is created - /// (like a wall instead of a pathway) - /// The pathway could be rotated by [rotation] in degrees. + /// 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 pathwayWidth, + required double width, double rotation = 0, bool singleWall = false, }) { final paths = >[]; final wall1 = calculateBezierCurve(controlPoints: controlPoints) - .map((vector) => vector..rotate(radians(rotation))) + .map((vector) => vector..rotate(rotation)) .toList(); paths.add(wall1); if (!singleWall) { final wall2 = calculateBezierCurve( controlPoints: controlPoints - .map((vector) => vector + Vector2(pathwayWidth, -pathwayWidth)) + .map((vector) => vector + Vector2(width, -width)) .toList(), - ).map((vector) => vector..rotate(radians(rotation))).toList(); + ).map((vector) => vector..rotate(rotation)).toList(); paths.add(wall2); } @@ -158,13 +159,13 @@ class Pathway extends BodyComponent { ..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); } diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 7c701c09..6fe6e571 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:pinball/game/game.dart'; diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index c992e5a2..8851ed25 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -4,7 +4,7 @@ import 'package:flame/extensions.dart'; /// Calculates all [Vector2]s of a circumference. /// /// Circumference is created from a [center] and a [radius] -/// Also semi circumference could be created, specifying its [angle] in degrees +/// Also semi circumference could be created, specifying its [angle] in radians /// and the offset start angle [offsetAngle] for this semi circumference. /// The higher the [precision], the more [Vector2]s will be calculated, /// achieving a more rounded arc. @@ -17,13 +17,12 @@ List calculateArc({ double offsetAngle = 0, int precision = 100, }) { - final stepAngle = radians(angle / (precision - 1)); - final stepOffset = radians(offsetAngle); + final stepAngle = angle / (precision - 1); final points = []; for (var i = 0; i < precision; i++) { - final xCoord = center.x + radius * math.cos((stepAngle * i) + stepOffset); - final yCoord = center.y - radius * math.sin((stepAngle * i) + stepOffset); + 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); @@ -35,11 +34,14 @@ List calculateArc({ /// 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 in range 0<=step<=1 and indicates the precision to -/// calculate the curve. +/// +/// 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, @@ -77,7 +79,7 @@ List calculateBezierCurve({ return points; } -/// Method to calculate the binomial coefficient of 'n' and 'k' +/// 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'); @@ -89,7 +91,7 @@ num binomial(num n, num k) { } } -/// Method to calculate the factorial of some number 'n' +/// 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)'); diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index e1c10455..7f8357d8 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -1,4 +1,5 @@ // 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'; @@ -10,20 +11,21 @@ void main() { final flameTester = FlameTester(PinballGame.new); group('Pathway', () { - const pathwayWidth = 50.0; + const width = 50.0; group('straight', () { group('color', () { flameTester.test( - 'has transparent color by default if not specified', + 'has transparent color by default when no color is specified', (game) async { final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); expect(pathway.paint, isNotNull); expect( @@ -32,8 +34,9 @@ void main() { ); }, ); + flameTester.test( - 'has a color if set', + 'has a color when is specified', (game) async { const defaultColor = Colors.blue; @@ -42,9 +45,10 @@ void main() { position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); expect(pathway.paint, isNotNull); expect(pathway.paint.color.value, equals(defaultColor.value)); @@ -59,9 +63,10 @@ void main() { position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -75,11 +80,11 @@ void main() { position: position, start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); - game.contains(pathway); + game.contains(pathway); expect(pathway.body.position, position); }, ); @@ -91,7 +96,7 @@ void main() { position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); @@ -102,13 +107,13 @@ void main() { group('fixtures', () { flameTester.test( - 'exists only one ChainShape if just one wall', + 'has only one ChainShape when singleWall is true', (game) async { final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, singleWall: true, ); await game.ensureAdd(pathway); @@ -121,13 +126,13 @@ void main() { ); flameTester.test( - 'exists two ChainShape if there is by default two walls', + 'has two ChainShape when singleWall is false (default)', (game) async { final pathway = Pathway.straight( position: Vector2.zero(), start: Vector2(10, 10), end: Vector2(20, 20), - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); @@ -147,11 +152,12 @@ void main() { (game) async { final pathway = Pathway.arc( position: Vector2.zero(), - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, + width: width, + radius: math.pi / 2, + angle: math.pi / 2, ); await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -163,13 +169,13 @@ void main() { final position = Vector2.all(10); final pathway = Pathway.arc( position: position, - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, + width: width, + radius: math.pi / 2, + angle: math.pi / 2, ); await game.ensureAdd(pathway); - game.contains(pathway); + game.contains(pathway); expect(pathway.body.position, position); }, ); @@ -179,9 +185,9 @@ void main() { (game) async { final pathway = Pathway.arc( position: Vector2.zero(), - pathwayWidth: pathwayWidth, - radius: 100, - angle: 90, + width: width, + radius: math.pi / 2, + angle: math.pi / 2, ); await game.ensureAdd(pathway); @@ -205,9 +211,10 @@ void main() { final pathway = Pathway.bezierCurve( position: Vector2.zero(), controlPoints: controlPoints, - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); + expect(game.contains(pathway), isTrue); }, ); @@ -220,11 +227,11 @@ void main() { final pathway = Pathway.bezierCurve( position: position, controlPoints: controlPoints, - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); - game.contains(pathway); + game.contains(pathway); expect(pathway.body.position, position); }, ); @@ -235,7 +242,7 @@ void main() { final pathway = Pathway.bezierCurve( position: Vector2.zero(), controlPoints: controlPoints, - pathwayWidth: pathwayWidth, + width: width, ); await game.ensureAdd(pathway); From 45dc583c86512ddf524d3157cffe1871f4dc19aa Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 17:42:54 +0100 Subject: [PATCH 31/37] test: fixed bug with radius in radians :S --- test/game/components/pathway_test.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 7f8357d8..6b11a2b3 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: cascade_invocations, prefer_const_constructors import 'dart:math' as math; +import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; @@ -153,7 +154,7 @@ void main() { final pathway = Pathway.arc( position: Vector2.zero(), width: width, - radius: math.pi / 2, + radius: 100, angle: math.pi / 2, ); await game.ensureAdd(pathway); @@ -170,7 +171,7 @@ void main() { final pathway = Pathway.arc( position: position, width: width, - radius: math.pi / 2, + radius: 100, angle: math.pi / 2, ); await game.ensureAdd(pathway); @@ -186,7 +187,7 @@ void main() { final pathway = Pathway.arc( position: Vector2.zero(), width: width, - radius: math.pi / 2, + radius: 100, angle: math.pi / 2, ); await game.ensureAdd(pathway); From 360b5876cf02fd74974538b4af4033c24b50986a Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 9 Mar 2022 17:10:32 +0000 Subject: [PATCH 32/37] feat: Flipper (#15) * feat: explicitely imported Anchor * feat: implemented Flipper * feat: implemented Flipper in PinballGame * feat: implemented calculateRequiredSpeed * feat: included right and left constructors * feat: used right and left constructors * refactor: cleaned calcualteSpeed method * refactor: used width and height instead of size * feat: implemented FlipperAnchor * feat: implemented BoardSide enum * docs: used prose in doc comment * feat: implemented BoardSideX * refactor: used isLeft instead of isRight * refactor: implemented unlock method * refactor: same line assignment Co-authored-by: Erick * feat: add themes * feat: add theme cubit * test: character themes * test: remove grouping * refactor: move themes to package * chore: add workflow * fix: workflow * docs: character themes update * refactor: one theme for entire game * chore: add to props * fix: changing ball spawning point to avoid context errors * refactor: modified unlock method due to invalid cast * feat: included test for BoardSide * refactor: removed sweepingAnimationDuration * feat: tested flipper.dart * refactor: included flippersPosition * refactor: implemented _addFlippers method * feat: centered vertices * feat: modified test to match new center * refactor: removed unecessary parenthesis * refactor: removed unecessary calculation * fix: changing ball spawning point to avoid context errors * chore: rebasing * docs: included FIXME comment * feat: moved key listening to Flipper * docs: include TOOD comment * feat: including test and refactor * docs: fixed doc comment typo Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * docs: fixed do comment template name Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: removed unnecessary verbose multiplication Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: removed unnecessary verbose multiplication * refactor: used ensureAddAll instead of ensureAdd Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * docs: fixed doc comment typo * refactor: used bigCircleShape.radius * refactor: reorganized methods * docs: improved doc comment * refactor: removed unecessary class variables * docs: fix doc comment typo * refactor: removed unused helper * fix: simplified keyEvents * fix: corrected erroneous key tests * refactor: modified component tests * refactor: capitalized Flipper test description * refactor: changed angle calculations * fix: tests * refactor: removed exta line Co-authored-by: Erick Co-authored-by: Allison Ryan Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/board_side.dart | 22 ++ lib/game/components/components.dart | 2 + lib/game/components/flipper.dart | 241 +++++++++++++ lib/game/components/plunger.dart | 2 +- lib/game/pinball_game.dart | 87 ++++- test/game/components/board_side_test.dart | 27 ++ test/game/components/flipper_test.dart | 401 ++++++++++++++++++++++ test/game/pinball_game_test.dart | 45 +++ test/helpers/helpers.dart | 1 + test/helpers/key_testers.dart | 37 ++ test/helpers/mocks.dart | 16 + 11 files changed, 869 insertions(+), 12 deletions(-) create mode 100644 lib/game/components/board_side.dart create mode 100644 lib/game/components/flipper.dart create mode 100644 test/game/components/board_side_test.dart create mode 100644 test/game/components/flipper_test.dart create mode 100644 test/helpers/key_testers.dart 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(); + } +} From 696ace8bfff9f632fd03f858667d5596dd26fdce Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 18:44:20 +0100 Subject: [PATCH 33/37] chore: removed unused import --- test/game/components/pathway_test.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 6b11a2b3..d3b82e96 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -1,6 +1,5 @@ // ignore_for_file: cascade_invocations, prefer_const_constructors import 'dart:math' as math; -import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/material.dart'; From 85cbe42958a8c0f8902463f0c9e980036f93b3b4 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 18:52:25 +0100 Subject: [PATCH 34/37] refactor: renamed wall1 and wall2 vars --- lib/game/components/pathway.dart | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 3404f2d2..d41ce3da 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -38,18 +38,20 @@ class Pathway extends BodyComponent { bool singleWall = false, }) { final paths = >[]; - final wall1 = [ + + // TODO(ruialonso): Refactor repetitive logic + final firstWall = [ start.clone(), end.clone(), ].map((vector) => vector..rotate(rotation)).toList(); - paths.add(wall1); + paths.add(firstWall); if (!singleWall) { - final wall2 = [ + final secondWall = [ start + Vector2(width, 0), end + Vector2(width, 0), ].map((vector) => vector..rotate(rotation)).toList(); - paths.add(wall2); + paths.add(secondWall); } return Pathway._( @@ -84,22 +86,23 @@ class Pathway extends BodyComponent { }) { final paths = >[]; - final wall1 = calculateArc( + // TODO(ruialonso): Refactor repetitive logic + final outerWall = calculateArc( center: position, radius: radius, angle: angle, offsetAngle: rotation, ); - paths.add(wall1); + paths.add(outerWall); if (!singleWall) { - final wall2 = calculateArc( + final innerWall = calculateArc( center: position, radius: radius - width, angle: angle, offsetAngle: rotation, ); - paths.add(wall2); + paths.add(innerWall); } return Pathway._( @@ -128,18 +131,19 @@ class Pathway extends BodyComponent { }) { final paths = >[]; - final wall1 = calculateBezierCurve(controlPoints: controlPoints) + // TODO(ruialonso): Refactor repetitive logic + final firstWall = calculateBezierCurve(controlPoints: controlPoints) .map((vector) => vector..rotate(rotation)) .toList(); - paths.add(wall1); + paths.add(firstWall); if (!singleWall) { - final wall2 = calculateBezierCurve( + final secondWall = calculateBezierCurve( controlPoints: controlPoints .map((vector) => vector + Vector2(width, -width)) .toList(), ).map((vector) => vector..rotate(rotation)).toList(); - paths.add(wall2); + paths.add(secondWall); } return Pathway._( From 3a092f8a1c33700081429d9d6dda8aa3d6912af1 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 18:55:30 +0100 Subject: [PATCH 35/37] test: removed old group --- packages/geometry/test/src/geometry_test.dart | 251 +++++++++--------- 1 file changed, 125 insertions(+), 126 deletions(-) diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 6ecf66c8..a3040a9c 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -11,149 +11,148 @@ class Binomial { } void main() { - group('Maths', () { - 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('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); }); - 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 as many points as indicated by precision', () { + final points = calculateArc( + center: Vector2.zero(), + radius: 100, + angle: 90, + precision: 50, + ); + expect(points.length, 50); + }); + }); - test('returns by default 1000 points as indicated by step', () { - final points = calculateBezierCurve( + group('calculateBezierCurve', () { + test('fails if step not in range', () { + expect( + () => calculateBezierCurve( controlPoints: [ Vector2(0, 0), Vector2(10, 10), ], - ); - expect(points.length, 1000); - }); + step: 2, + ), + throwsAssertionError, + ); + }); - 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); - }); + test('fails if not enough control points', () { + expect( + () => calculateBezierCurve(controlPoints: [Vector2.zero()]), + throwsAssertionError, + ); + expect( + () => calculateBezierCurve(controlPoints: []), + throwsAssertionError, + ); }); - group('binomial', () { - test('fails if k is negative', () { - expect(() => binomial(1, -1), 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('fails if n is negative', () { - expect(() => binomial(-1, 1), throwsAssertionError); - }); + 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); + }); + }); - test('fails if n < k', () { - expect(() => binomial(1, 2), throwsAssertionError); - }); + group('binomial', () { + test('fails if k is negative', () { + expect(() => binomial(1, -1), 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); - }); - }); + test('fails if n is negative', () { + expect(() => binomial(-1, 1), throwsAssertionError); + }); + + test('fails if n < k', () { + expect(() => binomial(1, 2), throwsAssertionError); }); - group('factorial', () { - test('fails if negative number', () { - expect(() => factorial(-1), 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); - }); + 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); }); }); }); From 78eb18eb0e14d763fb6aea1e4ebd80e21a1e95a3 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 19:33:25 +0100 Subject: [PATCH 36/37] chore: updated package geometry readme file --- packages/geometry/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/geometry/README.md b/packages/geometry/README.md index cbc4cca1..f0841d82 100644 --- a/packages/geometry/README.md +++ b/packages/geometry/README.md @@ -1,9 +1,9 @@ -# maths +# geometry [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] [![License: MIT][license_badge]][license_link] -A Very Good Project created by Very Good CLI. +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 From f75d5514cc5cfba51735162c76933b6ec124e6ba Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Wed, 9 Mar 2022 19:47:23 +0100 Subject: [PATCH 37/37] doc: geometry doc refactored --- packages/geometry/lib/src/geometry.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 8851ed25..dceb4e9e 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -3,10 +3,11 @@ import 'package:flame/extensions.dart'; /// Calculates all [Vector2]s of a circumference. /// -/// Circumference is created from a [center] and a [radius] -/// Also semi circumference could be created, specifying its [angle] in radians -/// and the offset start angle [offsetAngle] for this semi circumference. -/// The higher the [precision], the more [Vector2]s will be calculated, +/// 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. @@ -80,6 +81,7 @@ List calculateBezierCurve({ } /// 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'); @@ -92,6 +94,7 @@ num binomial(num n, num 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)');