2048 is a simple grid-based numbers game. The rules of the game are described here.
For this assignment, your task is to complete the implementation of a version of the 2048 game. Since we will provide a graphical user interface for the game, your task is to implement the game logic in terms of a TwentyFortyEight
class in Python. Although the original game is played on a \(4\times4\) grid, your version should be able to have an arbitrary height and width. Since we will use grids in several assignments, you should familiarize yourself with the grid conventions we will use in this course.
We have provided the following template that contains an outline of the TwentyFortyEight
class. The signature (name and parameters) of the functions, classes, and methods in this file must remain unchanged, but you may add any additional functions, methods, or other code that you need to.
You should paste your merge
function from the previous mini-project into the template for this mini-project. OwlTest and CourseraTest will run the same tests on this function that were used in the previous mini-project. These tests are for informational purposes and will not be worth any points.
In the template, we have provided the skeleton of a TwentyFortyEight
class. You should first implement the game initialization, which consist of the following methods:
__init__(self, grid_height, grid_width)
: This method takes the height and width of the grid and creates the initial 2048 board. You should store the height and width of the grid for use in other methods and then call the reset method to create an initial grid of the proper size.reset(self)
: This method should create a grid of height x width zeros and then use the new_tile
method to add two initial tiles. This method will be called by __init__
to create the initial grid. It will also be called by the GUI to start a new game, so the point of this method is to reset any state of the game, such as the grid, so that you are ready to play again.new_tile(self)
: This method should randomly select an empty grid square (one that currently has a value of 0) if one exists and place a new tile in that square. The new tile should have the value 2 90% of the time and the value 4 10% of the time. You should implement this by selecting a tile randomly with that proportion, not by guaranteeing that every 10th tile is a 4.You will also need to implement the following helper methods, which will help you develop and test the above methods. Further, they are used by both the GUI and OwlTest.
get_grid_height(self)
: This method should return the height of the grid. It will be used by the GUI to determine the size of the board.get_grid_width(self)
: This method should return the width of the grid. It will be used by the GUI to determine the size of the board.__str__(self)
: This method should return a human readable string representing your 2048 board. You may format this string however you would like. This method will be helpful to you as you develop and debug your code and will be used by OwlTest to display your game board when there are errors.set_tile(self, row, col, value)
: This method should set the tile at position (row,col) in the grid to value
. This method will be helpful to you as you test your code with different configurations and will be used by OwlTest for the same purpose. Note that the rows of the grid are indexed from top to bottom starting at zero while the columns are indexed from left to right starting at zero.get_tile(self, row, col)
: This method should return the value of the tile at position (row,col) in the grid. This method will be used by the GUI to draw the game board and by OwlTest to check your code.You should test all of these methods as you develop them. Note, however, that your reset method will not be completely correct until after you implement the new_tile
method. You can still call new_tile
from reset
before you implement it, it will just not add any tiles. During testing, you will want to use the set_tile
method so that you can start with different board states.
You are now ready to implement the final method: move
. The move
method is where the real logic of the game goes. This method should slide all of the tiles in the given direction. The direction
argument will be one of the constants, UP
, DOWN
, LEFT
, or RIGHT
. There are many ways of implementing the move
method. Here is one approach that will help you avoid writing separate pieces of code for each direction.
For each direction, we recommend pre-computing a list of the indices for the initial tiles in that direction. Initial tiles are those whose values appear first in the list passed to the merge
function. For example, the initial tiles for the UP
direction lie along the top row of the grid and in a \(4\times4\) grid have indices [(0, 0), (0, 1), (0, 2), (0, 3)]
. Since these lists of indices will be used throughout the game, we recommend computing them once in the __init__
method and then storing them in a dictionary where the keys are the direction constants (UP
, DOWN
, LEFT
, and RIGHT
).
With this dictionary computed, the move
method can be implemented as follows. Given a direction, iterate over the list of initial tiles for that direction and perform the following three steps for each initial tile:
OFFSETS
dictionary to iterate over the entries of the associated row or column starting at the specified initial tile. Retrieve the tile values from those entries, and store them in a temporary list.merge
function to merge the tile values in this temporary list. Iterate over the entries in the row or column again and store the merged tile values back into the gridIf you have done this correctly, a single call to the move
method should slide all of the tiles in the given direction. All that remains is that you must determine if any tiles have moved. You can easily do this when you put the line back into the grid. For each element, check if it has changed and keep track of whether any tiles have changed. If so, you should add a new tile to the grid, by calling your new_tile
method. Now, you are ready to run the GUI and play 2048!
Note that you have not written any logic at this point to determine whether the user has “won” or “lost” the game. This is not required for this assignment, but you can think about how to do so and add it if you would like.
"""
Clone of 2048 game.
"""
import poc_2048_gui
import random
# Directions, DO NOT MODIFY
UP = 1
DOWN = 2
LEFT = 3
RIGHT = 4
# Offsets for computing tile indices in each direction.
# DO NOT MODIFY this dictionary.
OFFSETS = {UP: (1, 0),
DOWN: (-1, 0),
LEFT: (0, 1),
RIGHT: (0, -1)}
# Merge from the previous assignment
def merge(line):
"""
Helper function that merges a single row or column in 2048
"""
def put_zero(lst):
"""
Put all non-zero tiles towards the beginning of list.
"""
lstb = []
for dummy_i in range(0,len(lst)):
if lst[dummy_i]!=0:
lstb.append(lst[dummy_i])
if len(lstb)<len(lst):
lstb.extend([0]*(len(lst)-len(lstb)))
return lstb
def combine_pair(lst):
"""
Combine pairs of tiles.
"""
for dummy_i in range(0,len(lst)-1):
if lst[dummy_i]==lst[dummy_i+1]:
lst[dummy_i] = lst[dummy_i]*2
lst[dummy_i+1] = 0
return lst
list_1 = put_zero(line)
list_2 = combine_pair(list_1)
list_3 = put_zero(list_2)
return list_3
class TwentyFortyEight:
"""
Class to run the game logic.
"""
def __init__(self, grid_height, grid_width):
self._grid_height = grid_height
self._grid_width = grid_width
self._intial_tiles = {UP: [(0, dummy_i) for dummy_i in range(grid_width)],
DOWN: [(grid_height-1, dummy_i) for dummy_i in range(grid_width)],
LEFT: [(dummy_i, 0) for dummy_i in range(grid_height)],
RIGHT: [(dummy_i, grid_width-1) for dummy_i in range(grid_height)],
}
self.reset()
def reset(self):
"""
Reset the game so the grid is empty except for two
initial tiles.
"""
self._grid = [[ 0 for dummy_col in range(self._grid_width)]
for dummy_row in range(self._grid_height)]
if self.count_empty() > 0:
self.new_tile()
if self.count_empty() > 0:
self.new_tile()
def __str__(self):
"""
Return a string representation of the grid for debugging.
"""
return str(self._grid)
def get_grid_height(self):
"""
Get the height of the board.
"""
return self._grid_height
def get_grid_width(self):
"""
Get the width of the board.
"""
return self._grid_width
def move(self, direction):
"""
Move all tiles in the given direction and add
a new tile if any tiles moved.
"""
if direction == 1 or direction == 2:
for temp_start in self._intial_tiles[direction]:
temp_list = []
for dummy_i in range(0, self._grid_height):
temp_row = temp_start[0] + OFFSETS[direction][0]*dummy_i
temp_col = temp_start[1] + OFFSETS[direction][1]*dummy_i
temp_list.append(self.get_tile(temp_row, temp_col))
list_res = merge(temp_list)
for dummy_i in range(0, self._grid_height):
temp_row = temp_start[0] + OFFSETS[direction][0]*dummy_i
temp_col = temp_start[1] + OFFSETS[direction][1]*dummy_i
self.set_tile(temp_row, temp_col, list_res[dummy_i])
else:
for temp_start in self._intial_tiles[direction]:
temp_list = []
for dummy_i in range(0, self._grid_width):
temp_row = temp_start[0] + OFFSETS[direction][0]*dummy_i
temp_col = temp_start[1] + OFFSETS[direction][1]*dummy_i
temp_list.append(self.get_tile(temp_row, temp_col))
list_res = merge(temp_list)
for dummy_i in range(0, self._grid_width):
temp_row = temp_start[0] + OFFSETS[direction][0]*dummy_i
temp_col = temp_start[1] + OFFSETS[direction][1]*dummy_i
self.set_tile(temp_row, temp_col, list_res[dummy_i])
if self.count_empty() > 0:
self.new_tile()
def count_empty(self):
"""
Conunt empty cells before create a new tile
"""
zero_count = 0
for dummy_i in range(0, self._grid_height):
for dummy_j in range(0, self._grid_width):
if self._grid[dummy_i][dummy_j] == 0:
zero_count = zero_count + 1
return zero_count
def new_tile(self):
"""
Create a new tile in a randomly selected empty
square. The tile should be 2 90% of the time and
4 10% of the time.
"""
random_row = random.randrange(0, self._grid_height)
random_col = random.randrange(0, self._grid_width)
if self._grid[random_row][random_col] == 0 :
if int(random.random() * 100) < 90:
self.set_tile(random_row, random_col, 2)
else:
self.set_tile(random_row, random_col, 4)
else: self.new_tile()
def set_tile(self, row, col, value):
"""
Set the tile at position row, col to have the given value.
"""
self._grid[row][col] = value
def get_tile(self, row, col):
"""
Return the value of the tile at position row, col.
"""
return self._grid[row][col]
poc_2048_gui.run_gui(TwentyFortyEight(4, 4))