I did an OOP to make the 2048 game using Matplotlib visualization. The color
dictionary is for setting different colors to each number in the game.
There are two classes written here: App2048
, through which we add interactivity and we run the app, and CellObject
, which is for the cell/square that will have a value/number. Objects of CellObject
can update itself visually, if the value/number changes, through the method update
. There are n*n
CellObject
objects in the game.
The algorithm for the swipe (left/right/up/down) is done through methods in App2048
. When we swipe left, for example, we want all numbers 'move' to the left; this can be done by changing the values/numbers in the cells. But, before changing the values, we must make Python decide which direction the numbers will move. To do that, we track the movement of the cursor while the mouse button is held, then if we reach some point P where the vector from the initial point to that last point P has length at least 0.5, then Python will analyze the tendency of the direction of the vector. For example: a vector of (0.1, 1) clearly indicates 'UP' direction because it tends to move north, if the vector is (0.8, -0.1), then it indicates 'RIGHT' direction, etc.
When the condition of swiping (left/right/up/down) is met, then the app object will perform the go_left
/
go_right
/go_up
/go_down
method, in which the numbers in the cells are updated to show movement. The algorithm: we move the numbers first, without accounting the 'combined two equal numbers'. After moving, then we find adjacent pairs with same numbers and then combined them according to the direction of the swipe, and then moving the numbers again just like we did just before finding adjacent pairs. After swipe, if the values are changed then we will set a new number, 2 or 4, to a randomly picked zero valued cell thorugh add_new_value
method.
The solvable
method is to check whether or not we can still make a movement. It checks if we still have zero valued cell, or if there is at least one adjacent pair with same numbers.
The solved_or_lost
method is quite clear.
import random
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
color = {0: (0,0,0,0), 2: (0, 0.5, 0, 0.5), 4: (0, 0.5, 0, 1), 8: (0, 1, 0, 0.5), \
16: (0, 1, 0, 1), 32: (0, 0, 0.5, 0.5), 64: (0, 0, 0.5, 1), 128: (0, 0, 1, 0.5), \
256: (0, 0, 1, 1), 512: (0.75, 0, 0, 0.5), 1024: (0.75, 0, 0, 1), 2048: (1, 0, 0, 1)}
class App2048:
def __init__(self, n):
self.n = n
self.fig, self.axes = plt.subplots()
self.axes.axis('scaled')
self.axes.set_xlim(0, n+1); self.axes.set_ylim(0, n+1)
self.cells = []
for i in range(1,n+1):
row_cells = []
for j in range(1, n+1):
cell = CellObject(i, j, 0, \
self.fig, self.axes)
row_cells.append(cell)
self.cells.append(row_cells)
self.cells_2 = []
for row in self.cells:
self.cells_2.extend(row)
c1 = random.sample(self.cells_2, 1)[0]
c2 = random.sample(self.cells_2, 1)[0]
while c2 == c1:
c2 = random.sample(self.cells_2, 1)[0]
c1.value = 2; c1.update()
c2.value = 2; c2.update()
self.connect()
self.press = False
self.path = []
self.win = False
plt.show()
def on_press(self, event):
pos = event.xdata, event.ydata
if None not in pos:
self.press = True
self.path.append(pos)
def on_motion(self, event):
if self.press:
pos = event.xdata, event.ydata
if None not in pos:
self.path.append(pos)
else:
self.path.clear()
self.press = False
return None
dx = self.path[-1][0] - self.path[0][0]
dy = self.path[-1][1] - self.path[0][1]
vector_length = math.sqrt((dx**2) + (dy**2))
direction = None
if vector_length >= 0.5:
if abs(dx) > abs(dy):
if dx > 0:
direction = 'right'
elif dx < 0:
direction = 'left'
elif abs(dy) > abs(dx):
if dy > 0:
direction = 'up'
elif dy < 0:
direction = 'down'
if direction != None:
self.press = False
self.path.clear()
previous_values = self.values
if direction == 'up':
self.go_up()
elif direction == 'down':
self.go_down()
elif direction == 'right':
self.go_right()
else:
self.go_left()
if self.changed(previous_values):
self.add_new_value()
self.solved_or_lost()
def on_release(self, event):
pos = event.xdata, event.ydata
self.press = False
self.path.clear()
def solvable(self):
current_values = self.values
if 0 in current_values:
return True
else:
for i in range(self.n):
for j in range(self.n):
if j+1 <= self.n-1:
if self.cells[i][j].value == self.cells[i][j+1].value:
return True
if 0 <= j-1:
if self.cells[i][j].value == self.cells[i][j-1].value:
return True
if i+1 <= self.n-1:
if self.cells[i][j].value == self.cells[i+1][j].value:
return True
if 0 <= i-1:
if self.cells[i][j].value == self.cells[i-1][j].value:
return True
def solved_or_lost(self):
current_values = self.values
if (2048 in current_values) and (not self.win):
self.axes.set_title('WIN', color= 'blue')
self.fig.canvas.draw()
self.win = True
return True
elif self.solvable():
return False
else:
self.axes.set_title('LOSE', color = 'red')
self.fig.canvas.draw()
plt.pause(2)
plt.close()
@property
def values(self):
return [cell.value for cell in self.cells_2]
def changed(self, previous_values):
current_values = self.values
return current_values != previous_values
def add_new_value(self):
new_number = random.sample([2,2,2,2,2,2,2,4,4,4], 1)[0]
random_cell = random.sample([cell for cell in self.cells_2 if cell.value==0], \
1)[0]
random_cell.value = new_number
random_cell.update(draw=True)
def go_up(self):
for j in range(self.n):
positives = [self.cells[self.n-1-i][j].value for i in range(self.n) if self.cells[self.n-1-i][j].value > 0]
for i in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for i in range(len(positives)):
self.cells[self.n-1-i][j].value = positives[i]; self.cells[self.n-1-i][j].update(draw=False)
self.fig.canvas.draw()
for j in range(self.n):
positives = [self.cells[self.n-1-i][j] for i in range(self.n) if self.cells[self.n-1-i][j].value > 0]
for i in range(len(positives)-1):
if self.cells[self.n-1-i][j].value == self.cells[self.n-2-i][j].value:
self.cells[self.n-1-i][j].value = 2*self.cells[self.n-2-i][j].value
self.cells[self.n-2-i][j].value = 0
self.cells[self.n-1-i][j].update(draw=False); self.cells[self.n-2-i][j].update(draw=False)
positives = [self.cells[self.n-1-i][j].value for i in range(self.n) if self.cells[self.n-1-i][j].value > 0]
for i in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for i in range(len(positives)):
self.cells[self.n-1-i][j].value = positives[i]; self.cells[self.n-1-i][j].update(draw=False)
self.fig.canvas.draw()
def go_down(self):
for j in range(self.n):
positives = [self.cells[i][j].value for i in range(self.n) if self.cells[i][j].value > 0]
for i in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for i in range(len(positives)):
self.cells[i][j].value = positives[i]; self.cells[i][j].update(draw=False)
self.fig.canvas.draw()
for j in range(self.n):
positives = [self.cells[i][j] for i in range(self.n) if self.cells[i][j].value != 0]
for i in range(len(positives)-1):
if self.cells[i][j].value == self.cells[i+1][j].value:
self.cells[i][j].value = 2*self.cells[i+1][j].value
self.cells[i+1][j].value = 0
self.cells[i][j].update(draw=False); self.cells[i+1][j].update(draw=False)
positives = [self.cells[i][j].value for i in range(self.n) if self.cells[i][j].value > 0]
for i in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for i in range(len(positives)):
self.cells[i][j].value = positives[i]; self.cells[i][j].update(draw=False)
self.fig.canvas.draw()
def go_left(self):
for i in range(self.n):
positives = [self.cells[i][j].value for j in range(self.n) if self.cells[i][j].value > 0]
for j in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for j in range(len(positives)):
self.cells[i][j].value = positives[j]; self.cells[i][j].update(draw=False)
self.fig.canvas.draw()
for i in range(self.n):
positives = [self.cells[i][j] for j in range(self.n) if self.cells[i][j].value > 0]
for j in range(len(positives)-1):
if self.cells[i][j].value == self.cells[i][j+1].value:
self.cells[i][j].value = 2*self.cells[i][j+1].value
self.cells[i][j+1].value = 0
self.cells[i][j].update(draw=False); self.cells[i][j+1].update(draw=False)
positives = [self.cells[i][j].value for j in range(self.n) if self.cells[i][j].value > 0]
for j in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for j in range(len(positives)):
self.cells[i][j].value = positives[j]; self.cells[i][j].update(draw=False)
self.fig.canvas.draw()
def go_right(self):
for i in range(self.n):
positives = [self.cells[i][self.n-1-j].value for j in range(self.n) if self.cells[i][self.n-1-j].value > 0]
for j in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for j in range(len(positives)):
self.cells[i][self.n-1-j].value = positives[j]; self.cells[i][self.n-1-j].update(draw=False)
self.fig.canvas.draw()
for i in range(self.n):
positives = [self.cells[i][self.n-1-j] for j in range(self.n) if self.cells[i][self.n-1-j].value > 0]
for j in range(len(positives)-1):
if self.cells[i][self.n-1-j].value == self.cells[i][self.n-2-j].value:
self.cells[i][self.n-1-j].value = 2*self.cells[i][self.n-2-j].value
self.cells[i][self.n-2-j].value = 0
self.cells[i][self.n-1-j].update(draw=False); self.cells[i][self.n-2-j].update(draw=False)
positives = [self.cells[i][self.n-1-j].value for j in range(self.n) if self.cells[i][self.n-1-j].value > 0]
for j in range(self.n):
self.cells[i][j].value = 0; self.cells[i][j].update(draw=False)
for j in range(len(positives)):
self.cells[i][self.n-1-j].value = positives[j]; self.cells[i][self.n-1-j].update(draw=False)
self.fig.canvas.draw()
def connect(self):
self.fig.canvas.mpl_connect('button_press_event', self.on_press)
self.fig.canvas.mpl_connect('motion_notify_event', self.on_motion)
self.fig.canvas.mpl_connect('button_release_event', self.on_release)
class CellObject:
def __init__(self, row, col, value, fig, axes):
self.row = row
self.col = col
self.value = value
self.fig, self.axes = fig, axes
self.start()
def start(self):
if self.value == 0:
self.ec = (0,0,0,0.1)
self.text_color = (0,0,0,0)
else:
self.ec = (0,0,0,1)
self.text_color = (1,1,1,1)
self.fc = color[self.value]
self.center = [self.col, self.row]
self.rect = Rectangle([self.col-0.5, self.row-0.5], \
width = 1, height = 1, fc = self.fc, ec = self.ec, \
linewidth = 2)
self.text = self.axes.text(self.col, self.row, str(self.value), \
color = self.text_color, \
ha = 'center', va = 'center', \
fontweight = 'bold')
self.axes.add_patch(self.rect)
self.fig.canvas.draw()
def update(self, draw = False):
if self.value == 0:
self.ec = (0,0,0,0.1)
self.text_color = (0,0,0,0)
else:
self.ec = (0,0,0,1)
self.text_color = (1,1,1,1)
self.fc = color[min(self.value, 2048)]
self.rect.set_fc(self.fc)
self.rect.set_ec(self.ec)
self.text.set_color(self.text_color)
self.text.set_text(str(self.value))
if draw:
self.fig.canvas.draw()
if __name__ == '__main__':
n = 8
app = App2048(n)
Tutorial video: https://youtu.be/GLTZCLn7WM0
1 Answer 1
UX
When I run the code, it launches a GUI, but it is not clear to me what I should do to play the game. Typically, 2048 uses the 4 arrow keys on the keyboard, but that did not work. It seems like it is necessary to click and drag with the mouse.
It would be helpful to add instructions to the GUI.
Naming
The variable named n
is not very descriptive:
def __init__(self, n):
I think it is the number of squares on a side in the game board.
The variable should be named something like num_of_squares
.
In CellObject
, ec
and fc
could be more descriptive as well.
In App2048
, you should describe the purpose of the cells_2
array.
You could add some comments that distinguish it from the cell
array.
Documentation
The PEP 8 style guide recommends adding docstrings for classes and functions to describe their purpose.
Tools
ruff finds several instances of this:
E702 Multiple statements on one line (semicolon)
| self.axes.set_xlim(0, n+1); self.axes.set_ylim(0, n+1)
| ^ E702
The black program can be used to automatically
separate those into individual lines. It can also help you see the structure
of those dense go_up
, go_down
, etc., functions which seem to share some
common code. Perhaps some of that code can be factored out.