diff --git a/android/gradle.properties b/android/gradle.properties index d96c32178c38990e00612de2cd3fa50f85d88126..621336f47b3be71128005116384f1c3636771131 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.39 -app.versionCode=40 +app.versionName=1.0.40 +app.versionCode=41 diff --git a/lib/models/game_data.dart b/lib/models/game_data.dart index fce00e1eaf10f90c4fe4b49b9e0fe17f9866e124..6a519d5bb925009b7cb338ad245eb00586130803 100644 --- a/lib/models/game_data.dart +++ b/lib/models/game_data.dart @@ -2,15 +2,21 @@ import 'dart:convert'; import 'dart:math'; class GameDataItem { - final int value; + final int? value; const GameDataItem({ required this.value, }); + factory GameDataItem.fromValue(int? value) { + return GameDataItem( + value: value, + ); + } + factory GameDataItem.fromJson(Map<String, dynamic>? json) { return GameDataItem( - value: (json?['value'] != null) ? (json?['value'] as int) : 0, + value: (json?['value'] != null) ? (json?['value'] as int) : null, ); } @@ -26,10 +32,12 @@ class GameDataItem { } class GameData { + final bool isReady; final int boardSize; final List<List<GameDataItem>> board; const GameData({ + required this.isReady, required this.boardSize, required this.board, }); @@ -40,7 +48,7 @@ class GameData { 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); + final GameDataItem item = new GameDataItem(value: null); line.add(item); } @@ -48,6 +56,7 @@ class GameData { } return GameData( + isReady: true, boardSize: boardSize, board: cells, ); @@ -71,13 +80,27 @@ class GameData { } return GameData( + isReady: true, boardSize: boardSize, board: cells, ); } + GameDataItem getCell(int x, int y) { + return this.board[y][x]; + } + + int? getCellValue(int x, int y) { + return this.getCell(x, y).value; + } + + void updateCellValue(int x, int y, int? value) { + this.board[y][x] = new GameDataItem.fromValue(value); + } + factory GameData.fromJson(Map<String, dynamic>? json) { return GameData( + isReady: (json?['isReady'] != null) ? (json?['isReady'] as bool) : false, boardSize: (json?['boardSize'] != null) ? (json?['boardSize'] as int) : 0, board: (json?['board'] != null) ? (json?['board'] as List<List<GameDataItem>>) : [], ); @@ -85,6 +108,7 @@ class GameData { Map<String, dynamic>? toJson() { return <String, dynamic>{ + 'isReady': this.isReady, 'boardSize': this.boardSize, 'board': this.board, }; diff --git a/lib/ui/painters/cell_painter.dart b/lib/ui/painters/cell_painter.dart index b67c76edde50c7dce6b39e50479660ddff90dccf..e50ce3d26370860f7137b60613de5b44a375c7c9 100644 --- a/lib/ui/painters/cell_painter.dart +++ b/lib/ui/painters/cell_painter.dart @@ -58,6 +58,6 @@ class CellPainter extends CustomPainter { @override bool shouldRepaint(CustomPainter oldDelegate) { - return false; + return true; } } diff --git a/lib/ui/screens/game_page.dart b/lib/ui/screens/game_page.dart index faf19a310679b1e6c2b783a7832fa8f3740d9792..93cd8573714e6ecd1e1a31767cee758f05619302 100644 --- a/lib/ui/screens/game_page.dart +++ b/lib/ui/screens/game_page.dart @@ -13,12 +13,89 @@ class GamePage extends StatefulWidget { State<GamePage> createState() => _GamePageState(); } -void createNewGame(GameCubit gameCubit, int boardSize) { - final GameData newGame = GameData.createRandom(boardSize); - gameCubit.updateGameState(newGame); -} +class _GamePageState extends State<GamePage> with TickerProviderStateMixin { + static const boardSize = 6; + + List<List<Animation<double>?>> animations = List.generate( + boardSize, + (i) => List.generate( + boardSize, + (i) => null, + ), + ); + + void resetAnimations() { + animations = List.generate( + boardSize, + (i) => List.generate( + boardSize, + (i) => null, + ), + ); + } + + void createNewGame(GameCubit gameCubit) { + final GameData newGame = GameData.createRandom(boardSize); + gameCubit.updateGameState(newGame); + } + + void removeCell(GameCubit gameCubit, int x, int y) { + final GameData newGame = gameCubit.state.game ?? GameData.createRandom(boardSize); + + // "remove" cell + newGame.updateCellValue(x, y, null); + setState(() {}); + + // "move down" cells + final controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 750), + )..addListener(() { + if (mounted) { + setState(() {}); + } + }); + + if (mounted) { + setState(() {}); + } + + Animation<double> animation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + curve: Curves.bounceOut, + parent: controller, + )) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + for (var i = 0; i < y; i++) { + newGame.updateCellValue(x, (y - i), newGame.getCellValue(x, (y - i) - 1)); + } + newGame.updateCellValue(x, 0, null); + + resetAnimations(); + setState(() {}); + + controller.dispose(); + } + }); + + for (var i = 0; i < y; i++) { + animations[(y - i) - 1][x] = animation; + } + + controller.forward().orCancel; + } + + void updateCellValue(GameCubit gameCubit, int x, int y, int value) { + final GameData newGame = gameCubit.state.game ?? GameData.createRandom(boardSize); + newGame.updateCellValue(x, y, value); + + gameCubit.updateGameState(newGame); + setState(() {}); + } -class _GamePageState extends State<GamePage> { @override Widget build(BuildContext context) { return BlocBuilder<GameCubit, GameState>( @@ -29,19 +106,36 @@ class _GamePageState extends State<GamePage> { final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + if (gameState.game?.isReady != true) { + createNewGame(gameCubit); + } + return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ - gameState.game != null - ? GameBoardWidget( - gameData: gameState.game!, - size: Size(boardWidgetWidth, boardWidgetHeight), - ) - : SizedBox.shrink(), + GestureDetector( + child: gameState.game != null + ? GameBoardWidget( + gameData: gameState.game!, + size: Size(boardWidgetWidth, boardWidgetHeight), + animations: animations, + ) + : SizedBox.shrink(), + onTapUp: (details) { + double xTap = details.localPosition.dx; + double yTap = details.localPosition.dy; + + int x = (xTap / boardWidgetWidth * boardSize).toInt(); + int y = (yTap / boardWidgetHeight * boardSize).toInt(); + print('[' + x.toString() + ',' + y.toString() + ']'); + + removeCell(gameCubit, x, y); + }, + ), IconButton( onPressed: () { - createNewGame(gameCubit, boardSize); + createNewGame(gameCubit); }, icon: Icon(UniconsSolid.star), color: Colors.white, diff --git a/lib/ui/widgets/game_board.dart b/lib/ui/widgets/game_board.dart index 30581d81eeac54524d089ba2830868182a30521b..32e980c2283e14547676784e17deb4a9e37113ff 100644 --- a/lib/ui/widgets/game_board.dart +++ b/lib/ui/widgets/game_board.dart @@ -3,64 +3,64 @@ import 'package:flutter/material.dart'; import 'package:random/models/game_data.dart'; import 'package:random/ui/painters/cell_painter.dart'; -class GameBoardWidget extends StatefulWidget { +class GameBoardWidget extends StatelessWidget { const GameBoardWidget({ super.key, required this.gameData, required this.size, + required this.animations, }); final GameData gameData; final Size size; + final List<List<Animation<double>?>> animations; - @override - State<GameBoardWidget> createState() => _GameBoardWidgetState(); -} - -class _GameBoardWidgetState extends State<GameBoardWidget> { @override Widget build(BuildContext context) { - final widgetWidth = widget.size.width; - final widgetHeight = widget.size.height; + final widgetWidth = this.size.width; + final widgetHeight = this.size.height; - final rowsCount = widget.gameData.board.length; - final columnsCount = widget.gameData.board[0].length; - print('counts: rows=' + rowsCount.toString() + ' / columns=' + columnsCount.toString()); + final rowsCount = this.gameData.board.length; + final columnsCount = this.gameData.board[0].length; final cellWidth = widgetWidth / columnsCount; final cellHeight = widgetHeight / 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 = widget.gameData.board[y][x]; + final GameDataItem item = this.gameData.board[y][x]; + int? value = item.value; + + if (value != null) { + final Animation<double>? translation = this.animations[y][x]; - final Widget cellContent = CustomPaint( - size: Size(cellWidth, cellHeight), - willChange: false, - painter: CellPainter(value: item.value), - ); + final Widget cellContent = CustomPaint( + size: Size(cellWidth, cellHeight), + willChange: false, + painter: CellPainter(value: value), + ); - final Widget cellWidget = Positioned( - left: (x * cellWidth).toDouble(), - top: (y * cellHeight).toDouble(), - child: Container( - width: cellWidth, - height: cellHeight, - child: cellContent, - ), - ); + final Widget cellWidget = Positioned( + left: (x * cellWidth).toDouble(), + top: ((y + (translation?.value ?? 0)) * cellHeight).toDouble(), + child: Container( + width: cellWidth, + height: cellHeight, + child: cellContent, + ), + ); - cells.add(cellWidget); + cells.add(cellWidget); + } } } return Container( width: widgetWidth, height: widgetHeight, - color: Colors.grey, + color: Colors.black, child: Stack( children: cells, ), diff --git a/pubspec.yaml b/pubspec.yaml index a0dad971e0249c47f72502fbac5d16fb069f0192..b0d6d8e00aee8565822bbc1ec0a993a2da234550 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.39+40 +version: 1.0.40+41 environment: sdk: '^3.0.0'