Home

A sliding puzzle

A sliding puzzle

You probably know the 15-puzzle, where you start with 15 tiles in a 4x4 square, initially in a random permutation, and the goal is to arrange them in the correct order. The only allowed move is to slide a tile into the empty spot, so there are at most four legal moves: up, down, left, and right.

In today's project you are going to implement this game, but we will generalize it to grids of arbitrary size. Here is an example run:

> scala sliding.scala 3 4
 o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  7 | |  6 | |  3 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  8 | |  1 | | 10 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
       o----o o----o o----o 
       |    | |    | |    | 
       |  9 | |  2 | |  5 | 
       |    | |    | |    | 
       o----o o----o o----o 
What is your move> d

o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  7 | |  6 | |  3 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
       o----o o----o o----o 
       |    | |    | |    | 
       |  8 | |  1 | | 10 | 
       |    | |    | |    | 
       o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> left

o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  7 | |  6 | |  3 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o        o----o o----o 
|    |        |    | |    | 
|  8 |        |  1 | | 10 | 
|    |        |    | |    | 
o----o        o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> l

o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  7 | |  6 | |  3 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o        o----o 
|    | |    |        |    | 
|  8 | |  1 |        | 10 | 
|    | |    |        |    | 
o----o o----o        o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> D

o----o o----o        o----o 
|    | |    |        |    | 
|  7 | |  6 |        | 11 | 
|    | |    |        |    | 
o----o o----o        o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  8 | |  1 | |  3 | | 10 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> R

o----o        o----o o----o 
|    |        |    | |    | 
|  7 |        |  6 | | 11 | 
|    |        |    | |    | 
o----o        o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  8 | |  1 | |  3 | | 10 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> u

o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  7 | |  1 | |  6 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o        o----o o----o 
|    |        |    | |    | 
|  8 |        |  3 | | 10 | 
|    |        |    | |    | 
o----o        o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  4 | |  9 | |  2 | |  5 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
What is your move> 

We will build up the game one function at a time. Write one function, and test each function using Scala's interactive mode before you go on to the next function.

makeField

This function will create the playing field for us. It takes two arguments, and returns a two-dimensional array of integers:

def makeField(rows: Int, cols: Int): Array[Array[Int]] = {

Here are some tests:

scala> :load sliding.scala
Loading sliding.scala...
makeField: (rows: Int, cols: Int)Array[Array[Int]]

scala> makeField(2, 2)
res1: Array[Array[Int]] = Array(Array(1, 2), Array(3, 0))

scala> makeField(3, 4)
res2: Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9, 10, 11, 0))

Note that field is a two-dimensional array, or, more precisely, an array of arrays. Two create such an array, you first need to create the outer array:

  field = new Array[Array[Int]](rows)
At this point, every slot of field is null. You have to fill them in one by one:
for (row <- 0 until rows)
  field(row) = new Array[Int](cols)
Once this is done, you can use field as a two-dimensional grid. The cell at position row, col can be accessed as field(row)(col).

displayField

This function displays the grid of tiles. The number zero corresponds to the empty tile—you have to leave its spot empty. Here are some tests:

scala> :load sliding.scala
Loading sliding.scala...
makeField: (rows: Int, cols: Int)Array[Array[Int]]
displayField: (field: Array[Array[Int]])Unit

scala> val f1 = makeField(2, 3)
f1: Array[Array[Int]] = Array(Array(1, 2, 3), Array(4, 5, 0))

scala> val f2 = makeField(4, 4)
f2: Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9, 10, 11, 12), Array(13, 14, 15, 0))

scala> val f3 = makeField(3, 9)
f3: Array[Array[Int]] = Array(Array(1, 2, 3, 4, 5, 6, 7, 8, 9), Array(10, 11, 12, 13, 14, 15, 16, 17, 18), Array(19, 20, 21, 22, 23, 24, 25, 26, 0))

scala> displayField(f1)
o----o o----o o----o 
|    | |    | |    | 
|  1 | |  2 | |  3 | 
|    | |    | |    | 
o----o o----o o----o 
o----o o----o        
|    | |    |        
|  4 | |  5 |        
|    | |    |        
o----o o----o        

scala> displayField(f2)
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  1 | |  2 | |  3 | |  4 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  5 | |  6 | |  7 | |  8 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  9 | | 10 | | 11 | | 12 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o        
|    | |    | |    |        
| 13 | | 14 | | 15 |        
|    | |    | |    |        
o----o o----o o----o        

scala> displayField(f3)
o----o o----o o----o o----o o----o o----o o----o o----o o----o 
|    | |    | |    | |    | |    | |    | |    | |    | |    | 
|  1 | |  2 | |  3 | |  4 | |  5 | |  6 | |  7 | |  8 | |  9 | 
|    | |    | |    | |    | |    | |    | |    | |    | |    | 
o----o o----o o----o o----o o----o o----o o----o o----o o----o 
o----o o----o o----o o----o o----o o----o o----o o----o o----o 
|    | |    | |    | |    | |    | |    | |    | |    | |    | 
| 10 | | 11 | | 12 | | 13 | | 14 | | 15 | | 16 | | 17 | | 18 | 
|    | |    | |    | |    | |    | |    | |    | |    | |    | 
o----o o----o o----o o----o o----o o----o o----o o----o o----o 
o----o o----o o----o o----o o----o o----o o----o o----o        
|    | |    | |    | |    | |    | |    | |    | |    |        
| 19 | | 20 | | 21 | | 22 | | 23 | | 24 | | 25 | | 26 |        
|    | |    | |    | |    | |    | |    | |    | |    |        
o----o o----o o----o o----o o----o o----o o----o o----o        

findEmpty

Since we want to practice case classes, let's define a class Pos to represent a position on the field:

  case class Pos(row: Int, col: Int)

Now write a function findEmpty that takes a field and finds the position of the empty tile:

def findEmpty(field: Array[Array[Int]]): Pos = {

Here are some example tests (using the definitions above):

scala> findEmpty(f1)
res6: Pos = Pos(1,2)
scala> findEmpty(f2)
res7: Pos = Pos(3,3)
scala> findEmpty(f3)
res8: Pos = Pos(2,8)

makeMove

The most important function performs a move on the board. It takes a Pos argument which indicates the direction of the move, namely:

scala> val left = Pos(0, 1)
left: Pos = Pos(0,1)
scala> val right = Pos(0, -1)
right: Pos = Pos(0,-1)
scala> val up = Pos(1, 0)
up: Pos = Pos(1,0)
scala> val down = Pos(-1, 0)
down: Pos = Pos(-1,0)

scala> displayField(f1)
o----o o----o o----o 
|    | |    | |    | 
|  1 | |  2 | |  3 | 
|    | |    | |    | 
o----o o----o o----o 
o----o o----o        
|    | |    |        
|  4 | |  5 |        
|    | |    |        
o----o o----o        

scala> makeMove(f1, down)

scala> displayField(f1)
o----o o----o        
|    | |    |        
|  1 | |  2 |        
|    | |    |        
o----o o----o        
o----o o----o o----o 
|    | |    | |    | 
|  4 | |  5 | |  3 | 
|    | |    | |    | 
o----o o----o o----o 

scala> makeMove(f1, right)

scala> displayField(f1)
o----o        o----o 
|    |        |    | 
|  1 |        |  2 | 
|    |        |    | 
o----o        o----o 
o----o o----o o----o 
|    | |    | |    | 
|  4 | |  5 | |  3 | 
|    | |    | |    | 
o----o o----o o----o 

So makeMove uses findEmpty to find the empty spot, and then moves the tile from the neighboring spot:

def makeMove(field: Array[Array[Int]], delta: Pos) {
  val now = findEmpty(field)
  val upd = Pos(now.row + delta.row, now.col + delta.col)
  field(now.row)(now.col) = field(upd.row)(upd.col)
  field(upd.row)(upd.col) = 0
}
Of course you also have to check that position upd is really on the field.

shuffle

Now we need a function that creates a random starting configuration. We cannot simply use a random permutation of the tiles, since then we wouldn't know if the puzzle can be solved. So we start with a solved puzzle, and then perform a sequence of, say, 1000 random moves to shuffle it. To make your life easier, here is a function that does this:

def shuffle(field: Array[Array[Int]], iter: Int) {
  val moves = Array(Pos(1,0), Pos(-1,0), Pos(0,1), Pos(0,-1))
  for (i <- 1 to iter) {
    val mov = moves((math.random * 4).toInt)
    makeMove(field, mov)
  }
}
Here is a test:
scala> val f = makeField(4, 4)
f: Array[Array[Int]] = Array(Array(1, 2, 3, 4), Array(5, 6, 7, 8), Array(9, 10, 11, 12), Array(13, 14, 15, 0))

scala> shuffle(f, 1000)

scala> displayField(f)
       o----o o----o o----o 
       |    | |    | |    | 
       |  3 | |  8 | | 10 | 
       |    | |    | |    | 
       o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  9 | |  5 | | 15 | | 11 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
|  1 | | 12 | |  4 | | 14 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 
o----o o----o o----o o----o 
|    | |    | |    | |    | 
| 13 | |  7 | |  6 | |  2 | 
|    | |    | |    | |    | 
o----o o----o o----o o----o 

strToMove

Now we need a function strToMove that takes the user's input and converts it to a Pos object:

def strToMove(s: String): Pos = {
The function should look at the first non-white letter, and both uppercase and lowercase letters should work:
scala> strToMove(" left ")
res2: Pos = Pos(0,1)
scala> strToMove("  Left ")
res3: Pos = Pos(0,1)
scala> strToMove("r")
res4: Pos = Pos(0,-1)
scala> strToMove("  UP")
res5: Pos = Pos(1,0)
scala> strToMove("  DN  ")
res6: Pos = Pos(-1,0)
scala> strToMove("  XYZ ")
res7: Pos = Pos(0,0)
scala> strToMove("")
res8: Pos = Pos(0,0)
Any unrecognized string (including the empty string) results in Pos(0,0).

The main game

Here is a function to run the game based on the functions we already have:

def playGame(rows: Int, cols: Int) {
  val f = makeField(rows, cols)
  shuffle(f, 1000)
  while (true) {
    displayField(f)
    val s = readLine("What is your move> ")
    println()
    if (s == "quit")
      return
    val move = strToMove(s)
    makeMove(f, move)
  }
}
Test this function from Scala's interactive mode.

Command line arguments

Finally, use the command line arguments so that you can start the script sliding.scala with arguments for the number of rows and columns. If the script is called without arguments, then the number of rows and columns is four.

Bonus: Finishing touches

You can now improve your program by adding two more features: First, keep a counter of the number of moves, and display it in every round. Second, the game should recognized when the puzzle has been solved, and terminate automatically, printing a congratulation message to the user with the number of moves that the player used.