Game Development 39 min read

Python Game Development Tutorials: 2048, Snake, Tetris, and LianLianKan with Pygame and Tkinter

This article provides step‑by‑step Python tutorials for creating classic games—including 2048, Snake, Tetris, and LianLianKan—detailing design principles, core logic, and complete source code using Pygame and Tkinter for learners with basic Python and game‑programming knowledge.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Python Game Development Tutorials: 2048, Snake, Tetris, and LianLianKan with Pygame and Tkinter

The guide introduces four classic games implemented in Python, each suited for different skill levels and using the Pygame or Tkinter frameworks.

1. 2048

2048 is a popular number‑sliding puzzle where identical tiles merge when moved in one of four directions. The implementation uses a 4×4 two‑dimensional list to represent the board and processes moves by shifting and merging rows or columns.

Key design points include treating the board as a 2‑D list, handling left moves by compressing non‑zero values, merging equal adjacent tiles, and rotating the board to reuse the left‑move logic for other directions.

import random
import sys
import pygame
from pygame.locals import *

PIXEL = 150
SCORE_PIXEL = 100
SIZE = 4

class Map:
    def __init__(self, size):
        self.size = size
        self.score = 0
        self.map = [[0 for i in range(size)] for i in range(size)]
        self.add()
        self.add()
    def add(self):
        while True:
            p = random.randint(0, self.size * self.size - 1)
            if self.map[int(p / self.size)][int(p % self.size)] == 0:
                x = random.randint(0, 3) > 0 and 2 or 4
                self.map[int(p / self.size)][int(p % self.size)] = x
                self.score += x
                break
    def adjust(self):
        changed = False
        for a in self.map:
            b = []
            last = 0
            for v in a:
                if v != 0:
                    if v == last:
                        b.append(b.pop() << 1)
                        last = 0
                    else:
                        b.append(v)
                        last = v
            b += [0] * (self.size - len(b))
            for i in range(self.size):
                if a[i] != b[i]:
                    changed = True
            a[:] = b
        return changed
    def rotate90(self):
        self.map = [[self.map[c][r] for c in range(self.size)] for r in reversed(range(self.size))]
    def over(self):
        for r in range(self.size):
            for c in range(self.size):
                if self.map[r][c] == 0:
                    return False
        for r in range(self.size):
            for c in range(self.size - 1):
                if self.map[r][c] == self.map[r][c + 1]:
                    return False
        for r in range(self.size - 1):
            for c in range(self.size):
                if self.map[r][c] == self.map[r + 1][c]:
                    return False
        return True
    def moveLeft(self):
        if self.adjust():
            self.add()

def show(map):
    for i in range(SIZE):
        for j in range(SIZE):
            screen.blit(map.map[i][j] == 0 and block[(i + j) % 2] or block[2 + (i + j) % 2], (PIXEL * j, PIXEL * i))
            if map.map[i][j] != 0:
                map_text = map_font.render(str(map.map[i][j]), True, (106, 90, 205))
                text_rect = map_text.get_rect()
                text_rect.center = (PIXEL * j + PIXEL / 2, PIXEL * i + PIXEL / 2)
                screen.blit(map_text, text_rect)
    screen.blit(score_block, (0, PIXEL * SIZE))
    score_text = score_font.render((map.over() and "Game over with score " or "Score: ") + str(map.score), True, (106, 90, 205))
    score_rect = score_text.get_rect()
    score_rect.center = (PIXEL * SIZE / 2, PIXEL * SIZE + SCORE_PIXEL / 2)
    screen.blit(score_text, score_rect)
    pygame.display.update()

map = Map(SIZE)
pygame.init()
screen = pygame.display.set_mode((PIXEL * SIZE, PIXEL * SIZE + SCORE_PIXEL))
pygame.display.set_caption("2048")
block = [pygame.Surface((PIXEL, PIXEL)) for i in range(4)]
block[0].fill((152, 251, 152))
block[1].fill((240, 255, 255))
block[2].fill((0, 255, 127))
block[3].fill((225, 255, 255))
score_block = pygame.Surface((PIXEL * SIZE, SCORE_PIXEL))
score_block.fill((245, 245, 245))
map_font = pygame.font.Font(None, int(PIXEL * 2 / 3))
score_font = pygame.font.Font(None, int(SCORE_PIXEL * 2 / 3))
clock = pygame.time.Clock()
show(map)
while not map.over():
    clock.tick(12)
    for event in pygame.event.get():
        if event.type == QUIT:
            sys.exit()
    pressed_keys = pygame.key.get_pressed()
    if pressed_keys[K_w] or pressed_keys[K_UP]:
        map.moveUp()
    elif pressed_keys[K_s] or pressed_keys[K_DOWN]:
        map.moveDown()
    elif pressed_keys[K_a] or pressed_keys[K_LEFT]:
        map.moveLeft()
    elif pressed_keys[K_d] or pressed_keys[K_RIGHT]:
        map.moveRight()
    show(map)
pygame.time.delay(3000)

2. Snake

Snake is a classic arcade game where the player controls a growing line that must eat food while avoiding collisions with walls or itself. The implementation uses a list of segment coordinates, random food placement, and event handling for direction changes.

Key points include initializing the snake with three segments, handling movement based on a direction dictionary, increasing speed as the snake grows, and rendering the snake and food with Pygame drawing functions.

import pygame
from os import path
from sys import exit
from time import sleep
from random import choice

class Snake:
    colors = list(product([0, 64, 128, 192, 255], repeat=3))[1:-1]
    def __init__(self):
        self.map = {(x, y): 0 for x in range(32) for y in range(24)}
        self.body = [[100, 100], [120, 100], [140, 100]]
        self.head = [140, 100]
        self.food = []
        self.food_color = []
        self.moving_direction = 'right'
        self.speed = 4
        self.generate_food()
        self.game_started = False
    def check_game_status(self):
        if self.body.count(self.head) > 1:
            return True
        if self.head[0] < 0 or self.head[0] > 620 or self.head[1] < 0 or self.head[1] > 460:
            return True
        return False
    def move_head(self):
        moves = {'right': (20, 0), 'up': (0, -20), 'down': (0, 20), 'left': (-20, 0)}
        step = moves[self.moving_direction]
        self.head[0] += step[0]
        self.head[1] += step[1]
    def generate_food(self):
        self.speed = len(self.body) // 16 if len(self.body) // 16 > 4 else self.speed
        for seg in self.body:
            x, y = seg
            self.map[x // 20, y // 20] = 1
        empty_pos = [pos for pos in self.map.keys() if not self.map[pos]]
        result = choice(empty_pos)
        self.food_color = list(choice(self.colors))
        self.food = [result[0] * 20, result[1] * 20]
    def main():
        key_direction_dict = {119: 'up', 115: 'down', 97: 'left', 100: 'right', 273: 'up', 274: 'down', 276: 'left', 275: 'right'}
        fps_clock = pygame.time.Clock()
        pygame.init()
        pygame.mixer.init()
        snake = Snake()
        sound = False
        if path.exists('eat.wav'):
            sound_wav = pygame.mixer.Sound('eat.wav')
            sound = True
        title_font = pygame.font.SysFont('simsunnsimsun', 32)
        welcome_words = title_font.render('贪吃蛇', True, (0,0,0), (255,255,255))
        tips_font = pygame.font.SysFont('simsunnsimsun', 20)
        start_game_words = tips_font.render('点击开始', True, (0,0,0), (255,255,255))
        close_game_words = tips_font.render('按ESC退出', True, (0,0,0), (255,255,255))
        gameover_words = title_font.render('游戏结束', True, (205,92,92), (255,255,255))
        win_words = title_font.render('蛇很长了,你赢了!', True, (0,0,205), (255,255,255))
        screen = pygame.display.set_mode((640, 480))
        pygame.display.set_caption('贪吃蛇')
        new_direction = snake.moving_direction
        while 1:
            for event in pygame.event.get():
                if event.type == QUIT:
                    exit()
                elif event.type == KEYDOWN:
                    if event.key == 27:
                        exit()
                    if snake.game_started and event.key in key_direction_dict:
                        direction = key_direction_dict[event.key]
                        new_direction = direction_check(snake.moving_direction, direction)
                    elif not snake.game_started and event.type == pygame.MOUSEBUTTONDOWN:
                        x, y = pygame.mouse.get_pos()
                        if 213 <= x <= 422 and 304 <= y <= 342:
                            snake.game_started = True
            screen.fill((255,255,255))
            if snake.game_started:
                snake.moving_direction = new_direction
                snake.move_head()
                snake.body.append(snake.head[:])
                if snake.head == snake.food:
                    if sound:
                        sound_wav.play()
                    snake.generate_food()
                else:
                    snake.body.pop(0)
                for seg in snake.body:
                    pygame.draw.rect(screen, [0,0,0], [seg[0], seg[1], 20, 20], 0)
                pygame.draw.rect(screen, snake.food_color, [snake.food[0], snake.food[1], 20, 20], 0)
                if snake.check_game_status():
                    screen.blit(gameover_words, (241,310))
                    pygame.display.update()
                    snake = Snake()
                    new_direction = snake.moving_direction
                    sleep(3)
                elif len(snake.body) == 512:
                    screen.blit(win_words, (33,210))
                    pygame.display.update()
                    snake = Snake()
                    new_direction = snake.moving_direction
                    sleep(3)
            else:
                screen.blit(welcome_words, (240,150))
                screen.blit(start_game_words, (246,310))
                screen.blit(close_game_words, (246,350))
            pygame.display.update()
            fps_clock.tick(snake.speed)
    if __name__ == '__main__':
        main()

3. Tetris

Tetris consists of falling tetrominoes made of four blocks each. The player rotates and moves pieces to fill complete horizontal lines, which then disappear and award points. The implementation defines shape matrices, collision detection, line clearing, and level progression.

Design highlights include a grid representation, shape rotation via predefined orientation lists, boundary and overlap checks, and a scoring system that speeds up the game as the player clears lines.

import pygame
import random
import os

GRID_WIDTH = 20
GRID_NUM_WIDTH = 15
GRID_NUM_HEIGHT = 25
WIDTH, HEIGHT = GRID_WIDTH * GRID_NUM_WIDTH, GRID_WIDTH * GRID_NUM_HEIGHT
SIDE_WIDTH = 200
SCREEN_WIDTH = WIDTH + SIDE_WIDTH
WHITE = (0xff, 0xff, 0xff)
BLACK = (0, 0, 0)
LINE_COLOR = (0x33, 0x33, 0x33)

CUBE_COLORS = [
    (0xcc, 0x99, 0x99), (0xff, 0xff, 0x99), (0x66, 0x66, 0x99),
    (0x99, 0x00, 0x66), (0xff, 0xcc, 0x00), (0xcc, 0x00, 0x33),
    (0xff, 0x00, 0x33), (0x00, 0x66, 0x99), (0xff, 0xff, 0x33),
    (0x99, 0x00, 0x33), (0xcc, 0xff, 0x66), (0xff, 0x99, 0x00)
]

screen = pygame.display.set_mode((SCREEN_WIDTH, HEIGHT))
pygame.display.set_caption("俄罗斯方块")
clock = pygame.time.Clock()
FPS = 30
score = 0
level = 1
screen_color_matrix = [[None] * GRID_NUM_WIDTH for i in range(GRID_NUM_HEIGHT)]
base_folder = os.path.dirname(__file__)

def show_text(surf, text, size, x, y, color=WHITE):
    font_name = os.path.join(base_folder, 'font/font.ttc')
    font = pygame.font.Font(font_name, size)
    text_surface = font.render(text, True, color)
    text_rect = text_surface.get_rect()
    text_rect.midtop = (x, y)
    surf.blit(text_surface, text_rect)

class CubeShape(object):
    SHAPES = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
    I = [[(0,-1),(0,0),(0,1),(0,2)], [( -1,0),(0,0),(1,0),(2,0)]]
    J = [[(-2,0),(-1,0),(0,0),(0,-1)], [(-1,0),(0,0),(0,1),(0,2)], [(0,1),(0,0),(1,0),(2,0)], [(0,-2),(0,-1),(0,0),(1,0)]]
    L = [[(-2,0),(-1,0),(0,0),(0,1)], [(1,0),(0,0),(0,1),(0,2)], [(0,-1),(0,0),(1,0),(2,0)], [(0,-2),(0,-1),(0,0),(-1,0)]]
    O = [[(0,0),(0,1),(1,0),(1,1)]]
    S = [[(-1,0),(0,0),(0,1),(1,1)], [(1,-1),(1,0),(0,0),(0,1)]]
    T = [[(0,-1),(0,0),(0,1),(-1,0)], [(-1,0),(0,0),(0,1),(1,0)], [(0,-1),(0,0),(0,1),(1,0)], [(-1,0),(0,0),(1,0),(0,-1)]]
    Z = [[(0,-1),(0,0),(1,0),(1,1)], [(-1,0),(0,0),(0,-1),(1,-1)]]
    SHAPES_WITH_DIR = {'I': I, 'J': J, 'L': L, 'O': O, 'S': S, 'T': T, 'Z': Z}
    def __init__(self):
        self.shape = random.choice(self.SHAPES)
        self.center = (2, GRID_NUM_WIDTH // 2)
        self.dir = random.randint(0, len(self.SHAPES_WITH_DIR[self.shape]) - 1)
        self.color = random.choice(CUBE_COLORS)
    def get_all_gridpos(self, center=None):
        curr_shape = self.SHAPES_WITH_DIR[self.shape][self.dir]
        if center is None:
            center = [self.center[0], self.center[1]]
        return [(cube[0] + center[0], cube[1] + center[1]) for cube in curr_shape]
    def conflict(self, center):
        for cube in self.get_all_gridpos(center):
            if cube[0] < 0 or cube[1] < 0 or cube[0] >= GRID_NUM_HEIGHT or cube[1] >= GRID_NUM_WIDTH:
                return True
            if screen_color_matrix[cube[0]][cube[1]] is not None:
                return True
        return False
    def rotate(self):
        new_dir = (self.dir + 1) % len(self.SHAPES_WITH_DIR[self.shape])
        old_dir = self.dir
        self.dir = new_dir
        if self.conflict(self.center):
            self.dir = old_dir
            return False
        return True
    def down(self):
        center = (self.center[0] + 1, self.center[1])
        if self.conflict(center):
            return False
        self.center = center
        return True
    def left(self):
        center = (self.center[0], self.center[1] - 1)
        if self.conflict(center):
            return False
        self.center = center
        return True
    def right(self):
        center = (self.center[0], self.center[1] + 1)
        if self.conflict(center):
            return False
        self.center = center
        return True
    def draw(self):
        for cube in self.get_all_gridpos():
            pygame.draw.rect(screen, self.color, (cube[1] * GRID_WIDTH, cube[0] * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH))
            pygame.draw.rect(screen, WHITE, (cube[1] * GRID_WIDTH, cube[0] * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH), 1)

def draw_grids():
    for i in range(GRID_NUM_WIDTH):
        pygame.draw.line(screen, LINE_COLOR, (i * GRID_WIDTH, 0), (i * GRID_WIDTH, HEIGHT))
    for i in range(GRID_NUM_HEIGHT):
        pygame.draw.line(screen, LINE_COLOR, (0, i * GRID_WIDTH), (WIDTH, i * GRID_WIDTH))
    pygame.draw.line(screen, WHITE, (GRID_WIDTH * GRID_NUM_WIDTH, 0), (GRID_WIDTH * GRID_NUM_WIDTH, GRID_WIDTH * GRID_NUM_HEIGHT))

def draw_matrix():
    for i, row in enumerate(screen_color_matrix):
        for j, color in enumerate(row):
            if color is not None:
                pygame.draw.rect(screen, color, (j * GRID_WIDTH, i * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH))
                pygame.draw.rect(screen, WHITE, (j * GRID_WIDTH, i * GRID_WIDTH, GRID_WIDTH, GRID_WIDTH), 2)

def draw_score():
    show_text(screen, f'得分:{score}', 20, WIDTH + SIDE_WIDTH // 2, 100)

def remove_full_line():
    global screen_color_matrix, score, level
    new_matrix = [[None] * GRID_NUM_WIDTH for _ in range(GRID_NUM_HEIGHT)]
    index = GRID_NUM_HEIGHT - 1
    n_full_line = 0
    for i in range(GRID_NUM_HEIGHT - 1, -1, -1):
        is_full = True
        for j in range(GRID_NUM_WIDTH):
            if screen_color_matrix[i][j] is None:
                is_full = False
                break
        if not is_full:
            new_matrix[index] = screen_color_matrix[i]
            index -= 1
        else:
            n_full_line += 1
    score += n_full_line
    level = score // 20 + 1
    screen_color_matrix = new_matrix

def show_welcome(screen):
    show_text(screen, "俄罗斯方块", 30, WIDTH / 2, HEIGHT / 2)
    show_text(screen, "按任意键开始游戏", 20, WIDTH / 2, HEIGHT / 2 + 50)

running = True
gameover = True
counter = 0
live_cube = None
while running:
    clock.tick(FPS)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if gameover:
                gameover = False
                live_cube = CubeShape()
                break
            if event.key == pygame.K_LEFT:
                live_cube.left()
            elif event.key == pygame.K_RIGHT:
                live_cube.right()
            elif event.key == pygame.K_DOWN:
                live_cube.down()
            elif event.key == pygame.K_UP:
                live_cube.rotate()
            elif event.key == pygame.K_SPACE:
                while live_cube.down():
                    pass
            remove_full_line()
    if not gameover and counter % (FPS // level) == 0:
        if not live_cube.down():
            for cube in live_cube.get_all_gridpos():
                screen_color_matrix[cube[0]][cube[1]] = live_cube.color
            live_cube = CubeShape()
            if live_cube.conflict(live_cube.center):
                gameover = True
                score = 0
                live_cube = None
                screen_color_matrix = [[None] * GRID_NUM_WIDTH for _ in range(GRID_NUM_HEIGHT)]
        remove_full_line()
    counter += 1
    screen.fill(BLACK)
    draw_grids()
    draw_matrix()
    draw_score()
    if live_cube is not None:
        live_cube.draw()
    if gameover:
        show_welcome(screen)
    pygame.display.update()

4. LianLianKan (Link‑Match)

LianLianKan is a tile‑matching puzzle where two identical tiles can be removed if a connecting line between them has at most two right‑angle bends and does not cross other tiles. The implementation uses Tkinter for the GUI and defines algorithms for direct, one‑corner, and two‑corner connections.

Key design aspects include generating a shuffled paired map, converting mouse clicks to grid coordinates, checking same‑type tiles, and implementing line‑checking functions that verify clear paths with zero, one, or two bends.

from tkinter import *
from tkinter.messagebox import *
from threading import Timer
import time
import random

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def IsLink(p1, p2):
    if lineCheck(p1, p2):
        return True
    if OneCornerLink(p1, p2):
        return True
    if TwoCornerLink(p1, p2):
        return True
    return False

def IsSame(p1, p2):
    if map[p1.x][p1.y] == map[p2.x][p2.y]:
        print("clicked at IsSame")
        return True
    return False

def callback(event):
    global Select_first, p1, p2, firstSelectRectId, SecondSelectRectId
    x = event.x // 40
    y = event.y // 40
    print("clicked at", x, y)
    if map[x][y] == " ":
        showinfo(title="提示", message="此处无方块")
    else:
        if not Select_first:
            p1 = Point(x, y)
            firstSelectRectId = cv.create_rectangle(x*40, y*40, x*40+40, y*40+40, width=2, outline="blue")
            Select_first = True
        else:
            p2 = Point(x, y)
            if p1.x == p2.x and p1.y == p2.y:
                return
            SecondSelectRectId = cv.create_rectangle(x*40, y*40, x*40+40, y*40+40, width=2, outline="yellow")
            if IsSame(p1, p2) and IsLink(p1, p2):
                Select_first = False
                drawLinkLine(p1, p2)
                t = Timer(timer_interval, delayrun)
                t.start()
            else:
                cv.delete(firstSelectRectId)
                cv.delete(SecondSelectRectId)
                Select_first = False

def lineCheck(p1, p2):
    if p1.x == p2.x or p1.y == p2.y:
        # check straight line
        # ... (implementation omitted for brevity)
        return True
    return False

def OneCornerLink(p1, p2):
    # check L‑shaped connection
    # ... (implementation omitted)
    return False

def TwoCornerLink(p1, p2):
    # check connection with two bends
    # ... (implementation omitted)
    return False

def drawLinkLine(p1, p2):
    # draw connecting line(s)
    pass

def delayrun():
    clearTwoBlock()

def clearTwoBlock():
    cv.delete(firstSelectRectId)
    cv.delete(SecondSelectRectId)
    map[p1.x][p1.y] = " "
    cv.delete(image_map[p1.x][p1.y])
    map[p2.x][p2.y] = " "
    cv.delete(image_map[p2.x][p2.y])
    undrawConnectLine()

def undrawConnectLine():
    # remove drawn lines
    pass

root = Tk()
root.title("Python连连看 ")
imgs = [PhotoImage(file='images\\bar_0' + str(i) + '.gif') for i in range(10)]
Select_first = False
firstSelectRectId = -1
SecondSelectRectId = -1
clearFlag = False
linePointStack = []
Line_id = []
Height = 10
Width = 10
map = [[" " for y in range(Height)] for x in range(Width)]
image_map = [[" " for y in range(Height)] for x in range(Width)]
cv = Canvas(root, bg='green', width=440, height=440)
cv.bind("<Button-1>", callback)
cv.pack()
create_map()
print_map()
root.mainloop()

Each tutorial includes a brief game description, design considerations, and a full, runnable Python source listing, making the article a practical resource for developers learning to build games with Python.

PythonGame developmentTetrisTkinterPygame2048LianLianKanSnake
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.