Building a 2048 Game in Python with Tkinter

Faraz

By Faraz - October 09, 2023

Learn how to code a 2048 game in Python with Tkinter. Follow our step-by-step tutorial for a fun and educational game development experience.


Building a 2048 Game in Python with Tkinter.jpg

Table of Contents

  1. Introduction
  2. Setting Up Your Development Environment
  3. Full Source Code
  4. Explanation of Source Code
  5. Conclusion
  6. Frequently Asked Questions

Introduction

In the realm of programming and game development, Python stands out as a versatile and accessible language. One of the fascinating projects you can embark on is creating your own version of the popular 2048 game. This article will guide you through the process, step by step, from setting up your development environment to coding the game logic, all the way to adding a graphical user interface (GUI) using Tkinter.

Setting Up Your Development Environment

Before we delve into the world of game development, you need to ensure your Python environment is ready for action. Here's what you should do:

1. Install Python

If you haven't already, download and install Python from the official website (https://www.python.org/downloads/). Make sure to add Python to your system's PATH during installation.

2. Install Tkinter

Tkinter is Python's standard GUI library and comes bundled with Python. There's no need to install it separately.

3. Create a New Python Project

Create a dedicated folder for your 2048 game project and open it in your favorite code editor or integrated development environment (IDE).

Full Source Code

Create 03 Python (.py) files, such as constants.py, logic.py, and main.py.

1. constants.py

Define various constants and color dictionaries for a 2048 game

SIZE = 400
GRID_LEN = 4
GRID_PADDING = 10

BACKGROUND_COLOR_GAME = "#92877d"
BACKGROUND_COLOR_CELL_EMPTY = "#9e948a"

BACKGROUND_COLOR_DICT = {
2:      "#eee4da",
4:      "#ede0c8",
8:      "#f2b179",
16:     "#f59563",
32:     "#f67c5f",
64:     "#f65e3b",
128:    "#edcf72",
256:    "#edcc61",
512:    "#edc850",
1024:   "#edc53f",
2048:   "#edc22e",
4096:   "#eee4da",
8192:   "#edc22e",
16384:  "#f2b179",
32768:  "#f59563",
65536:  "#f67c5f",
}

CELL_COLOR_DICT = {
2:      "#776e65",
4:      "#776e65",
8:      "#f9f6f2",
16:     "#f9f6f2",
32:     "#f9f6f2",
64:     "#f9f6f2",
128:    "#f9f6f2",
256:    "#f9f6f2",
512:    "#f9f6f2",
1024:   "#f9f6f2",
2048:   "#f9f6f2",
4096:   "#776e65",
8192:   "#f9f6f2",
16384:  "#776e65",
32768:  "#776e65",
65536:  "#f9f6f2",
}

FONT = ("Verdana",40,"bold")

KEY_QUIT = "Escape"
KEY_BACK = "b"

KEY_UP = "Up"
KEY_DOWN = "Down"
KEY_LEFT = "Left"
KEY_RIGHT = "Right"

KEY_UP_ALT1 = "w"
KEY_DOWN_ALT1 = "s"
KEY_LEFT_ALT1 = "a"
KEY_RIGHT_ALT1 = "d"

KEY_UP_ALT2 = "i"
KEY_DOWN_ALT2 = "k"
KEY_LEFT_ALT2 = "j"
KEY_RIGHT_ALT2 = "l"

2. logic.py

#
# CS1010FC --- Programming Methodology
#
# Mission N Solutions
#
# Note that written answers are commented out to allow us to run your
# code easily while grading your problem set.

import random
import constant as c

#######
# Task 1a #
#######

# [Marking Scheme]
# Points to note:
# Matrix elements must be equal but not identical
# 1 mark for creating the correct matrix

def new_game(n):
    matrix = []
    for i in range(n):
        matrix.append([0] * n)
    matrix = add_two(matrix)
    matrix = add_two(matrix)
    return matrix

###########
# Task 1b #
###########

# [Marking Scheme]
# Points to note:
# Must ensure that it is created on a zero entry
# 1 mark for creating the correct loop

def add_two(mat):
    a = random.randint(0, len(mat)-1)
    b = random.randint(0, len(mat)-1)
    while mat[a][b] != 0:
        a = random.randint(0, len(mat)-1)
        b = random.randint(0, len(mat)-1)
    mat[a][b] = 2
    return mat

###########
# Task 1c #
###########

# [Marking Scheme]
# Points to note:
# Matrix elements must be equal but not identical
# 0 marks for completely wrong solutions
# 1 mark for getting only one condition correct
# 2 marks for getting two of the three conditions
# 3 marks for correct checking

def game_state(mat):
    # check for win cell
    for i in range(len(mat)):
        for j in range(len(mat[0])):
            if mat[i][j] == 2048:
                return 'win'
    # check for any zero entries
    for i in range(len(mat)):
        for j in range(len(mat[0])):
            if mat[i][j] == 0:
                return 'not over'
    # check for same cells that touch each other
    for i in range(len(mat)-1):
        # intentionally reduced to check the row on the right and below
        # more elegant to use exceptions but most likely this will be their solution
        for j in range(len(mat[0])-1):
            if mat[i][j] == mat[i+1][j] or mat[i][j+1] == mat[i][j]:
                return 'not over'
    for k in range(len(mat)-1):  # to check the left/right entries on the last row
        if mat[len(mat)-1][k] == mat[len(mat)-1][k+1]:
            return 'not over'
    for j in range(len(mat)-1):  # check up/down entries on last column
        if mat[j][len(mat)-1] == mat[j+1][len(mat)-1]:
            return 'not over'
    return 'lose'

###########
# Task 2a #
###########

# [Marking Scheme]
# Points to note:
# 0 marks for completely incorrect solutions
# 1 mark for solutions that show general understanding
# 2 marks for correct solutions that work for all sizes of matrices

def reverse(mat):
    new = []
    for i in range(len(mat)):
        new.append([])
        for j in range(len(mat[0])):
            new[i].append(mat[i][len(mat[0])-j-1])
    return new

###########
# Task 2b #
###########

# [Marking Scheme]
# Points to note:
# 0 marks for completely incorrect solutions
# 1 mark for solutions that show general understanding
# 2 marks for correct solutions that work for all sizes of matrices

def transpose(mat):
    new = []
    for i in range(len(mat[0])):
        new.append([])
        for j in range(len(mat)):
            new[i].append(mat[j][i])
    return new

##########
# Task 3 #
##########

# [Marking Scheme]
# Points to note:
# The way to do movement is compress -> merge -> compress again
# Basically if they can solve one side, and use transpose and reverse correctly they should
# be able to solve the entire thing just by flipping the matrix around
# No idea how to grade this one at the moment. I have it pegged to 8 (which gives you like,
# 2 per up/down/left/right?) But if you get one correct likely to get all correct so...
# Check the down one. Reverse/transpose if ordered wrongly will give you wrong result.

def cover_up(mat):
    new = []
    for j in range(c.GRID_LEN):
        partial_new = []
        for i in range(c.GRID_LEN):
            partial_new.append(0)
        new.append(partial_new)
    done = False
    for i in range(c.GRID_LEN):
        count = 0
        for j in range(c.GRID_LEN):
            if mat[i][j] != 0:
                new[i][count] = mat[i][j]
                if j != count:
                    done = True
                count += 1
    return new, done

def merge(mat, done):
    for i in range(c.GRID_LEN):
        for j in range(c.GRID_LEN-1):
            if mat[i][j] == mat[i][j+1] and mat[i][j] != 0:
                mat[i][j] *= 2
                mat[i][j+1] = 0
                done = True
    return mat, done

def up(game):
    print("up")
    # return matrix after shifting up
    game = transpose(game)
    game, done = cover_up(game)
    game, done = merge(game, done)
    game = cover_up(game)[0]
    game = transpose(game)
    return game, done

def down(game):
    print("down")
    # return matrix after shifting down
    game = reverse(transpose(game))
    game, done = cover_up(game)
    game, done = merge(game, done)
    game = cover_up(game)[0]
    game = transpose(reverse(game))
    return game, done

def left(game):
    print("left")
    # return matrix after shifting left
    game, done = cover_up(game)
    game, done = merge(game, done)
    game = cover_up(game)[0]
    return game, done

def right(game):
    print("right")
    # return matrix after shifting right
    game = reverse(game)
    game, done = cover_up(game)
    game, done = merge(game, done)
    game = cover_up(game)[0]
    game = reverse(game)
    return game, done

3. main.py

from tkinter import Frame, Label, CENTER
import random
import logic
import constant as c

def gen():
    return random.randint(0, c.GRID_LEN - 1)

class GameGrid(Frame):
    def __init__(self):
        Frame.__init__(self)

        self.grid()
        self.master.title('2048')
        self.master.bind("", self.key_down)

        self.commands = {
            c.KEY_UP: logic.up,
            c.KEY_DOWN: logic.down,
            c.KEY_LEFT: logic.left,
            c.KEY_RIGHT: logic.right,
            c.KEY_UP_ALT1: logic.up,
            c.KEY_DOWN_ALT1: logic.down,
            c.KEY_LEFT_ALT1: logic.left,
            c.KEY_RIGHT_ALT1: logic.right,
            c.KEY_UP_ALT2: logic.up,
            c.KEY_DOWN_ALT2: logic.down,
            c.KEY_LEFT_ALT2: logic.left,
            c.KEY_RIGHT_ALT2: logic.right,
        }

        self.grid_cells = []
        self.init_grid()
        self.matrix = logic.new_game(c.GRID_LEN)
        self.history_matrixs = []
        self.update_grid_cells()

        self.mainloop()

    def init_grid(self):
        background = Frame(self, bg=c.BACKGROUND_COLOR_GAME,width=c.SIZE, height=c.SIZE)
        background.grid()

        for i in range(c.GRID_LEN):
            grid_row = []
            for j in range(c.GRID_LEN):
                cell = Frame(
                    background,
                    bg=c.BACKGROUND_COLOR_CELL_EMPTY,
                    width=c.SIZE / c.GRID_LEN,
                    height=c.SIZE / c.GRID_LEN
                )
                cell.grid(
                    row=i,
                    column=j,
                    padx=c.GRID_PADDING,
                    pady=c.GRID_PADDING
                )
                t = Label(
                    master=cell,
                    text="",
                    bg=c.BACKGROUND_COLOR_CELL_EMPTY,
                    justify=CENTER,
                    font=c.FONT,
                    width=5,
                    height=2)
                t.grid()
                grid_row.append(t)
            self.grid_cells.append(grid_row)

    def update_grid_cells(self):
        for i in range(c.GRID_LEN):
            for j in range(c.GRID_LEN):
                new_number = self.matrix[i][j]
                if new_number == 0:
                    self.grid_cells[i][j].configure(text="",bg=c.BACKGROUND_COLOR_CELL_EMPTY)
                else:
                    self.grid_cells[i][j].configure(
                        text=str(new_number),
                        bg=c.BACKGROUND_COLOR_DICT[new_number],
                        fg=c.CELL_COLOR_DICT[new_number]
                    )
        self.update_idletasks()

    def key_down(self, event):
        key = event.keysym
        print(event)
        if key == c.KEY_QUIT: exit()
        if key == c.KEY_BACK and len(self.history_matrixs) > 1:
            self.matrix = self.history_matrixs.pop()
            self.update_grid_cells()
            print('back on step total step:', len(self.history_matrixs))
        elif key in self.commands:
            self.matrix, done = self.commands[key](self.matrix)
            if done:
                self.matrix = logic.add_two(self.matrix)
                # record last move
                self.history_matrixs.append(self.matrix)
                self.update_grid_cells()
                if logic.game_state(self.matrix) == 'win':
                    self.grid_cells[1][1].configure(text="You", bg=c.BACKGROUND_COLOR_CELL_EMPTY)
                    self.grid_cells[1][2].configure(text="Win!", bg=c.BACKGROUND_COLOR_CELL_EMPTY)
                if logic.game_state(self.matrix) == 'lose':
                    self.grid_cells[1][1].configure(text="You", bg=c.BACKGROUND_COLOR_CELL_EMPTY)
                    self.grid_cells[1][2].configure(text="Lose!", bg=c.BACKGROUND_COLOR_CELL_EMPTY)

    def generate_next(self):
        index = (gen(), gen())
        while self.matrix[index[0]][index[1]] != 0:
            index = (gen(), gen())
        self.matrix[index[0]][index[1]] = 2

game_grid = GameGrid()

Explanation of Source Code

1. constants.py

i. SIZE, GRID_LEN, and GRID_PADDING: These constants define the size of the game board and grid. SIZE is set to 400, which likely represents the pixel size of the game window. GRID_LEN is set to 4, indicating that the game grid is a 4x4 grid. GRID_PADDING is set to 10, representing the padding (empty space) between grid cells.

ii. BACKGROUND_COLOR_GAME and BACKGROUND_COLOR_CELL_EMPTY: These constants define the background colors for the game board and empty grid cells, respectively. They are represented as hexadecimal color codes.

iii. BACKGROUND_COLOR_DICT and CELL_COLOR_DICT: These dictionaries define background colors and text colors for different values (powers of 2) that can appear on the grid. For example, the key-value pairs in BACKGROUND_COLOR_DICT associate a color with each possible tile value, such as 2, 4, 8, 16, and so on. Similarly, CELL_COLOR_DICT associates text colors with tile values.

iv. FONT: This constant specifies the font properties to be used for displaying text on the grid. It appears to use the "Verdana" font with a size of 40 and a bold style.

v. KEY_QUIT, KEY_BACK, KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT, KEY_UP_ALT1, KEY_DOWN_ALT1, KEY_LEFT_ALT1, KEY_RIGHT_ALT1, KEY_UP_ALT2, KEY_DOWN_ALT2, KEY_LEFT_ALT2, KEY_RIGHT_ALT2: These constants define keybindings for controlling the game. For example, "Up" corresponds to the up arrow key, "Down" corresponds to the down arrow key, and so on. These keybindings are used to move tiles in the game or perform other actions.

2. logic.py

Task 1a: Creating a New Game Board

  • The new_game function takes an integer n as input, which likely represents the size of the game board (e.g., 4x4).
  • It initializes an empty matrix called matrix with all elements set to 0.
  • Then, it calls the add_two function twice to add two random 2's to the matrix.
  • Finally, it returns the initialized game board matrix.

Task 1b: Adding a Random '2' Tile

  • The add_two function takes a matrix mat as input.
  • It generates random coordinates a and b within the bounds of the matrix using random.randint(0, len(mat)-1).
  • It repeatedly generates new random coordinates until it finds an empty cell (cell value equal to 0) in the matrix.
  • Once an empty cell is found, it places a 2 in that cell.
  • It returns the updated matrix with the new '2' tile.

Task 1c: Checking the Game State

The game_state function takes a matrix mat as input.

It checks three conditions to determine the game state:

  • It checks if any cell contains the value 2048, which would indicate a win.
  • It checks if there are any empty cells in the matrix, indicating that the game is not over.
  • It checks if there are any adjacent cells (horizontally or vertically) with the same value, indicating that the game is not over.

Depending on the conditions met, it returns 'win', 'not over', or 'lose' as the game state.

Task 2a: Reversing the Game Board

  • The reverse function takes a matrix mat as input.
  • It creates a new matrix new where each row of the input matrix is reversed.
  • It returns the reversed matrix.

Task 2b: Transposing the Game Board

  • The transpose function takes a matrix mat as input.
  • It creates a new matrix new by swapping rows and columns of the input matrix.
  • It returns the transposed matrix.

Task 3: Implementing Game Moves

Four functions (up, down, left, and right) are defined to handle game moves in different directions.

Each function represents a move in the game:

  • up shifts the matrix upwards.
  • down shifts the matrix downwards.
  • left shifts the matrix to the left.
  • right shifts the matrix to the right.

Each function performs the following steps:

  • Transpose the matrix (to convert the move into a leftward shift).
  • Call the cover_up function to move tiles as far left as possible.
  • Call the merge function to combine adjacent identical tiles.
  • Call the cover_up function again to move tiles left after merging.
  • Transpose the matrix back to its original orientation.

Each function returns the updated matrix after the move and a boolean done indicating whether any tiles were moved or merged during the move.

3. main.py

Import Statements:

  • The code imports necessary modules such as Frame, Label, CENTER from tkinter, random, and custom modules logic and constant.

gen() Function:

  • This function generates a random integer within the range [0, c.GRID_LEN - 1]. It is used to randomly select grid positions for adding new tiles (2s) in the game.

GameGrid Class:

  • This class represents the main game window and inherits from the Frame class.
  • The __init__ method initializes the game by setting up the window, key bindings, grid cells, game matrix, and starting the game loop.

Initializing the Game Grid (init_grid Method):

  • It sets up the game grid by creating a background frame and grid cells for the tiles.
  • The background frame (background) serves as the game board and is colored with the background color defined in constant.py.
  • It uses nested loops to create individual grid cells as Frame widgets, with each cell representing a tile in the game.
  • Each cell contains a Label widget (t) that will display the tile's number.
  • Grid cells are configured with appropriate colors, fonts, and dimensions.

update_grid_cells Method:

  • This method updates the displayed grid cells based on the current state of the game matrix.
  • It iterates through each cell in the grid, updating the text (number) displayed and the cell's background and text colors.
  • The colors and text are determined by the values in the game matrix and are defined in constant.py.

key_down Method:

This method handles key events when a key is pressed during the game.

It checks the pressed key (event.keysym) and performs the following actions:

  • If the "Quit" key (specified in constant.py) is pressed, the game exits.
  • If the "Back" key is pressed and there are moves in the history, it reverts to the previous game state.
  • If a valid movement key (up, down, left, or right) is pressed, it executes the corresponding move from the logic module. If the move is successful (tiles are moved or merged), it updates the grid.
  • It checks for win or lose conditions and updates the grid accordingly.
  • It keeps track of the game's history, allowing players to undo moves.

generate_next Method:

  • This method generates a new tile (with a value of 2) in a random empty cell on the grid.
  • It repeatedly selects random coordinates until an empty cell is found in the game matrix, and then it places a 2 in that cell.

Creating an Instance of GameGrid:

  • The last line of code creates an instance of the GameGrid class, initializing and starting the game.

Conclusion

Congratulations! You've successfully created a 2048 game in Python with Tkinter. You've learned game development, GUI design, and logic implementation. Feel free to customize your game further and share it with others.

Now, go ahead and enjoy your newly developed game! If you have any questions or need assistance, feel free to ask in the comments below. Happy coding!

Code by: Yangshun Tay

Frequently Asked Questions

Q1. Can I customize the appearance of my 2048 game?

Absolutely! You can personalize the game's look by modifying the GUI elements and adding your graphics.

Q2. Is it possible to change the winning tile value from "2048" to something else?

Yes, you can set a different winning condition by modifying the game logic code.

Q3. Can I make the game more challenging by increasing the board size?

Indeed, you can create a larger grid by changing the dimensions of the game board.

Q4. What resources can I use to learn more about Python and Tkinter?

There are many online tutorials and documentation available to help you master Python and Tkinter. Websites like W3Schools and Python.org are great places to start.

Q5. Where can I find additional Python game development projects for practice?

You can explore various game development courses and forums, such as GitHub, to discover and contribute to open-source game projects.

That’s a wrap!

I hope you enjoyed this article

Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee.

And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!

Thanks!
Faraz 😊

End of the article

Subscribe to my Newsletter

Get the latest posts delivered right to your inbox


Latest Post