Home

2048 game

2048 game

In this project we implement the 2048 game.

The Board class

We also practice working with classes. Start by creating a class Board that stores a 4x4 array of integers. A zero represents an empty cell, other numbers represent themselves.

Add a method insert(), that randomly selects an empty cell (a cell with contents zero) and set its contents to \(2\) or \(4\). A good approach is to make a list (for instance of type List[(Int,Int)]) with the coordinates of all empty cells. Then choose a random element from this list. You should set the cell to \(2\) with probability 90%, and to \(4\) with probability 10%.

You should also implement the toString method so that the board is printed nicely, like this:

scala> val b = new Board
b: Board =
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

scala> b.insert()

scala> b.insert()

scala> b
res2: Board =
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |  2 |
|    |    |    |    |
o----o----o----o----o

scala> for (i <- 1 to 7)
     |   b.insert()

scala> b
res4: Board =
o----o----o----o----o
|    |    |    |    |
|    |  2 |    |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |  2 |  2 |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |    |    |  2 |
|    |    |    |    |
o----o----o----o----o

Remember that toString simply returns a string with the entire board contents, it does not print anything itself.

Pushing

The 2048 game has four basic moves: Pushing cells left, right, up, and down. Start by implementing the left push, as a method pushLeft(). Here is what happens exactly:

The pushLeft method returns the number of points received by the left push.

Here are some examples:

scala> b
res1: Board =
o----o----o----o----o
|    |    |    |    |
|    |  2 |  2 |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |  2 |  2 |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |  2 |    |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |  2 |    |
|    |    |    |    |
o----o----o----o----o

scala> b.pushLeft()
res2: Int = 12

scala> b
res3: Board =
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |    |    |    |
|    |    |    |    |
o----o----o----o----o

scala> b.insert(); b.insert()

scala> b
res5: Board =
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |    |  2 |  2 |
|    |    |    |    |
o----o----o----o----o

scala> b.pushLeft()
res6: Int = 4

scala> b
res7: Board =
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o

The other three moves work exactly the same, only the direction is different. One way to implement this would be to write private methods for mirroring the board horizontally and for transposing it (that is, flipping rows and columns). Then you can implement the right push as mirror, left push, mirror, and the upwards push as transpose, left push, transpose, etc.

Write a method

  def push(ch: Char): Int 
that performs one of the four pushes, depending on the character (one of "l", "r", "u", "d"), and returns the number of points.

Here are some more tests:

scala> b
res7: Board =
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  4 |  2 |    |    |
|    |    |    |    |
o----o----o----o----o

scala> b.push('d')
res8: Int = 20

scala> b
res9: Board =
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  8 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  8 |  4 |    |    |
|    |    |    |    |
o----o----o----o----o

scala> b.push('d')
res10: Int = 16

scala> b
res11: Board =
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
| 16 |  4 |    |    |
|    |    |    |    |
o----o----o----o----o

scala> for (i <- 1 to 5) b.insert()

scala> b
res13: Board =
o----o----o----o----o
|    |    |    |    |
|    |    |  2 |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |  2 |  2 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|  2 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
| 16 |  4 |    |    |
|    |    |    |    |
o----o----o----o----o

scala> b.push('u')
res14: Int = 8

scala> b
res15: Board =
o----o----o----o----o
|    |    |    |    |
|  2 |  4 |  4 |  4 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
| 16 |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o

scala> b.push('r')
res16: Int = 8

scala> b
res17: Board =
o----o----o----o----o
|    |    |    |    |
|    |  2 |  4 |  8 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    | 16 |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o
|    |    |    |    |
|    |    |    |    |
|    |    |    |    |
o----o----o----o----o

A simple game

Now we can play the basic 2048 game already. Here is the game code:

def play() {
  val b = new Board
  b.insert()
  b.insert()

  while (true) {
    println(b)
    val s = readLine("What is your move: ").toLowerCase.trim
    println()
    if (s.length == 1 && ("lrud" contains s)) {
      b.push(s(0))
      b.insert()
    }
  }
}

Try it and check that all your moves are implemented correctly!

Graphical user interface

We want to go a step further and implement a graphical user interface using the cs109ui module. It should look like this:

The 2048 game display

Add a method to the Board class that draws the board on a canvas.
  def draw(canvas: BufferedImage)
The canvas should have size 500x500 pixels, each tile is 107x107 pixels, and the gap between tiles is 15 pixels.

To make your life easy, here are definitions for the colors and for the text size:

// color for the background (shows in the gaps between tiles)
val backgroundColor = new Color(0xbbada0)

// colors of the tiles
val tileColors = Map(0 -> new Color(205, 192, 180),
		     2 -> new Color(0xeee4da),
		     4 -> new Color(0xede0c8),
		     8 -> new Color(0xf2b179),
		     16 -> new Color(0xf59563),
		     32 -> new Color(0xf67c5f),
		     64 -> new Color(0xf65e3b), 
		     128 -> new Color(0xedcf72),
		     256 -> new Color(0xedcc61),
		     512 -> new Color(0xedc850),
		     1024 -> new Color(0xedc53f),
		     2048 -> new Color(0xedc22e))
// color for other tiles (4096 etc.)
val otherTileColor = new Color(0x3c3a32)

val lightTextColor = new Color(119, 110, 101)
val darkTextColor = new Color(0xf9f6f2)

// returns color for the number in the tile, depending on tile value
def textColor(tileValue: Int) = 
  if (tileValue <= 4) lightTextColor else darkTextColor

// returns font size for the number in the tile, depending on tile value
def textSize(tileValue: Int) = 
  if (tileValue <= 64) 55
  else if (tileValue <= 512) 45
  else if (tileValue <= 2048) 35
  else 30

To get the number nicely centered in the tile, you can use methods of the Graphics2D object:

  val g = canvas.createGraphics()
  // ... 
  g.setFont(new Font("sans-serif", Font.PLAIN, textSize(tile)))
  // ...
  val w = g.getFontMetrics().stringWidth(s)

Now w contains the width in pixels that will be used for drawing the string s.

Once you have the draw method working, you can run the game with the following function:

def playUI() {
  val b = new Board
  b.insert()
  b.insert()

  setTitle("2048")
  val canvas = new BufferedImage(500, 500, BufferedImage.TYPE_INT_RGB)

  while (true) {
    b.draw(canvas)
    show(canvas)

    val ch = waitKey()
    if ("lrud" contains ch) {
      b.push(ch)
      b.insert()
    }
  }
}

Finishing touches

First, add the number of points obtained so far to the title of the Window.

Second, the game is over when after a move there is no empty cell where a new tile can be inserted. Add a method isFull to your Board class, which returns true when there is no empty cell.

The game should check after the push() operation if the board is full. In that case, it should display a dialog with the message: "Game over. You achieved XXX points." Use the method showMessage of the cs109ui module to display this dialog.