From c5bc3578e24d785989a58fc28c54f4acdb8027bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr>
Date: Tue, 2 Jan 2024 16:03:28 +0100
Subject: [PATCH] Create minimal playable game

---
 android/gradle.properties                     |  4 +-
 assets/translations/en.json                   |  5 ++
 assets/translations/fr.json                   |  5 ++
 .../metadata/android/en-US/changelogs/12.txt  |  1 +
 .../metadata/android/fr-FR/changelogs/12.txt  |  1 +
 lib/cubit/game_cubit.dart                     | 39 +++++++++++
 lib/cubit/game_state.dart                     | 19 +++++
 lib/cubit/settings_state.dart                 |  2 +-
 lib/main.dart                                 |  2 +
 lib/models/move.dart                          | 37 ++++++++++
 lib/models/twister_color.dart                 | 25 +++++++
 lib/models/twister_member.dart                | 25 +++++++
 lib/ui/screens/home.dart                      |  3 +-
 lib/ui/skeleton.dart                          |  7 +-
 lib/ui/widgets/game.dart                      | 45 ++++++++++++
 lib/ui/widgets/show_move.dart                 | 69 +++++++++++++++++++
 lib/utils/color_extensions.dart               | 33 +++++++++
 pubspec.yaml                                  |  2 +-
 18 files changed, 316 insertions(+), 8 deletions(-)
 create mode 100644 fastlane/metadata/android/en-US/changelogs/12.txt
 create mode 100644 fastlane/metadata/android/fr-FR/changelogs/12.txt
 create mode 100644 lib/cubit/game_cubit.dart
 create mode 100644 lib/cubit/game_state.dart
 create mode 100644 lib/models/move.dart
 create mode 100644 lib/models/twister_color.dart
 create mode 100644 lib/models/twister_member.dart
 create mode 100644 lib/ui/widgets/game.dart
 create mode 100644 lib/ui/widgets/show_move.dart
 create mode 100644 lib/utils/color_extensions.dart

diff --git a/android/gradle.properties b/android/gradle.properties
index f0be9fb..d9abd55 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx1536M
 android.useAndroidX=true
 android.enableJetifier=true
