java – Save and recover state of game object in Android

I am trying to save and recover the state of a simple game I made in Android. I am utilizing, the following events: onSaveInstanceState(), onBackPressed(), onCreate() and onPostCreate(). I am making my objects Serializable and Parcelable.

However, it does not successfully remember or recover in the state, before, pushing the back button or when just switching apps. Not, sure, of what I am doing wrong here. It’s a behavioral error, ie, no real error occurs.

Dice.java

public class Dice implements Parcelable {
    private int value;
    private int currentImage;
    private boolean marked = false; 
    private boolean enabled = true;
    private final Random random = new Random();

    // Mapping of drawable resources:
    private final int[] defaultDiceImages = {0,
            R.drawable.white1, R.drawable.white2,
            R.drawable.white3, R.drawable.white4,
            R.drawable.white5, R.drawable.white6
    };

    private final int[] selectedDiceImages = { 0,
            R.drawable.grey1, R.drawable.grey2, R.drawable.grey3,
            R.drawable.grey4, R.drawable.grey5, R.drawable.grey6
    };

    private final int[] redDiceImages = { 0,
            R.drawable.red1, R.drawable.red2, R.drawable.red3,
            R.drawable.red4, R.drawable.red5, R.drawable.red6
    };

    // Constructor
    Dice() { }

    Dice(int value){
        this.value = value;
        this.currentImage = defaultDiceImages[this.value];
    }

    protected Dice(Parcel in) {
        value = in.readInt();
     /*   currentImage = in.readInt();
        marked = in.readByte() != 0;
        enabled = in.readByte() != 0;
        defaultDiceImages = in.createIntArray();
        selectedDiceImages = in.createIntArray();
        redDiceImages = in.createIntArray(); */
    }

    public static final Creator<Dice> CREATOR = new Creator<Dice>() {
        @Override
        public Dice createFromParcel(Parcel in) { return new Dice(in); }

        @Override
        public Dice[] newArray(int size) { return new Dice[size]; }
    };

    public boolean IsMarked() { return marked; }

    public int GetValue() { return value; }

    public void Toss() {
        if(enabled) {
            this.value = random.nextInt(6) + 1;
            this.currentImage = defaultDiceImages[this.value];
        }
    }

    public int GetCurrentImage(){ return currentImage; }


    public void ToggleMarked() {
        marked = !marked;
        currentImage = (this.marked) ? selectedDiceImages[this.value] : defaultDiceImages[this.value];
    }

    public void ToggleEnabled() { enabled = !enabled; }

    @NonNull
    @Override
    public String toString() {
        return "Dice{" +
                "   value=" + this.value +
                ", currentImage=" + this.currentImage +
                ", marked=" + this.marked +
                ", enabled=" + this.enabled +
                ", random=" + this.random +
                '}';
    }

    @Override
    public int describeContents() { return 0; }

    @Override
    public void writeToParcel(Parcel parcel, int i)
    {
        parcel.writeInt(this.value);
    }
} 

score.java

public class Score implements Serializable {

    private int score = 0;
    private String choice;
    private static final int SCORE_LOW = 3;

    public Score() {}

    public Score(int score, String choice) {
        this.score = score;
        this.choice = choice;
    }

    public int getScore(){
        return this.score;
    }
    public void setScore(int score){
        this.score = score;
    }

    public String getChoice() {
        return choice;
    }

    public void setChoice(String choice) {
        choice = choice;
    }
}

GameRound.java

import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.Gravity;
import android.widget.Toast;

import java.io.Serializable;
import java.util.ArrayList;

public class GameRound implements Parcelable, Serializable {

    // Constants
    private final int NUMBER_OF_DICES = 6;
    private final int MAX_ALLOWED_THROWS = 3;

    private ArrayList<Dice> dices;

    public int totalDices;
    private int throwsLeft;
    private Score roundScore;
    
    public GameRound() {
        this.dices = GenerateNewDices();
        this.roundScore = new Score();
        this.throwsLeft = MAX_ALLOWED_THROWS;
    }

    protected GameRound(Parcel in) {
        this.totalDices = in.readInt();
        this.throwsLeft = in.readInt();
        this.dices = (ArrayList<Dice>)in.readSerializable();
        this.roundScore = (Score)in.readSerializable();
    }

    public ArrayList<Dice> GetDices() { return dices; }

    public ArrayList<Dice> GenerateNewDices() {
        ArrayList<Dice> tmp = new ArrayList<>(NUMBER_OF_DICES);
        if(totalDices == 0)
            totalDices = NUMBER_OF_DICES;

        for(int i = 0; i<totalDices; i++){
            tmp.add(new Dice(i));
        }
        return tmp;
    }

    public Score GetScore() { return roundScore; }

    public int GetThrowsLeftCount() { return throwsLeft; }

    public void SetRoundScore(Score score) {
        this.roundScore = score;
    }

    public void TossDices(Context context){
        if(ValidateAttempt(context)){
            int selectedCount = (int)this.dices.stream().filter(Dice::IsMarked).count();
            if(selectedCount > 0) {
                for (Dice d : this.dices) {
                    if(d.IsMarked()) {
                        d.Toss();
                        d.ToggleMarked(); // Reset
                    }
                }
            } else {
                for (Dice d : this.dices) {
                    d.Toss();
                }
            }
        }
    }

    private boolean ValidateAttempt(Context context) {
        if(throwsLeft > 0){
            --throwsLeft;
            return true;
        } else {
            Toast toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
            toast.setGravity(Gravity.TOP, 0, 200);
            toast.show();
            return false;
        }
    }

    public boolean CanPlay(){
        return throwsLeft != 0;
    }

    public void Reset() {
        for(Dice d: this.dices){
            d.Toss();
        }
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(totalDices);
        dest.writeInt(throwsLeft);
        dest.writeSerializable(dices);
        dest.writeSerializable(roundScore);
    }

    @Override
    public int describeContents() { return 0; }

    public static final Creator<GameRound> CREATOR = new Creator<GameRound>() {
        @Override
        public GameRound createFromParcel(Parcel in) { return new GameRound(in); }

        @Override
        public GameRound[] newArray(int size) { return new GameRound[size]; }
    };
}

ThirtyThrowsGame.java:

import android.os.Parcel;
import android.os.Parcelable;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;

import androidx.core.util.Pair;

public class ThirtyThrowsGame implements Parcelable, Serializable {
    public enum ScoreChoice {
        LOW(3),
        FOUR(4),
        FIVE(5),
        SIX(6),
        SEVEN(7),
        EIGHT(8),
        NINE(9),
        TEN(10),
        ELEVEN(11),
        TWELVE(12),
        ;

        private final int value;
        ScoreChoice(int n) {
            value = n;
        }
        public int getValue() {
            return value;
        }
    }

    public final int MAX_ROUNDS; 
    private int currentRound;
    private GameRound round;
    private ArrayList<Score> scores = new ArrayList<>();
    private ArrayList<ScoreChoice> availableScoreChoices = new ArrayList<>();


    public ThirtyThrowsGame(){
        this.currentRound = 1;
        MAX_ROUNDS = 10;
        this.round = new GameRound();
        this.availableScoreChoices.addAll(Arrays.asList(ScoreChoice.values()));
        Collections.reverse(availableScoreChoices);
    }

    protected ThirtyThrowsGame(Parcel in) {
        this.scores = (ArrayList<Score>)in.readSerializable();
        MAX_ROUNDS = in.readInt();
        this.currentRound = in.readInt();
        this.round = in.readParcelable(GameRound.class.getClassLoader());
        this.availableScoreChoices.addAll(Arrays.asList(ScoreChoice.values()));
        Collections.reverse(availableScoreChoices);
    }

    private static int[] DiceValuesToArray(ArrayList<Dice> dices) {
        int[] a = new int[dices.size()];
        for (int i = 0; i < dices.size(); i++) {
            a[i] = dices.get(i).GetValue();
        }
        return a;
    }

    public int calculateScoreLow(ArrayList<Dice> dices, int value) {
        int sum = 0;
        int[] a = DiceValuesToArray(dices);
        for (int j : a) {
            if (j <= value) {
                sum += j;
            }
        }
        return sum;
    }

    public int calculateScore(ArrayList<Dice> dices, int value) {
        ArrayList<Integer> matches = new ArrayList<>();
        int[] a = DiceValuesToArray(dices);
        for(int i = 0; i<a.length; ++i)
            for(int j = i + 1; j<a.length; ++j)
                if(a[i] + a[j] == value){ // <- Pairs that match sum
                    matches.add(a[i]);
                    matches.add(a[j]);
                    break;
                } else if(a[i] == value){ // <- Single candidates that match sum
                    matches.add(a[i]); break;
                }
        return matches.stream().mapToInt(Integer::intValue).sum(); // <- Return the sum of ints
    }

    public void Restart(){
        Clear();
        this.round = new GameRound();
    }

    public ArrayList<Score> GetRegistredScores() { return scores; }
    public void SaveScore() { this.scores.add(round.GetScore()); }
    public int TotalScore() {
       return this.scores.stream().mapToInt(Score::getScore).sum();
    }
    public boolean NextRound() {
        if(currentRound < MAX_ROUNDS) {
            ++currentRound;
            SaveScore();
            this.round = null;
            this.round = new GameRound();
            return false;
        }

        SaveScore();
        return true;
    }

    public int GetCurrentRound() {
        return currentRound;
    }
    
    public GameRound GetCurrentGameRound() { return round; }

    public ArrayList<ScoreChoice> GetAvailableScoreChoices(){
        return new ArrayList<>(availableScoreChoices);
    }

    public void Clear(){
        this.currentRound = 1;
        //rounds.clear();
        this.scores.clear();
        this.round = null;
    }

   
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeSerializable((scores));
        dest.writeInt(MAX_ROUNDS);
        dest.writeInt(currentRound);
        dest.writeSerializable(round);
    }

    @Override
    public int describeContents() { return 0; }

    public static final Creator<ThirtyThrowsGame> CREATOR = new Creator<ThirtyThrowsGame>() {
        @Override
        public ThirtyThrowsGame createFromParcel(Parcel in) { return new ThirtyThrowsGame(in); }

        @Override
        public ThirtyThrowsGame[] newArray(int size) { return new ThirtyThrowsGame[size]; }
  

}; }

MainActivity.java:

 // Parcelable key
    private final String STATE_GAME = "STATE_GAME";

    // Classes
    private ThirtyThrowsGame game;

    // View components
    private Button rollBtn;
    private Button collectScoreBtn;
    private TextView roundText;
    private TextView currentScoreText;
    private Spinner scoreSelectionSpinner;
    private ArrayAdapter<ThirtyThrowsGame.ScoreChoice> adapter;
    private ArrayList<ImageView> diceViews;

    // Data
    int score = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Find elements on the UI by id.
        rollBtn = findViewById(R.id.btnRoll);
        collectScoreBtn = findViewById(R.id.btnCollectScore);
        scoreSelectionSpinner = findViewById(R.id.spinner);
        roundText = findViewById(R.id.RoundText);
        currentScoreText = findViewById(R.id.CurrentScoreText);

        if (savedInstanceState != null) {
            this.game = (ThirtyThrowsGame) savedInstanceState.getSerializable(STATE_GAME);
        } else {
            this.game = new ThirtyThrowsGame();
        }

        rollBtn.setOnClickListener(e -> {
            if (game.GetCurrentGameRound().CanPlay()) {
                RefreshScene(this);
                SetScore();
            } else {
                Toast toast = Toast.makeText(
                        this,
                        "Please collect score to run next round.",
                        Toast.LENGTH_SHORT
                );
                toast.show();
            }
        });

        collectScoreBtn.setOnClickListener(e -> {
            scoreSelectionSpinner.setSelection(0);
            boolean _continue = game.NextRound();
            score = 0;
            SetDefaultDiceView();
            RefreshScene();

            if (_continue) {
                NextActivity();
            }
        });

        SetupDropDown();
        GetDiceViews();
        SetupDiceClickEventListeners();

        currentScoreText.setText(getString(R.string.score, 0));
        roundText.setText(getString(R.string.round, game.GetCurrentRound()));
        RefreshRollButtonText();
    }

    @Override
    protected void onPostCreate(@Nullable Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        if (savedInstanceState != null) {
            if (game != null) {
                score = game.GetCurrentGameRound().GetScore().getScore();
                if (score == 0) {
                    SetDefaultDiceView();
                } else {
                    UpdateImageViews();
                }
                RefreshScene();
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putSerializable(STATE_GAME, game);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onBackPressed() {
        moveTaskToBack(true);
    }

 private void SetDefaultDiceView() {
        diceViews.get(0).setImageResource(R.drawable.white1);
        diceViews.get(1).setImageResource(R.drawable.white2);
        diceViews.get(2).setImageResource(R.drawable.white3);
        diceViews.get(3).setImageResource(R.drawable.white4);
        diceViews.get(4).setImageResource(R.drawable.white5);
        diceViews.get(5).setImageResource(R.drawable.white6);
    }

private void SetupDiceClickEventListeners() {
        int index = 0;
        for (ImageView v : diceViews) {
            int finalIndex = index;
            v.setOnClickListener(e -> {
              this.game.GetCurrentGameRound().GetDices().get(finalIndex).ToggleMarked();
                diceViews.get(finalIndex).setImageResource(game.GetCurrentGameRound()
                        .GetDices().get(finalIndex).GetCurrentImage());
            });
            ++index;
        }
    }

 public void RefreshScene() {
        roundText.setText(getString(R.string.round, game.GetCurrentRound()));
        currentScoreText.setText(getString(R.string.score, game.GetCurrentGameRound().GetScore().getScore()));
        RefreshRollButtonText();
    }

    public void RefreshScene(Context context) {
        game.GetCurrentGameRound().TossDices(context);
        roundText.setText(getString(R.string.round, game.GetCurrentRound()));
        UpdateImageViews();
        RefreshRollButtonText();
    }

This is pretty much almost all of the code here. If someone, can spot the issues, please let me know. I have been struggling with this for weeks now, and it is getting deeply annoying. I need to ensure, it works when you leave the back in the background, so that it recovers the game state and continues to reflect this on the UI.

EDIT: Yes, I ran this in debug mode stepping throw the code.

Leave a Comment