From 149a5dbb6f366d313fcb5291d620db45b8a7345f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Sat, 13 Jan 2024 13:27:22 +0100 Subject: [PATCH] Add minimal "board game" --- android/gradle.properties | 4 +- assets/translations/en.json | 1 + assets/translations/fr.json | 1 + lib/cubit/game_cubit.dart | 34 +++++++++++ lib/cubit/game_state.dart | 15 +++++ lib/main.dart | 2 + lib/models/game_data.dart | 96 ++++++++++++++++++++++++++++++ lib/ui/painters/cell_painter.dart | 39 ++++++++++++ lib/ui/screens/game_page.dart | 50 ++++++++++++++++ lib/ui/skeleton.dart | 2 + lib/ui/widgets/bottom_nav_bar.dart | 4 ++ lib/ui/widgets/game_board.dart | 60 +++++++++++++++++++ pubspec.yaml | 2 +- 13 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 lib/cubit/game_cubit.dart create mode 100644 lib/cubit/game_state.dart create mode 100644 lib/models/game_data.dart create mode 100644 lib/ui/painters/cell_painter.dart create mode 100644 lib/ui/screens/game_page.dart create mode 100644 lib/ui/widgets/game_board.dart diff --git a/android/gradle.properties b/android/gradle.properties index 8f1cf62..cd511d2 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=1.0.37 -app.versionCode=38 +app.versionName=1.0.38 +app.versionCode=39 diff --git a/assets/translations/en.json b/assets/translations/en.json index e57ee8a..6b6d027 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -4,6 +4,7 @@ "bottom_nav_sample": "Sample", "bottom_nav_api": "API", "bottom_nav_chart": "Graph", + "bottom_nav_game": "Game", "bottom_nav_settings": "Settings", "bottom_nav_about": "About", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index ef1e45b..7268206 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -4,6 +4,7 @@ "bottom_nav_sample": "Démo", "bottom_nav_api": "API", "bottom_nav_chart": "Graph", + "bottom_nav_game": "Jeu", "bottom_nav_settings": "Paramètres", "bottom_nav_about": "À propos", diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000..90146dd --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:random/models/game_data.dart'; + +part 'game_state.dart'; + +class GameCubit extends HydratedCubit<GameState> { + GameCubit() : super(const GameState()); + + void getData(GameState state) { + emit(state); + } + + void updateGameState(GameData game) { + emit(GameState(game: game)); + } + + @override + GameState? fromJson(Map<String, dynamic> json) { + GameData game = json['game'] as GameData; + + return GameState( + game: game, + ); + } + + @override + Map<String, dynamic>? toJson(GameState state) { + return <String, dynamic>{ + 'game': state.game?.toJson(), + }; + } +} diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart new file mode 100644 index 0000000..ea6c33c --- /dev/null +++ b/lib/cubit/game_state.dart @@ -0,0 +1,15 @@ +part of 'game_cubit.dart'; + +@immutable +class GameState extends Equatable { + const GameState({ + this.game, + }); + + final GameData? game; + + @override + List<Object?> get props => <Object?>[ + game, + ]; +} diff --git a/lib/main.dart b/lib/main.dart index 765b84c..6b6e38b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:random/config/theme.dart'; import 'package:random/cubit/bottom_nav_cubit.dart'; import 'package:random/cubit/data_cubit.dart'; +import 'package:random/cubit/game_cubit.dart'; import 'package:random/cubit/settings_cubit.dart'; import 'package:random/cubit/api_cubit.dart'; import 'package:random/repository/api.dart'; @@ -50,6 +51,7 @@ class MyApp extends StatelessWidget { BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()), BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()), BlocProvider<DataCubit>(create: (context) => DataCubit()), + BlocProvider<GameCubit>(create: (context) => GameCubit()), BlocProvider<ApiDataCubit>( create: (context) => ApiDataCubit( apiRepository: ApiRepository( diff --git a/lib/models/game_data.dart b/lib/models/game_data.dart new file mode 100644 index 0000000..fce00e1 --- /dev/null +++ b/lib/models/game_data.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; +import 'dart:math'; + +class GameDataItem { + final int value; + + const GameDataItem({ + required this.value, + }); + + factory GameDataItem.fromJson(Map<String, dynamic>? json) { + return GameDataItem( + value: (json?['value'] != null) ? (json?['value'] as int) : 0, + ); + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'value': this.value, + }; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} + +class GameData { + final int boardSize; + final List<List<GameDataItem>> board; + + const GameData({ + required this.boardSize, + required this.board, + }); + + factory GameData.createNew(int boardSize) { + final List<List<GameDataItem>> cells = []; + + for (var y = 0; y < boardSize; y++) { + final List<GameDataItem> line = []; + for (var x = 0; x < boardSize; x++) { + final GameDataItem item = new GameDataItem(value: 0); + line.add(item); + } + + cells.add(line); + } + + return GameData( + boardSize: boardSize, + board: cells, + ); + } + + factory GameData.createRandom(int boardSize) { + const allowedValues = [0, 1, 2, 3, 4, 5]; + final allowedValuesSize = allowedValues.length; + + final List<List<GameDataItem>> cells = []; + + for (var y = 0; y < boardSize; y++) { + final List<GameDataItem> line = []; + for (var x = 0; x < boardSize; x++) { + final value = allowedValues[Random().nextInt(allowedValuesSize)]; + final GameDataItem item = new GameDataItem(value: value); + line.add(item); + } + + cells.add(line); + } + + return GameData( + boardSize: boardSize, + board: cells, + ); + } + + factory GameData.fromJson(Map<String, dynamic>? json) { + return GameData( + boardSize: (json?['boardSize'] != null) ? (json?['boardSize'] as int) : 0, + board: (json?['board'] != null) ? (json?['board'] as List<List<GameDataItem>>) : [], + ); + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'boardSize': this.boardSize, + 'board': this.board, + }; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} diff --git a/lib/ui/painters/cell_painter.dart b/lib/ui/painters/cell_painter.dart new file mode 100644 index 0000000..26ee97d --- /dev/null +++ b/lib/ui/painters/cell_painter.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import 'package:random/config/app_colors.dart'; + +class CellPainter extends CustomPainter { + const CellPainter({required this.value}); + + final int value; + + Color getIndexedColor(int index) { + const List<Color> availableColors = [ + AppColors.contentColorCyan, + AppColors.contentColorGreen, + AppColors.contentColorOrange, + AppColors.contentColorPurple, + AppColors.contentColorYellow, + AppColors.contentColorPink, + AppColors.contentColorWhite, + AppColors.mainTextColor3, + ]; + + return availableColors[index % availableColors.length]; + } + + @override + void paint(Canvas canvas, Size size) { + final paintBackground = Paint(); + paintBackground.color = getIndexedColor(value); + paintBackground.style = PaintingStyle.fill; + + final Rect rectBackground = Rect.fromPoints(Offset(0, 0), Offset(size.width, size.height)); + canvas.drawRect(rectBackground, paintBackground); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} diff --git a/lib/ui/screens/game_page.dart b/lib/ui/screens/game_page.dart new file mode 100644 index 0000000..8ccb67f --- /dev/null +++ b/lib/ui/screens/game_page.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:random/cubit/game_cubit.dart'; +import 'package:random/models/game_data.dart'; + +import 'package:random/ui/widgets/game_board.dart'; +import 'package:unicons/unicons.dart'; + +class GamePage extends StatefulWidget { + const GamePage({super.key}); + + @override + State<GamePage> createState() => _GamePageState(); +} + +class _GamePageState extends State<GamePage> { + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (context, gameState) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + gameState.game != null + ? Container( + margin: EdgeInsets.all(4), + padding: EdgeInsets.all(4), + child: GameBoardWidget( + gameData: gameState.game!, + ), + ) + : SizedBox.shrink(), + IconButton( + onPressed: () { + const boardSize = 6; + + final GameData newGame = GameData.createRandom(boardSize); + BlocProvider.of<GameCubit>(context).updateGameState(newGame); + print(gameState); + }, + icon: Icon(UniconsSolid.star), + color: Colors.white, + ), + ], + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart index f44393d..275a446 100644 --- a/lib/ui/skeleton.dart +++ b/lib/ui/skeleton.dart @@ -6,6 +6,7 @@ import 'package:random/cubit/bottom_nav_cubit.dart'; import 'package:random/ui/screens/about_page.dart'; import 'package:random/ui/screens/api_page.dart'; import 'package:random/ui/screens/demo_page.dart'; +import 'package:random/ui/screens/game_page.dart'; import 'package:random/ui/screens/graph_page.dart'; import 'package:random/ui/screens/settings_page.dart'; import 'package:random/ui/widgets/app_bar.dart'; @@ -25,6 +26,7 @@ class _SkeletonScreenState extends State<SkeletonScreen> { DemoPage(), ApiPage(), GraphPage(), + GamePage(), SettingsPage(), AboutPage(), ]; diff --git a/lib/ui/widgets/bottom_nav_bar.dart b/lib/ui/widgets/bottom_nav_bar.dart index 9ab56ea..6d6f83d 100644 --- a/lib/ui/widgets/bottom_nav_bar.dart +++ b/lib/ui/widgets/bottom_nav_bar.dart @@ -54,6 +54,10 @@ class BottomNavBar extends StatelessWidget { icon: const Icon(UniconsLine.pen), label: tr('bottom_nav_chart'), ), + BottomNavigationBarItem( + icon: const Icon(UniconsLine.star), + label: tr('bottom_nav_game'), + ), BottomNavigationBarItem( icon: const Icon(UniconsLine.setting), label: tr('bottom_nav_settings'), diff --git a/lib/ui/widgets/game_board.dart b/lib/ui/widgets/game_board.dart new file mode 100644 index 0000000..427ff46 --- /dev/null +++ b/lib/ui/widgets/game_board.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +import 'package:random/models/game_data.dart'; +import 'package:random/ui/painters/cell_painter.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key, required this.gameData}); + + final GameData gameData; + + @override + Widget build(BuildContext context) { + const staticBoardWidth = 300; + const staticBoardHeight = 300; + + final rowsCount = this.gameData.board.length; + final columnsCount = this.gameData.board[0].length; + print('counts: rows=' + rowsCount.toString() + ' / columns=' + columnsCount.toString()); + + final cellWidth = staticBoardWidth / columnsCount; + final cellHeight = staticBoardHeight / rowsCount; + print('cell: width=' + cellWidth.toString() + ' / height=' + cellHeight.toString()); + + final List<Widget> cells = []; + + for (var y = 0; y < rowsCount; y++) { + for (var x = 0; x < columnsCount; x++) { + final GameDataItem item = this.gameData.board[y][x]; + + final Widget cellContent = CustomPaint( + size: Size(cellWidth, cellHeight), + willChange: false, + painter: CellPainter(value: item.value), + ); + + final Widget widget = Positioned( + left: (x * cellWidth).toDouble(), + top: (y * cellHeight).toDouble(), + child: Container( + width: cellWidth, + height: cellHeight, + color: Colors.deepPurpleAccent, + child: cellContent, + ), + ); + + cells.add(widget); + } + } + + return Container( + width: staticBoardWidth.toDouble(), + height: staticBoardHeight.toDouble(), + color: Colors.grey, + child: Stack( + children: cells, + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 614ad4a..5e18bdd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A random application, for testing purpose only. publish_to: 'none' -version: 1.0.37+38 +version: 1.0.38+39 environment: sdk: '^3.0.0' -- GitLab