-app.versionName=0.0.11
-app.versionCode=11
+app.versionName=0.0.12
+app.versionCode=12
diff --git a/assets/translations/en.json b/assets/translations/en.json
index 435402a..a170877 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -1,6 +1,11 @@
 {
   "app_name": "Twister",
 
+  "left_hand": "left hand",
+  "right_hand": "right hand",
+  "left_foot": "left foot",
+  "right_foot": "right foot",
+
   "bottom_nav_game": "Game",
   "bottom_nav_settings": "Settings",
 
diff --git a/assets/translations/fr.json b/assets/translations/fr.json
index 45fb897..05bbe10 100644
--- a/assets/translations/fr.json
+++ b/assets/translations/fr.json
@@ -1,6 +1,11 @@
 {
   "app_name": "Twister",
 
+  "left_hand": "main gauche",
+  "right_hand": "main droite",
+  "left_foot": "pied gauche",
+  "right_foot": "pied droit",
+
   "bottom_nav_game": "Jeu",
   "bottom_nav_settings": "Réglages",
 
diff --git a/fastlane/metadata/android/en-US/changelogs/12.txt b/fastlane/metadata/android/en-US/changelogs/12.txt
new file mode 100644
index 0000000..c4767c3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/12.txt
@@ -0,0 +1 @@
+Add minimal playable game.
diff --git a/fastlane/metadata/android/fr-FR/changelogs/12.txt b/fastlane/metadata/android/fr-FR/changelogs/12.txt
new file mode 100644
index 0000000..90a1f5e
--- /dev/null
+++ b/fastlane/metadata/android/fr-FR/changelogs/12.txt
@@ -0,0 +1 @@
+Création du jeu minimal mais jouable.
diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart
new file mode 100644
index 0000000..ddc7353
--- /dev/null
+++ b/lib/cubit/game_cubit.dart
@@ -0,0 +1,39 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/material.dart';
+import 'package:hydrated_bloc/hydrated_bloc.dart';
+
+import 'package:twister/models/move.dart';
+
+part 'game_state.dart';
+
+class GameCubit extends HydratedCubit<GameState> {
+  GameCubit() : super(const GameState());
+
+  Move getMove() {
+    return state.move ?? Move.createNull();
+  }
+
+  void setValues({
+    Move? move,
+  }) {
+    emit(GameState(
+      move: move ?? state.move,
+    ));
+  }
+
+  @override
+  GameState? fromJson(Map<String, dynamic> json) {
+    Move move = json['move'] as Move;
+
+    return GameState(
+      move: move,
+    );
+  }
+
+  @override
+  Map<String, dynamic>? toJson(GameState state) {
+    return <String, dynamic>{
+      'move': state.move?.toJson(),
+    };
+  }
+}
diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart
new file mode 100644
index 0000000..f859fd5
--- /dev/null
+++ b/lib/cubit/game_state.dart
@@ -0,0 +1,19 @@
+part of 'game_cubit.dart';
+
+@immutable
+class GameState extends Equatable {
+  const GameState({
+    this.move,
+  });
+
+  final Move? move;
+
+  @override
+  List<dynamic> get props => <dynamic>[
+        move,
+      ];
+
+  Map<String, dynamic> get values => <String, dynamic>{
+        'move': move,
+      };
+}
diff --git a/lib/cubit/settings_state.dart b/lib/cubit/settings_state.dart
index 3811631..6048256 100644
--- a/lib/cubit/settings_state.dart
+++ b/lib/cubit/settings_state.dart
@@ -14,6 +14,6 @@ class SettingsState extends Equatable {
       ];
 
   Map<String, dynamic> get values => <String, dynamic>{
-        'discoveriesDaysCount': timerValue,
+        'timerValue': timerValue,
       };
 }
diff --git a/lib/main.dart b/lib/main.dart
index d30fd3e..04f3952 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart';
 
 import 'package:twister/config/theme.dart';
 import 'package:twister/cubit/bottom_nav_cubit.dart';
+import 'package:twister/cubit/game_cubit.dart';
 import 'package:twister/cubit/settings_cubit.dart';
 import 'package:twister/ui/skeleton.dart';
 
@@ -44,6 +45,7 @@ class MyApp extends StatelessWidget {
     return MultiBlocProvider(
       providers: [
         BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
+        BlocProvider<GameCubit>(create: (context) => GameCubit()),
         BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()),
       ],
       child: MaterialApp(
diff --git a/lib/models/move.dart b/lib/models/move.dart
new file mode 100644
index 0000000..b415b2b
--- /dev/null
+++ b/lib/models/move.dart
@@ -0,0 +1,37 @@
+import 'package:twister/models/twister_color.dart';
+import 'package:twister/models/twister_member.dart';
+
+class Move {
+  final TwisterColor? color;
+  final TwisterMember? member;
+
+  Move({
+    required this.color,
+    required this.member,
+  });
+
+  factory Move.createNull() {
+    return Move(
+      color: null,
+      member: null,
+    );
+  }
+
+  factory Move.pickRandom() {
+    return Move(
+      color: TwisterColor.pickRandom(),
+      member: TwisterMember.pickRandom(),
+    );
+  }
+
+  String toString() {
+    return 'Move(' + this.toJson().toString() + ')';
+  }
+
+  Map<String, dynamic>? toJson() {
+    return <String, dynamic>{
+      'color': this.color.toString(),
+      'member': this.member.toString(),
+    };
+  }
+}
diff --git a/lib/models/twister_color.dart b/lib/models/twister_color.dart
new file mode 100644
index 0000000..eb8c21b
--- /dev/null
+++ b/lib/models/twister_color.dart
@@ -0,0 +1,25 @@
+import 'dart:math';
+
+enum TwisterAllowedColors {
+  blue,
+  green,
+  red,
+  yellow,
+}
+
+class TwisterColor {
+  final TwisterAllowedColors value;
+
+  TwisterColor({
+    required this.value,
+  });
+
+  factory TwisterColor.pickRandom() {
+    int random = Random().nextInt(TwisterAllowedColors.values.length);
+    return TwisterColor(value: TwisterAllowedColors.values[random]);
+  }
+
+  String toString() {
+    return this.value.toString();
+  }
+}
diff --git a/lib/models/twister_member.dart b/lib/models/twister_member.dart
new file mode 100644
index 0000000..875f7d5
--- /dev/null
+++ b/lib/models/twister_member.dart
@@ -0,0 +1,25 @@
+import 'dart:math';
+
+enum TwisterAllowedMembers {
+  leftHand,
+  rightHand,
+  leftFoot,
+  rightFoot,
+}
+
+class TwisterMember {
+  final TwisterAllowedMembers value;
+
+  TwisterMember({
+    required this.value,
+  });
+
+  factory TwisterMember.pickRandom() {
+    int random = Random().nextInt(TwisterAllowedMembers.values.length);
+    return TwisterMember(value: TwisterAllowedMembers.values[random]);
+  }
+
+  String toString() {
+    return this.value.toString();
+  }
+}
diff --git a/lib/ui/screens/home.dart b/lib/ui/screens/home.dart
index d5ba4df..dbda139 100644
--- a/lib/ui/screens/home.dart
+++ b/lib/ui/screens/home.dart
@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:twister/ui/widgets/game.dart';
 import 'package:unicons/unicons.dart';
 
 class ScreenHome extends StatelessWidget {
@@ -16,7 +17,7 @@ class ScreenHome extends StatelessWidget {
         physics: const BouncingScrollPhysics(),
         children: <Widget>[
           const SizedBox(height: 8),
-          const Text('HOME'),
+          const Game(),
           const SizedBox(height: 36),
         ],
       ),
diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart
index ca33599..a9fae72 100644
--- a/lib/ui/skeleton.dart
+++ b/lib/ui/skeleton.dart
@@ -18,7 +18,7 @@ class _SkeletonScreenState extends State<SkeletonScreen> {
   @override
   Widget build(BuildContext context) {
     List<Widget> pageNavigation = <Widget>[
-      const ScreenHome(),
+      ScreenHome(),
       const ScreenSettings(),
     ];
 
@@ -28,8 +28,9 @@ class _SkeletonScreenState extends State<SkeletonScreen> {
       body: BlocBuilder<BottomNavCubit, int>(
         builder: (BuildContext context, int state) {
           return AnimatedSwitcher(
-              duration: const Duration(milliseconds: 300),
-              child: pageNavigation.elementAt(state));
+            duration: const Duration(milliseconds: 300),
+            child: pageNavigation.elementAt(state),
+          );
         },
       ),
       backgroundColor: Theme.of(context).colorScheme.background,
diff --git a/lib/ui/widgets/game.dart b/lib/ui/widgets/game.dart
new file mode 100644
index 0000000..1d4a181
--- /dev/null
+++ b/lib/ui/widgets/game.dart
@@ -0,0 +1,45 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:unicons/unicons.dart';
+
+import 'package:twister/cubit/game_cubit.dart';
+import 'package:twister/models/move.dart';
+import 'package:twister/ui/widgets/show_move.dart';
+
+class Game extends StatefulWidget {
+  const Game({super.key});
+
+  @override
+  State<Game> createState() => _GameState();
+}
+
+class _GameState extends State<Game> {
+  Move move = Move.createNull();
+
+  Widget pickNewMove() {
+    return BlocBuilder<GameCubit, GameState>(builder: (BuildContext context, GameState state) {
+      return TextButton(
+        onPressed: () {
+          BlocProvider.of<GameCubit>(context).setValues(
+            move: Move.pickRandom(),
+          );
+        },
+        child: Icon(UniconsSolid.refresh),
+      );
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GameCubit, GameState>(
+      builder: (context, gameState) {
+        return Column(
+          children: [
+            ShowMove(move: gameState.move ?? Move.createNull()),
+            pickNewMove(),
+          ],
+        );
+      },
+    );
+  }
+}
diff --git a/lib/ui/widgets/show_move.dart b/lib/ui/widgets/show_move.dart
new file mode 100644
index 0000000..adead4b
--- /dev/null
+++ b/lib/ui/widgets/show_move.dart
@@ -0,0 +1,69 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+import 'package:twister/models/move.dart';
+import 'package:twister/models/twister_color.dart';
+import 'package:twister/models/twister_member.dart';
+import 'package:twister/utils/color_extensions.dart';
+
+class ShowMove extends StatelessWidget {
+  const ShowMove({super.key, required this.move});
+
+  final Move move;
+
+  Color getColor(Move move) {
+    switch (move.color?.value) {
+      case TwisterAllowedColors.blue:
+        return Colors.blue;
+      case TwisterAllowedColors.green:
+        return Colors.green;
+      case TwisterAllowedColors.red:
+        return Colors.red;
+      case TwisterAllowedColors.yellow:
+        return Colors.yellow;
+      default:
+        return Colors.grey;
+    }
+  }
+
+  Widget getWidget(Move move) {
+    TextStyle style = TextStyle(
+      color: Colors.black,
+      fontSize: 30,
+      fontWeight: FontWeight.bold,
+    );
+
+    switch (move.member?.value) {
+      case TwisterAllowedMembers.leftHand:
+        return Text(tr('left_hand'), style: style);
+      case TwisterAllowedMembers.rightHand:
+        return Text(tr('right_hand'), style: style);
+      case TwisterAllowedMembers.leftFoot:
+        return Text(tr('left_foot'), style: style);
+      case TwisterAllowedMembers.rightFoot:
+        return Text(tr('right_foot'), style: style);
+      default:
+        return Text('?', style: style);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Color color = getColor(move);
+
+    return Container(
+      child: Padding(
+        padding: EdgeInsets.all(20),
+        child: getWidget(move),
+      ),
+      decoration: BoxDecoration(
+        color: color,
+        borderRadius: BorderRadius.all(Radius.circular(50)),
+        border: Border.all(
+          color: color.darken(20),
+          width: 10,
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart
new file mode 100644
index 0000000..4e55e33
--- /dev/null
+++ b/lib/utils/color_extensions.dart
@@ -0,0 +1,33 @@
+import 'dart:ui';
+
+extension ColorExtension on Color {
+  Color darken([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = 1 - percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red * value).round(),
+      (green * value).round(),
+      (blue * value).round(),
+    );
+  }
+
+  Color lighten([int percent = 40]) {
+    assert(1 <= percent && percent <= 100);
+    final value = percent / 100;
+    return Color.fromARGB(
+      alpha,
+      (red + ((255 - red) * value)).round(),
+      (green + ((255 - green) * value)).round(),
+      (blue + ((255 - blue) * value)).round(),
+    );
+  }
+
+  Color avg(Color other) {
+    final red = (this.red + other.red) ~/ 2;
+    final green = (this.green + other.green) ~/ 2;
+    final blue = (this.blue + other.blue) ~/ 2;
+    final alpha = (this.alpha + other.alpha) ~/ 2;
+    return Color.fromARGB(alpha, red, green, blue);
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index ac2c248..1a01090 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@ description: twister game companion
 
 publish_to: 'none'
 
-version: 0.0.11+11
+version: 0.0.12+12
 
 environment:
   sdk: '^3.0.0'
-- 
GitLab