Merge branch 'main' into feat/bonus-letter-bloc

pull/24/head
Erick 4 years ago committed by GitHub
commit b8cbd6bab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

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

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

@ -1,3 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';

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

@ -1,3 +1,5 @@
// ignore_for_file: public_member_api_docs
part of 'game_bloc.dart'; part of 'game_bloc.dart';
/// Defines bonuses that a player can gain during a PinballGame. /// Defines bonuses that a player can gain during a PinballGame.

@ -3,29 +3,36 @@ import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
/// {@template ball}
/// A solid, [BodyType.dynamic] sphere that rolls and bounces along the
/// [PinballGame].
/// {@endtemplate}
class Ball extends PositionBodyComponent<PinballGame, SpriteComponent> class Ball extends PositionBodyComponent<PinballGame, SpriteComponent>
with BlocComponent<GameBloc, GameState> { with BlocComponent<GameBloc, GameState> {
/// {@macro ball}
Ball({ Ball({
required Vector2 position, required Vector2 position,
}) : _position = position, }) : _position = position,
super(size: ballSize); super(size: Vector2.all(2));
static final ballSize = Vector2.all(2);
/// The initial position of the [Ball] body.
final Vector2 _position; final Vector2 _position;
/// Asset location of the sprite that renders with the [Ball].
///
/// Sprite is preloaded by [PinballGameAssetsX].
static const spritePath = 'components/ball.png'; static const spritePath = 'components/ball.png';
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath); final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(sprite: sprite, size: ballSize); positionComponent = SpriteComponent(sprite: sprite, size: size);
} }
@override @override
Body createBody() { Body createBody() {
final shape = CircleShape()..radius = ballSize.x / 2; final shape = CircleShape()..radius = size.x / 2;
final fixtureDef = FixtureDef(shape)..density = 1; final fixtureDef = FixtureDef(shape)..density = 1;
@ -37,6 +44,11 @@ class Ball extends PositionBodyComponent<PinballGame, SpriteComponent>
return world.createBody(bodyDef)..createFixture(fixtureDef); return world.createBody(bodyDef)..createFixture(fixtureDef);
} }
/// Removes the [Ball] from a [PinballGame]; spawning a new [Ball] if
/// any are left.
///
/// Triggered by [BottomWallBallContactCallback] when the [Ball] falls into
/// a [BottomWall].
void lost() { void lost() {
shouldRemove = true; shouldRemove = true;

@ -2,6 +2,7 @@ export 'anchor.dart';
export 'ball.dart'; export 'ball.dart';
export 'board_side.dart'; export 'board_side.dart';
export 'flipper.dart'; export 'flipper.dart';
export 'pathway.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'score_points.dart'; export 'score_points.dart';
export 'wall.dart'; export 'wall.dart';

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flame/components.dart' show SpriteComponent;
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
@ -12,19 +12,15 @@ import 'package:pinball/game/game.dart';
/// ///
/// [Flipper] can be controlled by the player in an arc motion. /// [Flipper] can be controlled by the player in an arc motion.
/// {@endtemplate flipper} /// {@endtemplate flipper}
class Flipper extends BodyComponent with KeyboardHandler { class Flipper extends PositionBodyComponent with KeyboardHandler {
/// {@macro flipper} /// {@macro flipper}
Flipper._({ Flipper._({
required Vector2 position, required Vector2 position,
required this.side, required this.side,
required List<LogicalKeyboardKey> keys, required List<LogicalKeyboardKey> keys,
}) : _position = position, }) : _position = position,
_keys = keys { _keys = keys,
// TODO(alestiago): Use sprite instead of color when provided. super(size: Vector2(width, height));
paint = Paint()
..color = const Color(0xFF00FF00)
..style = PaintingStyle.fill;
}
/// A left positioned [Flipper]. /// A left positioned [Flipper].
Flipper.left({ Flipper.left({
@ -50,6 +46,11 @@ class Flipper extends BodyComponent with KeyboardHandler {
], ],
); );
/// Asset location of the sprite that renders with the [Flipper].
///
/// Sprite is preloaded by [PinballGameAssetsX].
static const spritePath = 'components/flipper.png';
/// The width of the [Flipper]. /// The width of the [Flipper].
static const width = 12.0; static const width = 12.0;
@ -75,6 +76,20 @@ class Flipper extends BodyComponent with KeyboardHandler {
/// [onKeyEvent] method listens to when one of these keys is pressed. /// [onKeyEvent] method listens to when one of these keys is pressed.
final List<LogicalKeyboardKey> _keys; final List<LogicalKeyboardKey> _keys;
@override
Future<void> onLoad() async {
await super.onLoad();
final sprite = await gameRef.loadSprite(spritePath);
positionComponent = SpriteComponent(
sprite: sprite,
size: size,
);
if (side == BoardSide.right) {
positionComponent?.flipHorizontally();
}
}
/// Applies downward linear velocity to the [Flipper], moving it to its /// Applies downward linear velocity to the [Flipper], moving it to its
/// resting position. /// resting position.
void _moveDown() { void _moveDown() {
@ -148,6 +163,7 @@ class Flipper extends BodyComponent with KeyboardHandler {
// TODO(erickzanardo): Remove this once the issue is solved: // TODO(erickzanardo): Remove this once the issue is solved:
// https://github.com/flame-engine/flame/issues/1417 // https://github.com/flame-engine/flame/issues/1417
// ignore: public_member_api_docs
final Completer hasMounted = Completer<void>(); final Completer hasMounted = Completer<void>();
@override @override

@ -0,0 +1,178 @@
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'package:geometry/geometry.dart';
/// {@template pathway}
/// [Pathway] creates lines of various shapes.
///
/// [BodyComponent]s such as a Ball can collide and move along a [Pathway].
/// {@endtemplate}
class Pathway extends BodyComponent {
Pathway._({
// TODO(ruialonso): remove color when assets added.
Color? color,
required Vector2 position,
required List<List<Vector2>> paths,
}) : _position = position,
_paths = paths {
paint = Paint()
..color = color ?? const Color.fromARGB(0, 0, 0, 0)
..style = PaintingStyle.stroke;
}
/// Creates a uniform unidirectional (straight) [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Placed
/// at a [position] between [start] and [end] points. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.straight({
Color? color,
required Vector2 position,
required Vector2 start,
required Vector2 end,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = [
start.clone(),
end.clone(),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = [
start + Vector2(width, 0),
end + Vector2(width, 0),
].map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
/// Creates an arc [Pathway].
///
/// The [angle], in radians, specifies the size of the arc. For example, 2*pi
/// returns a complete circumference and minor angles a semi circumference.
///
/// The center of the arc is placed at [position].
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// The outer radius is specified by [radius], whilst the inner one is
/// equivalent to [radius] - [width].
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.arc({
Color? color,
required Vector2 position,
required double width,
required double radius,
required double angle,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateArc(
center: position,
radius: radius,
angle: angle,
offsetAngle: rotation,
);
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateArc(
center: position,
radius: radius - width,
angle: angle,
offsetAngle: rotation,
);
paths.add(innerWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
/// Creates a bezier curve [Pathway].
///
/// Does so with two [ChainShape] separated by a [width]. Which can be
/// rotated by a given [rotation] in radians.
///
/// First and last [controlPoints] set the beginning and end of the curve,
/// inner points between them set its final shape.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.bezierCurve({
Color? color,
required Vector2 position,
required List<Vector2> controlPoints,
required double width,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final firstWall = calculateBezierCurve(controlPoints: controlPoints)
.map((vector) => vector..rotate(rotation))
.toList();
paths.add(firstWall);
if (!singleWall) {
final secondWall = calculateBezierCurve(
controlPoints: controlPoints
.map((vector) => vector + Vector2(width, -width))
.toList(),
).map((vector) => vector..rotate(rotation)).toList();
paths.add(secondWall);
}
return Pathway._(
color: color,
position: position,
paths: paths,
);
}
final Vector2 _position;
final List<List<Vector2>> _paths;
@override
Body createBody() {
final bodyDef = BodyDef()
..type = BodyType.static
..position = _position;
final body = world.createBody(bodyDef);
for (final path in _paths) {
final chain = ChainShape()
..createChain(
path.map(gameRef.screenToWorld).toList(),
);
final fixtureDef = FixtureDef(chain);
body.createFixture(fixtureDef);
}
return body;
}
}

@ -6,13 +6,18 @@ import 'package:pinball/game/components/components.dart';
/// {@template wall} /// {@template wall}
/// A continuos generic and [BodyType.static] barrier that divides a game area. /// A continuos generic and [BodyType.static] barrier that divides a game area.
/// {@endtemplate} /// {@endtemplate}
// TODO(alestiago): Remove [Wall] for [Pathway.straight].
class Wall extends BodyComponent { class Wall extends BodyComponent {
/// {@macro wall}
Wall({ Wall({
required this.start, required this.start,
required this.end, required this.end,
}); });
/// The [start] of the [Wall].
final Vector2 start; final Vector2 start;
/// The [end] of the [Wall].
final Vector2 end; final Vector2 end;
@override @override
@ -39,6 +44,7 @@ class Wall extends BodyComponent {
/// [BottomWallBallContactCallback]. /// [BottomWallBallContactCallback].
/// {@endtemplate} /// {@endtemplate}
class BottomWall extends Wall { class BottomWall extends Wall {
/// {@macro bottom_wall}
BottomWall(Forge2DGame game) BottomWall(Forge2DGame game)
: super( : super(
start: game.screenToWorld(game.camera.viewport.effectiveSize), start: game.screenToWorld(game.camera.viewport.effectiveSize),

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

@ -1,5 +1,6 @@
import 'dart:async'; // ignore_for_file: public_member_api_docs
import 'dart:async';
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart'; import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';

@ -1,3 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';

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

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

@ -1,3 +1,5 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/l10n/l10n.dart'; import 'package:pinball/l10n/l10n.dart';

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

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

@ -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

@ -0,0 +1,11 @@
# geometry
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Helper package to calculate points of lines, arcs and curves for the pathways of the ball.
[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg
[license_link]: https://opensource.org/licenses/MIT
[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg
[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis

@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.2.4.0.yaml

@ -0,0 +1,3 @@
library geometry;
export 'src/geometry.dart';

@ -0,0 +1,107 @@
import 'dart:math' as math;
import 'package:flame/extensions.dart';
/// Calculates all [Vector2]s of a circumference.
///
/// A circumference can be achieved by specifying a [center] and a [radius].
/// In addition, a semi-circle can be achieved by specifying its [angle] and an
/// [offsetAngle] (both in radians).
///
/// The higher the [precision], the more [Vector2]s will be calculated;
/// achieving a more rounded arc.
///
/// For more information read: https://en.wikipedia.org/wiki/Trigonometric_functions.
List<Vector2> calculateArc({
required Vector2 center,
required double radius,
required double angle,
double offsetAngle = 0,
int precision = 100,
}) {
final stepAngle = angle / (precision - 1);
final points = <Vector2>[];
for (var i = 0; i < precision; i++) {
final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle);
final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle);
final point = Vector2(xCoord, yCoord);
points.add(point);
}
return points;
}
/// Calculates all [Vector2]s of a bezier curve.
///
/// A bezier curve of [controlPoints] that say how to create this curve.
///
/// First and last points specify the beginning and the end respectively
/// of the curve. The inner points specify the shape of the curve and
/// its turning points.
///
/// The [step] must be between zero and one (inclusive), indicating the
/// precision to calculate the curve.
///
/// For more information read: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
List<Vector2> calculateBezierCurve({
required List<Vector2> controlPoints,
double step = 0.001,
}) {
assert(
0 <= step && step <= 1,
'Step ($step) must be in range 0 <= step <= 1',
);
assert(
controlPoints.length >= 2,
'At least 2 control points needed to create a bezier curve',
);
var t = 0.0;
final n = controlPoints.length - 1;
final points = <Vector2>[];
do {
var xCoord = 0.0;
var yCoord = 0.0;
for (var i = 0; i <= n; i++) {
final point = controlPoints[i];
xCoord +=
binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.x;
yCoord +=
binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y;
}
points.add(Vector2(xCoord, yCoord));
t = t + step;
} while (t <= 1);
return points;
}
/// Calculates the binomial coefficient of 'n' and 'k'.
///
/// For more information read: https://en.wikipedia.org/wiki/Binomial_coefficient
num binomial(num n, num k) {
assert(0 <= k && k <= n, 'k ($k) and n ($n) must be in range 0 <= k <= n');
if (k == 0 || n == k) {
return 1;
} else {
return factorial(n) / (factorial(k) * factorial(n - k));
}
}
/// Calculate the factorial of 'n'.
///
/// For more information read: https://en.wikipedia.org/wiki/Factorial
num factorial(num n) {
assert(n >= 0, 'Factorial is not defined for negative number n ($n)');
if (n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}

@ -0,0 +1,19 @@
name: geometry
description: Helper package to calculate points of lines, arcs and curves for the pathways of the ball
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flame: ^1.0.0
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^0.2.0
test: ^1.19.2
very_good_analysis: ^2.4.0

@ -0,0 +1,159 @@
// ignore_for_file: prefer_const_constructors, cascade_invocations
import 'package:flame/extensions.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:geometry/geometry.dart';
class Binomial {
Binomial({required this.n, required this.k});
final num n;
final num k;
}
void main() {
group('calculateArc', () {
test('returns by default 100 points as indicated by precision', () {
final points = calculateArc(
center: Vector2.zero(),
radius: 100,
angle: 90,
);
expect(points.length, 100);
});
test('returns as many points as indicated by precision', () {
final points = calculateArc(
center: Vector2.zero(),
radius: 100,
angle: 90,
precision: 50,
);
expect(points.length, 50);
});
});
group('calculateBezierCurve', () {
test('fails if step not in range', () {
expect(
() => calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
step: 2,
),
throwsAssertionError,
);
});
test('fails if not enough control points', () {
expect(
() => calculateBezierCurve(controlPoints: [Vector2.zero()]),
throwsAssertionError,
);
expect(
() => calculateBezierCurve(controlPoints: []),
throwsAssertionError,
);
});
test('returns by default 1000 points as indicated by step', () {
final points = calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
);
expect(points.length, 1000);
});
test('returns as many points as indicated by step', () {
final points = calculateBezierCurve(
controlPoints: [
Vector2(0, 0),
Vector2(10, 10),
],
step: 0.01,
);
expect(points.length, 100);
});
});
group('binomial', () {
test('fails if k is negative', () {
expect(() => binomial(1, -1), throwsAssertionError);
});
test('fails if n is negative', () {
expect(() => binomial(-1, 1), throwsAssertionError);
});
test('fails if n < k', () {
expect(() => binomial(1, 2), throwsAssertionError);
});
test('for a specific input gives a correct value', () {
final binomialInputsToExpected = {
Binomial(n: 0, k: 0): 1,
Binomial(n: 1, k: 0): 1,
Binomial(n: 1, k: 1): 1,
Binomial(n: 2, k: 0): 1,
Binomial(n: 2, k: 1): 2,
Binomial(n: 2, k: 2): 1,
Binomial(n: 3, k: 0): 1,
Binomial(n: 3, k: 1): 3,
Binomial(n: 3, k: 2): 3,
Binomial(n: 3, k: 3): 1,
Binomial(n: 4, k: 0): 1,
Binomial(n: 4, k: 1): 4,
Binomial(n: 4, k: 2): 6,
Binomial(n: 4, k: 3): 4,
Binomial(n: 4, k: 4): 1,
Binomial(n: 5, k: 0): 1,
Binomial(n: 5, k: 1): 5,
Binomial(n: 5, k: 2): 10,
Binomial(n: 5, k: 3): 10,
Binomial(n: 5, k: 4): 5,
Binomial(n: 5, k: 5): 1,
Binomial(n: 6, k: 0): 1,
Binomial(n: 6, k: 1): 6,
Binomial(n: 6, k: 2): 15,
Binomial(n: 6, k: 3): 20,
Binomial(n: 6, k: 4): 15,
Binomial(n: 6, k: 5): 6,
Binomial(n: 6, k: 6): 1,
};
binomialInputsToExpected.forEach((input, value) {
expect(binomial(input.n, input.k), value);
});
});
});
group('factorial', () {
test('fails if negative number', () {
expect(() => factorial(-1), throwsAssertionError);
});
test('for a specific input gives a correct value', () {
final factorialInputsToExpected = {
0: 1,
1: 1,
2: 2,
3: 6,
4: 24,
5: 120,
6: 720,
7: 5040,
8: 40320,
9: 362880,
10: 3628800,
11: 39916800,
12: 479001600,
13: 6227020800,
};
factorialInputsToExpected.forEach((input, expected) {
expect(factorial(input), expected);
});
});
});
}

@ -198,6 +198,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
geometry:
dependency: "direct main"
description:
path: "packages/geometry"
relative: true
source: path
version: "1.0.0+1"
glob: glob:
dependency: transitive dependency: transitive
description: description:

@ -17,6 +17,8 @@ dependencies:
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
geometry:
path: packages/geometry
intl: ^0.17.0 intl: ^0.17.0
pinball_theme: pinball_theme:
path: packages/pinball_theme path: packages/pinball_theme

@ -15,6 +15,7 @@ void main() {
'loads correctly', 'loads correctly',
(game) async { (game) async {
final anchor = Anchor(position: Vector2.zero()); final anchor = Anchor(position: Vector2.zero());
await game.ready();
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
expect(game.contains(anchor), isTrue); expect(game.contains(anchor), isTrue);
@ -25,6 +26,7 @@ void main() {
flameTester.test( flameTester.test(
'positions correctly', 'positions correctly',
(game) async { (game) async {
await game.ready();
final position = Vector2.all(10); final position = Vector2.all(10);
final anchor = Anchor(position: position); final anchor = Anchor(position: position);
await game.ensureAdd(anchor); await game.ensureAdd(anchor);
@ -37,6 +39,7 @@ void main() {
flameTester.test( flameTester.test(
'is static', 'is static',
(game) async { (game) async {
await game.ready();
final anchor = Anchor(position: Vector2.zero()); final anchor = Anchor(position: Vector2.zero());
await game.ensureAdd(anchor); await game.ensureAdd(anchor);

@ -0,0 +1,268 @@
// ignore_for_file: cascade_invocations, prefer_const_constructors
import 'dart:math' as math;
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final flameTester = FlameTester(PinballGame.new);
group('Pathway', () {
const width = 50.0;
group('straight', () {
group('color', () {
flameTester.test(
'has transparent color by default when no color is specified',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(
pathway.paint.color,
equals(Color.fromARGB(0, 0, 0, 0)),
);
},
);
flameTester.test(
'has a color when is specified',
(game) async {
await game.ready();
const defaultColor = Colors.blue;
final pathway = Pathway.straight(
color: defaultColor,
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
expect(pathway.paint, isNotNull);
expect(pathway.paint.color.value, equals(defaultColor.value));
},
);
});
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.straight(
position: position,
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
group('fixtures', () {
flameTester.test(
'has only one ChainShape when singleWall is true',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
singleWall: true,
);
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 1);
final fixture = pathway.body.fixtures[0];
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
},
);
flameTester.test(
'has two ChainShape when singleWall is false (default)',
(game) async {
await game.ready();
final pathway = Pathway.straight(
position: Vector2.zero(),
start: Vector2(10, 10),
end: Vector2(20, 20),
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.fixtures.length, 2);
for (final fixture in pathway.body.fixtures) {
expect(fixture, isA<Fixture>());
expect(fixture.shape.shapeType, equals(ShapeType.chain));
}
},
);
});
});
group('arc', () {
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.arc(
position: position,
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.arc(
position: Vector2.zero(),
width: width,
radius: 100,
angle: math.pi / 2,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('bezier curve', () {
final controlPoints = [
Vector2(0, 0),
Vector2(50, 0),
Vector2(0, 50),
Vector2(50, 50),
];
flameTester.test(
'loads correctly',
(game) async {
await game.ready();
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
await game.ready();
final position = Vector2.all(10);
final pathway = Pathway.bezierCurve(
position: position,
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
game.contains(pathway);
expect(pathway.body.position, position);
},
);
flameTester.test(
'is static',
(game) async {
await game.ready();
final pathway = Pathway.bezierCurve(
position: Vector2.zero(),
controlPoints: controlPoints,
width: width,
);
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
});
}

@ -16,6 +16,7 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready();
final plunger = Plunger(position: Vector2.zero()); final plunger = Plunger(position: Vector2.zero());
await game.ensureAdd(plunger); await game.ensureAdd(plunger);

@ -37,6 +37,7 @@ void main() {
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
await game.ready();
final wall = Wall( final wall = Wall(
start: Vector2.zero(), start: Vector2.zero(),
end: Vector2(100, 0), end: Vector2(100, 0),

Loading…
Cancel
Save