Implement auto-save of old game when starting a new game

The 20 most recently auto-saved games are kept in the file
DroidFish/pgn/.autosave.pgn.

Overwriting an existing game when saving a new game also auto-saves
the old game.
This commit is contained in:
Peter Osterlund 2020-02-09 13:25:15 +01:00
parent ce544c6be8
commit 683a238bff
9 changed files with 202 additions and 57 deletions

View File

@ -588,7 +588,7 @@ public class DroidFish extends Activity
ctrl.startGame();
if (intentPgnOrFen != null) {
try {
ctrl.setFENOrPGN(intentPgnOrFen);
ctrl.setFENOrPGN(intentPgnOrFen, true);
setBoardFlip(true);
} catch (ChessParseError e) {
// If FEN corresponds to illegal chess position, go into edit board mode.
@ -1019,6 +1019,8 @@ public class DroidFish extends Activity
});
}
private static final int serializeVersion = 4;
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@ -1026,7 +1028,7 @@ public class DroidFish extends Activity
byte[] data = ctrl.toByteArray();
byte[] token = data == null ? null : cache.storeBytes(data);
outState.putByteArray("gameStateT", token);
outState.putInt("gameStateVersion", 3);
outState.putInt("gameStateVersion", serializeVersion);
}
}
@ -1049,7 +1051,7 @@ public class DroidFish extends Activity
Editor editor = settings.edit();
String dataStr = byteArrToString(data);
editor.putString("gameState", dataStr);
editor.putInt("gameStateVersion", 3);
editor.putInt("gameStateVersion", serializeVersion);
editor.apply();
}
lastVisibleMillis = System.currentTimeMillis();
@ -1568,16 +1570,17 @@ public class DroidFish extends Activity
}
}
static private final int RESULT_EDITBOARD = 0;
static private final int RESULT_SETTINGS = 1;
static private final int RESULT_LOAD_PGN = 2;
static private final int RESULT_LOAD_FEN = 3;
static private final int RESULT_SELECT_SCID = 4;
static private final int RESULT_OI_PGN_SAVE = 5;
static private final int RESULT_OI_PGN_LOAD = 6;
static private final int RESULT_OI_FEN_LOAD = 7;
static private final int RESULT_GET_FEN = 8;
static private final int RESULT_EDITOPTIONS = 9;
static private final int RESULT_EDITBOARD = 0;
static private final int RESULT_SETTINGS = 1;
static private final int RESULT_LOAD_PGN = 2;
static private final int RESULT_LOAD_FEN = 3;
static private final int RESULT_SAVE_PGN = 4;
static private final int RESULT_SELECT_SCID = 5;
static private final int RESULT_OI_PGN_SAVE = 6;
static private final int RESULT_OI_PGN_LOAD = 7;
static private final int RESULT_OI_FEN_LOAD = 8;
static private final int RESULT_GET_FEN = 9;
static private final int RESULT_EDITOPTIONS = 10;
private void startEditBoard(String fen) {
Intent i = new Intent(DroidFish.this, EditBoard.class);
@ -1595,7 +1598,7 @@ public class DroidFish extends Activity
if (resultCode == RESULT_OK) {
try {
String fen = data.getAction();
ctrl.setFENOrPGN(fen);
ctrl.setFENOrPGN(fen, true);
setBoardFlip(false);
} catch (ChessParseError ignore) {
}
@ -1609,13 +1612,19 @@ public class DroidFish extends Activity
int modeNr = ctrl.getGameMode().getModeNr();
if ((modeNr != GameMode.ANALYSIS) && (modeNr != GameMode.EDIT_GAME))
newGameMode(GameMode.EDIT_GAME);
ctrl.setFENOrPGN(pgn);
ctrl.setFENOrPGN(pgn, false);
setBoardFlip(true);
} catch (ChessParseError e) {
DroidFishApp.toast(getParseErrString(e), Toast.LENGTH_SHORT);
}
}
break;
case RESULT_SAVE_PGN:
if (resultCode == RESULT_OK) {
long hash = data.getLongExtra("org.petero.droidfish.treeHash", -1);
ctrl.setLastSaveHash(hash);
}
break;
case RESULT_SELECT_SCID:
if (resultCode == RESULT_OK) {
String pathName = data.getAction();
@ -1662,13 +1671,13 @@ public class DroidFish extends Activity
String pathName = getFilePathFromUri(data.getData());
loadFENFromFile(pathName);
}
setFenHelper(fen);
setFenHelper(fen, true);
}
break;
case RESULT_LOAD_FEN:
if (resultCode == RESULT_OK) {
String fen = data.getAction();
setFenHelper(fen);
setFenHelper(fen, false);
}
break;
case RESULT_EDITOPTIONS:
@ -2090,7 +2099,6 @@ public class DroidFish extends Activity
editor.apply();
gameMode = new GameMode(gameModeType);
}
// savePGNToFile(".autosave.pgn", true);
TimeControlData tcData = new TimeControlData();
tcData.setTimeControl(timeControl, movesPerSession, timeIncrement);
speech.flushQueue();
@ -2161,10 +2169,10 @@ public class DroidFish extends Activity
writer.close();
loadPGNFromFile(fn);
} catch (IOException ex) {
ctrl.setFENOrPGN(fenPgnData);
ctrl.setFENOrPGN(fenPgnData, true);
}
} else {
ctrl.setFENOrPGN(fenPgnData);
ctrl.setFENOrPGN(fenPgnData, true);
}
setBoardFlip(true);
} catch (ChessParseError e) {
@ -3476,8 +3484,33 @@ public class DroidFish extends Activity
i.setAction("org.petero.droidfish.saveFile");
i.putExtra("org.petero.droidfish.pathname", pathName);
i.putExtra("org.petero.droidfish.pgn", pgnToken);
i.putExtra("org.petero.droidfish.silent", false);
startActivity(i);
setEditPGNBackup(i, pathName);
startActivityForResult(i, RESULT_SAVE_PGN);
}
/** Set a Boolean value in the Intent to decide if backups should be made
* when games in a PGN file are overwritten or deleted. */
private void setEditPGNBackup(Intent i, String pathName) {
boolean backup = storageAvailable() && !pathName.equals(getAutoSaveFile());
i.putExtra("org.petero.droidfish.backup", backup);
}
/** Get the full path to the auto-save file. */
private static String getAutoSaveFile() {
String sep = File.separator;
return Environment.getExternalStorageDirectory() + sep + pgnDir + sep + ".autosave.pgn";
}
@Override
public void autoSaveGameIfAllowed(String pgn) {
if (storageAvailable())
autoSaveGame(pgn);
}
/** Save a copy of the pgn data in the .autosave.pgn file. */
public static void autoSaveGame(String pgn) {
PGNFile pgnFile = new PGNFile(getAutoSaveFile());
pgnFile.autoSave(pgn);
}
/** Load a PGN game from a file. */
@ -3489,6 +3522,7 @@ public class DroidFish extends Activity
Intent i = new Intent(DroidFish.this, EditPGNLoad.class);
i.setAction("org.petero.droidfish.loadFile");
i.putExtra("org.petero.droidfish.pathname", pathName);
setEditPGNBackup(i, pathName);
startActivityForResult(i, RESULT_LOAD_PGN);
}
@ -3506,11 +3540,11 @@ public class DroidFish extends Activity
startActivityForResult(i, RESULT_LOAD_FEN);
}
private void setFenHelper(String fen) {
private void setFenHelper(String fen, boolean setModified) {
if (fen == null)
return;
try {
ctrl.setFENOrPGN(fen);
ctrl.setFENOrPGN(fen, setModified);
} catch (ChessParseError e) {
// If FEN corresponds to illegal chess position, go into edit board mode.
try {

View File

@ -108,4 +108,7 @@ public interface GUIInterface {
/** Return true if only main-line moves are to be kept. */
boolean discardVariations();
/** Save the current game to the auto-save file, if storage permission has been granted. */
void autoSaveGameIfAllowed(String pgn);
}

View File

@ -128,4 +128,26 @@ public final class Util {
((MoveListView) v).setTextColor(fg);
}
}
/** Return a hash value for a string, with better quality than String.hashCode(). */
public static long stringHash(String s) {
int n = s.length();
long h = n;
for (int i = 0; i < n; i += 4) {
long tmp = s.charAt(i) & 0xffff;
try {
tmp = (tmp << 16) | (s.charAt(i+1) & 0xffff);
tmp = (tmp << 16) | (s.charAt(i+2) & 0xffff);
tmp = (tmp << 16) | (s.charAt(i+3) & 0xffff);
} catch (IndexOutOfBoundsException ignore) {}
h += tmp;
h *= 0x7CF9ADC6FE4A7653L;
h ^= h >>> 37;
h *= 0xC25D3F49433E7607L;
h ^= h >>> 43;
}
return h;
}
}

View File

@ -30,7 +30,6 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Pair;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -45,6 +44,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import org.petero.droidfish.ColorTheme;
import org.petero.droidfish.DroidFish;
import org.petero.droidfish.DroidFishApp;
import org.petero.droidfish.ObjectCache;
import org.petero.droidfish.R;
@ -74,6 +74,7 @@ public abstract class EditPGN extends AppCompatActivity {
private String lastFileName = "";
private long lastModTime = -1;
private boolean useRegExp = false;
private boolean backup = false; // If true, backup PGN games before overwriting
private Thread workThread = null;
private boolean canceled = false;
@ -109,6 +110,7 @@ public abstract class EditPGN extends AppCompatActivity {
Intent i = getIntent();
String action = i.getAction();
String fileName = i.getStringExtra("org.petero.droidfish.pathname");
backup = i.getBooleanExtra("org.petero.droidfish.backup", false);
canceled = false;
if ("org.petero.droidfish.loadFile".equals(action)) {
pgnFile = new PGNFile(fileName);
@ -167,36 +169,37 @@ public abstract class EditPGN extends AppCompatActivity {
loadGame = false;
String token = i.getStringExtra("org.petero.droidfish.pgn");
pgnToSave = (new ObjectCache()).retrieveString(token);
boolean silent = i.getBooleanExtra("org.petero.droidfish.silent", false);
if (silent) { // Silently append to file
PGNFile pgnFile2 = new PGNFile(fileName);
pgnFile2.appendPGN(pgnToSave);
} else {
pgnFile = new PGNFile(fileName);
showDialog(PROGRESS_DIALOG);
workThread = new Thread(() -> {
if (!readFile())
return;
runOnUiThread(() -> {
if (canceled) {
setResult(RESULT_CANCELED);
finish();
} else if (gamesInFile.isEmpty()) {
pgnFile.appendPGN(pgnToSave);
finish();
} else {
showList();
}
});
pgnFile = new PGNFile(fileName);
showDialog(PROGRESS_DIALOG);
workThread = new Thread(() -> {
if (!readFile())
return;
runOnUiThread(() -> {
if (canceled) {
setResult(RESULT_CANCELED);
finish();
} else if (gamesInFile.isEmpty()) {
pgnFile.appendPGN(pgnToSave, false);
saveFileFinished();
} else {
showList();
}
});
workThread.start();
}
});
workThread.start();
} else { // Unsupported action
setResult(RESULT_CANCELED);
finish();
}
}
private void saveFileFinished() {
Intent i = new Intent();
i.putExtra("org.petero.droidfish.treeHash", Util.stringHash(pgnToSave));
setResult(RESULT_OK, i);
finish();
}
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(DroidFishApp.setLanguage(newBase, false));
@ -403,8 +406,9 @@ public abstract class EditPGN extends AppCompatActivity {
finish();
return;
}
pgnFile.replacePGN(pgnToSave, giToReplace);
finish();
doBackup(giToReplace);
pgnFile.replacePGN(pgnToSave, giToReplace, false);
saveFileFinished();
});
return builder.create();
}
@ -475,6 +479,7 @@ public abstract class EditPGN extends AppCompatActivity {
}
private void deleteGame(GameInfo gi) {
doBackup(gi);
if (pgnFile.deleteGame(gi, gamesInFile)) {
createAdapter();
String s = binding.selectGameFilter.getText().toString();
@ -485,6 +490,15 @@ public abstract class EditPGN extends AppCompatActivity {
}
}
private void doBackup(GameInfo gi) {
if (!backup)
return;
String pgn = pgnFile.readOneGame(gi);
if (pgn == null || pgn.isEmpty())
return;
DroidFish.autoSaveGame(pgn);
}
private void createAdapter() {
aa = new GameAdapter<GameInfo>(this, R.layout.select_game_list_item, gamesInFile) {
@Override

View File

@ -121,6 +121,7 @@ public class PGNFile {
public void reset() {
len = 0;
}
@Override
public String toString() {
return new String(buf, 0, len);
}
@ -421,11 +422,37 @@ public class PGNFile {
}
/** Append PGN to the end of this PGN file. */
public void appendPGN(String pgn) {
public void appendPGN(String pgn, boolean silent) {
mkDirs();
try (FileWriter fw = new FileWriter(fileName, true)) {
fw.write(pgn);
DroidFishApp.toast(R.string.game_saved, Toast.LENGTH_SHORT);
if (!silent)
DroidFishApp.toast(R.string.game_saved, Toast.LENGTH_SHORT);
} catch (IOException e) {
DroidFishApp.toast(R.string.failed_to_save_game, Toast.LENGTH_SHORT);
}
}
/** Save a PGN game first in the file and remove games at the end of the file
* to enforce a maximum number of games in the auto-save file. */
public void autoSave(String pgn) {
final int maxAutoSaveGames = 20;
try {
if (!fileName.exists()) {
appendPGN(pgn, true);
} else {
ArrayList<GameInfo> gamesInFile = getGameInfo(null, null);
for (int i = gamesInFile.size() - 1; i >= 0; i--) {
GameInfo gi = gamesInFile.get(i);
String oldGame = readOneGame(gi);
if (pgn.equals(oldGame))
deleteGame(gi, gamesInFile);
}
while (gamesInFile.size() > maxAutoSaveGames - 1)
deleteGame(gamesInFile.get(gamesInFile.size() - 1), gamesInFile);
GameInfo gi = new GameInfo().setNull(0);
replacePGN(pgn, gi, true);
}
} catch (IOException e) {
DroidFishApp.toast(R.string.failed_to_save_game, Toast.LENGTH_SHORT);
}
@ -463,7 +490,7 @@ public class PGNFile {
return false;
}
void replacePGN(String pgnToSave, GameInfo gi) {
void replacePGN(String pgnToSave, GameInfo gi, boolean silent) {
try {
File tmpFile = new File(fileName + ".tmp_delete");
try (RandomAccessFile fileReader = new RandomAccessFile(fileName, "r");
@ -475,7 +502,8 @@ public class PGNFile {
}
if (!tmpFile.renameTo(fileName))
throw new IOException();
DroidFishApp.toast(R.string.game_saved, Toast.LENGTH_SHORT);
if (!silent)
DroidFishApp.toast(R.string.game_saved, Toast.LENGTH_SHORT);
} catch (IOException e) {
DroidFishApp.toast(R.string.failed_to_save_game, Toast.LENGTH_SHORT);
}
@ -494,6 +522,7 @@ public class PGNFile {
}
}
/** Delete the file. */
boolean delete() {
return fileName.delete();
}

View File

@ -49,7 +49,7 @@ import org.petero.droidfish.gamelogic.GameTree.Node;
/** The glue between the chess engine and the GUI. */
public class DroidChessController {
private DroidComputerPlayer computerPlayer = null;
private PgnToken.PgnTokenReceiver gameTextListener = null;
private PgnToken.PgnTokenReceiver gameTextListener;
private BookOptions bookOptions = new BookOptions();
private EngineOptions engineOptions = new EngineOptions();
private Game game = null;
@ -94,10 +94,25 @@ public class DroidChessController {
}
computerPlayer.queueStartEngine(searchId, engine);
searchId++;
Game oldGame = game;
game = new Game(gameTextListener, tcData);
computerPlayer.uciNewGame();
setPlayerNames(game);
updateGameMode();
game.resetModified(pgnOptions);
autoSaveOldGame(oldGame, game.treeHashSignature);
}
/** Save old game if has been modified since start/load of game and is
* not equal to the new game. */
private void autoSaveOldGame(Game oldGame, long newGameHash) {
if (oldGame == null)
return;
String pgn = oldGame.tree.toPGN(pgnOptions);
long oldGameOrigHash = oldGame.treeHashSignature;
long oldGameCurrHash = Util.stringHash(pgn);
if (oldGameCurrHash != oldGameOrigHash && oldGameCurrHash != newGameHash)
gui.autoSaveGameIfAllowed(pgn);
}
/** Start playing a new game. Should be called after newGame(). */
@ -137,7 +152,7 @@ public class DroidChessController {
}
}
public GameMode getGameMode() {
public final GameMode getGameMode() {
return gameMode;
}
@ -245,7 +260,7 @@ public class DroidChessController {
}
/** Parse a string as FEN or PGN data. */
public final synchronized void setFENOrPGN(String fenPgn) throws ChessParseError {
public final synchronized void setFENOrPGN(String fenPgn, boolean setModified) throws ChessParseError {
if (!fenPgn.isEmpty() && fenPgn.charAt(0) == '\ufeff')
fenPgn = fenPgn.substring(1); // Remove BOM
Game newGame = new Game(gameTextListener, game.timeController.tcData);
@ -260,6 +275,7 @@ public class DroidChessController {
newGame.tree.translateMoves();
}
searchId++;
Game oldGame = game;
game = newGame;
gameTextListener.clear();
updateGameMode();
@ -268,6 +284,14 @@ public class DroidChessController {
updateComputeThreads();
gui.setSelection(-1);
updateGUI();
game.resetModified(pgnOptions);
autoSaveOldGame(oldGame, game.treeHashSignature);
if (setModified)
game.treeHashSignature = oldGame.treeHashSignature;
}
public final synchronized void setLastSaveHash(long hash) {
game.treeHashSignature = hash;
}
/** True if human's turn to make a move. (True in analysis mode.) */

View File

@ -27,11 +27,13 @@ import java.util.ArrayList;
import java.util.List;
import org.petero.droidfish.PGNOptions;
import org.petero.droidfish.Util;
import org.petero.droidfish.gamelogic.GameTree.Node;
public class Game {
boolean pendingDrawOffer;
public GameTree tree;
long treeHashSignature; // Hash corresponding to "tree" when last saved to file
TimeControl timeController;
private boolean gamePaused;
/** If true, add new moves as mainline moves. */
@ -51,6 +53,8 @@ public class Game {
/** De-serialize from input stream. */
final void readFromStream(DataInputStream dis, int version) throws IOException, ChessParseError {
tree.readFromStream(dis, version);
if (version >= 4)
treeHashSignature = dis.readLong();
if (version >= 3)
timeController.readFromStream(dis, version);
updateTimeControl(true);
@ -59,9 +63,15 @@ public class Game {
/** Serialize to output stream. */
final synchronized void writeToStream(DataOutputStream dos) throws IOException {
tree.writeToStream(dos);
dos.writeLong(treeHashSignature);
timeController.writeToStream(dos);
}
/** Set game to "not modified" state. */
final void resetModified(PGNOptions options) {
treeHashSignature = Util.stringHash(tree.toPGN(options));
}
public final void setGamePaused(boolean gamePaused) {
if (gamePaused != this.gamePaused) {
this.gamePaused = gamePaused;

View File

@ -458,6 +458,15 @@ game with the selected position.
Saving a position to a FEN/EPD file has not been implemented.
## Autosave
When an action is performed that causes the current game to be discarded, the
game is automatically saved in the file `DroidFish/pgn/.autosave.pgn` before
being discarded. The autosave file has a maximum size of 20 games and the most
recently autosaved game is stored first in the file. If the number of games
becomes too large, the oldest stored game is removed from the file.
## OI File Manager
If the [*OI File Manager*](https://play.google.com/store/apps/details?id=org.openintents.filemanager)

Binary file not shown.