Skip to content
Snippets Groups Projects
Commit 8aab5f6c authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Auto save current game and allow resume saved game

parent 4b140c49
No related branches found
No related tags found
1 merge request!29Resolve "Save current game state, allow resume game"
Pipeline #3576 passed
This commit is part of merge request !29. Comments created here will be created in the context of that merge request.
Showing with 189 additions and 8 deletions
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
assets/icons/button_delete_saved_game.png

5.68 KiB

assets/icons/button_resume_game.png

3.57 KiB

assets/icons/button_start.png

3.91 KiB | W: | H:

assets/icons/button_start.png

3.58 KiB | W: | H:

assets/icons/button_start.png
assets/icons/button_start.png
assets/icons/button_start.png
assets/icons/button_start.png
  • 2-up
  • Swipe
  • Onion skin
Autosave current game, allow to continue current game on home screen
Sauvegarde automatique de la partie en cours, proposition de continuer sur l'écran d'accueil
......@@ -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
......
<?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>
<?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>
<?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>
......@@ -92,7 +92,7 @@ class Game {
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.resetGame(myProvider),
onPressed: () => GameUtils.quitGame(myProvider),
);
}
......
......@@ -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);
......
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();
}
......
......@@ -79,7 +79,7 @@ class _HomeState extends State<Home> {
),
),
onPressed: () => toast('Long press to quit game...'),
onLongPress: () => GameUtils.resetGame(myProvider),
onLongPress: () => GameUtils.quitGame(myProvider),
),
];
}
......
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);
......
......@@ -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);
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment