Coming undone

Our game is starting to look the part, but when I tried to teach the game to someone new they immediately made an illegal move, taking one of my pieces in the process. The game was an immediate wash and they were not inclined to try again (not to mention the current difficulty of getting a fresh game).

The obvious answer is to permit moves to be undone, and android devices provide a back button we can usurp for that very purpose.

Trapping the undo key can be done two ways depending on the API version you are using. Since API version 5 there is a function to override on the activity class, onBackPressed(), which will be called if the back key is pressed while the activity is displayed. If you are using a previous API level (and I am) then it’s a tiny amount more complicated, but only to the extent that a generic onKeyDown() will be called with a constant indicating the key code.

//RPSChess.java
@Override 
public boolean onKeyDown(int keyCode, KeyEvent keyEvent) { 
    switch (keyCode) { 
    case KeyEvent.KEYCODE_BACK: 
	    b.undo();
	    return true; 
    } 
    return false; 
}

If the key code indicates the back button then we call undo on the board and return true. If it’s not then we return false to indicate we’ve not handled the event and something else might be interested.

Dropping a stub undo() function on the board is enough to get the app running and we can see we’ve effectively broken the back button. To make the undo function work we are going to need to record some details about what’s been going on. In fact we are going to have to be careful about defining what counts as a move so attempts to drag empty squares around the board don’t invalidate the history. First let’s build the undo function so we know what we will need to remember to undo the move.

//Board.java
//Start: We are going to delete these variables later
int mFromX = 0; 
int mFromY = 0; 
int mToX = 0; 
int mToY = 0; 
int mTakenPiece = -1; 
//End: We are going to delete these variables later

public void undo() {
	if (mTakenPiece >= 0) {
		mPieces[mFromX][mFromY] = mPieces[mToX][mToY];
		mPieces[mToX][mToY] = mTakenPiece;
		mTakenPiece = -1; 
		invalidate(); 
	} 
}

Now we can remember where a piece was moved from and to, and if a piece was taken we can reverse the move. We are going to use a negative number in the taken piece variable to indicate our one step history is invalid in case the user hammers on the back button.

Of course, we will also need to populate the history as we are playing the game, which will involve tapping into handleDownAction() and handleUpAction() functions.

//Board.java
private boolean handleDownAction(MotionEvent event) {
	int x = (int)(event.getX() / mTileWidth); 
	int y = (int)(event.getY() / mTileHeight);

	if (mPieces[x][y] != 0) {
		mFromX = x;
		mFromY = y;
		mMovingPiece = mPieces[x][y];
		mPieces[x][y] = 0; 
		return true;
	}else
		return false;
}

private boolean handleUpAction(MotionEvent event) {
	mToX = (int)(event.getX() / mTileWidth); 
	mToY = (int)(event.getY() / mTileHeight);
	
	mTakenPiece = mPieces[mToX][mToY];
	mPieces[mToX][mToY] = mMovingPiece; 
	mMovingPiece = 0;
	
	invalidate(); 
	return true;
}

The key is the return value from the handleDownAction(). If the user presses on a piece then we are making a new move so return true. If the user presses on an empty square then return false. The false return value indicates we didn’t handle the down action so we will never be sent the subsequent up action. After that it’s just a case of filling in the values required by the undo() procedure.

Making this into a complete history of the game is just a case of storing a collection of history variables. This is where we delete the variables identified above, then we will create a private class that stores the same information and use the Stack class Java provides. Undo will need to check if the history still contains Moves so it doesn’t try undoing something random.

private class Move { 
	int mFromX = 0; 
	int mFromY = 0; 
	int mToX = 0; 
	int mToY = 0; 
	int mTakenPiece = 0; 
} 
private Stack<Move> mHistory = new Stack<Move>(); 
private Move mCurrentMove = new Move(); 

public void undo() {
	if (mHistory.size() > 0) {
		Move undoMove = mHistory.pop();
		mPieces[undoMove.mFromX][undoMove.mFromY] = mPieces[undoMove.mToX][undoMove.mToY];
		mPieces[undoMove.mToX][undoMove.mToY] = undoMove.mTakenPiece;
		invalidate(); 
	} 
}

Finally we update the action handlers to store the history as needed.

private boolean handleDownAction(MotionEvent event) {
	int x = (int)(event.getX() / mTileWidth); 
	int y = (int)(event.getY() / mTileHeight);

	if (mPieces[x][y] != 0) {
		mCurrentMove.mFromX = x;
		mCurrentMove.mFromY = y;
		mMovingPiece = mPieces[x][y];
		mPieces[x][y] = 0; 
		return true;
	}else
		return false;
}

private boolean handleUpAction(MotionEvent event) {
	mCurrentMove.mToX = (int)(event.getX() / mTileWidth); 
	mCurrentMove.mToY = (int)(event.getY() / mTileHeight);
	
	if ((mCurrentMove.mFromX != mCurrentMove.mToX)||(mCurrentMove.mFromY != mCurrentMove.mToY)) {
		mCurrentMove.mTakenPiece = mPieces[mCurrentMove.mToX][mCurrentMove.mToY];
		mPieces[mCurrentMove.mToX][mCurrentMove.mToY] = mMovingPiece; 
		
		mHistory.push(mCurrentMove);
		mCurrentMove = new Move();
	}else
		mPieces[mCurrentMove.mToX][mCurrentMove.mToY] = mMovingPiece; 

	mMovingPiece = 0;
	
	invalidate(); 
	return true;
}

Note the bolded line will check if a piece has been moved, then moved back to the same square. We don’t want to fill the history with lots of non-action steps if someone is taking a long time over their move so we filter them out.

New mCurrentMove objects are created as needed but we’ll recycle them if the move is invalid. We could have simply always created one in the down action and always nulled it at the end of the up action, but we’ll come to why not later. Finally, those of you who are really paying attention will see the frowned-upon invalidate(). Everything you need to limit that to the affected area is available and is left as an exercise for the reader.

This entry was posted in android and tagged . Bookmark the permalink.