From 7ab1ed6c78c96eb05530d452c9c42ef3eaee46d2 Mon Sep 17 00:00:00 2001 From: Gregory Conrad Date: Tue, 30 Jan 2024 19:05:52 -0500 Subject: [PATCH] docs(example): add pickleball score tracking example (#88) --- .github/dependabot.yaml | 4 + examples/scorus/lib/game_management.dart | 63 +++++++++++ examples/scorus/lib/main.dart | 131 +++++++++++++++++++++++ examples/scorus/lib/score.dart | 26 +++++ examples/scorus/lib/serving_player.dart | 46 ++++++++ examples/scorus/lib/volley.dart | 52 +++++++++ examples/scorus/pubspec.yaml | 20 ++++ 7 files changed, 342 insertions(+) create mode 100644 examples/scorus/lib/game_management.dart create mode 100644 examples/scorus/lib/main.dart create mode 100644 examples/scorus/lib/score.dart create mode 100644 examples/scorus/lib/serving_player.dart create mode 100644 examples/scorus/lib/volley.dart create mode 100644 examples/scorus/pubspec.yaml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 727ff99..54948f9 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -41,3 +41,7 @@ updates: directory: "/examples/presentation" schedule: interval: weekly + - package-ecosystem: pub + directory: "/examples/scorus" + schedule: + interval: weekly diff --git a/examples/scorus/lib/game_management.dart b/examples/scorus/lib/game_management.dart new file mode 100644 index 0000000..7271cf8 --- /dev/null +++ b/examples/scorus/lib/game_management.dart @@ -0,0 +1,63 @@ +import 'package:rearch/rearch.dart'; +import 'package:scorus/score.dart'; +import 'package:scorus/serving_player.dart'; + +/// Represents a team. +enum Team { + /// The first team. + team1, + + /// The second team. + team2, +} + +/// Provides [next]. +extension NextTeam on Team { + /// Returns the next [Team] up for possesion. + Team get next => switch (this) { + Team.team1 => Team.team2, + Team.team2 => Team.team1, + }; +} + +/// Manages the current team with possesion. +({ + Team teamWithPossesion, + void Function() givePossesionToNextTeam, + void Function() resetTeamWithPossesion, +}) teamWithPossesionManager(CapsuleHandle use) { + const startingTeam = Team.team1; + final (team, setTeam) = use.state(startingTeam); + return ( + teamWithPossesion: team, + givePossesionToNextTeam: () => setTeam(team.next), + resetTeamWithPossesion: () => setTeam(startingTeam), + ); +} + +/// Returns which team and which player on that team is serving. +(Team, ServingPlayer) currServingTeamAndPlayerCapsule(CapsuleHandle use) { + final teamWithPossesion = use(teamWithPossesionManager).teamWithPossesion; + final servingPlayerOnTeamWithPossesion = switch (teamWithPossesion) { + Team.team1 => use(team1ServingPlayerManager).servingPlayer, + Team.team2 => use(team2ServingPlayerManager).servingPlayer, + }; + return (teamWithPossesion, servingPlayerOnTeamWithPossesion); +} + +/// Action capsule that returns a function to reset the game. +void Function() resetGameAction(CapsuleHandle use) { + final resets = [ + use(team1ScoreManager).resetScore, + use(team2ScoreManager).resetScore, + use(team1ServingPlayerManager).resetServingPlayer, + use(team2ServingPlayerManager).resetServingPlayer, + use(teamWithPossesionManager).resetTeamWithPossesion, + ]; + final runTxn = use.transactionRunner(); + return () => runTxn(() { + for (final reset in resets) { + reset(); + } + }); +} diff --git a/examples/scorus/lib/main.dart b/examples/scorus/lib/main.dart new file mode 100644 index 0000000..13bfde4 --- /dev/null +++ b/examples/scorus/lib/main.dart @@ -0,0 +1,131 @@ +// ignore_for_file: public_member_api_docs +import 'package:flutter/material.dart'; +import 'package:flutter_rearch/flutter_rearch.dart'; +import 'package:scorus/game_management.dart'; +import 'package:scorus/score.dart'; +import 'package:scorus/serving_player.dart'; +import 'package:scorus/volley.dart'; + +void main() { + runApp(const ScorusApp()); +} + +class ScorusApp extends StatelessWidget { + const ScorusApp({super.key}); + + @override + Widget build(BuildContext context) { + return const RearchBootstrapper( + child: MaterialApp(home: ScorusBody()), + ); + } +} + +class ScorusBody extends RearchConsumer { + const ScorusBody({super.key}); + + @override + Widget build(BuildContext context, WidgetHandle use) { + final scoreTextStyle = Theme.of(context).textTheme.displayLarge; + final (servingTeam, servingPlayer) = use(currServingTeamAndPlayerCapsule); + + return Scaffold( + appBar: AppBar( + title: const Text('Scorus'), + actions: [ + IconButton( + icon: const Icon(Icons.restart_alt_rounded), + onPressed: use(resetGameAction), + ), + ], + ), + body: Column( + children: [ + Expanded( + child: InkWell( + onTap: use(team1WonVolleyAction), + child: Stack( + children: [ + ColoredBox( + color: Colors.red, + child: Center( + child: Text( + '${use(team1ScoreManager).score}', + style: scoreTextStyle, + ), + ), + ), + if (servingTeam == Team.team1) + ServingPlayerCard( + servingPlayer: servingPlayer, + anchorOnTop: true, + ), + ], + ), + ), + ), + Expanded( + child: InkWell( + onTap: use(team2WonVolleyAction), + child: Stack( + children: [ + ColoredBox( + color: Colors.blue, + child: Center( + child: Text( + '${use(team2ScoreManager).score}', + style: scoreTextStyle, + ), + ), + ), + if (servingTeam == Team.team2) + ServingPlayerCard( + servingPlayer: servingPlayer, + anchorOnTop: false, + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class ServingPlayerCard extends StatelessWidget { + const ServingPlayerCard({ + required this.servingPlayer, + required this.anchorOnTop, + super.key, + }); + + final ServingPlayer servingPlayer; + final bool anchorOnTop; + + @override + Widget build(BuildContext context) { + final servingPlayerNumber = switch (servingPlayer) { + ServingPlayer.player1 => 1, + ServingPlayer.player2 => 2, + }; + return Positioned( + left: 0, + right: 0, + top: anchorOnTop ? 32 : null, + bottom: anchorOnTop ? null : 32, + child: Center( + child: Card( + color: Colors.white.withOpacity(0.1), + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Player $servingPlayerNumber serving', + style: Theme.of(context).textTheme.displaySmall, + ), + ), + ), + ), + ); + } +} diff --git a/examples/scorus/lib/score.dart b/examples/scorus/lib/score.dart new file mode 100644 index 0000000..540351c --- /dev/null +++ b/examples/scorus/lib/score.dart @@ -0,0 +1,26 @@ +import 'package:rearch/rearch.dart'; + +/// Defines what a score management [Capsule] can do. +typedef ScoreManager = ({ + int score, + void Function() incrementScore, + void Function() resetScore, +}); + +/// Manages the score for the first team. +ScoreManager team1ScoreManager(CapsuleHandle use) => use.teamScore(); + +/// Manages the score for the second team. +ScoreManager team2ScoreManager(CapsuleHandle use) => use.teamScore(); + +extension on SideEffectRegistrar { + SideEffectRegistrar get use => this; + ScoreManager teamScore() { + final (score, setScore) = use.state(0); + return ( + score: score, + incrementScore: () => setScore(score + 1), + resetScore: () => setScore(0) + ); + } +} diff --git a/examples/scorus/lib/serving_player.dart b/examples/scorus/lib/serving_player.dart new file mode 100644 index 0000000..1c3687f --- /dev/null +++ b/examples/scorus/lib/serving_player.dart @@ -0,0 +1,46 @@ +import 'package:rearch/rearch.dart'; + +/// Represents the currently serving player. +enum ServingPlayer { + /// Player #1. + player1, + + /// Player #2. + player2, +} + +/// Provides [next]. +extension NextPlayer on ServingPlayer { + /// Returns the next [ServingPlayer] in the serving order. + ServingPlayer get next => switch (this) { + ServingPlayer.player1 => ServingPlayer.player2, + ServingPlayer.player2 => ServingPlayer.player1, + }; +} + +/// Represents what a [ServingPlayer] manager should be able to do. +typedef ServingPlayerManager = ({ + ServingPlayer servingPlayer, + void Function() giveServeToNextPlayer, + void Function() resetServingPlayer, +}); + +/// Manages the [ServingPlayer] for the first team. +ServingPlayerManager team1ServingPlayerManager(CapsuleHandle use) => + use.servingPlayer(startingPlayer: ServingPlayer.player2); + +/// Manages the [ServingPlayer] for the second team. +ServingPlayerManager team2ServingPlayerManager(CapsuleHandle use) => + use.servingPlayer(startingPlayer: ServingPlayer.player1); + +extension on SideEffectRegistrar { + SideEffectRegistrar get use => this; + ServingPlayerManager servingPlayer({required ServingPlayer startingPlayer}) { + final (servingPlayer, setServingPlayer) = use.state(startingPlayer); + return ( + servingPlayer: servingPlayer, + giveServeToNextPlayer: () => setServingPlayer(servingPlayer.next), + resetServingPlayer: () => setServingPlayer(startingPlayer), + ); + } +} diff --git a/examples/scorus/lib/volley.dart b/examples/scorus/lib/volley.dart new file mode 100644 index 0000000..923dcf7 --- /dev/null +++ b/examples/scorus/lib/volley.dart @@ -0,0 +1,52 @@ +import 'package:rearch/rearch.dart'; +import 'package:scorus/game_management.dart'; +import 'package:scorus/score.dart'; +import 'package:scorus/serving_player.dart'; + +/// An action capsule that when invoked indicates team 1 won a volley. +void Function() team1WonVolleyAction(CapsuleHandle use) => use.volleyWinner( + thisTeam: Team.team1, + teamWithPossesion: use(teamWithPossesionManager).teamWithPossesion, + incrementThisTeamScore: use(team1ScoreManager).incrementScore, + giveServeToOtherTeamNextPlayer: + use(team2ServingPlayerManager).giveServeToNextPlayer, + otherTeamServingPlayer: use(team2ServingPlayerManager).servingPlayer, + givePossesionToNextTeam: + use(teamWithPossesionManager).givePossesionToNextTeam, + ); + +/// An action capsule that when invoked indicates team 2 won a volley. +void Function() team2WonVolleyAction(CapsuleHandle use) => use.volleyWinner( + thisTeam: Team.team2, + teamWithPossesion: use(teamWithPossesionManager).teamWithPossesion, + incrementThisTeamScore: use(team2ScoreManager).incrementScore, + giveServeToOtherTeamNextPlayer: + use(team1ServingPlayerManager).giveServeToNextPlayer, + otherTeamServingPlayer: use(team1ServingPlayerManager).servingPlayer, + givePossesionToNextTeam: + use(teamWithPossesionManager).givePossesionToNextTeam, + ); + +extension on SideEffectRegistrar { + SideEffectRegistrar get use => this; + void Function() volleyWinner({ + required Team thisTeam, + required Team teamWithPossesion, + required void Function() incrementThisTeamScore, + required void Function() giveServeToOtherTeamNextPlayer, + required ServingPlayer otherTeamServingPlayer, + required void Function() givePossesionToNextTeam, + }) { + final runTxn = use.transactionRunner(); + return () => runTxn(() { + if (teamWithPossesion == thisTeam) { + incrementThisTeamScore(); + } else { + giveServeToOtherTeamNextPlayer(); + if (otherTeamServingPlayer == ServingPlayer.player2) { + givePossesionToNextTeam(); + } + } + }); + } +} diff --git a/examples/scorus/pubspec.yaml b/examples/scorus/pubspec.yaml new file mode 100644 index 0000000..fdb7165 --- /dev/null +++ b/examples/scorus/pubspec.yaml @@ -0,0 +1,20 @@ +name: scorus +description: "Pickleball score tracker" +publish_to: none + +environment: + sdk: '>=3.2.5 <4.0.0' + flutter: '>=3.16.9 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_rearch: ^1.4.0 + rearch: ^1.5.0 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true