From 823cdb6af58ee52d01361f09bbba5d21078e6ff5 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 23 Mar 2022 16:15:06 +0000 Subject: [PATCH 1/6] feat: include DashNest bonus (#80) * feat: included dash nest logic Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/bloc/game_bloc.dart | 25 +++++++++++ lib/game/bloc/game_event.dart | 9 ++++ lib/game/bloc/game_state.dart | 12 +++++ test/game/bloc/game_bloc_test.dart | 52 ++++++++++++++++++++++ test/game/bloc/game_event_test.dart | 17 +++++++ test/game/bloc/game_state_test.dart | 16 +++++++ test/game/components/ball_test.dart | 1 + test/game/components/board_test.dart | 19 ++++---- test/game/components/bonus_word_test.dart | 3 ++ test/game/view/game_hud_test.dart | 1 + test/game/view/pinball_game_page_test.dart | 1 + test/helpers/extensions.dart | 39 ---------------- 12 files changed, 146 insertions(+), 49 deletions(-) diff --git a/lib/game/bloc/game_bloc.dart b/lib/game/bloc/game_bloc.dart index 663fee35..c02417a7 100644 --- a/lib/game/bloc/game_bloc.dart +++ b/lib/game/bloc/game_bloc.dart @@ -12,6 +12,7 @@ class GameBloc extends Bloc { on(_onBallLost); on(_onScored); on(_onBonusLetterActivated); + on(_onDashNestActivated); } static const bonusWord = 'GOOGLE'; @@ -52,4 +53,28 @@ class GameBloc extends Bloc { ); } } + + 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), + ); + } + } } diff --git a/lib/game/bloc/game_event.dart b/lib/game/bloc/game_event.dart index 0edc91ab..b05c5336 100644 --- a/lib/game/bloc/game_event.dart +++ b/lib/game/bloc/game_event.dart @@ -45,3 +45,12 @@ class BonusLetterActivated extends GameEvent { @override List get props => [letterIndex]; } + +class DashNestActivated extends GameEvent { + const DashNestActivated(this.nestId); + + final String nestId; + + @override + List get props => [nestId]; +} diff --git a/lib/game/bloc/game_state.dart b/lib/game/bloc/game_state.dart index e2c39d1f..5c722946 100644 --- a/lib/game/bloc/game_state.dart +++ b/lib/game/bloc/game_state.dart @@ -7,6 +7,10 @@ enum GameBonus { /// Bonus achieved when the user activate all of the bonus /// letters on the board, forming the bonus 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} @@ -19,6 +23,7 @@ class GameState extends Equatable { required this.balls, required this.activatedBonusLetters, required this.bonusHistory, + required this.activatedDashNests, }) : assert(score >= 0, "Score can't be negative"), assert(balls >= 0, "Number of balls can't be negative"); @@ -26,6 +31,7 @@ class GameState extends Equatable { : score = 0, balls = 3, activatedBonusLetters = const [], + activatedDashNests = const {}, bonusHistory = const []; /// The current score of the game. @@ -39,6 +45,9 @@ class GameState extends Equatable { /// Active bonus letters. final List activatedBonusLetters; + /// Active dash nests. + final Set activatedDashNests; + /// Holds the history of all the [GameBonus]es earned by the player during a /// PinballGame. final List bonusHistory; @@ -57,6 +66,7 @@ class GameState extends Equatable { int? score, int? balls, List? activatedBonusLetters, + Set? activatedDashNests, List? bonusHistory, }) { assert( @@ -69,6 +79,7 @@ class GameState extends Equatable { balls: balls ?? this.balls, activatedBonusLetters: activatedBonusLetters ?? this.activatedBonusLetters, + activatedDashNests: activatedDashNests ?? this.activatedDashNests, bonusHistory: bonusHistory ?? this.bonusHistory, ); } @@ -78,6 +89,7 @@ class GameState extends Equatable { score, balls, activatedBonusLetters, + activatedDashNests, bonusHistory, ]; } diff --git a/test/game/bloc/game_bloc_test.dart b/test/game/bloc/game_bloc_test.dart index 18e50858..f4b79001 100644 --- a/test/game/bloc/game_bloc_test.dart +++ b/test/game/bloc/game_bloc_test.dart @@ -25,18 +25,21 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -56,12 +59,14 @@ void main() { score: 2, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 5, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -82,18 +87,21 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), const GameState( score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -113,18 +121,21 @@ void main() { score: 0, balls: 3, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2], + activatedDashNests: {}, bonusHistory: [], ), ], @@ -145,46 +156,87 @@ void main() { score: 0, balls: 3, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2, 3], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [0, 1, 2, 3, 4], + activatedDashNests: {}, bonusHistory: [], ), GameState( score: 0, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [GameBonus.word], ), GameState( score: GameBloc.bonusWordScore, balls: 3, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [GameBonus.word], ), ], ); }); + + group('DashNestActivated', () { + blocTest( + '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], + ), + ], + ); + }); }); } diff --git a/test/game/bloc/game_event_test.dart b/test/game/bloc/game_event_test.dart index d6d2278b..af9f6148 100644 --- a/test/game/bloc/game_event_test.dart +++ b/test/game/bloc/game_event_test.dart @@ -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'))), + ); + }); + }); }); } diff --git a/test/game/bloc/game_state_test.dart b/test/game/bloc/game_state_test.dart index 8ab72e6c..aa8bdf66 100644 --- a/test/game/bloc/game_state_test.dart +++ b/test/game/bloc/game_state_test.dart @@ -11,6 +11,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), equals( @@ -18,6 +19,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ), @@ -31,6 +33,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), isNotNull, @@ -47,6 +50,7 @@ void main() { balls: -1, score: 0, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -63,6 +67,7 @@ void main() { balls: 0, score: -1, activatedBonusLetters: const [], + activatedDashNests: const {}, bonusHistory: const [], ), throwsAssertionError, @@ -78,6 +83,7 @@ void main() { balls: 0, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isTrue); @@ -90,6 +96,7 @@ void main() { balls: 1, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isGameOver, isFalse); @@ -105,6 +112,7 @@ void main() { balls: 1, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLastBall, isTrue); @@ -119,6 +127,7 @@ void main() { balls: 2, score: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLastBall, isFalse); @@ -134,6 +143,7 @@ void main() { balls: 3, score: 0, activatedBonusLetters: [1], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLetterActivated(1), isTrue); @@ -147,6 +157,7 @@ void main() { balls: 3, score: 0, activatedBonusLetters: [1], + activatedDashNests: {}, bonusHistory: [], ); expect(gameState.isLetterActivated(0), isFalse); @@ -163,6 +174,7 @@ void main() { balls: 0, score: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect( @@ -180,6 +192,7 @@ void main() { balls: 0, score: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); expect( @@ -197,12 +210,14 @@ void main() { score: 2, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); final otherGameState = GameState( score: gameState.score + 1, balls: gameState.balls + 1, activatedBonusLetters: const [0], + activatedDashNests: const {'1'}, bonusHistory: const [GameBonus.word], ); expect(gameState, isNot(equals(otherGameState))); @@ -212,6 +227,7 @@ void main() { score: otherGameState.score, balls: otherGameState.balls, activatedBonusLetters: otherGameState.activatedBonusLetters, + activatedDashNests: otherGameState.activatedDashNests, bonusHistory: otherGameState.bonusHistory, ), equals(otherGameState), diff --git a/test/game/components/ball_test.dart b/test/game/components/ball_test.dart index 7a48b21f..f94c1526 100644 --- a/test/game/components/ball_test.dart +++ b/test/game/components/ball_test.dart @@ -156,6 +156,7 @@ void main() { score: 10, balls: 1, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ), ); diff --git a/test/game/components/board_test.dart b/test/game/components/board_test.dart index 7791d891..f0cd0e16 100644 --- a/test/game/components/board_test.dart +++ b/test/game/components/board_test.dart @@ -30,9 +30,9 @@ void main() { await game.ready(); await game.ensureAdd(board); - final leftFlippers = board.findNestedChildren( - condition: (flipper) => flipper.side.isLeft, - ); + final leftFlippers = board.descendants().whereType().where( + (flipper) => flipper.side.isLeft, + ); expect(leftFlippers.length, equals(1)); }, ); @@ -43,10 +43,9 @@ void main() { final board = Board(); await game.ready(); await game.ensureAdd(board); - - final rightFlippers = board.findNestedChildren( - condition: (flipper) => flipper.side.isRight, - ); + final rightFlippers = board.descendants().whereType().where( + (flipper) => flipper.side.isRight, + ); expect(rightFlippers.length, equals(1)); }, ); @@ -58,7 +57,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final baseboards = board.findNestedChildren(); + final baseboards = board.descendants().whereType(); expect(baseboards.length, equals(2)); }, ); @@ -70,7 +69,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final kickers = board.findNestedChildren(); + final kickers = board.descendants().whereType(); expect(kickers.length, equals(2)); }, ); @@ -83,7 +82,7 @@ void main() { await game.ready(); await game.ensureAdd(board); - final roundBumpers = board.findNestedChildren(); + final roundBumpers = board.descendants().whereType(); expect(roundBumpers.length, equals(3)); }, ); diff --git a/test/game/components/bonus_word_test.dart b/test/game/components/bonus_word_test.dart index 47a7e257..a12a5a74 100644 --- a/test/game/components/bonus_word_test.dart +++ b/test/game/components/bonus_word_test.dart @@ -234,6 +234,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); whenListen( @@ -259,6 +260,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); @@ -283,6 +285,7 @@ void main() { score: 0, balls: 2, activatedBonusLetters: [0], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/game/view/game_hud_test.dart b/test/game/view/game_hud_test.dart index 536edbad..953b89eb 100644 --- a/test/game/view/game_hud_test.dart +++ b/test/game/view/game_hud_test.dart @@ -13,6 +13,7 @@ void main() { score: 10, balls: 2, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/game/view/pinball_game_page_test.dart b/test/game/view/pinball_game_page_test.dart index dcf0c001..5298d6ac 100644 --- a/test/game/view/pinball_game_page_test.dart +++ b/test/game/view/pinball_game_page_test.dart @@ -88,6 +88,7 @@ void main() { score: 0, balls: 0, activatedBonusLetters: [], + activatedDashNests: {}, bonusHistory: [], ); diff --git a/test/helpers/extensions.dart b/test/helpers/extensions.dart index abea191d..b3c4c6f8 100644 --- a/test/helpers/extensions.dart +++ b/test/helpers/extensions.dart @@ -1,4 +1,3 @@ -import 'package:flame/components.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_theme/pinball_theme.dart'; @@ -21,41 +20,3 @@ extension DebugPinballGameTest on DebugPinballGame { ), ); } - -extension ComponentX on Component { - T findNestedChild({ - bool Function(T)? condition, - }) { - T? nestedChild; - propagateToChildren((child) { - final foundChild = (condition ?? (_) => true)(child); - if (foundChild) { - nestedChild = child; - } - - return !foundChild; - }); - - if (nestedChild == null) { - throw Exception('No child of type $T found.'); - } else { - return nestedChild!; - } - } - - List findNestedChildren({ - bool Function(T)? condition, - }) { - final nestedChildren = []; - propagateToChildren((child) { - final foundChild = (condition ?? (_) => true)(child); - if (foundChild) { - nestedChildren.add(child); - } - - return true; - }); - - return nestedChildren; - } -} From 5bbfd0c3628a978ea2aaf0159f3f50087ac5c451 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Wed, 23 Mar 2022 16:19:44 +0000 Subject: [PATCH 2/6] refactor: made Pathway createFixtureDefs public (#81) * refactor: made createFixtureDefs public --- lib/game/components/pathway.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 0c29dd7b..8604e0f3 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -146,7 +146,8 @@ class Pathway extends BodyComponent with InitialPosition, Layered { final List> _paths; - List _createFixtureDefs() { + /// Constructs different [ChainShape]s to form the [Pathway] shape. + List createFixtureDefs() { final fixturesDef = []; for (final path in _paths) { @@ -161,7 +162,7 @@ class Pathway extends BodyComponent with InitialPosition, Layered { Body createBody() { final bodyDef = BodyDef()..position = initialPosition; final body = world.createBody(bodyDef); - _createFixtureDefs().forEach(body.createFixture); + createFixtureDefs().forEach(body.createFixture); return body; } From 92404fbdba78f15f4f8b65f633e5aaadda81e9f0 Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 23 Mar 2022 14:16:48 -0300 Subject: [PATCH 3/6] feat: adding pinball components scaffold project (#82) * refactor: removed findNested extensions (#77) * feat: bootstraping the components package Co-authored-by: Alejandro Santiago --- .github/workflows/pinball_components.yaml | 19 +++++++++ packages/pinball_components/.gitignore | 39 +++++++++++++++++++ packages/pinball_components/README.md | 11 ++++++ .../pinball_components/analysis_options.yaml | 1 + .../lib/pinball_components.dart | 3 ++ .../lib/src/pinball_components.dart | 7 ++++ packages/pinball_components/pubspec.yaml | 16 ++++++++ .../test/src/pinball_components_test.dart | 11 ++++++ 8 files changed, 107 insertions(+) create mode 100644 .github/workflows/pinball_components.yaml create mode 100644 packages/pinball_components/.gitignore create mode 100644 packages/pinball_components/README.md create mode 100644 packages/pinball_components/analysis_options.yaml create mode 100644 packages/pinball_components/lib/pinball_components.dart create mode 100644 packages/pinball_components/lib/src/pinball_components.dart create mode 100644 packages/pinball_components/pubspec.yaml create mode 100644 packages/pinball_components/test/src/pinball_components_test.dart diff --git a/.github/workflows/pinball_components.yaml b/.github/workflows/pinball_components.yaml new file mode 100644 index 00000000..cab60a54 --- /dev/null +++ b/.github/workflows/pinball_components.yaml @@ -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" diff --git a/packages/pinball_components/.gitignore b/packages/pinball_components/.gitignore new file mode 100644 index 00000000..d6130351 --- /dev/null +++ b/packages/pinball_components/.gitignore @@ -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 diff --git a/packages/pinball_components/README.md b/packages/pinball_components/README.md new file mode 100644 index 00000000..81ea9893 --- /dev/null +++ b/packages/pinball_components/README.md @@ -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 \ No newline at end of file diff --git a/packages/pinball_components/analysis_options.yaml b/packages/pinball_components/analysis_options.yaml new file mode 100644 index 00000000..3742fc3d --- /dev/null +++ b/packages/pinball_components/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/pinball_components/lib/pinball_components.dart b/packages/pinball_components/lib/pinball_components.dart new file mode 100644 index 00000000..a08579e5 --- /dev/null +++ b/packages/pinball_components/lib/pinball_components.dart @@ -0,0 +1,3 @@ +library pinball_components; + +export 'src/pinball_components.dart'; diff --git a/packages/pinball_components/lib/src/pinball_components.dart b/packages/pinball_components/lib/src/pinball_components.dart new file mode 100644 index 00000000..dbc4d6fd --- /dev/null +++ b/packages/pinball_components/lib/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(); +} diff --git a/packages/pinball_components/pubspec.yaml b/packages/pinball_components/pubspec.yaml new file mode 100644 index 00000000..a3a990a0 --- /dev/null +++ b/packages/pinball_components/pubspec.yaml @@ -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 \ No newline at end of file diff --git a/packages/pinball_components/test/src/pinball_components_test.dart b/packages/pinball_components/test/src/pinball_components_test.dart new file mode 100644 index 00000000..7359ddcb --- /dev/null +++ b/packages/pinball_components/test/src/pinball_components_test.dart @@ -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); + }); + }); +} From 573e16d0d5047ae82e0ea4ca906208b234489986 Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 23 Mar 2022 22:24:18 +0100 Subject: [PATCH 4/6] refactor: leaderboard model (#78) * refactor: changed name of LeaderboardEntry at LeaderboardRepository to LeaderboardEntryData * chore: doc and analysis errors * refactor: removed findNested extensions (#77) * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * Update lib/leaderboard/bloc/leaderboard_bloc.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * fix: leaderboard gen file * refactor: moved leaderboard models to separate path * test: fixed tests with leaderboard model * chore: doc Co-authored-by: Alejandro Santiago Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/leaderboard/bloc/leaderboard_bloc.dart | 9 ++- lib/leaderboard/bloc/leaderboard_event.dart | 8 +- lib/leaderboard/leaderboard.dart | 1 + .../models/leader_board_entry.dart | 80 +++++++++++++++++++ .../lib/src/leaderboard_repository.dart | 12 +-- ...entry.dart => leaderboard_entry_data.dart} | 28 +++---- ...y.g.dart => leaderboard_entry_data.g.dart} | 8 +- .../lib/src/models/leaderboard_ranking.dart | 8 +- .../lib/src/models/models.dart | 2 +- .../test/src/leaderboard_repository_test.dart | 4 +- ....dart => leaderboard_entry_data_test.dart} | 10 +-- test/helpers/mocks.dart | 3 + .../bloc/leaderboard_bloc_test.dart | 43 +++++++++- .../bloc/leaderboard_event_test.dart | 2 +- .../bloc/leaderboard_state_test.dart | 8 +- 15 files changed, 179 insertions(+), 47 deletions(-) create mode 100644 lib/leaderboard/models/leader_board_entry.dart rename packages/leaderboard_repository/lib/src/models/{leaderboard_entry.dart => leaderboard_entry_data.dart} (64%) rename packages/leaderboard_repository/lib/src/models/{leaderboard_entry.g.dart => leaderboard_entry_data.g.dart} (76%) rename packages/leaderboard_repository/test/src/models/{leaderboard_entry_test.dart => leaderboard_entry_data_test.dart} (72%) diff --git a/lib/leaderboard/bloc/leaderboard_bloc.dart b/lib/leaderboard/bloc/leaderboard_bloc.dart index 6542548d..49a35474 100644 --- a/lib/leaderboard/bloc/leaderboard_bloc.dart +++ b/lib/leaderboard/bloc/leaderboard_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; +import 'package:pinball/leaderboard/leaderboard.dart'; part 'leaderboard_event.dart'; part 'leaderboard_state.dart'; @@ -30,10 +31,16 @@ class LeaderboardBloc extends Bloc { try { final top10Leaderboard = await _leaderboardRepository.fetchTop10Leaderboard(); + + final leaderboardEntries = []; + top10Leaderboard.asMap().forEach( + (index, value) => leaderboardEntries.add(value.toEntry(index + 1)), + ); + emit( state.copyWith( status: LeaderboardStatus.success, - leaderboard: top10Leaderboard, + leaderboard: leaderboardEntries, ), ); } catch (error) { diff --git a/lib/leaderboard/bloc/leaderboard_event.dart b/lib/leaderboard/bloc/leaderboard_event.dart index 34152163..b9e6955a 100644 --- a/lib/leaderboard/bloc/leaderboard_event.dart +++ b/lib/leaderboard/bloc/leaderboard_event.dart @@ -9,7 +9,7 @@ abstract class LeaderboardEvent extends Equatable { } /// {@template top_10_fetched} -/// Request the top 10 [LeaderboardEntry]s. +/// Request the top 10 [LeaderboardEntryData]s. /// {endtemplate} class Top10Fetched extends LeaderboardEvent { /// {@macro top_10_fetched} @@ -20,7 +20,7 @@ class Top10Fetched extends LeaderboardEvent { } /// {@template leaderboard_entry_added} -/// Writes a new [LeaderboardEntry]. +/// Writes a new [LeaderboardEntryData]. /// /// Should be added when a player finishes a game. /// {endtemplate} @@ -28,8 +28,8 @@ class LeaderboardEntryAdded extends LeaderboardEvent { /// {@macro leaderboard_entry_added} const LeaderboardEntryAdded({required this.entry}); - /// [LeaderboardEntry] to be written to the remote storage. - final LeaderboardEntry entry; + /// [LeaderboardEntryData] to be written to the remote storage. + final LeaderboardEntryData entry; @override List get props => [entry]; diff --git a/lib/leaderboard/leaderboard.dart b/lib/leaderboard/leaderboard.dart index 13d71e40..156b7f78 100644 --- a/lib/leaderboard/leaderboard.dart +++ b/lib/leaderboard/leaderboard.dart @@ -1 +1,2 @@ export 'bloc/leaderboard_bloc.dart'; +export 'models/leader_board_entry.dart'; diff --git a/lib/leaderboard/models/leader_board_entry.dart b/lib/leaderboard/models/leader_board_entry.dart new file mode 100644 index 00000000..194f7cb6 --- /dev/null +++ b/lib/leaderboard/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; + } + } +} diff --git a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart index 5a5fa42c..d75a88b3 100644 --- a/packages/leaderboard_repository/lib/src/leaderboard_repository.dart +++ b/packages/leaderboard_repository/lib/src/leaderboard_repository.dart @@ -83,9 +83,9 @@ class LeaderboardRepository { final FirebaseFirestore _firebaseFirestore; - /// Acquires top 10 [LeaderboardEntry]s. - Future> fetchTop10Leaderboard() async { - final leaderboardEntries = []; + /// Acquires top 10 [LeaderboardEntryData]s. + Future> fetchTop10Leaderboard() async { + final leaderboardEntries = []; late List documents; try { @@ -103,7 +103,7 @@ class LeaderboardRepository { final data = document.data() as Map?; if (data != null) { try { - leaderboardEntries.add(LeaderboardEntry.fromJson(data)); + leaderboardEntries.add(LeaderboardEntryData.fromJson(data)); } catch (error, stackTrace) { throw LeaderboardDeserializationException(error, stackTrace); } @@ -115,7 +115,9 @@ class LeaderboardRepository { /// Adds player's score entry to the leaderboard and gets their /// [LeaderboardRanking]. - Future addLeaderboardEntry(LeaderboardEntry entry) async { + Future addLeaderboardEntry( + LeaderboardEntryData entry, + ) async { late DocumentReference entryReference; try { entryReference = await _firebaseFirestore diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart similarity index 64% rename from packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart rename to packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart index 86cb2464..c8137520 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.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 { /// Dash character. dash, @@ -18,7 +18,7 @@ enum CharacterType { dino, } -/// {@template leaderboard_entry} +/// {@template leaderboard_entry_data} /// A model representing a leaderboard entry containing the player's initials, /// score, and chosen character. /// @@ -34,42 +34,42 @@ enum CharacterType { /// ``` /// {@endtemplate} @JsonSerializable() -class LeaderboardEntry extends Equatable { - /// {@macro leaderboard_entry} - const LeaderboardEntry({ +class LeaderboardEntryData extends Equatable { + /// {@macro leaderboard_entry_data} + const LeaderboardEntryData({ required this.playerInitials, required this.score, required this.character, }); - /// Factory which converts a [Map] into a [LeaderboardEntry]. - factory LeaderboardEntry.fromJson(Map json) { + /// Factory which converts a [Map] into a [LeaderboardEntryData]. + factory LeaderboardEntryData.fromJson(Map json) { return _$LeaderboardEntryFromJson(json); } - /// Converts the [LeaderboardEntry] to [Map]. + /// Converts the [LeaderboardEntryData] to [Map]. Map toJson() => _$LeaderboardEntryToJson(this); - /// Player's chosen initials for [LeaderboardEntry]. + /// Player's chosen initials for [LeaderboardEntryData]. /// /// Example: 'ABC'. @JsonKey(name: 'playerInitials') final String playerInitials; - /// Score for [LeaderboardEntry]. + /// Score for [LeaderboardEntryData]. /// /// Example: 1500. @JsonKey(name: 'score') final int score; - /// [CharacterType] for [LeaderboardEntry]. + /// [CharacterType] for [LeaderboardEntryData]. /// /// Example: [CharacterType.dash]. @JsonKey(name: 'character') final CharacterType character; - /// An empty [LeaderboardEntry] object. - static const empty = LeaderboardEntry( + /// An empty [LeaderboardEntryData] object. + static const empty = LeaderboardEntryData( playerInitials: '', score: 0, character: CharacterType.dash, diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart similarity index 76% rename from packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart rename to packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart index fc685220..e57e43b8 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_entry.g.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_entry_data.g.dart @@ -1,19 +1,19 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'leaderboard_entry.dart'; +part of 'leaderboard_entry_data.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** -LeaderboardEntry _$LeaderboardEntryFromJson(Map json) => - LeaderboardEntry( +LeaderboardEntryData _$LeaderboardEntryFromJson(Map json) => + LeaderboardEntryData( playerInitials: json['playerInitials'] as String, score: json['score'] as int, character: $enumDecode(_$CharacterTypeEnumMap, json['character']), ); -Map _$LeaderboardEntryToJson(LeaderboardEntry instance) => +Map _$LeaderboardEntryToJson(LeaderboardEntryData instance) => { 'playerInitials': instance.playerInitials, 'score': instance.score, diff --git a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart index 7ec90ef4..4a322e00 100644 --- a/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart +++ b/packages/leaderboard_repository/lib/src/models/leaderboard_ranking.dart @@ -2,17 +2,17 @@ import 'package:equatable/equatable.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; /// {@template leaderboard_ranking} -/// Contains [ranking] for a single [LeaderboardEntry] and the number of players -/// the [ranking] is [outOf]. +/// Contains [ranking] for a single [LeaderboardEntryData] and the number of +/// players the [ranking] is [outOf]. /// {@endtemplate} class LeaderboardRanking extends Equatable { /// {@macro leaderboard_ranking} 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; - /// Number of [LeaderboardEntry]s at the time of score entry. + /// Number of [LeaderboardEntryData]s at the time of score entry. final int outOf; @override diff --git a/packages/leaderboard_repository/lib/src/models/models.dart b/packages/leaderboard_repository/lib/src/models/models.dart index 3dabe2bf..e10a743b 100644 --- a/packages/leaderboard_repository/lib/src/models/models.dart +++ b/packages/leaderboard_repository/lib/src/models/models.dart @@ -1,2 +1,2 @@ -export 'leaderboard_entry.dart'; +export 'leaderboard_entry_data.dart'; export 'leaderboard_ranking.dart'; diff --git a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart index cd632638..592425ec 100644 --- a/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart +++ b/packages/leaderboard_repository/test/src/leaderboard_repository_test.dart @@ -57,7 +57,7 @@ void main() { final top10Leaderboard = top10Scores .map( - (score) => LeaderboardEntry( + (score) => LeaderboardEntryData( playerInitials: 'user$score', score: score, character: CharacterType.dash, @@ -144,7 +144,7 @@ void main() { entryScore, 1000, ]; - final leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: entryScore, character: CharacterType.dash, diff --git a/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart b/packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart similarity index 72% rename from packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart rename to packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart index 21056529..f6e27e8a 100644 --- a/packages/leaderboard_repository/test/src/models/leaderboard_entry_test.dart +++ b/packages/leaderboard_repository/test/src/models/leaderboard_entry_data_test.dart @@ -9,21 +9,21 @@ void main() { 'character': 'dash', }; - const leaderboardEntry = LeaderboardEntry( + const leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, character: CharacterType.dash, ); test('can be instantiated', () { - const leaderboardEntry = LeaderboardEntry.empty; + const leaderboardEntry = LeaderboardEntryData.empty; expect(leaderboardEntry, isNotNull); }); test('supports value equality.', () { - const leaderboardEntry = LeaderboardEntry.empty; - const leaderboardEntry2 = LeaderboardEntry.empty; + const leaderboardEntry = LeaderboardEntryData.empty; + const leaderboardEntry2 = LeaderboardEntryData.empty; expect(leaderboardEntry, equals(leaderboardEntry2)); }); @@ -33,7 +33,7 @@ void main() { }); test('can be obtained from json', () { - final leaderboardEntryFrom = LeaderboardEntry.fromJson(data); + final leaderboardEntryFrom = LeaderboardEntryData.fromJson(data); expect(leaderboardEntry, equals(leaderboardEntryFrom)); }); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index e1bd8a0c..1e6b7289 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,6 +2,7 @@ import 'package:flame/input.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/theme/theme.dart'; @@ -32,6 +33,8 @@ class MockGameState extends Mock implements GameState {} class MockThemeCubit extends Mock implements ThemeCubit {} +class MockLeaderboardRepository extends Mock implements LeaderboardRepository {} + class MockRawKeyDownEvent extends Mock implements RawKeyDownEvent { @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { diff --git a/test/leaderboard/bloc/leaderboard_bloc_test.dart b/test/leaderboard/bloc/leaderboard_bloc_test.dart index c44f7d3a..2b217704 100644 --- a/test/leaderboard/bloc/leaderboard_bloc_test.dart +++ b/test/leaderboard/bloc/leaderboard_bloc_test.dart @@ -5,8 +5,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:mocktail/mocktail.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() { group('LeaderboardBloc', () { @@ -42,7 +43,7 @@ void main() { final top10Leaderboard = top10Scores .map( - (score) => LeaderboardEntry( + (score) => LeaderboardEntryData( playerInitials: 'user$score', score: score, character: CharacterType.dash, @@ -101,7 +102,7 @@ void main() { }); group('LeaderboardEntryAdded', () { - final leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, 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)); + }); + }); } diff --git a/test/leaderboard/bloc/leaderboard_event_test.dart b/test/leaderboard/bloc/leaderboard_event_test.dart index f74296af..33199ca1 100644 --- a/test/leaderboard/bloc/leaderboard_event_test.dart +++ b/test/leaderboard/bloc/leaderboard_event_test.dart @@ -20,7 +20,7 @@ void main() { }); group('LeaderboardEntryAdded', () { - const leaderboardEntry = LeaderboardEntry( + const leaderboardEntry = LeaderboardEntryData( playerInitials: 'ABC', score: 1500, character: CharacterType.dash, diff --git a/test/leaderboard/bloc/leaderboard_state_test.dart b/test/leaderboard/bloc/leaderboard_state_test.dart index 6ff5df13..a40a1cdb 100644 --- a/test/leaderboard/bloc/leaderboard_state_test.dart +++ b/test/leaderboard/bloc/leaderboard_state_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:leaderboard_repository/leaderboard_repository.dart'; import 'package:pinball/leaderboard/leaderboard.dart'; +import 'package:pinball_theme/pinball_theme.dart'; void main() { group('LeaderboardState', () { @@ -25,10 +26,11 @@ void main() { }); group('copyWith', () { - const leaderboardEntry = LeaderboardEntry( + final leaderboardEntry = LeaderboardEntry( + rank: '1', playerInitials: 'ABC', score: 1500, - character: CharacterType.dash, + character: DashTheme().characterAsset, ); test( @@ -51,7 +53,7 @@ void main() { final otherLeaderboardState = LeaderboardState( status: LeaderboardStatus.success, ranking: LeaderboardRanking(ranking: 0, outOf: 0), - leaderboard: const [leaderboardEntry], + leaderboard: [leaderboardEntry], ); expect(leaderboardState, isNot(equals(otherLeaderboardState))); From f1b35d3eb217ed4941c9c4523309eeb0b942b77c Mon Sep 17 00:00:00 2001 From: Rui Miguel Alonso Date: Wed, 23 Mar 2022 23:08:15 +0100 Subject: [PATCH 5/6] feat: create ellipses from geometry (#84) * feat: create ellipses from geometry * test: geometry test for ellipse * feat: removed required angle and added tests * test: completed tests for geometry * chore: unused import * Update lib/game/components/pathway.dart Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * refactor: renaming params * chore: missed test saved Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> --- lib/game/components/pathway.dart | 40 ++++++++++++++ lib/game/pinball_game.dart | 1 - packages/geometry/lib/src/geometry.dart | 53 +++++++++++++++---- packages/geometry/test/src/geometry_test.dart | 40 ++++++++++++++ test/game/components/pathway_test.dart | 36 +++++++++++++ 5 files changed, 159 insertions(+), 11 deletions(-) diff --git a/lib/game/components/pathway.dart b/lib/game/components/pathway.dart index 8604e0f3..819ed5f4 100644 --- a/lib/game/components/pathway.dart +++ b/lib/game/components/pathway.dart @@ -144,6 +144,46 @@ 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 = >[]; + + // 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> _paths; /// Constructs different [ChainShape]s to form the [Pathway] shape. diff --git a/lib/game/pinball_game.dart b/lib/game/pinball_game.dart index 44a7ec01..3ce7fd77 100644 --- a/lib/game/pinball_game.dart +++ b/lib/game/pinball_game.dart @@ -1,5 +1,4 @@ // ignore_for_file: public_member_api_docs - import 'dart:async'; import 'package:flame/extensions.dart'; import 'package:flame/input.dart'; diff --git a/packages/geometry/lib/src/geometry.dart b/packages/geometry/lib/src/geometry.dart index 8574bc73..6975f8cb 100644 --- a/packages/geometry/lib/src/geometry.dart +++ b/packages/geometry/lib/src/geometry.dart @@ -23,10 +23,45 @@ List calculateArc({ final points = []; for (var i = 0; i < precision; i++) { - final xCoord = center.x + radius * math.cos((stepAngle * i) + offsetAngle); - final yCoord = center.y - radius * math.sin((stepAngle * i) + offsetAngle); + final x = center.x + radius * math.cos((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 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 = []; + 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); } @@ -63,17 +98,15 @@ List calculateBezierCurve({ final points = []; do { - var xCoord = 0.0; - var yCoord = 0.0; + var x = 0.0; + var y = 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; + 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; } - points.add(Vector2(xCoord, yCoord)); + points.add(Vector2(x, y)); t = t + step; } while (t <= 1); diff --git a/packages/geometry/test/src/geometry_test.dart b/packages/geometry/test/src/geometry_test.dart index 5c33d70f..7a49b2b2 100644 --- a/packages/geometry/test/src/geometry_test.dart +++ b/packages/geometry/test/src/geometry_test.dart @@ -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()), + ); + expect( + () => calculateEllipse( + center: Vector2.zero(), + majorRadius: 100, + minorRadius: 0, + ), + throwsA(isA()), + ); + }); + }); + group('calculateBezierCurve', () { test('fails if step not in range', () { expect( diff --git a/test/game/components/pathway_test.dart b/test/game/components/pathway_test.dart index 03b67c62..63e74d4d 100644 --- a/test/game/components/pathway_test.dart +++ b/test/game/components/pathway_test.dart @@ -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', () { final controlPoints = [ Vector2(0, 0), From d5d3640f0ac58b1c7ebd8c1474a677d808ead51a Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Thu, 24 Mar 2022 08:50:35 +0000 Subject: [PATCH 6/6] refactor: define `FlipperJoint` (#72) * refactor: defined FlipperJoint * refactor: simplified logic * refactor: removed tests * docs: included TODO comment * refactor: simplified shape logic * docs: included asset TODO comment * refactor: removed verbose constructors * refactor: reordered classes * refactor: used renderBody * chore: removed unused import * refactor: moved renderBody to onLoad --- lib/game/components/board.dart | 2 +- lib/game/components/flipper.dart | 135 ++++++++----------- test/game/components/flipper_test.dart | 178 ++----------------------- 3 files changed, 67 insertions(+), 248 deletions(-) diff --git a/lib/game/components/board.dart b/lib/game/components/board.dart index af03efdd..efa3f137 100644 --- a/lib/game/components/board.dart +++ b/lib/game/components/board.dart @@ -127,7 +127,7 @@ class _BottomGroupSide extends Component { Future onLoad() async { final direction = _side.direction; - final flipper = Flipper.fromSide( + final flipper = Flipper( side: _side, )..initialPosition = _position; final baseboard = Baseboard(side: _side) diff --git a/lib/game/components/flipper.dart b/lib/game/components/flipper.dart index 92b2ddd4..48913934 100644 --- a/lib/game/components/flipper.dart +++ b/lib/game/components/flipper.dart @@ -3,11 +3,20 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame_forge2d/flame_forge2d.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball/gen/assets.gen.dart'; +const _leftFlipperKeys = [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyA, +]; + +const _rightFlipperKeys = [ + LogicalKeyboardKey.arrowRight, + LogicalKeyboardKey.keyD, +]; + /// {@template flipper} /// 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} class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// {@macro flipper} - Flipper._({ + Flipper({ required this.side, - required List keys, - }) : _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(); - } - } + }) : _keys = side.isLeft ? _leftFlipperKeys : _rightFlipperKeys; /// The size of the [Flipper]. 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. Future _anchorToJoint() async { - final anchor = FlipperAnchor(flipper: this); + final anchor = _FlipperAnchor(flipper: this); await add(anchor); - final jointDef = FlipperAnchorRevoluteJointDef( + final jointDef = _FlipperAnchorRevoluteJointDef( flipper: this, anchor: anchor, ); - // TODO(alestiago): Remove casting once the following is closed: - // https://github.com/flame-engine/forge2d/issues/36 - final joint = world.createJoint(jointDef) as RevoluteJoint; + final joint = _FlipperJoint(jointDef)..create(world); // FIXME(erickzanardo): when mounted the initial position is not fully // reached. unawaited( - mounted.whenComplete( - () => FlipperAnchorRevoluteJointDef.unlock(joint, side), - ), + mounted.whenComplete(joint.unlock), ); } List _createFixtureDefs() { final fixturesDef = []; - final isLeft = side.isLeft; + final direction = side.direction; final bigCircleShape = CircleShape()..radius = 1.75; bigCircleShape.position.setValues( - isLeft - ? -(size.x / 2) + bigCircleShape.radius - : (size.x / 2) - bigCircleShape.radius, + ((size.x / 2) * direction) + (bigCircleShape.radius * -direction), 0, ); final bigCircleFixtureDef = FixtureDef(bigCircleShape); @@ -140,15 +109,13 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { final smallCircleShape = CircleShape()..radius = 0.9; smallCircleShape.position.setValues( - isLeft - ? (size.x / 2) - smallCircleShape.radius - : -(size.x / 2) + smallCircleShape.radius, + ((size.x / 2) * -direction) + (smallCircleShape.radius * direction), 0, ); final smallCircleFixtureDef = FixtureDef(smallCircleShape); fixturesDef.add(smallCircleFixtureDef); - final trapeziumVertices = isLeft + final trapeziumVertices = side.isLeft ? [ Vector2(bigCircleShape.position.x, bigCircleShape.radius), Vector2(smallCircleShape.position.x, smallCircleShape.radius), @@ -173,7 +140,8 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { @override Future onLoad() async { await super.onLoad(); - paint = Paint()..color = Colors.transparent; + renderBody = false; + await Future.wait([ _loadSprite(), _anchorToJoint(), @@ -214,61 +182,66 @@ class Flipper extends BodyComponent with KeyboardHandler, InitialPosition { /// /// The end of a [Flipper] depends on its [Flipper.side]. /// {@endtemplate} -class FlipperAnchor extends JointAnchor { +class _FlipperAnchor extends JointAnchor { /// {@macro flipper_anchor} - FlipperAnchor({ + _FlipperAnchor({ required Flipper flipper, }) { initialPosition = Vector2( - flipper.side.isLeft - ? flipper.body.position.x - Flipper.size.x / 2 - : flipper.body.position.x + Flipper.size.x / 2, + flipper.body.position.x + ((Flipper.size.x * flipper.side.direction) / 2), flipper.body.position.y, ); } } /// {@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} -class FlipperAnchorRevoluteJointDef extends RevoluteJointDef { +class _FlipperAnchorRevoluteJointDef extends RevoluteJointDef { /// {@macro flipper_anchor_revolute_joint_def} - FlipperAnchorRevoluteJointDef({ + _FlipperAnchorRevoluteJointDef({ required Flipper flipper, - required FlipperAnchor anchor, - }) { + required _FlipperAnchor anchor, + }) : side = flipper.side { initialize( flipper.body, anchor.body, anchor.body.position, ); - enableLimit = true; - final angle = (flipper.side.isLeft ? _sweepingAngle : -_sweepingAngle) / 2; + enableLimit = true; + final angle = (_sweepingAngle * -side.direction) / 2; lowerAngle = upperAngle = angle; } /// The total angle of the arc motion. 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. /// /// 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); + void unlock() { + setLimits( + lowerLimit * side.direction, + -upperLimit * side.direction, + ); } } diff --git a/test/game/components/flipper_test.dart b/test/game/components/flipper_test.dart index 64d2f77b..3c12e37e 100644 --- a/test/game/components/flipper_test.dart +++ b/test/game/components/flipper_test.dart @@ -17,13 +17,14 @@ void main() { group( 'Flipper', () { + // TODO(alestiago): Add golden tests. flameTester.test( 'loads correctly', (game) async { - final leftFlipper = Flipper.fromSide( + final leftFlipper = Flipper( side: BoardSide.left, ); - final rightFlipper = Flipper.fromSide( + final rightFlipper = Flipper( side: BoardSide.right, ); await game.ready(); @@ -36,13 +37,13 @@ void main() { group('constructor', () { test('sets BoardSide', () { - final leftFlipper = Flipper.fromSide( + final leftFlipper = Flipper( side: BoardSide.left, ); expect(leftFlipper.side, equals(leftFlipper.side)); - final rightFlipper = Flipper.fromSide( + final rightFlipper = Flipper( side: BoardSide.right, ); expect(rightFlipper.side, equals(rightFlipper.side)); @@ -53,7 +54,7 @@ void main() { flameTester.test( 'is dynamic', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -65,7 +66,7 @@ void main() { flameTester.test( 'ignores gravity', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -77,7 +78,7 @@ void main() { flameTester.test( 'has greater mass than Ball', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); final ball = Ball(); @@ -97,7 +98,7 @@ void main() { flameTester.test( 'has three', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -109,7 +110,7 @@ void main() { flameTester.test( 'has density', (game) async { - final flipper = Flipper.fromSide( + final flipper = Flipper( side: BoardSide.left, ); await game.ensureAdd(flipper); @@ -139,7 +140,7 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.fromSide( + flipper = Flipper( side: BoardSide.left, ); }); @@ -205,7 +206,7 @@ void main() { late Flipper flipper; setUp(() { - flipper = Flipper.fromSide( + flipper = Flipper( 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)), - ); - }, - ); - }, - ); - }); }