Merge remote-tracking branch 'origin' into feat/dash-nest-bumper-add-new-ball

pull/89/head
alestiago 4 years ago
commit e05103754d

@ -0,0 +1,19 @@
name: pinball_components
on:
push:
paths:
- "packages/pinball_components/**"
- ".github/workflows/pinball_components.yaml"
pull_request:
paths:
- "packages/pinball_components/**"
- ".github/workflows/pinball_components.yaml"
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
working_directory: packages/pinball_components
coverage_excludes: "lib/src/generated/*.dart"

@ -12,6 +12,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
on<BallLost>(_onBallLost); on<BallLost>(_onBallLost);
on<Scored>(_onScored); on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated); on<BonusLetterActivated>(_onBonusLetterActivated);
on<DashNestActivated>(_onDashNestActivated);
} }
static const bonusWord = 'GOOGLE'; static const bonusWord = 'GOOGLE';
@ -52,4 +53,28 @@ class GameBloc extends Bloc<GameEvent, GameState> {
); );
} }
} }
void _onDashNestActivated(DashNestActivated event, Emitter emit) {
const nestsRequiredForBonus = 3;
final newNests = {
...state.activatedDashNests,
event.nestId,
};
if (newNests.length == nestsRequiredForBonus) {
emit(
state.copyWith(
activatedDashNests: {},
bonusHistory: [
...state.bonusHistory,
GameBonus.dashNest,
],
),
);
} else {
emit(
state.copyWith(activatedDashNests: newNests),
);
}
}
} }

@ -45,3 +45,12 @@ class BonusLetterActivated extends GameEvent {
@override @override
List<Object?> get props => [letterIndex]; List<Object?> get props => [letterIndex];
} }
class DashNestActivated extends GameEvent {
const DashNestActivated(this.nestId);
final String nestId;
@override
List<Object?> get props => [nestId];
}

@ -7,6 +7,10 @@ enum GameBonus {
/// Bonus achieved when the user activate all of the bonus /// Bonus achieved when the user activate all of the bonus
/// letters on the board, forming the bonus word /// letters on the board, forming the bonus word
word, word,
/// Bonus achieved when the user activates all of the Dash
/// nests on the board, adding a new ball to the board.
dashNest,
} }
/// {@template game_state} /// {@template game_state}
@ -19,6 +23,7 @@ class GameState extends Equatable {
required this.balls, required this.balls,
required this.activatedBonusLetters, required this.activatedBonusLetters,
required this.bonusHistory, required this.bonusHistory,
required this.activatedDashNests,
}) : assert(score >= 0, "Score can't be negative"), }) : assert(score >= 0, "Score can't be negative"),
assert(balls >= 0, "Number of balls can't be negative"); assert(balls >= 0, "Number of balls can't be negative");
@ -26,6 +31,7 @@ class GameState extends Equatable {
: score = 0, : score = 0,
balls = 3, balls = 3,
activatedBonusLetters = const [], activatedBonusLetters = const [],
activatedDashNests = const {},
bonusHistory = const []; bonusHistory = const [];
/// The current score of the game. /// The current score of the game.
@ -39,6 +45,9 @@ class GameState extends Equatable {
/// Active bonus letters. /// Active bonus letters.
final List<int> activatedBonusLetters; final List<int> activatedBonusLetters;
/// Active dash nests.
final Set<String> activatedDashNests;
/// Holds the history of all the [GameBonus]es earned by the player during a /// Holds the history of all the [GameBonus]es earned by the player during a
/// PinballGame. /// PinballGame.
final List<GameBonus> bonusHistory; final List<GameBonus> bonusHistory;
@ -57,6 +66,7 @@ class GameState extends Equatable {
int? score, int? score,
int? balls, int? balls,
List<int>? activatedBonusLetters, List<int>? activatedBonusLetters,
Set<String>? activatedDashNests,
List<GameBonus>? bonusHistory, List<GameBonus>? bonusHistory,
}) { }) {
assert( assert(
@ -69,6 +79,7 @@ class GameState extends Equatable {
balls: balls ?? this.balls, balls: balls ?? this.balls,
activatedBonusLetters: activatedBonusLetters:
activatedBonusLetters ?? this.activatedBonusLetters, activatedBonusLetters ?? this.activatedBonusLetters,
activatedDashNests: activatedDashNests ?? this.activatedDashNests,
bonusHistory: bonusHistory ?? this.bonusHistory, bonusHistory: bonusHistory ?? this.bonusHistory,
); );
} }
@ -78,6 +89,7 @@ class GameState extends Equatable {
score, score,
balls, balls,
activatedBonusLetters, activatedBonusLetters,
activatedDashNests,
bonusHistory, bonusHistory,
]; ];
} }

@ -127,7 +127,7 @@ class _BottomGroupSide extends Component {
Future<void> onLoad() async { Future<void> onLoad() async {
final direction = _side.direction; final direction = _side.direction;
final flipper = Flipper.fromSide( final flipper = Flipper(
side: _side, side: _side,
)..initialPosition = _position; )..initialPosition = _position;
final baseboard = Baseboard(side: _side) final baseboard = Baseboard(side: _side)

@ -3,11 +3,20 @@ import 'dart:math' as math;
import 'package:flame/components.dart'; import 'package:flame/components.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';
import 'package:pinball/gen/assets.gen.dart'; import 'package:pinball/gen/assets.gen.dart';
const _leftFlipperKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
];
const _rightFlipperKeys = [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
];
/// {@template flipper} /// {@template flipper}
/// A bat, typically found in pairs at the bottom of the board. /// A bat, typically found in pairs at the bottom of the board.
/// ///
@ -15,43 +24,9 @@ import 'package:pinball/gen/assets.gen.dart';
/// {@endtemplate flipper} /// {@endtemplate flipper}
class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// {@macro flipper} /// {@macro flipper}
Flipper._({ Flipper({
required this.side, required this.side,
required List<LogicalKeyboardKey> keys, }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys;
}) : _keys = keys;
Flipper._left()
: this._(
side: BoardSide.left,
keys: [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.keyA,
],
);
Flipper._right()
: this._(
side: BoardSide.right,
keys: [
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.keyD,
],
);
/// Constructs a [Flipper] from a [BoardSide].
///
/// A [Flipper._right] and [Flipper._left] besides being mirrored
/// horizontally, also have different [LogicalKeyboardKey]s that control them.
factory Flipper.fromSide({
required BoardSide side,
}) {
switch (side) {
case BoardSide.left:
return Flipper._left();
case BoardSide.right:
return Flipper._right();
}
}
/// The size of the [Flipper]. /// The size of the [Flipper].
static final size = Vector2(12, 2.8); static final size = Vector2(12, 2.8);
@ -104,35 +79,29 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion. /// Anchors the [Flipper] to the [RevoluteJoint] that controls its arc motion.
Future<void> _anchorToJoint() async { Future<void> _anchorToJoint() async {
final anchor = FlipperAnchor(flipper: this); final anchor = _FlipperAnchor(flipper: this);
await add(anchor); await add(anchor);
final jointDef = FlipperAnchorRevoluteJointDef( final jointDef = _FlipperAnchorRevoluteJointDef(
flipper: this, flipper: this,
anchor: anchor, anchor: anchor,
); );
// TODO(alestiago): Remove casting once the following is closed: final joint = _FlipperJoint(jointDef)..create(world);
// https://github.com/flame-engine/forge2d/issues/36
final joint = world.createJoint(jointDef) as RevoluteJoint;
// FIXME(erickzanardo): when mounted the initial position is not fully // FIXME(erickzanardo): when mounted the initial position is not fully
// reached. // reached.
unawaited( unawaited(
mounted.whenComplete( mounted.whenComplete(joint.unlock),
() => FlipperAnchorRevoluteJointDef.unlock(joint, side),
),
); );
} }
List<FixtureDef> _createFixtureDefs() { List<FixtureDef> _createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
final isLeft = side.isLeft; final direction = side.direction;
final bigCircleShape = CircleShape()..radius = 1.75; final bigCircleShape = CircleShape()..radius = 1.75;
bigCircleShape.position.setValues( bigCircleShape.position.setValues(
isLeft ((size.x / 2) * direction) + (bigCircleShape.radius * -direction),
? -(size.x / 2) + bigCircleShape.radius
: (size.x / 2) - bigCircleShape.radius,
0, 0,
); );
final bigCircleFixtureDef = FixtureDef(bigCircleShape); final bigCircleFixtureDef = FixtureDef(bigCircleShape);
@ -140,15 +109,13 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
final smallCircleShape = CircleShape()..radius = 0.9; final smallCircleShape = CircleShape()..radius = 0.9;
smallCircleShape.position.setValues( smallCircleShape.position.setValues(
isLeft ((size.x / 2) * -direction) + (smallCircleShape.radius * direction),
? (size.x / 2) - smallCircleShape.radius
: -(size.x / 2) + smallCircleShape.radius,
0, 0,
); );
final smallCircleFixtureDef = FixtureDef(smallCircleShape); final smallCircleFixtureDef = FixtureDef(smallCircleShape);
fixturesDef.add(smallCircleFixtureDef); fixturesDef.add(smallCircleFixtureDef);
final trapeziumVertices = isLeft final trapeziumVertices = side.isLeft
? [ ? [
Vector2(bigCircleShape.position.x, bigCircleShape.radius), Vector2(bigCircleShape.position.x, bigCircleShape.radius),
Vector2(smallCircleShape.position.x, smallCircleShape.radius), Vector2(smallCircleShape.position.x, smallCircleShape.radius),
@ -173,7 +140,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
paint = Paint()..color = Colors.transparent; renderBody = false;
await Future.wait([ await Future.wait([
_loadSprite(), _loadSprite(),
_anchorToJoint(), _anchorToJoint(),
@ -214,61 +182,66 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition {
/// ///
/// The end of a [Flipper] depends on its [Flipper.side]. /// The end of a [Flipper] depends on its [Flipper.side].
/// {@endtemplate} /// {@endtemplate}
class FlipperAnchor extends JointAnchor { class _FlipperAnchor extends JointAnchor {
/// {@macro flipper_anchor} /// {@macro flipper_anchor}
FlipperAnchor({ _FlipperAnchor({
required Flipper flipper, required Flipper flipper,
}) { }) {
initialPosition = Vector2( initialPosition = Vector2(
flipper.side.isLeft flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2),
? flipper.body.position.x - Flipper.size.x / 2
: flipper.body.position.x + Flipper.size.x / 2,
flipper.body.position.y, flipper.body.position.y,
); );
} }
} }
/// {@template flipper_anchor_revolute_joint_def} /// {@template flipper_anchor_revolute_joint_def}
/// Hinges one end of [Flipper] to a [FlipperAnchor] to achieve an arc motion. /// Hinges one end of [Flipper] to a [_FlipperAnchor] to achieve an arc motion.
/// {@endtemplate} /// {@endtemplate}
class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef {
/// {@macro flipper_anchor_revolute_joint_def} /// {@macro flipper_anchor_revolute_joint_def}
FlipperAnchorRevoluteJointDef({ _FlipperAnchorRevoluteJointDef({
required Flipper flipper, required Flipper flipper,
required FlipperAnchor anchor, required _FlipperAnchor anchor,
}) { }) : side = flipper.side {
initialize( initialize(
flipper.body, flipper.body,
anchor.body, anchor.body,
anchor.body.position, anchor.body.position,
); );
enableLimit = true;
final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2; enableLimit = true;
final angle = (_sweepingAngle * -side.direction) / 2;
lowerAngle = upperAngle = angle; lowerAngle = upperAngle = angle;
} }
/// The total angle of the arc motion. /// The total angle of the arc motion.
static const _sweepingAngle = math.pi / 3.5; static const _sweepingAngle = math.pi / 3.5;
final BoardSide side;
}
class _FlipperJoint extends RevoluteJoint {
_FlipperJoint(_FlipperAnchorRevoluteJointDef def)
: side = def.side,
super(def);
final BoardSide side;
// TODO(alestiago): Remove once Forge2D supports custom joints.
void create(World world) {
world.joints.add(this);
bodyA.joints.add(this);
bodyB.joints.add(this);
}
/// Unlocks the [Flipper] from its resting position. /// Unlocks the [Flipper] from its resting position.
/// ///
/// The [Flipper] is locked when initialized in order to force it to be at /// The [Flipper] is locked when initialized in order to force it to be at
/// its resting position. /// its resting position.
// TODO(alestiago): consider refactor once the issue is solved: void unlock() {
// https://github.com/flame-engine/forge2d/issues/36 setLimits(
static void unlock(RevoluteJoint joint, BoardSide side) { lowerLimit * side.direction,
late final double upperLimit, lowerLimit; -upperLimit * side.direction,
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);
} }
} }

@ -144,9 +144,50 @@ class Pathway extends BodyComponent with InitialPosition, Layered {
); );
} }
/// Creates an ellipse [Pathway].
///
/// Does so with two [ChainShape]s separated by a [width]. Can
/// be rotated by a given [rotation] in radians.
///
/// If [singleWall] is true, just one [ChainShape] is created.
factory Pathway.ellipse({
Color? color,
required Vector2 center,
required double width,
required double majorRadius,
required double minorRadius,
double rotation = 0,
bool singleWall = false,
}) {
final paths = <List<Vector2>>[];
// TODO(ruialonso): Refactor repetitive logic
final outerWall = calculateEllipse(
center: center,
majorRadius: majorRadius,
minorRadius: minorRadius,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(outerWall);
if (!singleWall) {
final innerWall = calculateEllipse(
center: center,
majorRadius: majorRadius - width,
minorRadius: minorRadius - width,
).map((vector) => vector..rotate(rotation)).toList();
paths.add(innerWall);
}
return Pathway._(
color: color,
paths: paths,
);
}
final List<List<Vector2>> _paths; final List<List<Vector2>> _paths;
List<FixtureDef> _createFixtureDefs() { /// Constructs different [ChainShape]s to form the [Pathway] shape.
List<FixtureDef> createFixtureDefs() {
final fixturesDef = <FixtureDef>[]; final fixturesDef = <FixtureDef>[];
for (final path in _paths) { for (final path in _paths) {
@ -161,7 +202,7 @@ class Pathway extends BodyComponent with InitialPosition, Layered {
Body createBody() { Body createBody() {
final bodyDef = BodyDef()..position = initialPosition; final bodyDef = BodyDef()..position = initialPosition;
final body = world.createBody(bodyDef); final body = world.createBody(bodyDef);
_createFixtureDefs().forEach(body.createFixture); createFixtureDefs().forEach(body.createFixture);
return body; return body;
} }

@ -6,7 +6,7 @@ import 'package:pinball/game/game.dart';
/// {@template score_points} /// {@template score_points}
/// Specifies the amount of points received on [Ball] collision. /// Specifies the amount of points received on [Ball] collision.
/// {@endtemplate} /// {@endtemplate}
mixin ScorePoints on BodyComponent { mixin ScorePoints<T extends Forge2DGame> on BodyComponent<T> {
/// {@macro score_points} /// {@macro score_points}
int get points; int get points;

@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; import 'dart:async';
import 'package:flame/extensions.dart'; import 'package:flame/extensions.dart';
import 'package:flame/input.dart'; import 'package:flame/input.dart';

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/leaderboard/leaderboard.dart';
part 'leaderboard_event.dart'; part 'leaderboard_event.dart';
part 'leaderboard_state.dart'; part 'leaderboard_state.dart';
@ -30,10 +31,16 @@ class LeaderboardBloc extends Bloc<LeaderboardEvent, LeaderboardState> {
try { try {
final top10Leaderboard = final top10Leaderboard =
await _leaderboardRepository.fetchTop10Leaderboard(); await _leaderboardRepository.fetchTop10Leaderboard();
final leaderboardEntries = <LeaderboardEntry>[];
top10Leaderboard.asMap().forEach(
(index, value) => leaderboardEntries.add(value.toEntry(index + 1)),
);
emit( emit(
state.copyWith( state.copyWith(
status: LeaderboardStatus.success, status: LeaderboardStatus.success,
leaderboard: top10Leaderboard, leaderboard: leaderboardEntries,
), ),
); );
} catch (error) { } catch (error) {

@ -9,7 +9,7 @@ abstract class LeaderboardEvent extends Equatable {
} }
/// {@template top_10_fetched} /// {@template top_10_fetched}
/// Request the top 10 [LeaderboardEntry]s. /// Request the top 10 [LeaderboardEntryData]s.
/// {endtemplate} /// {endtemplate}
class Top10Fetched extends LeaderboardEvent { class Top10Fetched extends LeaderboardEvent {
/// {@macro top_10_fetched} /// {@macro top_10_fetched}
@ -20,7 +20,7 @@ class Top10Fetched extends LeaderboardEvent {
} }
/// {@template leaderboard_entry_added} /// {@template leaderboard_entry_added}
/// Writes a new [LeaderboardEntry]. /// Writes a new [LeaderboardEntryData].
/// ///
/// Should be added when a player finishes a game. /// Should be added when a player finishes a game.
/// {endtemplate} /// {endtemplate}
@ -28,8 +28,8 @@ class LeaderboardEntryAdded extends LeaderboardEvent {
/// {@macro leaderboard_entry_added} /// {@macro leaderboard_entry_added}
const LeaderboardEntryAdded({required this.entry}); const LeaderboardEntryAdded({required this.entry});
/// [LeaderboardEntry] to be written to the remote storage. /// [LeaderboardEntryData] to be written to the remote storage.
final LeaderboardEntry entry; final LeaderboardEntryData entry;
@override @override
List<Object?> get props => [entry]; List<Object?> get props => [entry];

@ -1 +1,2 @@
export 'bloc/leaderboard_bloc.dart'; export 'bloc/leaderboard_bloc.dart';
export 'models/leader_board_entry.dart';

@ -0,0 +1,80 @@
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball_theme/pinball_theme.dart';
/// {@template leaderboard_entry}
/// A model representing a leaderboard entry containing the ranking position,
/// player's initials, score, and chosen character.
///
/// {@endtemplate}
class LeaderboardEntry {
/// {@macro leaderboard_entry}
LeaderboardEntry({
required this.rank,
required this.playerInitials,
required this.score,
required this.character,
});
/// Ranking position for [LeaderboardEntry].
final String rank;
/// Player's chosen initials for [LeaderboardEntry].
final String playerInitials;
/// Score for [LeaderboardEntry].
final int score;
/// [CharacterTheme] for [LeaderboardEntry].
final AssetGenImage character;
}
/// Converts [LeaderboardEntryData] from repository to [LeaderboardEntry].
extension LeaderboardEntryDataX on LeaderboardEntryData {
/// Conversion method to [LeaderboardEntry]
LeaderboardEntry toEntry(int position) {
return LeaderboardEntry(
rank: position.toString(),
playerInitials: playerInitials,
score: score,
character: character.toTheme.characterAsset,
);
}
}
/// Converts [CharacterType] to [CharacterTheme] to show on UI character theme
/// from repository.
extension CharacterTypeX on CharacterType {
/// Conversion method to [CharacterTheme]
CharacterTheme get toTheme {
switch (this) {
case CharacterType.dash:
return const DashTheme();
case CharacterType.sparky:
return const SparkyTheme();
case CharacterType.android:
return const AndroidTheme();
case CharacterType.dino:
return const DinoTheme();
}
}
}
/// Converts [CharacterTheme] to [CharacterType] to persist at repository the
/// character theme from UI.
extension CharacterThemeX on CharacterTheme {
/// Conversion method to [CharacterType]
CharacterType get toType {
switch (runtimeType) {
case DashTheme:
return CharacterType.dash;
case SparkyTheme:
return CharacterType.sparky;
case AndroidTheme:
return CharacterType.android;
case DinoTheme:
return CharacterType.dino;
default:
return CharacterType.dash;
}
}
}

@ -23,10 +23,45 @@ List<Vector2> calculateArc({
final points = <Vector2>[]; final points = <Vector2>[];
for (var i = 0; i < precision; i++) { for (var i = 0; i < precision; i++) {
final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); final x = center.x + radius * math.cos((stepAngle * i) + offsetAngle);
final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); final y = center.y - radius * math.sin((stepAngle * i) + offsetAngle);
final point = Vector2(xCoord, yCoord); final point = Vector2(x, y);
points.add(point);
}
return points;
}
/// Calculates all [Vector2]s of an ellipse.
///
/// An ellipse can be achieved by specifying a [center], a [majorRadius] and a
/// [minorRadius].
///
/// The higher the [precision], the more [Vector2]s will be calculated;
/// achieving a more rounded ellipse.
///
/// For more information read: https://en.wikipedia.org/wiki/Ellipse.
List<Vector2> calculateEllipse({
required Vector2 center,
required double majorRadius,
required double minorRadius,
int precision = 100,
}) {
assert(
0 < minorRadius && minorRadius <= majorRadius,
'smallRadius ($minorRadius) and bigRadius ($majorRadius) must be in '
'range 0 < smallRadius <= bigRadius',
);
final stepAngle = 2 * math.pi / (precision - 1);
final points = <Vector2>[];
for (var i = 0; i < precision; i++) {
final x = center.x + minorRadius * math.cos(stepAngle * i);
final y = center.y - majorRadius * math.sin(stepAngle * i);
final point = Vector2(x, y);
points.add(point); points.add(point);
} }
@ -63,17 +98,15 @@ List<Vector2> calculateBezierCurve({
final points = <Vector2>[]; final points = <Vector2>[];
do { do {
var xCoord = 0.0; var x = 0.0;
var yCoord = 0.0; var y = 0.0;
for (var i = 0; i <= n; i++) { for (var i = 0; i <= n; i++) {
final point = controlPoints[i]; final point = controlPoints[i];
xCoord += x += 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; y += binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y;
yCoord +=
binomial(n, i) * math.pow(1 - t, n - i) * math.pow(t, i) * point.y;
} }
points.add(Vector2(xCoord, yCoord)); points.add(Vector2(x, y));
t = t + step; t = t + step;
} while (t <= 1); } while (t <= 1);

@ -33,6 +33,46 @@ void main() {
}); });
}); });
group('calculateEllipse', () {
test('returns by default 100 points as indicated by precision', () {
final points = calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 50,
);
expect(points.length, 100);
});
test('returns as many points as indicated by precision', () {
final points = calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 50,
precision: 50,
);
expect(points.length, 50);
});
test('fails if radius not in range', () {
expect(
() => calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 150,
),
throwsA(isA<AssertionError>()),
);
expect(
() => calculateEllipse(
center: Vector2.zero(),
majorRadius: 100,
minorRadius: 0,
),
throwsA(isA<AssertionError>()),
);
});
});
group('calculateBezierCurve', () { group('calculateBezierCurve', () {
test('fails if step not in range', () { test('fails if step not in range', () {
expect( expect(

@ -83,9 +83,9 @@ class LeaderboardRepository {
final FirebaseFirestore _firebaseFirestore; final FirebaseFirestore _firebaseFirestore;
/// Acquires top 10 [LeaderboardEntry]s. /// Acquires top 10 [LeaderboardEntryData]s.
Future<List<LeaderboardEntry>> fetchTop10Leaderboard() async { Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async {
final leaderboardEntries = <LeaderboardEntry>[]; final leaderboardEntries = <LeaderboardEntryData>[];
late List<QueryDocumentSnapshot> documents; late List<QueryDocumentSnapshot> documents;
try { try {
@ -103,7 +103,7 @@ class LeaderboardRepository {
final data = document.data() as Map<String, dynamic>?; final data = document.data() as Map<String, dynamic>?;
if (data != null) { if (data != null) {
try { try {
leaderboardEntries.add(LeaderboardEntry.fromJson(data)); leaderboardEntries.add(LeaderboardEntryData.fromJson(data));
} catch (error, stackTrace) { } catch (error, stackTrace) {
throw LeaderboardDeserializationException(error, stackTrace); throw LeaderboardDeserializationException(error, stackTrace);
} }
@ -115,7 +115,9 @@ class LeaderboardRepository {
/// Adds player's score entry to the leaderboard and gets their /// Adds player's score entry to the leaderboard and gets their
/// [LeaderboardRanking]. /// [LeaderboardRanking].
Future<LeaderboardRanking> addLeaderboardEntry(LeaderboardEntry entry) async { Future<LeaderboardRanking> addLeaderboardEntry(
LeaderboardEntryData entry,
) async {
late DocumentReference entryReference; late DocumentReference entryReference;
try { try {
entryReference = await _firebaseFirestore entryReference = await _firebaseFirestore

@ -1,9 +1,9 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'leaderboard_entry.g.dart'; part 'leaderboard_entry_data.g.dart';
/// Google character type associated with a [LeaderboardEntry]. /// Google character type associated with a [LeaderboardEntryData].
enum CharacterType { enum CharacterType {
/// Dash character. /// Dash character.
dash, dash,
@ -18,7 +18,7 @@ enum CharacterType {
dino, dino,
} }
/// {@template leaderboard_entry} /// {@template leaderboard_entry_data}
/// A model representing a leaderboard entry containing the player's initials, /// A model representing a leaderboard entry containing the player's initials,
/// score, and chosen character. /// score, and chosen character.
/// ///
@ -34,42 +34,42 @@ enum CharacterType {
/// ``` /// ```
/// {@endtemplate} /// {@endtemplate}
@JsonSerializable() @JsonSerializable()
class LeaderboardEntry extends Equatable { class LeaderboardEntryData extends Equatable {
/// {@macro leaderboard_entry} /// {@macro leaderboard_entry_data}
const LeaderboardEntry({ const LeaderboardEntryData({
required this.playerInitials, required this.playerInitials,
required this.score, required this.score,
required this.character, required this.character,
}); });
/// Factory which converts a [Map] into a [LeaderboardEntry]. /// Factory which converts a [Map] into a [LeaderboardEntryData].
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) { factory LeaderboardEntryData.fromJson(Map<String, dynamic> json) {
return _$LeaderboardEntryFromJson(json); return _$LeaderboardEntryFromJson(json);
} }
/// Converts the [LeaderboardEntry] to [Map]. /// Converts the [LeaderboardEntryData] to [Map].
Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this); Map<String, dynamic> toJson() => _$LeaderboardEntryToJson(this);
/// Player's chosen initials for [LeaderboardEntry]. /// Player's chosen initials for [LeaderboardEntryData].
/// ///
/// Example: 'ABC'. /// Example: 'ABC'.
@JsonKey(name: 'playerInitials') @JsonKey(name: 'playerInitials')
final String playerInitials; final String playerInitials;
/// Score for [LeaderboardEntry]. /// Score for [LeaderboardEntryData].
/// ///
/// Example: 1500. /// Example: 1500.
@JsonKey(name: 'score') @JsonKey(name: 'score')
final int score; final int score;
/// [CharacterType] for [LeaderboardEntry]. /// [CharacterType] for [LeaderboardEntryData].
/// ///
/// Example: [CharacterType.dash]. /// Example: [CharacterType.dash].
@JsonKey(name: 'character') @JsonKey(name: 'character')
final CharacterType character; final CharacterType character;
/// An empty [LeaderboardEntry] object. /// An empty [LeaderboardEntryData] object.
static const empty = LeaderboardEntry( static const empty = LeaderboardEntryData(
playerInitials: '', playerInitials: '',
score: 0, score: 0,
character: CharacterType.dash, character: CharacterType.dash,

@ -1,19 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'leaderboard_entry.dart'; part of 'leaderboard_entry_data.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
LeaderboardEntry _$LeaderboardEntryFromJson(Map<String, dynamic> json) => LeaderboardEntryData _$LeaderboardEntryFromJson(Map<String, dynamic> json) =>
LeaderboardEntry( LeaderboardEntryData(
playerInitials: json['playerInitials'] as String, playerInitials: json['playerInitials'] as String,
score: json['score'] as int, score: json['score'] as int,
character: $enumDecode(_$CharacterTypeEnumMap, json['character']), character: $enumDecode(_$CharacterTypeEnumMap, json['character']),
); );
Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntry instance) => Map<String, dynamic> _$LeaderboardEntryToJson(LeaderboardEntryData instance) =>
<String, dynamic>{ <String, dynamic>{
'playerInitials': instance.playerInitials, 'playerInitials': instance.playerInitials,
'score': instance.score, 'score': instance.score,

@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
/// {@template leaderboard_ranking} /// {@template leaderboard_ranking}
/// Contains [ranking] for a single [LeaderboardEntry] and the number of players /// Contains [ranking] for a single [LeaderboardEntryData] and the number of
/// the [ranking] is [outOf]. /// players the [ranking] is [outOf].
/// {@endtemplate} /// {@endtemplate}
class LeaderboardRanking extends Equatable { class LeaderboardRanking extends Equatable {
/// {@macro leaderboard_ranking} /// {@macro leaderboard_ranking}
const LeaderboardRanking({required this.ranking, required this.outOf}); const LeaderboardRanking({required this.ranking, required this.outOf});
/// Place ranking by score for a [LeaderboardEntry]. /// Place ranking by score for a [LeaderboardEntryData].
final int ranking; final int ranking;
/// Number of [LeaderboardEntry]s at the time of score entry. /// Number of [LeaderboardEntryData]s at the time of score entry.
final int outOf; final int outOf;
@override @override

@ -1,2 +1,2 @@
export 'leaderboard_entry.dart'; export 'leaderboard_entry_data.dart';
export 'leaderboard_ranking.dart'; export 'leaderboard_ranking.dart';

@ -57,7 +57,7 @@ void main() {
final top10Leaderboard = top10Scores final top10Leaderboard = top10Scores
.map( .map(
(score) => LeaderboardEntry( (score) => LeaderboardEntryData(
playerInitials: 'user$score', playerInitials: 'user$score',
score: score, score: score,
character: CharacterType.dash, character: CharacterType.dash,
@ -144,7 +144,7 @@ void main() {
entryScore, entryScore,
1000, 1000,
]; ];
final leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: entryScore, score: entryScore,
character: CharacterType.dash, character: CharacterType.dash,

@ -9,21 +9,21 @@ void main() {
'character': 'dash', 'character': 'dash',
}; };
const leaderboardEntry = LeaderboardEntry( const leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,
); );
test('can be instantiated', () { test('can be instantiated', () {
const leaderboardEntry = LeaderboardEntry.empty; const leaderboardEntry = LeaderboardEntryData.empty;
expect(leaderboardEntry, isNotNull); expect(leaderboardEntry, isNotNull);
}); });
test('supports value equality.', () { test('supports value equality.', () {
const leaderboardEntry = LeaderboardEntry.empty; const leaderboardEntry = LeaderboardEntryData.empty;
const leaderboardEntry2 = LeaderboardEntry.empty; const leaderboardEntry2 = LeaderboardEntryData.empty;
expect(leaderboardEntry, equals(leaderboardEntry2)); expect(leaderboardEntry, equals(leaderboardEntry2));
}); });
@ -33,7 +33,7 @@ void main() {
}); });
test('can be obtained from json', () { test('can be obtained from json', () {
final leaderboardEntryFrom = LeaderboardEntry.fromJson(data); final leaderboardEntryFrom = LeaderboardEntryData.fromJson(data);
expect(leaderboardEntry, equals(leaderboardEntryFrom)); expect(leaderboardEntry, equals(leaderboardEntryFrom));
}); });

@ -0,0 +1,39 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# VSCode related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

@ -0,0 +1,11 @@
# pinball_components
[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link]
[![License: MIT][license_badge]][license_link]
Package with the UI game components for the Pinball Game
[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 pinball_components;
export 'src/pinball_components.dart';

@ -0,0 +1,7 @@
/// {@template pinball_components}
/// Package with the UI game components for the Pinball Game
/// {@endtemplate}
class PinballComponents {
/// {@macro pinball_components}
const PinballComponents();
}

@ -0,0 +1,16 @@
name: pinball_components
description: Package with the UI game components for the Pinball Game
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=2.16.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
very_good_analysis: ^2.4.0

@ -0,0 +1,11 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball_components/pinball_components.dart';
void main() {
group('PinballComponents', () {
test('can be instantiated', () {
expect(PinballComponents(), isNotNull);
});
});
}

@ -25,18 +25,21 @@ void main() {
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 1, balls: 1,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -56,12 +59,14 @@ void main() {
score: 2, score: 2,
balls: 3, balls: 3,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 5, score: 5,
balls: 3, balls: 3,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -82,18 +87,21 @@ void main() {
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 1, balls: 1,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
const GameState( const GameState(
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -113,18 +121,21 @@ void main() {
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0], activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1], activatedBonusLetters: [0, 1],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1, 2], activatedBonusLetters: [0, 1, 2],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
], ],
@ -145,46 +156,87 @@ void main() {
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0], activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1], activatedBonusLetters: [0, 1],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1, 2], activatedBonusLetters: [0, 1, 2],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1, 2, 3], activatedBonusLetters: [0, 1, 2, 3],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [0, 1, 2, 3, 4], activatedBonusLetters: [0, 1, 2, 3, 4],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
GameState( GameState(
score: 0, score: 0,
balls: 3, balls: 3,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.word], bonusHistory: [GameBonus.word],
), ),
GameState( GameState(
score: GameBloc.bonusWordScore, score: GameBloc.bonusWordScore,
balls: 3, balls: 3,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.word], bonusHistory: [GameBonus.word],
), ),
], ],
); );
}); });
group('DashNestActivated', () {
blocTest<GameBloc, GameState>(
'adds the bonus when all nests are activated',
build: GameBloc.new,
act: (bloc) => bloc
..add(const DashNestActivated('0'))
..add(const DashNestActivated('1'))
..add(const DashNestActivated('2')),
expect: () => const [
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {'0', '1'},
bonusHistory: [],
),
GameState(
score: 0,
balls: 3,
activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [GameBonus.dashNest],
),
],
);
});
}); });
} }

@ -67,5 +67,22 @@ void main() {
}, },
); );
}); });
group('DashNestActivated', () {
test('can be instantiated', () {
expect(const DashNestActivated('0'), isNotNull);
});
test('supports value equality', () {
expect(
DashNestActivated('0'),
equals(DashNestActivated('0')),
);
expect(
DashNestActivated('0'),
isNot(equals(DashNestActivated('1'))),
);
});
});
}); });
} }

@ -11,6 +11,7 @@ void main() {
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: const [], activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
equals( equals(
@ -18,6 +19,7 @@ void main() {
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
), ),
@ -31,6 +33,7 @@ void main() {
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
isNotNull, isNotNull,
@ -47,6 +50,7 @@ void main() {
balls: -1, balls: -1,
score: 0, score: 0,
activatedBonusLetters: const [], activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -63,6 +67,7 @@ void main() {
balls: 0, balls: 0,
score: -1, score: -1,
activatedBonusLetters: const [], activatedBonusLetters: const [],
activatedDashNests: const {},
bonusHistory: const [], bonusHistory: const [],
), ),
throwsAssertionError, throwsAssertionError,
@ -78,6 +83,7 @@ void main() {
balls: 0, balls: 0,
score: 0, score: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isTrue); expect(gameState.isGameOver, isTrue);
@ -90,6 +96,7 @@ void main() {
balls: 1, balls: 1,
score: 0, score: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isGameOver, isFalse); expect(gameState.isGameOver, isFalse);
@ -105,6 +112,7 @@ void main() {
balls: 1, balls: 1,
score: 0, score: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isLastBall, isTrue); expect(gameState.isLastBall, isTrue);
@ -119,6 +127,7 @@ void main() {
balls: 2, balls: 2,
score: 0, score: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isLastBall, isFalse); expect(gameState.isLastBall, isFalse);
@ -134,6 +143,7 @@ void main() {
balls: 3, balls: 3,
score: 0, score: 0,
activatedBonusLetters: [1], activatedBonusLetters: [1],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isLetterActivated(1), isTrue); expect(gameState.isLetterActivated(1), isTrue);
@ -147,6 +157,7 @@ void main() {
balls: 3, balls: 3,
score: 0, score: 0,
activatedBonusLetters: [1], activatedBonusLetters: [1],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect(gameState.isLetterActivated(0), isFalse); expect(gameState.isLetterActivated(0), isFalse);
@ -163,6 +174,7 @@ void main() {
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -180,6 +192,7 @@ void main() {
balls: 0, balls: 0,
score: 2, score: 2,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
expect( expect(
@ -197,12 +210,14 @@ void main() {
score: 2, score: 2,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
final otherGameState = GameState( final otherGameState = GameState(
score: gameState.score + 1, score: gameState.score + 1,
balls: gameState.balls + 1, balls: gameState.balls + 1,
activatedBonusLetters: const [0], activatedBonusLetters: const [0],
activatedDashNests: const {'1'},
bonusHistory: const [GameBonus.word], bonusHistory: const [GameBonus.word],
); );
expect(gameState, isNot(equals(otherGameState))); expect(gameState, isNot(equals(otherGameState)));
@ -212,6 +227,7 @@ void main() {
score: otherGameState.score, score: otherGameState.score,
balls: otherGameState.balls, balls: otherGameState.balls,
activatedBonusLetters: otherGameState.activatedBonusLetters, activatedBonusLetters: otherGameState.activatedBonusLetters,
activatedDashNests: otherGameState.activatedDashNests,
bonusHistory: otherGameState.bonusHistory, bonusHistory: otherGameState.bonusHistory,
), ),
equals(otherGameState), equals(otherGameState),

@ -156,6 +156,7 @@ void main() {
score: 10, score: 10,
balls: 1, balls: 1,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
), ),
); );

@ -234,6 +234,7 @@ void main() {
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [0], activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
whenListen( whenListen(
@ -259,6 +260,7 @@ void main() {
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [0], activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );
@ -283,6 +285,7 @@ void main() {
score: 0, score: 0,
balls: 2, balls: 2,
activatedBonusLetters: [0], activatedBonusLetters: [0],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );

@ -17,13 +17,14 @@ void main() {
group( group(
'Flipper', 'Flipper',
() { () {
// TODO(alestiago): Add golden tests.
flameTester.test( flameTester.test(
'loads correctly', 'loads correctly',
(game) async { (game) async {
final leftFlipper = Flipper.fromSide( final leftFlipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
final rightFlipper = Flipper.fromSide( final rightFlipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
await game.ready(); await game.ready();
@ -36,13 +37,13 @@ void main() {
group('constructor', () { group('constructor', () {
test('sets BoardSide', () { test('sets BoardSide', () {
final leftFlipper = Flipper.fromSide( final leftFlipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
expect(leftFlipper.side, equals(leftFlipper.side)); expect(leftFlipper.side, equals(leftFlipper.side));
final rightFlipper = Flipper.fromSide( final rightFlipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
expect(rightFlipper.side, equals(rightFlipper.side)); expect(rightFlipper.side, equals(rightFlipper.side));
@ -53,7 +54,7 @@ void main() {
flameTester.test( flameTester.test(
'is dynamic', 'is dynamic',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -65,7 +66,7 @@ void main() {
flameTester.test( flameTester.test(
'ignores gravity', 'ignores gravity',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -77,7 +78,7 @@ void main() {
flameTester.test( flameTester.test(
'has greater mass than Ball', 'has greater mass than Ball',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
final ball = Ball(); final ball = Ball();
@ -97,7 +98,7 @@ void main() {
flameTester.test( flameTester.test(
'has three', 'has three',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -109,7 +110,7 @@ void main() {
flameTester.test( flameTester.test(
'has density', 'has density',
(game) async { (game) async {
final flipper = Flipper.fromSide( final flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
await game.ensureAdd(flipper); await game.ensureAdd(flipper);
@ -139,7 +140,7 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.fromSide( flipper = Flipper(
side: BoardSide.left, side: BoardSide.left,
); );
}); });
@ -205,7 +206,7 @@ void main() {
late Flipper flipper; late Flipper flipper;
setUp(() { setUp(() {
flipper = Flipper.fromSide( flipper = Flipper(
side: BoardSide.right, side: BoardSide.right,
); );
}); });
@ -269,159 +270,4 @@ void main() {
}); });
}, },
); );
group('FlipperAnchor', () {
flameTester.test(
'position is at the left of the left Flipper',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(-Flipper.size.x / 2));
},
);
flameTester.test(
'position is at the right of the right Flipper',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.right,
);
await game.ensureAdd(flipper);
final flipperAnchor = FlipperAnchor(flipper: flipper);
await game.ensureAdd(flipperAnchor);
expect(flipperAnchor.body.position.x, equals(Flipper.size.x / 2));
},
);
});
group('FlipperAnchorRevoluteJointDef', () {
group('initializes with', () {
flameTester.test(
'limits enabled',
(game) async {
final flipper = Flipper.fromSide(
side: BoardSide.left,
);
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.fromSide(
side: BoardSide.left,
);
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.fromSide(
side: BoardSide.right,
);
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.fromSide(
side: BoardSide.left,
);
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.fromSide(
side: BoardSide.right,
);
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)),
);
},
);
},
);
});
} }

@ -165,6 +165,42 @@ void main() {
}); });
}); });
group('ellipse', () {
flameTester.test(
'loads correctly',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(game.contains(pathway), isTrue);
},
);
group('body', () {
flameTester.test(
'is static',
(game) async {
final pathway = Pathway.ellipse(
center: Vector2.zero(),
width: width,
majorRadius: 150,
minorRadius: 70,
);
await game.ready();
await game.ensureAdd(pathway);
expect(pathway.body.bodyType, equals(BodyType.static));
},
);
});
});
group('bezier curve', () { group('bezier curve', () {
final controlPoints = [ final controlPoints = [
Vector2(0, 0), Vector2(0, 0),

@ -13,6 +13,7 @@ void main() {
score: 10, score: 10,
balls: 2, balls: 2,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );

@ -88,6 +88,7 @@ void main() {
score: 0, score: 0,
balls: 0, balls: 0,
activatedBonusLetters: [], activatedBonusLetters: [],
activatedDashNests: {},
bonusHistory: [], bonusHistory: [],
); );

@ -2,6 +2,7 @@ import 'package:flame/input.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
import 'package:pinball/theme/theme.dart'; import 'package:pinball/theme/theme.dart';
@ -32,6 +33,8 @@ class MockGameState extends Mock implements GameState {}
class MockThemeCubit extends Mock implements ThemeCubit {} class MockThemeCubit extends Mock implements ThemeCubit {}
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {}
class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent {
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {

@ -5,8 +5,9 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} import '../../helpers/helpers.dart';
void main() { void main() {
group('LeaderboardBloc', () { group('LeaderboardBloc', () {
@ -42,7 +43,7 @@ void main() {
final top10Leaderboard = top10Scores final top10Leaderboard = top10Scores
.map( .map(
(score) => LeaderboardEntry( (score) => LeaderboardEntryData(
playerInitials: 'user$score', playerInitials: 'user$score',
score: score, score: score,
character: CharacterType.dash, character: CharacterType.dash,
@ -101,7 +102,7 @@ void main() {
}); });
group('LeaderboardEntryAdded', () { group('LeaderboardEntryAdded', () {
final leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,
@ -163,4 +164,40 @@ void main() {
); );
}); });
}); });
group('CharacterTypeX', () {
test('converts CharacterType.android to AndroidTheme', () {
expect(CharacterType.android.toTheme, equals(AndroidTheme()));
});
test('converts CharacterType.dash to DashTheme', () {
expect(CharacterType.dash.toTheme, equals(DashTheme()));
});
test('converts CharacterType.dino to DinoTheme', () {
expect(CharacterType.dino.toTheme, equals(DinoTheme()));
});
test('converts CharacterType.sparky to SparkyTheme', () {
expect(CharacterType.sparky.toTheme, equals(SparkyTheme()));
});
});
group('CharacterThemeX', () {
test('converts AndroidTheme to CharacterType.android', () {
expect(AndroidTheme().toType, equals(CharacterType.android));
});
test('converts DashTheme to CharacterType.dash', () {
expect(DashTheme().toType, equals(CharacterType.dash));
});
test('converts DinoTheme to CharacterType.dino', () {
expect(DinoTheme().toType, equals(CharacterType.dino));
});
test('converts SparkyTheme to CharacterType.sparky', () {
expect(SparkyTheme().toType, equals(CharacterType.sparky));
});
});
} }

@ -20,7 +20,7 @@ void main() {
}); });
group('LeaderboardEntryAdded', () { group('LeaderboardEntryAdded', () {
const leaderboardEntry = LeaderboardEntry( const leaderboardEntry = LeaderboardEntryData(
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: CharacterType.dash,

@ -3,6 +3,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart';
import 'package:pinball/leaderboard/leaderboard.dart'; import 'package:pinball/leaderboard/leaderboard.dart';
import 'package:pinball_theme/pinball_theme.dart';
void main() { void main() {
group('LeaderboardState', () { group('LeaderboardState', () {
@ -25,10 +26,11 @@ void main() {
}); });
group('copyWith', () { group('copyWith', () {
const leaderboardEntry = LeaderboardEntry( final leaderboardEntry = LeaderboardEntry(
rank: '1',
playerInitials: 'ABC', playerInitials: 'ABC',
score: 1500, score: 1500,
character: CharacterType.dash, character: DashTheme().characterAsset,
); );
test( test(
@ -51,7 +53,7 @@ void main() {
final otherLeaderboardState = LeaderboardState( final otherLeaderboardState = LeaderboardState(
status: LeaderboardStatus.success, status: LeaderboardStatus.success,
ranking: LeaderboardRanking(ranking: 0, outOf: 0), ranking: LeaderboardRanking(ranking: 0, outOf: 0),
leaderboard: const [leaderboardEntry], leaderboard: [leaderboardEntry],
); );
expect(leaderboardState, isNot(equals(otherLeaderboardState))); expect(leaderboardState, isNot(equals(otherLeaderboardState)));

Loading…
Cancel
Save