I spent a ton of time on this, and made a perfect grade on the design! (That usually NEVER happens with my teacher). So here it is on display :P
HUMAN LEVEL:
The most popular sliding block puzzle contains fifteen tiles in a four by four grid, with one slot. The game is solved when all tiles are placed in numerical order, with the blank space in the lower right corner. To achieve this state, the player attempts to slide each tile to its correct position. A common strategy is to solve the first two rows (as these are trivial to place), and then concentrate on the final two rows. Upon solving the puzzle, the player might then randomize the tiles so she can play again.
CLASS LEVEL:
I will design a sliding puzzle allowing a grid of up to row=10 by col=10. As I first attempt to design the puzzle, I think to have a Tile class with a position (row, col) and a label of row + col + 1. I then realize that a button on a grid will have these properties; there will be no need for such a class, and I will use the term “button” and “tile” interchangeably in my design. I then find that a Board class will not be necessary either, as the board is merely a double array of buttons. Indeed, I realize this puzzle is purely graphical, so I decide my implementation will not need a business layer and subsequently will not
need a Model class. I further realize that there are only two operations of the game: randomize the board and move a tile. I decide that the simplicity of this does not call for an external Controller class. Therefore, I find it is only necessary for me to have two classes: a SlidingBlock class which will display the sliding block puzzle, and a Main class which will input the number of rows and columns and create the SlidingBlock object with those values.
Class Main will take either two integer arguments in the form of Strings or no arguments. No arguments will create a default SlidingBlock object; two arguments will create a SlidingBlock with that many rows and columns. For example, “java Main 7 9″ will create a SlidingBlock with 7 rows and 9 columns.
Class SlidingBlock will have attributes for a default board size, for keeping up with the blank tile’s position, and for a grid of buttons which represent the board. Its constructor will either create a default board or one with the given parameters, and then initialize the board with initBoard().
We initialize the board by first creating and labeling each button in order (making sure the last tile is blankly labeled), and then randomizing the board with randomize().
Randomizing the board will switch the tiles randomly so that the client can play a new game.
Clicking a button will only be of consequence if that button is to the right, left, top, or bottom of the blank tile. When the client clicks one of the tiles adjacent to the blank tile, the labels of those buttons switch and the values which maintain the blank tile’s position update accordingly. If it happens that the blank tile ends up in the bottom right corner, we check the board for accuracy. The board is solved if all tiles are in numerical order. If the board is solved, then we display a congratulatory message and randomize the board for another game. We check if the board is solved with checkBoard().
COMPUTER LEVEL:
Class SlidingBlock will implement JFrame and have a GridLayout with a dimension of “row” by “col”. It will have attributes of integers DEFAULT_ROW = DEFAULT_COL = 4, OFFSET = 1, row, col, blankRow, blankCol, where blankRow and blankCol are the row and col position respectively of the blank tile. It will have a String WHITESPACE used for parsing
actionCommands. It will also have a double dimensioned array of JButtons called board. Its default constructor will assign row to DEFAULT_ROW and col to DEFAULT_COL, make a call to super() with a new GridLayout, and then initialize the board with initBoard(). Another constructor will take two integer arguments and set row and col to those values as well as call
initBoard() after creating a new GridLayout. The pseudo-code for the non-default constructor:
row = Integer.parseInt( r )
col = Integer.parseInt( c )
super( new GridLayout( row, col ) )
initBoard()
The method intiBoard() initializes the board. First it initializes the board attribute as a JButton[row][col]. Then for each row r and each col c it creates a new JButton with an action command of “$r $c” and a label of r * c + OFFSET. It puts that button in board in the correct location [r][c] and adds the actionListener for the button. The actionPerformed() will call the method respond() with the actionCommand. It then makes the last JButton the blank tile by calling setText() on board[row-1][col-1]
with a null String and setting blankRow to row-1 and blankCol to col-1. Finally, initBoard() calls randomize() to shuffle the tiles. The pseudo-code for initBoard() follows:
board = new JButton[row][col]
for r from 0 to row:
. . for c from 0 to col:
. . . . set button's actionCommand to "$r $c"
. . . . set button's text to r * col + c + OFFSET
. . . . board[r][c] = button
. . . . add actionListener to button where actionPerformed calls respond() with the actionCommand
. . . . add(button)
. . set the text of the button at board[row-1][col-1] to ""
randomize()
To randomize the board, I decide to swap the blank tile with an adjacent tile for row * col * 10 times. Choosing this random tile means choosing if the row or column will remain the same as the tiles, and then choosing whether to pick the tile before or after the blank tile on that row or column. At first glance, intuition tells me that swapping two places will mean switching two JButtons in board. Upon further examination, I realize I will be able to switch just the label and the actionCommand. However, switching the actionCommands would mean that the JButton would have an incorrect position describing it, as its
position and actionCommand are one and the same. Indeed, I realize that it does not matter what the position of the adjacent tile is with which I switch. The only tile whose position interests us is the blank tile — the relative position of the clicked tile to the blank tile is how we determine a move. So we switch the labels of the two tiles in a board for row * col * 10 times, and to deal with the blank tile, we set blankRow to the other tile’s row number, and blankCol to the other tile’s col number. The tile is blank if its label is a null String. I omit the minute detail of actually choosing random numbers as this is
language-specific, and I do not know the technical details of Java’s Random object. It strikes me that switching two tiles will be necessary when the client moves a tile, so I decide to have another method swap() which deals with this process. Swap will take two JButton objects blank and other as parameters. I decide that the methods which utilize swap() *must* send the blank tile as the first parameter. The pseudo-code for randomize():
for random number of times between 10 and 100:
. . assign blankTile as the JButton at (blankRow, blankCol)
. . pick random tile randTile adjacent to blankTile
. . swap( blankTile, randTile )
The pseudo-code for swap():
set blankRow and blankCol to the row and col of JButton other.
blank.setText(other.getText())
other.setText( "" )
The method respond() will respond every time a button is clicked. We parse the clicked tile’s actionCommand (cmd) into two integers clickedRow and clickedCol. The only time we act is if clickedRow and the blankRow are the same and their col values are off by one, or if their col values are the same and their row values are off by one. When this condition is true, we would switch the clicked tile and the blank tile with swap( board[blankRow][blankCol], board[clickedRow][clickedCol] ). Then if the blank tile is the last tile in the last position (board[row-1][col-1]), we call checkBoard() which returns a boolean value true if the game is solved or false if not. If it returns true, we tell the player that she won with a JOPtionPane. We then randomize() the board so that the client can play again if she so chooses. The pseudo-code for respond():
String [] tokens = cmd.split( WHITESPACE )
clickedRow = tokens[0]
clickedCol = tokens[1]
if ((clickedRow==blankRow && Math.abs(clickedCol-blankCol)==1)
. ||(clickedCol==blankCol && Math.abs(clickedRow-blankRow)==1)):
. . swap(board[blankRow][blankCol], board[clickedRow][clickedCol])
. . if (blankRow == row-1 && blankCol == col-1 && checkBoard()):
. . . . JOptionPane.showMessageDialog(null, "You won!")
. . . . randomize()
checkBoard() loops through board up to but not including the last tile, comparing each button’s label to the current row added to the current col plus the OFFSET (i.e. the current position). In other words, we make sure that the tile labeled “1″ is in the first position, “2″ in the second, etc. Return false if any of the compares is false. Return true when all are proven true. The pseudo-code for this method:
correct = true
for r from 0 to row (and correct):
. . for c from 0 to col (and correct):
. . . . button = board[r][c]
. . . . correct = button.getText().equals("" + (r*col+c+OFFSET))
return correct