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

Merge branch '29-save-current-game-state-allow-resume-game' into 'master'

Resolve "Save current game state, allow resume game"

Closes #29

See merge request !29
parents 4b140c49 8aab5f6c
No related branches found
No related tags found
1 merge request!29Resolve "Save current game state, allow resume game"
Pipeline #4303 passed
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