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
Showing
with 236 additions and 42 deletions
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=0.1.7
app.versionCode=56
app.versionName=0.1.8
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
AVAILABLE_GAME_IMAGES="
button_back
button_start
button_resume_game
button_delete_saved_game
button_help
button_show_conflicts
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"?>
<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 {
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.resetGame(myProvider),
onPressed: () => GameUtils.quitGame(myProvider),
);
}
......
......@@ -20,16 +20,21 @@ class Parameters {
List parameters = myProvider.availableParameters;
for (var index = 0; index < parameters.length; index++) {
lines.add(Parameters.buildParameterSelector(myProvider, parameters[index]));
lines.add(SizedBox(height: Parameters.separatorHeight));
lines.add(buildParameterSelector(myProvider, parameters[index]));
lines.add(SizedBox(height: separatorHeight));
}
myProvider.loadCurrentSavedState();
Widget buttonsBlock = myProvider.hasCurrentSavedState()
? buildResumeGameButton(myProvider)
: buildStartNewGameButton(myProvider);
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: Parameters.separatorHeight),
SizedBox(height: separatorHeight),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
......@@ -37,45 +42,91 @@ class Parameters {
children: lines,
),
),
SizedBox(height: Parameters.separatorHeight),
SizedBox(height: separatorHeight),
Container(
child: Parameters.buildStartGameButton(myProvider),
child: buttonsBlock,
),
],
),
);
}
static Container buildStartGameButton(Data myProvider) {
Column decorationImage = Column(
static Image buildImageWidget(String imageAssetCode) {
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: [
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(
margin: EdgeInsets.all(Parameters.blockMargin),
padding: EdgeInsets.all(Parameters.blockPadding),
margin: EdgeInsets.all(blockMargin),
padding: EdgeInsets.all(blockPadding),
child: Table(
defaultColumnWidth: IntrinsicColumnWidth(),
children: [
TableRow(
children: [
decorationImage,
Column(
children: [
TextButton(
child: Container(
child: Image(
image: AssetImage('assets/icons/button_start.png'),
fit: BoxFit.fill,
),
),
onPressed: () => GameUtils.startGame(myProvider),
child: buildImageContainerWidget('button_delete_saved_game'),
onPressed: () => GameUtils.deleteSavedGame(myProvider),
),
],
),
decorationImage,
Column(
children: [
TextButton(
child: buildImageContainerWidget('button_resume_game'),
onPressed: () => GameUtils.resumeSavedGame(myProvider),
),
],
),
buildDecorationImageWidget(),
],
),
],
......@@ -112,26 +163,21 @@ class Parameters {
String currentValue = myProvider.getParameterValue(parameterCode).toString();
bool isActive = (parameterValue == currentValue);
String imageAsset = 'assets/icons/' + parameterCode + '_' + parameterValue + '.png';
String imageAsset = parameterCode + '_' + parameterValue;
return TextButton(
child: Container(
margin: EdgeInsets.all(Parameters.buttonMargin),
padding: EdgeInsets.all(Parameters.buttonPadding),
margin: EdgeInsets.all(buttonMargin),
padding: EdgeInsets.all(buttonPadding),
decoration: BoxDecoration(
color: Parameters.buttonBackgroundColor,
borderRadius: BorderRadius.circular(Parameters.buttonBorderRadius),
color: buttonBackgroundColor,
borderRadius: BorderRadius.circular(buttonBorderRadius),
border: Border.all(
color: isActive
? Parameters.buttonBorderColorActive
: Parameters.buttonBorderColorInactive,
width: Parameters.buttonBorderWidth,
color: isActive ? buttonBorderColorActive : buttonBorderColorInactive,
width: buttonBorderWidth,
),
),
child: Image(
image: AssetImage(imageAsset),
fit: BoxFit.fill,
),
child: buildImageWidget(imageAsset),
),
onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue),
);
......
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
......@@ -42,6 +44,7 @@ class Data extends ChangeNotifier {
int? _currentCellValue;
bool _showConflicts = false;
int _givenTipsCount = 0;
String _currentState = '';
void updateParameterLevel(String parameterLevel) {
_parameterLevel = parameterLevel;
......@@ -109,6 +112,69 @@ class Data extends ChangeNotifier {
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;
void updateGameIsRunning(bool gameIsRunning) {
_gameIsRunning = gameIsRunning;
......@@ -143,6 +209,10 @@ class Data extends ChangeNotifier {
_shuffledCellValues = values;
}
void setShuffleCellValues(List values) {
_shuffledCellValues = values;
}
int getTranslatedValueForDisplay(int originalValue) {
return _shuffledCellValues[originalValue - 1];
}
......@@ -168,14 +238,19 @@ class Data extends ChangeNotifier {
int get givenTipsCount => _givenTipsCount;
increaseGivenTipsCount() {
_givenTipsCount = _givenTipsCount + 1;
saveCurrentGameState();
notifyListeners();
}
resetGivenTipsCount() {
_givenTipsCount = 0;
setGivenTipsCount(int value) {
_givenTipsCount = value;
notifyListeners();
}
resetGivenTipsCount() {
setGivenTipsCount(0);
}
selectCell(int? col, int? row) {
_currentCellCol = col;
_currentCellRow = row;
......@@ -192,8 +267,10 @@ class Data extends ChangeNotifier {
if ((col != null) && (row != null)) {
if (!_cells[row][col].isFixed) {
_cells[row][col].value = value;
saveCurrentGameState();
notifyListeners();
}
notifyListeners();
}
}
......@@ -205,6 +282,7 @@ class Data extends ChangeNotifier {
void toggleShowConflicts() {
updateShowConflicts(!showConflicts);
saveCurrentGameState();
}
bool get animationInProgress => _animationInProgress;
......
......@@ -22,6 +22,7 @@ class _HomeState extends State<Home> {
Data myProvider = Provider.of<Data>(context, listen: false);
myProvider.initParametersValues();
myProvider.loadCurrentSavedState();
}
List getImagesAssets(Data myProvider) {
......@@ -81,7 +82,7 @@ class _HomeState extends State<Home> {
),
),
onPressed: () => toast('Long press to quit game...'),
onLongPress: () => GameUtils.resetGame(myProvider),
onLongPress: () => GameUtils.quitGame(myProvider),
),
Spacer(flex: 6),
TextButton(
......
......@@ -58,6 +58,30 @@ class BoardUtils {
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) {
List copiedGrid = [];
for (var rowIndex = 0; rowIndex < cells.length; rowIndex++) {
......
......@@ -3,11 +3,14 @@ import '../utils/board_animate.dart';
import '../utils/board_utils.dart';
class GameUtils {
static Future<void> resetGame(Data myProvider) async {
static Future<void> quitGame(Data myProvider) async {
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.updateGameIsRunning(true);
myProvider.resetGivenTipsCount();
......@@ -18,6 +21,39 @@ class GameUtils {
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) {
if (myProvider.currentCellCol == null || myProvider.currentCellRow == null) {
// no selected cell -> pick one
......@@ -91,6 +127,7 @@ class GameUtils {
allowedValuesCount == 1 ? eligibleValue : 0);
myProvider.selectCell(null, null);
if (BoardUtils.checkBoardIsSolved(myProvider)) {
myProvider.resetCurrentSavedState();
BoardAnimate.startAnimation(myProvider, 'win');
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment