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

Save current game state, allow resume/restart game

parent dc80ed17
No related branches found
No related tags found
1 merge request!62Resolve "Save current game, allow resume / restart"
Pipeline #3013 passed
This commit is part of merge request !62. Comments created here will be created in the context of that merge request.
Showing
with 236 additions and 42 deletions
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
app.versionName=0.1.7 app.versionName=0.1.8
app.versionCode=56 app.versionCode=57
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 ...@@ -18,6 +18,8 @@ ICON_SIZE=192
AVAILABLE_GAME_IMAGES=" AVAILABLE_GAME_IMAGES="
button_back button_back
button_start button_start
button_resume_game
button_delete_saved_game
button_help button_help
button_show_conflicts button_show_conflicts
game_win game_win
......
<?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"?> <?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>
...@@ -75,7 +75,7 @@ class Game { ...@@ -75,7 +75,7 @@ class Game {
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
), ),
onPressed: () => GameUtils.resetGame(myProvider), onPressed: () => GameUtils.quitGame(myProvider),
); );
} }
......
...@@ -20,16 +20,21 @@ class Parameters { ...@@ -20,16 +20,21 @@ class Parameters {
List parameters = myProvider.availableParameters; List parameters = myProvider.availableParameters;
for (var index = 0; index < parameters.length; index++) { for (var index = 0; index < parameters.length; index++) {
lines.add(Parameters.buildParameterSelector(myProvider, parameters[index])); lines.add(buildParameterSelector(myProvider, parameters[index]));
lines.add(SizedBox(height: Parameters.separatorHeight)); lines.add(SizedBox(height: separatorHeight));
} }
myProvider.loadCurrentSavedState();
Widget buttonsBlock = myProvider.hasCurrentSavedState()
? buildResumeGameButton(myProvider)
: buildStartNewGameButton(myProvider);
return Container( return Container(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
SizedBox(height: Parameters.separatorHeight), SizedBox(height: separatorHeight),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
...@@ -37,45 +42,91 @@ class Parameters { ...@@ -37,45 +42,91 @@ class Parameters {
children: lines, children: lines,
), ),
), ),
SizedBox(height: Parameters.separatorHeight), SizedBox(height: separatorHeight),
Container( Container(
child: Parameters.buildStartGameButton(myProvider), child: buttonsBlock,
), ),
], ],
), ),
); );
} }
static Container buildStartGameButton(Data myProvider) { static Image buildImageWidget(String imageAssetCode) {
Column decorationImage = Column( return Image(
image: AssetImage('assets/icons/' + imageAssetCode + '.png'),
fit: BoxFit.fill,
);
}
static Container buildImageContainerWidget(String imageAssetCode) {
return Container(
child: buildImageWidget(imageAssetCode),
);
}
static Column buildDecorationImageWidget() {
return Column(
children: [ children: [
Image(image: AssetImage('assets/icons/placeholder.png'), fit: BoxFit.fill), TextButton(
child: buildImageContainerWidget('placeholder'),
onPressed: () => null,
),
], ],
); );
}
static Container buildStartNewGameButton(Data myProvider) {
return Container(
margin: EdgeInsets.all(blockMargin),
padding: EdgeInsets.all(blockPadding),
child: Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
TableRow(
children: [
buildDecorationImageWidget(),
Column(
children: [
TextButton(
child: buildImageContainerWidget('button_start'),
onPressed: () => GameUtils.startNewGame(myProvider),
),
],
),
buildDecorationImageWidget(),
],
),
],
),
);
}
static Container buildResumeGameButton(Data myProvider) {
return Container( return Container(
margin: EdgeInsets.all(Parameters.blockMargin), margin: EdgeInsets.all(blockMargin),
padding: EdgeInsets.all(Parameters.blockPadding), padding: EdgeInsets.all(blockPadding),
child: Table( child: Table(
defaultColumnWidth: IntrinsicColumnWidth(), defaultColumnWidth: IntrinsicColumnWidth(),
children: [ children: [
TableRow( TableRow(
children: [ children: [
decorationImage,
Column( Column(
children: [ children: [
TextButton( TextButton(
child: Container( child: buildImageContainerWidget('button_delete_saved_game'),
child: Image( onPressed: () => GameUtils.deleteSavedGame(myProvider),
image: AssetImage('assets/icons/button_start.png'),
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.startGame(myProvider),
), ),
], ],
), ),
decorationImage, Column(
children: [
TextButton(
child: buildImageContainerWidget('button_resume_game'),
onPressed: () => GameUtils.resumeSavedGame(myProvider),
),
],
),
buildDecorationImageWidget(),
], ],
), ),
], ],
...@@ -112,26 +163,21 @@ class Parameters { ...@@ -112,26 +163,21 @@ class Parameters {
String currentValue = myProvider.getParameterValue(parameterCode).toString(); String currentValue = myProvider.getParameterValue(parameterCode).toString();
bool isActive = (parameterValue == currentValue); bool isActive = (parameterValue == currentValue);
String imageAsset = 'assets/icons/' + parameterCode + '_' + parameterValue + '.png'; String imageAsset = parameterCode + '_' + parameterValue;
return TextButton( return TextButton(
child: Container( child: Container(
margin: EdgeInsets.all(Parameters.buttonMargin), margin: EdgeInsets.all(buttonMargin),
padding: EdgeInsets.all(Parameters.buttonPadding), padding: EdgeInsets.all(buttonPadding),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Parameters.buttonBackgroundColor, color: buttonBackgroundColor,
borderRadius: BorderRadius.circular(Parameters.buttonBorderRadius), borderRadius: BorderRadius.circular(buttonBorderRadius),
border: Border.all( border: Border.all(
color: isActive color: isActive ? buttonBorderColorActive : buttonBorderColorInactive,
? Parameters.buttonBorderColorActive width: buttonBorderWidth,
: Parameters.buttonBorderColorInactive,
width: Parameters.buttonBorderWidth,
), ),
), ),
child: Image( child: buildImageWidget(imageAsset),
image: AssetImage(imageAsset),
fit: BoxFit.fill,
),
), ),
onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue), onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue),
); );
......
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
...@@ -42,6 +44,7 @@ class Data extends ChangeNotifier { ...@@ -42,6 +44,7 @@ class Data extends ChangeNotifier {
int? _currentCellValue; int? _currentCellValue;
bool _showConflicts = false; bool _showConflicts = false;
int _givenTipsCount = 0; int _givenTipsCount = 0;
String _currentState = '';
void updateParameterLevel(String parameterLevel) { void updateParameterLevel(String parameterLevel) {
_parameterLevel = parameterLevel; _parameterLevel = parameterLevel;
...@@ -109,6 +112,69 @@ class Data extends ChangeNotifier { ...@@ -109,6 +112,69 @@ class Data extends ChangeNotifier {
setParameterValue('skin', prefs.getString('skin') ?? _parameterSkinDefault); setParameterValue('skin', prefs.getString('skin') ?? _parameterSkinDefault);
} }
String get currentState => _currentState;
String computeCurrentGameState() {
String cellsValues = '';
String stringValues = '0123456789ABCDEFG';
for (var rowIndex = 0; rowIndex < _cells.length; rowIndex++) {
for (var colIndex = 0; colIndex < _cells[rowIndex].length; colIndex++) {
cellsValues += stringValues[_cells[rowIndex][colIndex].value];
cellsValues += _cells[rowIndex][colIndex].isFixed ? 'x' : ' ';
}
}
var currentState = {
'level': _parameterLevel,
'size': _parameterSize,
'skin': _parameterSkin,
'tipsCount': _givenTipsCount,
'showConflicts': _showConflicts,
'boardValues': cellsValues,
'shuffledCellValues': _shuffledCellValues,
};
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; bool get gameIsRunning => _gameIsRunning;
void updateGameIsRunning(bool gameIsRunning) { void updateGameIsRunning(bool gameIsRunning) {
_gameIsRunning = gameIsRunning; _gameIsRunning = gameIsRunning;
...@@ -143,6 +209,10 @@ class Data extends ChangeNotifier { ...@@ -143,6 +209,10 @@ class Data extends ChangeNotifier {
_shuffledCellValues = values; _shuffledCellValues = values;
} }
void setShuffleCellValues(List values) {
_shuffledCellValues = values;
}
int getTranslatedValueForDisplay(int originalValue) { int getTranslatedValueForDisplay(int originalValue) {
return _shuffledCellValues[originalValue - 1]; return _shuffledCellValues[originalValue - 1];
} }
...@@ -168,14 +238,19 @@ class Data extends ChangeNotifier { ...@@ -168,14 +238,19 @@ class Data extends ChangeNotifier {
int get givenTipsCount => _givenTipsCount; int get givenTipsCount => _givenTipsCount;
increaseGivenTipsCount() { increaseGivenTipsCount() {
_givenTipsCount = _givenTipsCount + 1; _givenTipsCount = _givenTipsCount + 1;
saveCurrentGameState();
notifyListeners(); notifyListeners();
} }
resetGivenTipsCount() { setGivenTipsCount(int value) {
_givenTipsCount = 0; _givenTipsCount = value;
notifyListeners(); notifyListeners();
} }
resetGivenTipsCount() {
setGivenTipsCount(0);
}
selectCell(int? col, int? row) { selectCell(int? col, int? row) {
_currentCellCol = col; _currentCellCol = col;
_currentCellRow = row; _currentCellRow = row;
...@@ -192,8 +267,10 @@ class Data extends ChangeNotifier { ...@@ -192,8 +267,10 @@ class Data extends ChangeNotifier {
if ((col != null) && (row != null)) { if ((col != null) && (row != null)) {
if (!_cells[row][col].isFixed) { if (!_cells[row][col].isFixed) {
_cells[row][col].value = value; _cells[row][col].value = value;
saveCurrentGameState();
notifyListeners();
} }
notifyListeners();
} }
} }
...@@ -205,6 +282,7 @@ class Data extends ChangeNotifier { ...@@ -205,6 +282,7 @@ class Data extends ChangeNotifier {
void toggleShowConflicts() { void toggleShowConflicts() {
updateShowConflicts(!showConflicts); updateShowConflicts(!showConflicts);
saveCurrentGameState();
} }
bool get animationInProgress => _animationInProgress; bool get animationInProgress => _animationInProgress;
......
...@@ -22,6 +22,7 @@ class _HomeState extends State<Home> { ...@@ -22,6 +22,7 @@ class _HomeState extends State<Home> {
Data myProvider = Provider.of<Data>(context, listen: false); Data myProvider = Provider.of<Data>(context, listen: false);
myProvider.initParametersValues(); myProvider.initParametersValues();
myProvider.loadCurrentSavedState();
} }
List getImagesAssets(Data myProvider) { List getImagesAssets(Data myProvider) {
...@@ -81,7 +82,7 @@ class _HomeState extends State<Home> { ...@@ -81,7 +82,7 @@ class _HomeState extends State<Home> {
), ),
), ),
onPressed: () => toast('Long press to quit game...'), onPressed: () => toast('Long press to quit game...'),
onLongPress: () => GameUtils.resetGame(myProvider), onLongPress: () => GameUtils.quitGame(myProvider),
), ),
Spacer(flex: 6), Spacer(flex: 6),
TextButton( TextButton(
......
...@@ -58,6 +58,30 @@ class BoardUtils { ...@@ -58,6 +58,30 @@ class BoardUtils {
return cells; return cells;
} }
static List createBoardFromSavedState(Data myProvider, String savedBoard) {
List cells = [];
int boardSize = int.parse(pow((savedBoard.length / 2), 1 / 2).toStringAsFixed(0));
String stringValues = '0123456789ABCDEFG';
int index = 0;
for (var rowIndex = 0; rowIndex < boardSize; rowIndex++) {
List row = [];
for (var colIndex = 0; colIndex < boardSize; colIndex++) {
String stringValue = savedBoard[index++];
int value = stringValues.indexOf(stringValue);
String isFixedString = savedBoard[index++];
bool isFixed = (isFixedString != ' ');
row.add(Cell(value, isFixed));
}
cells.add(row);
}
return cells;
}
static List copyBoard(List cells) { static List copyBoard(List cells) {
List copiedGrid = []; List copiedGrid = [];
for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) { for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
......
...@@ -3,11 +3,14 @@ import '../utils/board_animate.dart'; ...@@ -3,11 +3,14 @@ import '../utils/board_animate.dart';
import '../utils/board_utils.dart'; import '../utils/board_utils.dart';
class GameUtils { class GameUtils {
static Future<void> resetGame(Data myProvider) async { static Future<void> quitGame(Data myProvider) async {
myProvider.updateGameIsRunning(false); myProvider.updateGameIsRunning(false);
if (BoardUtils.checkBoardIsSolved(myProvider)) {
myProvider.resetCurrentSavedState();
}
} }
static Future<void> startGame(Data myProvider) async { static Future<void> startNewGame(Data myProvider) async {
myProvider.updateParameterSize(myProvider.parameterSize); myProvider.updateParameterSize(myProvider.parameterSize);
myProvider.updateGameIsRunning(true); myProvider.updateGameIsRunning(true);
myProvider.resetGivenTipsCount(); myProvider.resetGivenTipsCount();
...@@ -18,6 +21,39 @@ class GameUtils { ...@@ -18,6 +21,39 @@ class GameUtils {
BoardAnimate.startAnimation(myProvider, 'start'); 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.setGivenTipsCount(savedState['tipsCount']);
myProvider.updateShowConflicts(savedState['showConflicts']);
myProvider.setShuffleCellValues(savedState['shuffledCellValues']);
myProvider.updateCells(
BoardUtils.createBoardFromSavedState(myProvider, savedState['boardValues']));
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);
}
}
static void showTip(Data myProvider) { static void showTip(Data myProvider) {
if (myProvider.currentCellCol == null || myProvider.currentCellRow == null) { if (myProvider.currentCellCol == null || myProvider.currentCellRow == null) {
// no selected cell -> pick one // no selected cell -> pick one
...@@ -91,6 +127,7 @@ class GameUtils { ...@@ -91,6 +127,7 @@ class GameUtils {
allowedValuesCount == 1 ? eligibleValue : 0); allowedValuesCount == 1 ? eligibleValue : 0);
myProvider.selectCell(null, null); myProvider.selectCell(null, null);
if (BoardUtils.checkBoardIsSolved(myProvider)) { if (BoardUtils.checkBoardIsSolved(myProvider)) {
myProvider.resetCurrentSavedState();
BoardAnimate.startAnimation(myProvider, 'win'); BoardAnimate.startAnimation(myProvider, 'win');
} }
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment