6
\$\begingroup\$

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

toolic
14.5k5 gold badges29 silver badges203 bronze badges
asked Nov 28, 2020 at 10:40
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

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.

answered Jan 23 at 11:12
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.