Rip it up and start again

I said at the end of the last post that I didn’t like the architecture of the move validation code. It’s a bit inelegant to say the least, but I did allude to a simple fix that will make it substantially better.

Plan to throw one away; you will, anyhow.

--Eric S. Raymond
The Cathedral and the Bazaar

Large quantities of philosophy aside, if you don’t like something there’s probably something wrong with it. Even if it’s good, if you can think of a better way: do it. This project overall is pretty simple but by recognising something isn’t ideal and re-doing it when it’s not important, when the rubber hits the road you will have been through it a few times and understand the pros and cons of various approaches.

So what was up with the previous version? It built up a 64 byte array that had to be scanned and checked even though most of it (according to the rules of this game anyway) was going to be zero. More than that, I had to check every possible valid move to ensure it wouldn’t go off the board or it would throw and out of bounds exception when I tried to update the moves array.

The new approach is going to return up to an eight by three array of the valid moves, storing the x and y position and the move type of up to eight possible moves. Now if the move is off the board I don’t care. In principle it is a valid move, but it will attempt to draw outside the control which means it will be ignored. Validation built into the game already forbids us from dragging the piece off the board so we can’t take advantage of the off-board valid move. The only thing that remains is to check if the moves would land on an invalid piece and not allow it.

Copy the code below into the activity class.

//RPSChess.java
private class rpsMoveHandler implements Board.onMoveHandler { 
	private int[][] mGameState = null; 
	private HashMap mCaptures = new HashMap(); 
	
	public int[][] getValidMoves(int type, int x, int y) { 
		int[][] validMoves = new int[8][3]; 
		Log.i("activity", "request " + String.valueOf(x) + " " + String.valueOf(y)); 
		
		switch (type) { 
		case R.drawable.blackpaper: 
		case R.drawable.whitepaper: 
			validMoves[0][0] = x-1; 
			validMoves[0][1] = y-1; 
			validMoves[0][2] = getMoveType(type, x-1, y-1); 
			if (validMoves[0][2] == 1) { 
				validMoves[1][0] = x-2; 
				validMoves[1][1] = y-2;
				validMoves[1][2] = getMoveType(type, x-2, y-2);
			} 
			validMoves[2][0] = x+1; 
			validMoves[2][1] = y-1; 
			validMoves[2][2] = getMoveType(type, x+1, y-1); 
			if (validMoves[2][2] == 1) { 
				validMoves[3][0] = x+2; 
				validMoves[3][1] = y-2;
				validMoves[3][2] = getMoveType(type, x+2, y-2);
			} 
			validMoves[4][0] = x-1; 
			validMoves[4][1] = y+1; 
			validMoves[4][2] = getMoveType(type, x-1, y+1); 
			if (validMoves[4][2] == 1) { 
				validMoves[5][0] = x-2; 
				validMoves[5][1] = y+2;
				validMoves[5][2] = getMoveType(type, x-2, y+2);
			} 
			validMoves[6][0] = x+1; 
			validMoves[6][1] = y+1; 
			validMoves[6][2] = getMoveType(type, x+1, y+1); 
			if (validMoves[6][2] == 1) { 
				validMoves[7][0] = x+2; 
				validMoves[7][1] = y+2;
				validMoves[7][2] = getMoveType(type, x+2, y+2);
			} 
			break; 
		case R.drawable.blackrock:
		case R.drawable.whiterock:
			validMoves[0][0] = x-1;
			validMoves[0][1] = y;
			validMoves[0][2] = getMoveType(type, x-1, y);
			if (validMoves[0][2] == 1) {
				validMoves[1][0] = x-2;
				validMoves[1][1] = y;
				validMoves[1][2] = getMoveType(type, x-2, y);
			}
			validMoves[2][0] = x+1;
			validMoves[2][1] = y;
			validMoves[2][2] = getMoveType(type, x+1, y);
			if (validMoves[2][2] == 1) {
				validMoves[3][0] = x+2;
				validMoves[3][1] = y;
				validMoves[3][2] = getMoveType(type, x+2, y);
			} 
			validMoves[4][0] = x;
			validMoves[4][1] = y-1;
			validMoves[4][2] = getMoveType(type, x, y-1);
			if (validMoves[4][2] == 1) {
				validMoves[5][0] = x;
				validMoves[5][1] = y-2;
				validMoves[5][2] = getMoveType(type, x, y-2);
			}
			validMoves[6][0] = x;
			validMoves[6][1] = y+1;
			validMoves[6][2] = getMoveType(type, x, y+1);
			if (validMoves[6][2] == 1) {
				validMoves[7][0] = x;
				validMoves[7][1] = y+2;
				validMoves[7][2] = getMoveType(type, x, y+2);
			} 
			break;
			case R.drawable.blackscissors:
			case R.drawable.whitescissors:
				validMoves[0][0] = x-1;
				validMoves[0][1] = y-2;
				validMoves[0][2] = getMoveType(type, x-1, y-2);
				validMoves[1][0] = x+1;
				validMoves[1][1] = y-2;
				validMoves[1][2] = getMoveType(type, x+1, y-2);
				validMoves[2][0] = x+2;
				validMoves[2][1] = y-1;
				validMoves[2][2] = getMoveType(type, x+2, y-1);
				validMoves[3][0] = x+2;
				validMoves[3][1] = y+1;
				validMoves[3][2] = getMoveType(type, x+2, y+1);
				validMoves[4][0] = x+1;
				validMoves[4][1] = y+2;
				validMoves[4][2] = getMoveType(type, x+1, y+2);
				validMoves[5][0] = x-1;
				validMoves[5][1] = y+2;
				validMoves[5][2] = getMoveType(type, x-1, y+2);
				validMoves[6][0] = x-2;
				validMoves[6][1] = y+1;
				validMoves[6][2] = getMoveType(type, x-2, y+1);
				validMoves[7][0] = x-2;
				validMoves[7][1] = y-1;
				validMoves[7][2] = getMoveType(type, x-2, y-1);
			} 
		return validMoves;
	}
}

