diff --git a/android/gradle.properties b/android/gradle.properties index 8f1cf62360192199ae6a5e17a062114475c411bc..cd511d2125450a944f1e2bd7839e9059a09ccdaa 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 e57ee8a1085d69a27cfce73eaa027ae8bdb146ab..6b6d02750729a4be993e296a06bcc5e0f63d4fe2 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 ef1e45bbdd2bdd2d787f6838a3ee09a145086988..72682066e0c519f9bdd04d866ef5fa0644db798f 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 0000000000000000000000000000000000000000..90146ddbee91b6248621d91e9f72bdec8b822329 --- /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 0000000000000000000000000000000000000000..ea6c33cf1ce0150ac8b9c358005e42a64447ab0d --- /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 765b84c7b8b3c00e7e4489d11ab5d77670453128..6b6e38b6dca4897664eb5ae5212927b9ae97574f 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 0000000000000000000000000000000000000000..fce00e1eaf10f90c4fe4b49b9e0fe17f9866e124 --- /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 0000000000000000000000000000000000000000..26ee97ddb41f0715a238a49196c97e72f2865d09 --- /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 0000000000000000000000000000000000000000..8ccb67fd641d663dd02f016515e98d0ece89e3a3 --- /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 f44393db9c3aee06006e962728fe4e521d7d5d8d..275a44618b4c0780dd33157aa5d81f492c09219e 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 9ab56ea6333ba956d3e362b20bf75c9586cbacd8..6d6f83d1e47a4c73acaca1aca929a59224dc43ec 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 0000000000000000000000000000000000000000..427ff46ce0685cf3e506889ebd010065af6a9c0a --- /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 614ad4a20d09270ad2000c910371483f6c373fef..5e18bdd7ad704a826e15dbb34241d0ad5be3432f 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'