diff --git a/.github/workflows/unlimited-undo.yml b/.github/workflows/unlimited-undo.yml new file mode 100644 index 0000000..b9f4494 --- /dev/null +++ b/.github/workflows/unlimited-undo.yml @@ -0,0 +1,38 @@ +name: Apply Unlimited Undo Patch + +on: + workflow_dispatch: # Manual trigger from GitHub UI + +permissions: + contents: write + +jobs: + apply-patch: + runs-on: ubuntu-latest + steps: + - name: Checkout code (main branch by default) + uses: actions/checkout@v4 + + - name: Set up git committer info + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Show status before patching + run: git status + + - name: Try applying unlimited_undo patch + id: apply_patch + run: | + git apply unlimited-undo.patch || (echo "Patch failed! See error above. This can happen if patch is already applied, or code changed. See details above." && git status && exit 1) + + - name: Show status after patch + run: git status + + - name: Commit and push changes + run: | + git add . + git commit -m "Apply unlimited undo support patch via workflow" || echo "No changes to commit" + git push + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java b/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java index dd44ed5..fd7a5e3 100644 --- a/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java +++ b/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java @@ -1,19 +1,19 @@ package com.tpcstld.twozerogame; import java.util.ArrayList; +import java.util.ArrayDeque; +import java.util.Deque; public class Grid { public final Tile[][] field; - public final Tile[][] undoField; private final Tile[][] bufferField; + private final Deque undoStack = new ArrayDeque<>(); public Grid(int sizeX, int sizeY) { field = new Tile[sizeX][sizeY]; - undoField = new Tile[sizeX][sizeY]; bufferField = new Tile[sizeX][sizeY]; clearGrid(); - clearUndoGrid(); } public Cell randomAvailableCell() { @@ -83,15 +83,17 @@ public void removeTile(Tile tile) { } public void saveTiles() { + Tile[][] snapshot = new Tile[bufferField.length][bufferField[0].length]; for (int xx = 0; xx < bufferField.length; xx++) { for (int yy = 0; yy < bufferField[0].length; yy++) { if (bufferField[xx][yy] == null) { - undoField[xx][yy] = null; + snapshot[xx][yy] = null; } else { - undoField[xx][yy] = new Tile(xx, yy, bufferField[xx][yy].getValue()); + snapshot[xx][yy] = new Tile(xx, yy, bufferField[xx][yy].getValue()); } } } + undoStack.push(snapshot); } public void prepareSaveTiles() { @@ -107,29 +109,49 @@ public void prepareSaveTiles() { } public void revertTiles() { - for (int xx = 0; xx < undoField.length; xx++) { - for (int yy = 0; yy < undoField[0].length; yy++) { - if (undoField[xx][yy] == null) { - field[xx][yy] = null; - } else { - field[xx][yy] = new Tile(xx, yy, undoField[xx][yy].getValue()); + if (!undoStack.isEmpty()) { + Tile[][] snapshot = undoStack.pop(); + for (int xx = 0; xx < snapshot.length; xx++) { + for (int yy = 0; yy < snapshot[0].length; yy++) { + if (snapshot[xx][yy] == null) { + field[xx][yy] = null; + } else { + field[xx][yy] = new Tile(xx, yy, snapshot[xx][yy].getValue()); + } } } } } - public void clearGrid() { - for (int xx = 0; xx < field.length; xx++) { - for (int yy = 0; yy < field[0].length; yy++) { - field[xx][yy] = null; - } + public boolean hasUndo() { + return !undoStack.isEmpty(); + } + + public int undoDepth() { + return undoStack.size(); + } + + public Tile[][][] getUndoSnapshots() { + Tile[][][] snapshots = new Tile[undoStack.size()][][]; + int i = 0; + for (Tile[][] snapshot : undoStack) { + snapshots[i++] = snapshot; } + return snapshots; + } + + public void pushUndoSnapshot(Tile[][] snapshot) { + undoStack.push(snapshot); } - private void clearUndoGrid() { + public void clearUndoStack() { + undoStack.clear(); + } + + public void clearGrid() { for (int xx = 0; xx < field.length; xx++) { for (int yy = 0; yy < field[0].length; yy++) { - undoField[xx][yy] = null; + field[xx][yy] = null; } } } diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java b/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java index b5e0f8b..cf5bec1 100644 --- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java +++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java @@ -15,15 +15,13 @@ public class MainActivity extends AppCompatActivity { - private static final String WIDTH = "width"; - private static final String HEIGHT = "height"; - private static final String SCORE = "score"; - private static final String HIGH_SCORE = "high score temp"; - private static final String UNDO_SCORE = "undo score"; - private static final String CAN_UNDO = "can undo"; - private static final String UNDO_GRID = "undo"; - private static final String GAME_STATE = "game state"; - private static final String UNDO_GAME_STATE = "undo game state"; + private static final String WIDTH = "width"; + private static final String HEIGHT = "height"; + private static final String SCORE = "score"; + private static final String HIGH_SCORE = "high score temp"; + private static final String GAME_STATE = "game state"; + private static final String UNDO_DEPTH = "undo_depth"; + // Per-level keys: "undo_score_N", "undo_game_state_N", "undo_grid_N_xx_yy" private static final String NO_LOGIN_PROMPT = "no_login_prompt"; @@ -88,31 +86,44 @@ protected void onPause() { private void save() { SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = settings.edit(); + + // --- Save live grid --- Tile[][] field = view.game.grid.field; - Tile[][] undoField = view.game.grid.undoField; editor.putInt(WIDTH, field.length); editor.putInt(HEIGHT, field.length); for (int xx = 0; xx < field.length; xx++) { for (int yy = 0; yy < field[0].length; yy++) { - if (field[xx][yy] != null) { - editor.putInt(xx + " " + yy, field[xx][yy].getValue()); - } else { - editor.putInt(xx + " " + yy, 0); - } - - if (undoField[xx][yy] != null) { - editor.putInt(UNDO_GRID + xx + " " + yy, undoField[xx][yy].getValue()); - } else { - editor.putInt(UNDO_GRID + xx + " " + yy, 0); - } + editor.putInt(xx + " " + yy, + field[xx][yy] != null ? field[xx][yy].getValue() : 0); } } + + // --- Save score and game state --- editor.putLong(SCORE, view.game.score); editor.putLong(HIGH_SCORE, view.game.highScore); - editor.putLong(UNDO_SCORE, view.game.lastScore); - editor.putBoolean(CAN_UNDO, view.game.canUndo); editor.putInt(GAME_STATE, view.game.gameState); - editor.putInt(UNDO_GAME_STATE, view.game.lastGameState); + + // --- Serialize undo stack --- + // getUndoSnapshots() returns snapshots with index 0 = top (most recent). + Tile[][][] snapshots = view.game.grid.getUndoSnapshots(); + int depth = snapshots.length; + editor.putInt(UNDO_DEPTH, depth); + + Long[] scores = view.game.scoreStack.toArray(new Long[0]); + Integer[] gameStates = view.game.gameStateStack.toArray(new Integer[0]); + + for (int i = 0; i < depth; i++) { + editor.putLong("undo_score_" + i, scores[i]); + editor.putInt("undo_game_state_" + i, gameStates[i]); + Tile[][] snap = snapshots[i]; + for (int xx = 0; xx < snap.length; xx++) { + for (int yy = 0; yy < snap[0].length; yy++) { + editor.putInt("undo_grid_" + i + "_" + xx + "_" + yy, + snap[xx][yy] != null ? snap[xx][yy].getValue() : 0); + } + } + } + editor.apply(); } @@ -122,34 +133,51 @@ protected void onResume() { } private void load() { - //Stopping all animations + // Stopping all animations view.game.aGrid.cancelAnimations(); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); - for (int xx = 0; xx < view.game.grid.field.length; xx++) { - for (int yy = 0; yy < view.game.grid.field[0].length; yy++) { + int w = view.game.grid.field.length; + int h = view.game.grid.field[0].length; + + // --- Load live grid --- + for (int xx = 0; xx < w; xx++) { + for (int yy = 0; yy < h; yy++) { int value = settings.getInt(xx + " " + yy, -1); if (value > 0) { view.game.grid.field[xx][yy] = new Tile(xx, yy, value); } else if (value == 0) { view.game.grid.field[xx][yy] = null; } - - int undoValue = settings.getInt(UNDO_GRID + xx + " " + yy, -1); - if (undoValue > 0) { - view.game.grid.undoField[xx][yy] = new Tile(xx, yy, undoValue); - } else if (value == 0) { - view.game.grid.undoField[xx][yy] = null; - } } } - view.game.score = settings.getLong(SCORE, view.game.score); + // --- Load score and game state --- + view.game.score = settings.getLong(SCORE, view.game.score); view.game.highScore = settings.getLong(HIGH_SCORE, view.game.highScore); - view.game.lastScore = settings.getLong(UNDO_SCORE, view.game.lastScore); - view.game.canUndo = settings.getBoolean(CAN_UNDO, view.game.canUndo); - view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); - view.game.lastGameState = settings.getInt(UNDO_GAME_STATE, view.game.lastGameState); + view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); + + // --- Rebuild undo stack --- + // Stacks must be cleared before rebuilding to avoid stale data on resume. + view.game.grid.clearUndoStack(); + view.game.scoreStack.clear(); + view.game.gameStateStack.clear(); + + int depth = settings.getInt(UNDO_DEPTH, 0); + // Push oldest first so that index 0 ends up on top (most recent). + for (int i = depth - 1; i >= 0; i--) { + // Rebuild tile snapshot for level i + Tile[][] snap = new Tile[w][h]; + for (int xx = 0; xx < w; xx++) { + for (int yy = 0; yy < h; yy++) { + int v = settings.getInt("undo_grid_" + i + "_" + xx + "_" + yy, -1); + snap[xx][yy] = (v > 0) ? new Tile(xx, yy, v) : null; + } + } + view.game.grid.pushUndoSnapshot(snap); + view.game.scoreStack.push(settings.getLong("undo_score_" + i, 0)); + view.game.gameStateStack.push(settings.getInt("undo_game_state_" + i, 0)); + } } private void setStatusBarColor(Window window, int color) { diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java b/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java index 9a2513e..b684d98 100644 --- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java +++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java @@ -9,7 +9,9 @@ import com.tpcstld.twozerogame.snapshot.SnapshotManager; import java.util.ArrayList; +import java.util.ArrayDeque; import java.util.Collections; +import java.util.Deque; import java.util.List; public class MainGame { @@ -31,7 +33,6 @@ public class MainGame { private static final int GAME_LOST = -1; private static final int GAME_NORMAL = 0; int gameState = GAME_NORMAL; - int lastGameState = GAME_NORMAL; private int bufferGameState = GAME_NORMAL; private static final int GAME_ENDLESS = 2; private static final int GAME_ENDLESS_WON = 3; @@ -44,12 +45,14 @@ public class MainGame { private final MainView mView; Grid grid = null; AnimationGrid aGrid; - boolean canUndo; public long score = 0; long highScore = 0; - long lastScore = 0; private long bufferScore = 0; + // Unlimited undo stacks (index 0 = most recent, i.e. top of stack) + final Deque scoreStack = new ArrayDeque<>(); + final Deque gameStateStack = new ArrayDeque<>(); + MainGame(Context context, MainView view) { mContext = context; mView = view; @@ -164,9 +167,8 @@ private void moveTile(Tile tile, Cell cell) { private void saveUndoState() { grid.saveTiles(); - canUndo = true; - lastScore = bufferScore; - lastGameState = bufferGameState; + scoreStack.push(bufferScore); + gameStateStack.push(bufferGameState); } private void prepareUndoState() { @@ -175,13 +177,16 @@ private void prepareUndoState() { bufferGameState = gameState; } + boolean canUndo() { + return grid.hasUndo(); + } + void revertUndoState() { - if (canUndo) { - canUndo = false; + if (canUndo()) { aGrid.cancelAnimations(); grid.revertTiles(); - score = lastScore; - gameState = lastGameState; + score = scoreStack.pop(); + gameState = gameStateStack.pop(); mView.refreshLastTime = true; mView.invalidate(); } diff --git a/unlimited-undo.patch b/unlimited-undo.patch new file mode 100644 index 0000000..c73132a --- /dev/null +++ b/unlimited-undo.patch @@ -0,0 +1,330 @@ +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java 2026-05-17 09:43:20.711960391 +0000 ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java 2026-05-17 09:43:55.666887918 +0000 +@@ -1,19 +1,19 @@ + package com.tpcstld.twozerogame; + + import java.util.ArrayList; ++import java.util.ArrayDeque; ++import java.util.Deque; + + public class Grid { + + public final Tile[][] field; +- public final Tile[][] undoField; + private final Tile[][] bufferField; ++ private final Deque undoStack = new ArrayDeque<>(); + + public Grid(int sizeX, int sizeY) { + field = new Tile[sizeX][sizeY]; +- undoField = new Tile[sizeX][sizeY]; + bufferField = new Tile[sizeX][sizeY]; + clearGrid(); +- clearUndoGrid(); + } + + public Cell randomAvailableCell() { +@@ -83,15 +83,17 @@ + } + + public void saveTiles() { ++ Tile[][] snapshot = new Tile[bufferField.length][bufferField[0].length]; + for (int xx = 0; xx < bufferField.length; xx++) { + for (int yy = 0; yy < bufferField[0].length; yy++) { + if (bufferField[xx][yy] == null) { +- undoField[xx][yy] = null; ++ snapshot[xx][yy] = null; + } else { +- undoField[xx][yy] = new Tile(xx, yy, bufferField[xx][yy].getValue()); ++ snapshot[xx][yy] = new Tile(xx, yy, bufferField[xx][yy].getValue()); + } + } + } ++ undoStack.push(snapshot); + } + + public void prepareSaveTiles() { +@@ -107,29 +109,49 @@ + } + + public void revertTiles() { +- for (int xx = 0; xx < undoField.length; xx++) { +- for (int yy = 0; yy < undoField[0].length; yy++) { +- if (undoField[xx][yy] == null) { +- field[xx][yy] = null; +- } else { +- field[xx][yy] = new Tile(xx, yy, undoField[xx][yy].getValue()); ++ if (!undoStack.isEmpty()) { ++ Tile[][] snapshot = undoStack.pop(); ++ for (int xx = 0; xx < snapshot.length; xx++) { ++ for (int yy = 0; yy < snapshot[0].length; yy++) { ++ if (snapshot[xx][yy] == null) { ++ field[xx][yy] = null; ++ } else { ++ field[xx][yy] = new Tile(xx, yy, snapshot[xx][yy].getValue()); ++ } + } + } + } + } + +- public void clearGrid() { +- for (int xx = 0; xx < field.length; xx++) { +- for (int yy = 0; yy < field[0].length; yy++) { +- field[xx][yy] = null; +- } ++ public boolean hasUndo() { ++ return !undoStack.isEmpty(); ++ } ++ ++ public int undoDepth() { ++ return undoStack.size(); ++ } ++ ++ public Tile[][][] getUndoSnapshots() { ++ Tile[][][] snapshots = new Tile[undoStack.size()][][]; ++ int i = 0; ++ for (Tile[][] snapshot : undoStack) { ++ snapshots[i++] = snapshot; + } ++ return snapshots; ++ } ++ ++ public void pushUndoSnapshot(Tile[][] snapshot) { ++ undoStack.push(snapshot); + } + +- private void clearUndoGrid() { ++ public void clearUndoStack() { ++ undoStack.clear(); ++ } ++ ++ public void clearGrid() { + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { +- undoField[xx][yy] = null; ++ field[xx][yy] = null; + } + } + } +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java 2026-05-17 09:43:20.712271254 +0000 ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java 2026-05-17 09:44:44.178886808 +0000 +@@ -9,7 +9,9 @@ + import com.tpcstld.twozerogame.snapshot.SnapshotManager; + + import java.util.ArrayList; ++import java.util.ArrayDeque; + import java.util.Collections; ++import java.util.Deque; + import java.util.List; + + public class MainGame { +@@ -31,7 +33,6 @@ + private static final int GAME_LOST = -1; + private static final int GAME_NORMAL = 0; + int gameState = GAME_NORMAL; +- int lastGameState = GAME_NORMAL; + private int bufferGameState = GAME_NORMAL; + private static final int GAME_ENDLESS = 2; + private static final int GAME_ENDLESS_WON = 3; +@@ -44,12 +45,14 @@ + private final MainView mView; + Grid grid = null; + AnimationGrid aGrid; +- boolean canUndo; + public long score = 0; + long highScore = 0; +- long lastScore = 0; + private long bufferScore = 0; + ++ // Unlimited undo stacks (index 0 = most recent, i.e. top of stack) ++ final Deque scoreStack = new ArrayDeque<>(); ++ final Deque gameStateStack = new ArrayDeque<>(); ++ + MainGame(Context context, MainView view) { + mContext = context; + mView = view; +@@ -164,9 +167,8 @@ + + private void saveUndoState() { + grid.saveTiles(); +- canUndo = true; +- lastScore = bufferScore; +- lastGameState = bufferGameState; ++ scoreStack.push(bufferScore); ++ gameStateStack.push(bufferGameState); + } + + private void prepareUndoState() { +@@ -175,13 +177,16 @@ + bufferGameState = gameState; + } + ++ boolean canUndo() { ++ return grid.hasUndo(); ++ } ++ + void revertUndoState() { +- if (canUndo) { +- canUndo = false; ++ if (canUndo()) { + aGrid.cancelAnimations(); + grid.revertTiles(); +- score = lastScore; +- gameState = lastGameState; ++ score = scoreStack.pop(); ++ gameState = gameStateStack.pop(); + mView.refreshLastTime = true; + mView.invalidate(); + } +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java 2026-05-17 09:43:20.712245692 +0000 ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java 2026-05-17 09:45:12.864887679 +0000 +@@ -15,15 +15,13 @@ + + public class MainActivity extends AppCompatActivity { + +- private static final String WIDTH = "width"; +- private static final String HEIGHT = "height"; +- private static final String SCORE = "score"; +- private static final String HIGH_SCORE = "high score temp"; +- private static final String UNDO_SCORE = "undo score"; +- private static final String CAN_UNDO = "can undo"; +- private static final String UNDO_GRID = "undo"; +- private static final String GAME_STATE = "game state"; +- private static final String UNDO_GAME_STATE = "undo game state"; ++ private static final String WIDTH = "width"; ++ private static final String HEIGHT = "height"; ++ private static final String SCORE = "score"; ++ private static final String HIGH_SCORE = "high score temp"; ++ private static final String GAME_STATE = "game state"; ++ private static final String UNDO_DEPTH = "undo_depth"; ++ // Per-level keys: "undo_score_N", "undo_game_state_N", "undo_grid_N_xx_yy" + + private static final String NO_LOGIN_PROMPT = "no_login_prompt"; + +@@ -88,31 +86,44 @@ + private void save() { + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = settings.edit(); ++ ++ // --- Save live grid --- + Tile[][] field = view.game.grid.field; +- Tile[][] undoField = view.game.grid.undoField; + editor.putInt(WIDTH, field.length); + editor.putInt(HEIGHT, field.length); + for (int xx = 0; xx < field.length; xx++) { + for (int yy = 0; yy < field[0].length; yy++) { +- if (field[xx][yy] != null) { +- editor.putInt(xx + " " + yy, field[xx][yy].getValue()); +- } else { +- editor.putInt(xx + " " + yy, 0); +- } +- +- if (undoField[xx][yy] != null) { +- editor.putInt(UNDO_GRID + xx + " " + yy, undoField[xx][yy].getValue()); +- } else { +- editor.putInt(UNDO_GRID + xx + " " + yy, 0); +- } ++ editor.putInt(xx + " " + yy, ++ field[xx][yy] != null ? field[xx][yy].getValue() : 0); + } + } ++ ++ // --- Save score and game state --- + editor.putLong(SCORE, view.game.score); + editor.putLong(HIGH_SCORE, view.game.highScore); +- editor.putLong(UNDO_SCORE, view.game.lastScore); +- editor.putBoolean(CAN_UNDO, view.game.canUndo); + editor.putInt(GAME_STATE, view.game.gameState); +- editor.putInt(UNDO_GAME_STATE, view.game.lastGameState); ++ ++ // --- Serialize undo stack --- ++ // getUndoSnapshots() returns snapshots with index 0 = top (most recent). ++ Tile[][][] snapshots = view.game.grid.getUndoSnapshots(); ++ int depth = snapshots.length; ++ editor.putInt(UNDO_DEPTH, depth); ++ ++ Long[] scores = view.game.scoreStack.toArray(new Long[0]); ++ Integer[] gameStates = view.game.gameStateStack.toArray(new Integer[0]); ++ ++ for (int i = 0; i < depth; i++) { ++ editor.putLong("undo_score_" + i, scores[i]); ++ editor.putInt("undo_game_state_" + i, gameStates[i]); ++ Tile[][] snap = snapshots[i]; ++ for (int xx = 0; xx < snap.length; xx++) { ++ for (int yy = 0; yy < snap[0].length; yy++) { ++ editor.putInt("undo_grid_" + i + "_" + xx + "_" + yy, ++ snap[xx][yy] != null ? snap[xx][yy].getValue() : 0); ++ } ++ } ++ } ++ + editor.apply(); + } + +@@ -122,34 +133,51 @@ + } + + private void load() { +- //Stopping all animations ++ // Stopping all animations + view.game.aGrid.cancelAnimations(); + + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); +- for (int xx = 0; xx < view.game.grid.field.length; xx++) { +- for (int yy = 0; yy < view.game.grid.field[0].length; yy++) { ++ int w = view.game.grid.field.length; ++ int h = view.game.grid.field[0].length; ++ ++ // --- Load live grid --- ++ for (int xx = 0; xx < w; xx++) { ++ for (int yy = 0; yy < h; yy++) { + int value = settings.getInt(xx + " " + yy, -1); + if (value > 0) { + view.game.grid.field[xx][yy] = new Tile(xx, yy, value); + } else if (value == 0) { + view.game.grid.field[xx][yy] = null; + } +- +- int undoValue = settings.getInt(UNDO_GRID + xx + " " + yy, -1); +- if (undoValue > 0) { +- view.game.grid.undoField[xx][yy] = new Tile(xx, yy, undoValue); +- } else if (value == 0) { +- view.game.grid.undoField[xx][yy] = null; +- } + } + } + +- view.game.score = settings.getLong(SCORE, view.game.score); ++ // --- Load score and game state --- ++ view.game.score = settings.getLong(SCORE, view.game.score); + view.game.highScore = settings.getLong(HIGH_SCORE, view.game.highScore); +- view.game.lastScore = settings.getLong(UNDO_SCORE, view.game.lastScore); +- view.game.canUndo = settings.getBoolean(CAN_UNDO, view.game.canUndo); +- view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); +- view.game.lastGameState = settings.getInt(UNDO_GAME_STATE, view.game.lastGameState); ++ view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); ++ ++ // --- Rebuild undo stack --- ++ // Stacks must be cleared before rebuilding to avoid stale data on resume. ++ view.game.grid.clearUndoStack(); ++ view.game.scoreStack.clear(); ++ view.game.gameStateStack.clear(); ++ ++ int depth = settings.getInt(UNDO_DEPTH, 0); ++ // Push oldest first so that index 0 ends up on top (most recent). ++ for (int i = depth - 1; i >= 0; i--) { ++ // Rebuild tile snapshot for level i ++ Tile[][] snap = new Tile[w][h]; ++ for (int xx = 0; xx < w; xx++) { ++ for (int yy = 0; yy < h; yy++) { ++ int v = settings.getInt("undo_grid_" + i + "_" + xx + "_" + yy, -1); ++ snap[xx][yy] = (v > 0) ? new Tile(xx, yy, v) : null; ++ } ++ } ++ view.game.grid.pushUndoSnapshot(snap); ++ view.game.scoreStack.push(settings.getLong("undo_score_" + i, 0)); ++ view.game.gameStateStack.push(settings.getInt("undo_game_state_" + i, 0)); ++ } + } + + private void setStatusBarColor(Window window, int color) { diff --git a/unlimited-undo.patch.bck b/unlimited-undo.patch.bck new file mode 100644 index 0000000..a3078cd --- /dev/null +++ b/unlimited-undo.patch.bck @@ -0,0 +1,277 @@ +diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java b/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java +index b5e0f8b..1234567 100644 +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainActivity.java +@@ -1,11 +1,16 @@ + package com.tpcstld.twozerogame; + + import android.content.SharedPreferences; ++import java.io.ByteArrayInputStream; ++import java.io.ByteArrayOutputStream; ++import java.io.ObjectInputStream; ++import java.io.ObjectOutputStream; ++import android.util.Base64; + import android.graphics.Insets; + import android.os.Build; + import android.os.Bundle; + import android.preference.PreferenceManager; + + import androidx.annotation.NonNull; + import androidx.appcompat.app.AppCompatActivity; + import android.view.KeyEvent; +@@ -21,10 +26,11 @@ + private static final String HEIGHT = "height"; + private static final String SCORE = "score"; + private static final String HIGH_SCORE = "high score temp"; + private static final String UNDO_SCORE = "undo score"; +- private static final String CAN_UNDO = "can undo"; + private static final String UNDO_GRID = "undo"; + private static final String GAME_STATE = "game state"; + private static final String UNDO_GAME_STATE = "undo game state"; ++ ++ private static final String UNDO_STACKS = "undo_stacks"; + + private static final String NO_LOGIN_PROMPT = "no_login_prompt"; +@@ -49,8 +55,10 @@ + } + setContentView(view); + + // Set status bar color + setStatusBarColor(getWindow(), getResources().getColor(R.color.status_background)); ++ // Load undo stacks from persistent storage ++ loadUndoStacks(this, view.game); + } + + @Override +@@ -82,7 +90,6 @@ + editor.putLong(SCORE, view.game.score); + editor.putLong(HIGH_SCORE, view.game.highScore); + editor.putLong(UNDO_SCORE, view.game.lastScore); +- editor.putBoolean(CAN_UNDO, view.game.canUndo); + editor.putInt(GAME_STATE, view.game.gameState); + editor.putInt(UNDO_GAME_STATE, view.game.lastGameState); + editor.apply(); +@@ -89,7 +96,9 @@ + + protected void onPause() { + super.onPause(); + save(); ++ // Save undo stacks to persistent storage ++ saveUndoStacks(this, view.game); + } + + private void load() { +@@ -112,7 +121,6 @@ + view.game.score = settings.getLong(SCORE, view.game.score); + view.game.highScore = settings.getLong(HIGH_SCORE, view.game.highScore); + view.game.lastScore = settings.getLong(UNDO_SCORE, view.game.lastScore); +- view.game.canUndo = settings.getBoolean(CAN_UNDO, view.game.canUndo); + view.game.gameState = settings.getInt(GAME_STATE, view.game.gameState); + view.game.lastGameState = settings.getInt(UNDO_GAME_STATE, view.game.lastGameState); + } +@@ -125,4 +133,43 @@ + window.setStatusBarColor(color); + } + } ++ ++ // -------- Undo Stacks persistence -------------- ++ @SuppressWarnings("unchecked") ++ private void loadUndoStacks(android.content.Context context, MainGame game) { ++ try { ++ SharedPreferences prefs = context.getSharedPreferences("undo_stack", MODE_PRIVATE); ++ String encoded = prefs.getString(UNDO_STACKS, null); ++ if (encoded != null && !encoded.isEmpty()) { ++ byte[] data = Base64.decode(encoded, Base64.DEFAULT); ++ ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); ++ game.undoGridStack = (java.util.Stack) ois.readObject(); ++ game.undoScoreStack = (java.util.Stack) ois.readObject(); ++ game.undoGameStateStack = (java.util.Stack) ois.readObject(); ++ ois.close(); ++ } ++ } catch (Exception e) { ++ e.printStackTrace(); ++ } ++ } ++ ++ private void saveUndoStacks(android.content.Context context, MainGame game) { ++ try { ++ SharedPreferences prefs = context.getSharedPreferences("undo_stack", MODE_PRIVATE); ++ SharedPreferences.Editor editor = prefs.edit(); ++ ByteArrayOutputStream baos = new ByteArrayOutputStream(); ++ ObjectOutputStream oos = new ObjectOutputStream(baos); ++ oos.writeObject(game.undoGridStack); ++ oos.writeObject(game.undoScoreStack); ++ oos.writeObject(game.undoGameStateStack); ++ oos.close(); ++ byte[] data = baos.toByteArray(); ++ String encoded = Base64.encodeToString(data, Base64.DEFAULT); ++ editor.putString(UNDO_STACKS, encoded); ++ editor.apply(); ++ baos.close(); ++ } catch (Exception e) { ++ e.printStackTrace(); ++ } ++ } + } +diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java b/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java +index 9a2513e..7654321 100644 +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/MainGame.java +@@ -1,6 +1,8 @@ + package com.tpcstld.twozerogame; + + import android.content.Context; ++import java.io.Serializable; ++import java.util.Stack; + import android.content.SharedPreferences; + import android.preference.PreferenceManager; + import androidx.annotation.NonNull; +@@ -10,9 +12,9 @@ + import com.tpcstld.twozerogame.snapshot.SnapshotManager; + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + +-public class MainGame { ++public class MainGame implements Serializable { + + static final int SPAWN_ANIMATION = -1; + static final int MOVE_ANIMATION = 0; +@@ -44,7 +46,10 @@ + private final MainView mView; + Grid grid = null; + AnimationGrid aGrid; +- boolean canUndo; ++ // Undo stacks for unlimited undo ++ public Stack undoGridStack = new Stack<>(); ++ public Stack undoScoreStack = new Stack<>(); ++ public Stack undoGameStateStack = new Stack<>(); + public long score = 0; + long highScore = 0; + long lastScore = 0; +@@ -75,8 +80,12 @@ + addStartTiles(); + mView.showHelp = firstRun(); + mView.refreshLastTime = true; + mView.resyncTime(); + mView.invalidate(); ++ // Clear undo stacks at the start of new game ++ undoGridStack.clear(); ++ undoScoreStack.clear(); ++ undoGameStateStack.clear(); + } + + private void addStartTiles() { +@@ -163,26 +172,37 @@ + tile.updatePosition(cell); + } + ++ private static final int MAX_UNDO = 100; + private void saveUndoState() { + grid.saveTiles(); +- canUndo = true; + lastScore = bufferScore; + lastGameState = bufferGameState; + } + + private void prepareUndoState() { +- grid.prepareSaveTiles(); ++ if (undoGridStack.size() >= MAX_UNDO) { ++ undoGridStack.remove(0); ++ undoScoreStack.remove(0); ++ undoGameStateStack.remove(0); ++ } ++ if (grid != null) { ++ undoGridStack.push(grid.deepCopy()); ++ } else { ++ undoGridStack.push(null); ++ } ++ undoScoreStack.push(score); ++ undoGameStateStack.push(gameState); ++ grid.prepareSaveTiles(); + bufferScore = score; + bufferGameState = gameState; + } + + void revertUndoState() { +- if (canUndo) { +- canUndo = false; ++ if (!undoGridStack.isEmpty() && !undoScoreStack.isEmpty() && !undoGameStateStack.isEmpty()) { ++ grid = undoGridStack.pop(); ++ score = undoScoreStack.pop(); ++ gameState = undoGameStateStack.pop(); + aGrid.cancelAnimations(); +- grid.revertTiles(); +- score = lastScore; +- gameState = lastGameState; + mView.refreshLastTime = true; + mView.invalidate(); + } +diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java b/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java +index dd44ed5..b6b6b6b 100644 +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/Grid.java +@@ -1,8 +1,9 @@ + package com.tpcstld.twozerogame; + + import java.util.ArrayList; ++import java.io.Serializable; + +-public class Grid { ++public class Grid implements Serializable { + + public final Tile[][] field; + public final Tile[][] undoField; +@@ -133,4 +134,23 @@ + } + } + } ++ ++ public Grid deepCopy() { ++ Grid copy = new Grid(field.length, field[0].length); ++ for (int xx = 0; xx < field.length; xx++) { ++ for (int yy = 0; yy < field[0].length; yy++) { ++ if (field[xx][yy] != null) { ++ copy.field[xx][yy] = field[xx][yy].deepCopy(); ++ } else { ++ copy.field[xx][yy] = null; ++ } ++ if (undoField[xx][yy] != null) { ++ copy.undoField[xx][yy] = undoField[xx][yy].deepCopy(); ++ } else { ++ copy.undoField[xx][yy] = null; ++ } ++ } ++ } ++ return copy; ++ } + } +diff --git a/2048/base/src/main/java/com/tpcstld/twozerogame/Tile.java b/2048/base/src/main/java/com/tpcstld/twozerogame/Tile.java +index 58c0683..b7b7b7b 100644 +--- a/2048/base/src/main/java/com/tpcstld/twozerogame/Tile.java ++++ b/2048/base/src/main/java/com/tpcstld/twozerogame/Tile.java +@@ -1,6 +1,7 @@ + package com.tpcstld.twozerogame; + ++import java.io.Serializable; +-public class Tile extends Cell { ++public class Tile extends Cell implements Serializable { + private final int value; + private Tile[] mergedFrom = null; + +@@ -31,4 +32,15 @@ + public void setMergedFrom(Tile[] tile) { + mergedFrom = tile; + } ++ ++ public Tile deepCopy() { ++ Tile t = new Tile(getX(), getY(), value); ++ if (mergedFrom != null) { ++ t.mergedFrom = new Tile[mergedFrom.length]; ++ for (int i = 0; i < mergedFrom.length; i++) { ++ t.mergedFrom[i] = (mergedFrom[i] != null) ? mergedFrom[i].deepCopy() : null; ++ } ++ } ++ return t; ++ } + }