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
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
game = board_game_2048()
game.board
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:
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)
fill_cell(game.board)
game.board
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.
from numpy import array, zeros
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
col = array([0, 2, 2, 0])
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.
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.
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)
game.board
array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 2, 0, 0], [0, 0, 0, 0]])
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.
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)
main_loop(game.board, 2)
(True, array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 2, 0, 2], [0, 0, 0, 0]]))