The setBoard callback is the same as it was in the previous post and just gets a reference to the board state and sets up the valid capture moves.

//RPSChess.java
public void setBoard(int[][] board) {
	mGameState = board;

	mCaptures = new HashMap();
	mCaptures.put(R.drawable.whitepaper, R.drawable.blackrock);
	mCaptures.put(R.drawable.whiterock, R.drawable.blackscissors);
	mCaptures.put(R.drawable.whitescissors, R.drawable.blackpaper);
	mCaptures.put(R.drawable.blackpaper, R.drawable.whiterock);
	mCaptures.put(R.drawable.blackrock, R.drawable.whitescissors);
	mCaptures.put(R.drawable.blackscissors, R.drawable.whitepaper);
}

The move types I’m making available are Invalid, Move and Capture. The code above relies on the move type because rock and paper can only move two squares if the one-square move in that direction is also a move. Though as discussed previously I don’t intend on making the captures display any differently from the moves it would also make that easier. The code below goes below the relevant comment above.

//RPSChess.java
private int getMoveType(int type, int x, int y) {
	int moveType = 0;
	try { 
		if (mGameState[x][y] == 0) {
			moveType = 1; 
			//move 
			} 
		else if (mGameState[x][y] == mCaptures.get(type)) {
			moveType = 2;
			//capture 
		} 
	}catch(ArrayIndexOutOfBoundsException e){ 
		// off the board, do nothing. 
	} 
	return moveType; 
}

I’m using the ArrayIndexOutOfBoundsException to detect when a piece would try to go off the board and leave that move as invalid. This will get picked up in a couple of places in the board control.

I like this code better. Much less clever. That’s right, I want the code to be dumb because that makes it easier to understand. The previous version was crammed with validation to make sure it was right, mixed in with the code that picked out correct moves.

On the board, we will need to replace the drawing of valid moves and the validation of the user’s move. First the drawValidMoves() update.


private void drawValidMoves(Canvas canvas) {
	if ((mMovingPiece != 0)&&(mMoveHandler!=null)) {
		int[][] moves = mMoveHandler.getValidMoves(mMovingPiece, mCurrentMove.mFromX, mCurrentMove.mFromY);
		for (int i = 0 ; i < moves.length ; i++){ 
			if (moves[i][2] != 0) {
				canvas.drawRect(moves[i][0] * mTileWidth , moves[i][1] * mTileHeight , (moves[i][0]+1) * mTileWidth , (moves[i][1]+1) * mTileHeight , mMovePaint);
			}
		}
	} 
}

Now instead of checking a 64 byte grid we are checking eight bytes (mMoves[i][2]) and then drawing the possible moves as needed. Not much of a change in the end, but I still prefer the logic here.

Finally, and this is the trade off, we need to update the validation code in the handleUpAction() function:


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];
		
		mHistory.push(mCurrentMove);
		if (mMoveHandler!=null) {
			int[][] moves = mMoveHandler.getValidMoves(mMovingPiece, mCurrentMove.mFromX, mCurrentMove.mFromY);
			boolean valid = false;
			
			for (int i = 0; i < moves.length; i++) {
				if ((moves[i][2] > 0) && (moves[i][0] == mCurrentMove.mToX) && (moves[i][1] == mCurrentMove.mToY)) {
					valid = true; 
					break;
				} 
			}
			mPieces[mCurrentMove.mToX][mCurrentMove.mToY] = mMovingPiece; 
			if (!valid) 
				undo(); 
		}
		else
			mPieces[mCurrentMove.mToX][mCurrentMove.mToY] = mMovingPiece; 
		mCurrentMove = new Move();
	}else
		mPieces[mCurrentMove.mToX][mCurrentMove.mToY] = mMovingPiece; 

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

As you can see, this part is a little more complicated because last time we could just ask, "is this aquare a valid move?" from the mMoves array. Now we must assume the move was invalid and then check, on average, half the array of valid moves to see if any of them match what the user is attempting. If so then we permit it, otherwise we instantly undo() the move.

With this we are finished with the board for now. The next steps are to make is easy to get at a new game in case the old game gets abandoned, but that all happens in the main activity.

On a side note, all the previous posts were coded up on a 32bit Ubuntu installation which was remarkably simple to set up. This one was coded on a new machine under 64bit Windows 7 which has proven to be almost as simpl to set up. The only major difference I found was device drivers were bundled with Linux but had to be downloaded from the manufacturer's site for Windows, and on Windows the android SDK needs to be installed to a path that doesn't contain spaces or Eclipse can have trouble finding the emulator. I've been particularly impressed with how simple hooking up a real phone to Eclipse was.

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

4 Responses to Rip it up and start again

  1. santhosh says:

    Hi Mr.Chris,

    I’m not getting the exact code for highlighting the path could u please atleast send the highlighting path code. if u send this code this would be great help for me. if u don’t want to post here please send it to my mail_id: [SNIP].

    Thanks

    [ed. Removed an email address. SPAM! Don’t let it happen to you!]

    • Chris says:

      I will update the article to try and make clear where each segment of code goes.

      Although I will be posting the whole code when it’s complete, I’m not going to email it to anyone privately. If you are having problems then so will a dozen other people, so something in the post needs fixing.

  2. kumar says:

    Hi Mr.Chris,

    I need to move the coins and highlighting using animations is it possible or not?

    • Chris says:

      There are a few options for animation covered in the Developer Guide, but I’m afraid it’s not something I’ve looked into yet. It is a topic I’m planning to visit before I’m finished with this project, but it will be a few weeks before it comes up.

Comments are closed.