diff --git a/android/gradle.properties b/android/gradle.properties index 803b8f008be460ba63d606ca4b77e27f898f60c8..32d7d3cada326741bc19ca507dc82ac2936c7a42 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.1.9 -app.versionCode=30 +app.versionName=0.1.10 +app.versionCode=31 diff --git a/assets/icons/button_delete_saved_game.png b/assets/icons/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d Binary files /dev/null and b/assets/icons/button_delete_saved_game.png differ diff --git a/assets/icons/button_resume_game.png b/assets/icons/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d Binary files /dev/null and b/assets/icons/button_resume_game.png differ diff --git a/assets/icons/button_start.png b/assets/icons/button_start.png index 6845e2f5c21598ab61f1684d2075aeec0334bf23..f0ead9744e59874fa15d70d7e5e49336a15009dd 100644 Binary files a/assets/icons/button_start.png and b/assets/icons/button_start.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/31.txt b/fastlane/metadata/android/en-US/changelogs/31.txt new file mode 100644 index 0000000000000000000000000000000000000000..36b352159c760713d57ccb833b89fc86a107dd06 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/31.txt @@ -0,0 +1 @@ +Autosave current game, allow to continue current game on home screen diff --git a/fastlane/metadata/android/fr-FR/changelogs/31.txt b/fastlane/metadata/android/fr-FR/changelogs/31.txt new file mode 100644 index 0000000000000000000000000000000000000000..f655421b1fe3f96223b905b3e16d7ee2b0be0e4e --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/31.txt @@ -0,0 +1 @@ +Sauvegarde automatique de la partie en cours, proposition de continuer sur l'écran d'accueil diff --git a/icons/build_game_icons.sh b/icons/build_game_icons.sh index 9e88ceb45b85c2208c045912bf3f7b9a9a0a175c..ea879011e365fb935736ba647b173df06085536f 100755 --- a/icons/build_game_icons.sh +++ b/icons/build_game_icons.sh @@ -18,6 +18,8 @@ ICON_SIZE=192 AVAILABLE_GAME_IMAGES=" button_back button_start + button_resume_game + button_delete_saved_game game_fail game_win placeholder diff --git a/icons/button_delete_saved_game.svg b/icons/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..ac7eefef476f761903fe781b8c86d0c94323550a --- /dev/null +++ b/icons/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/icons/button_resume_game.svg b/icons/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ad8b64202d0e70f898c16c520e756fe8a934add --- /dev/null +++ b/icons/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/icons/button_start.svg b/icons/button_start.svg index e9d49d2172b9a0305db82779971e3c1e12f34a70..633a63410502bc516703555911588df2143957f1 100644 --- a/icons/button_start.svg +++ b/icons/button_start.svg @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><g transform="matrix(.8268 0 0 .8268 9.0269 8.3829)" fill="#fefeff" stroke-linecap="round" stroke-linejoin="round"><path d="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" stroke="#105ca1" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" stroke="#feffff" stroke-width="4.314"/></g></svg> diff --git a/lib/layout/game.dart b/lib/layout/game.dart index 49573e1abe2817961453e017f80f850e37f921b9..7ed0df0601b1df5e2d2313127b11a43d58ef510d 100644 --- a/lib/layout/game.dart +++ b/lib/layout/game.dart @@ -92,7 +92,7 @@ class Game { fit: BoxFit.fill, ), ), - onPressed: () => GameUtils.resetGame(myProvider), + onPressed: () => GameUtils.quitGame(myProvider), ); } diff --git a/lib/layout/parameters.dart b/lib/layout/parameters.dart index f7a868cbf69da0efd3e1c6999daae3dd6aa14f9d..2f329d4106ce5a40cd48ee8bf1a14263a0082d26 100644 --- a/lib/layout/parameters.dart +++ b/lib/layout/parameters.dart @@ -23,6 +23,11 @@ class Parameters { lines.add(SizedBox(height: separatorHeight)); } + myProvider.loadCurrentSavedState(); + Widget buttonsBlock = myProvider.hasCurrentSavedState() + ? buildResumeGameButton(myProvider) + : buildStartNewGameButton(myProvider); + return Container( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -38,7 +43,7 @@ class Parameters { ), SizedBox(height: separatorHeight), Container( - child: buildStartNewGameButton(myProvider), + child: buttonsBlock, ), ], ), @@ -95,6 +100,39 @@ class Parameters { ); } + static Container buildResumeGameButton(Data myProvider) { + return Container( + margin: EdgeInsets.all(blockMargin), + padding: EdgeInsets.all(blockPadding), + child: Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [ + TextButton( + child: buildImageContainerWidget('button_delete_saved_game'), + onPressed: () => GameUtils.deleteSavedGame(myProvider), + ), + ], + ), + Column( + children: [ + TextButton( + child: buildImageContainerWidget('button_resume_game'), + onPressed: () => GameUtils.resumeSavedGame(myProvider), + ), + ], + ), + buildDecorationImageWidget(), + ], + ), + ], + ), + ); + } + static Widget buildParameterSelector(Data myProvider, String parameterCode) { List availableValues = myProvider.getParameterAvailableValues(parameterCode); diff --git a/lib/provider/data.dart b/lib/provider/data.dart index 96d5dbc71fa780519536eaf28ed5e005dbc7f8f5..2fd520e8c21a8fff47cabacbe0a1772e5cbf92e5 100644 --- a/lib/provider/data.dart +++ b/lib/provider/data.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -40,6 +42,7 @@ class Data extends ChangeNotifier { bool _reportMode = false; bool _gameWin = false; bool _gameFail = false; + String _currentState = ''; void updateParameterLevel(String parameterLevel) { _parameterLevel = parameterLevel; @@ -128,6 +131,69 @@ class Data extends ChangeNotifier { setParameterValue('skin', prefs.getString('skin') ?? _parameterSkinDefault); } + String get currentState => _currentState; + + String computeCurrentGameState() { + String cellsValues = ''; + for (var rowIndex = 0; rowIndex < _cells.length; rowIndex++) { + for (var colIndex = 0; colIndex < _cells[rowIndex].length; colIndex++) { + cellsValues += _cells[rowIndex][colIndex].isMined ? 'X' : ' '; + cellsValues += _cells[rowIndex][colIndex].isExplored ? 'E' : ' '; + cellsValues += _cells[rowIndex][colIndex].isMarked ? 'P' : ' '; + cellsValues += _cells[rowIndex][colIndex].isExploded ? '*' : ' '; + cellsValues += _cells[rowIndex][colIndex].minesCountAround.toString(); + cellsValues += ';'; + } + } + + var currentState = { + 'level': _parameterLevel, + 'size': _parameterSize, + 'skin': _parameterSkin, + 'board': cellsValues, + }; + + return json.encode(currentState); + } + + void saveCurrentGameState() async { + if (_gameIsRunning) { + _currentState = computeCurrentGameState(); + + final prefs = await SharedPreferences.getInstance(); + prefs.setString('savedState', _currentState); + } else { + resetCurrentSavedState(); + } + } + + void resetCurrentSavedState() async { + _currentState = ''; + + final prefs = await SharedPreferences.getInstance(); + prefs.setString('savedState', _currentState); + notifyListeners(); + } + + void loadCurrentSavedState() async { + final prefs = await SharedPreferences.getInstance(); + _currentState = prefs.getString('savedState') ?? ''; + } + + bool hasCurrentSavedState() { + return (_currentState != ''); + } + + Map<String, dynamic> getCurrentSavedState() { + if (_currentState != '') { + Map<String, dynamic> savedState = json.decode(_currentState); + if (savedState.isNotEmpty) { + return savedState; + } + } + return {}; + } + bool get gameIsRunning => _gameIsRunning; void updateGameIsRunning(bool gameIsRunning) { _gameIsRunning = gameIsRunning; @@ -162,11 +228,14 @@ class Data extends ChangeNotifier { _cells[row][col].isExploded = true; } + saveCurrentGameState(); notifyListeners(); } void toggleCellMark(int row, int col) { _cells[row][col].isMarked = !_cells[row][col].isMarked; + + saveCurrentGameState(); notifyListeners(); } diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 18986beeaff26baeaf9a058812265a4bf158d14e..39bb1f054e0f36897eb6845c0fbb917d408122ef 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -79,7 +79,7 @@ class _HomeState extends State<Home> { ), ), onPressed: () => toast('Long press to quit game...'), - onLongPress: () => GameUtils.resetGame(myProvider), + onLongPress: () => GameUtils.quitGame(myProvider), ), ]; } diff --git a/lib/utils/board_utils.dart b/lib/utils/board_utils.dart index a82c965b4a2b30c7cd434fc0a815905aeeb9033e..42437b1926df2411bef07abb3b4d3af95e0b64ac 100644 --- a/lib/utils/board_utils.dart +++ b/lib/utils/board_utils.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:minehunter/entities/cell.dart'; import 'package:minehunter/provider/data.dart'; @@ -98,7 +100,7 @@ class BoardUtils { int sizeHorizontal = myProvider.sizeHorizontal; int sizeVertical = myProvider.sizeVertical; - // Shuffle cells to put random mines, expect on currently selected one + // Shuffle cells to put random mines, except on currently selected one List allowedCells = []; for (var row = 0; row < sizeVertical; row++) { for (var col = 0; col < sizeHorizontal; col++) { @@ -126,6 +128,39 @@ class BoardUtils { return cells; } + static List createBoardFromSavedState(Data myProvider, String savedBoard) { + List<List<Cell?>> board = []; + int boardSize = pow((savedBoard.length / 6), 1 / 2).round(); + String boardSizeAsString = boardSize.toString() + 'x' + boardSize.toString(); + myProvider.updateParameterSize(boardSizeAsString); + + int index = 0; + for (var rowIndex = 0; rowIndex < boardSize; rowIndex++) { + List<Cell?> row = []; + for (var colIndex = 0; colIndex < boardSize; colIndex++) { + bool isMined = (savedBoard[index++] == 'X'); + bool isExplored = (savedBoard[index++] == 'E'); + bool isMarked = (savedBoard[index++] == 'P'); + bool isExploded = (savedBoard[index++] == '*'); + int minesCountAround = int.parse(savedBoard[index++]); + index++; // ";" + + Cell cell = Cell(isMined); + cell.isExplored = isExplored; + cell.isMarked = isMarked; + cell.isExploded = isExploded; + cell.minesCountAround = minesCountAround; + + row.add(cell); + } + board.add(row); + } + + printGrid(board); + + return board; + } + static void reportCell(Data myProvider, int row, int col) { if (!myProvider.cells[row][col].isExplored) { myProvider.toggleCellMark(row, col); diff --git a/lib/utils/game_utils.dart b/lib/utils/game_utils.dart index 02de51a917025dfbd62584e2bca9c78c0683356c..107cff4fa714f2c8769db349954b52953860c307 100644 --- a/lib/utils/game_utils.dart +++ b/lib/utils/game_utils.dart @@ -3,8 +3,11 @@ import 'package:minehunter/utils/board_animate.dart'; import 'package:minehunter/utils/board_utils.dart'; class GameUtils { - static void resetGame(Data myProvider) { + static Future<void> quitGame(Data myProvider) async { myProvider.updateGameIsRunning(false); + if (BoardUtils.checkGameIsFinished(myProvider)) { + myProvider.resetCurrentSavedState(); + } } static void startNewGame(Data myProvider) { @@ -16,4 +19,32 @@ class GameUtils { BoardUtils.createInitialEmptyBoard(myProvider); BoardAnimate.startAnimation(myProvider, 'start'); } + + static void deleteSavedGame(Data myProvider) { + myProvider.resetCurrentSavedState(); + } + + static void resumeSavedGame(Data myProvider) { + Map<String, dynamic> savedState = myProvider.getCurrentSavedState(); + if (savedState.isNotEmpty) { + try { + myProvider.setParameterValue('level', savedState['level']); + myProvider.setParameterValue('size', savedState['size']); + myProvider.setParameterValue('skin', savedState['skin']); + + myProvider.updateCells( + BoardUtils.createBoardFromSavedState(myProvider, savedState['board'])); + myProvider.updateGameIsRunning(true); + } catch (e) { + print('Failed to resume game. Will start new one instead.'); + myProvider.resetCurrentSavedState(); + myProvider.initParametersValues(); + startNewGame(myProvider); + } + } else { + myProvider.resetCurrentSavedState(); + myProvider.initParametersValues(); + startNewGame(myProvider); + } + } }