Implementing the 2048 game

Outline of post

Let's try to implement the 2048 game in Python, based on my experience playing it as well as the sources. We'll do this with a class, that will have the following methods :

  • an __init__ method that will initialize a board with two numbers
  • a move method that will apply a move (up, down, right or left) to the board and randomly add a new number to the board
  • a is_game_over method that determines if the game is finished or not
In [1]:
import numpy as np
from numpy import zeros

class board_game_2048():
    def __init__(self):
        self.board = zeros((4, 4), dtype=np.int)
        self.game_over = False
        
    def move(self, direction):
        pass
    
    def is_game_over(self):
        pass
In [2]:
game = board_game_2048()
In [3]:
game.board
Out[3]:
array([[0, 0, 0, 0],
       [0, 0, 0, 0],
       [0, 0, 0, 0],
       [0, 0, 0, 0]])

Randomly filling a cell of the board

So far, so good. The first thing I want to do is to fill a cell randomly with a 2 or a 4. The way to do this would be:

In [4]:
from random import randint, random

def fill_cell(board):
    i, j = (board == 0).nonzero()
    if i.size != 0:
        rnd = randint(0, i.size - 1) 
        board[i[rnd], j[rnd]] = 2 * ((random() > .9) + 1)
In [5]:
fill_cell(game.board)
game.board
Out[5]:
array([[0, 0, 0, 0],
       [0, 0, 0, 0],
       [0, 2, 0, 0],
       [0, 0, 0, 0]])

Moving a vector to the left

Next step would be to address the moving of the tiles. To do this, we'll start by moving just a single column, seen as a 4 vector and we'll implement the move to the left.

In [6]:
from numpy import array, zeros
In [7]:
def move_left(col):
    new_col = zeros((4), dtype=col.dtype)
    j = 0
    previous = None
    for i in range(col.size):
        if col[i] != 0: # number different from zero
            if previous == None:
                previous = col[i]
            else:
                if previous == col[i]:
                    new_col[j] = 2 * col[i]
                    j += 1
                    previous = None
                else:
                    new_col[j] = previous
                    j += 1
                    previous = col[i]
    if previous != None:
        new_col[j] = previous
    return new_col
In [8]:
col = array([0, 2, 2, 0])
In [9]:
print col
print move_left(col)
[0 2 2 0]
[4 0 0 0]

In the next cell, we're building a couple of test cases to check that we implemented the right algorithm.

In [10]:
for col in [array([0, 2, 2, 0]),
            array([2, 2, 2, 8]),
            array([0, 2, 2, 4]),
            array([2, 2, 2, 2]),
            array([256, 256, 2, 4]),
            array([256, 128, 64, 32]),
            array([2, 0, 2, 0])]: # the last one doesn't work yet!
    print col, '->', move_left(col)
[0 2 2 0] -> [4 0 0 0]
[2 2 2 8] -> [4 2 8 0]
[0 2 2 4] -> [4 4 0 0]
[2 2 2 2] -> [4 4 0 0]
[256 256   2   4] -> [512   2   4   0]
[256 128  64  32] -> [256 128  64  32]
[2 0 2 0] -> [4 0 0 0]

Putting it all together: moving the grid

Since this seems to work like the real game, we can now go on to the main algorithm for moving the board along the four directions. This is done using the algorithm from the previous section but taking into account the symmetries of the board: individual moves are just a couple rotations away from the move to the left we have implemented above.

In [11]:
from numpy import rot90

def move(board, direction):
    # 0: left, 1: up, 2: right, 3: down
    rotated_board = rot90(board, direction)
    cols = [rotated_board[i, :] for i in range(4)]
    new_board = array([move_left(col) for col in cols])
    return rot90(new_board, -direction)
In [14]:
game.board
Out[14]:
array([[0, 0, 0, 0],
       [0, 0, 0, 0],
       [0, 2, 0, 0],
       [0, 0, 0, 0]])
In [15]:
for i in range(4):
    print move(game.board, i), '\n' 
[[0 0 0 0]
 [0 0 0 0]
 [2 0 0 0]
 [0 0 0 0]] 

[[0 2 0 0]
 [0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]] 

[[0 0 0 0]
 [0 0 0 0]
 [0 0 0 2]
 [0 0 0 0]] 

[[0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]
 [0 2 0 0]] 

As this seems to work, we can complete the class design. At every move step, we apply the movement to the existing grid. If the returned grid is the same as before, then it's not a valid move. If the move was valid, we add a random tile.

In [20]:
def main_loop(board, direction):
    new_board = move(board, direction)
    moved = False
    if (new_board == board).all():
        # move is invalid
        pass
    else:
        moved = True
        fill_cell(new_board)
        
    return (moved, new_board)
In [24]:
main_loop(game.board, 2)
Out[24]:
(True,
 array([[0, 0, 0, 0],
       [0, 0, 0, 0],
       [0, 2, 0, 2],
       [0, 0, 0, 0]]))
In [ ]:
 

Comments