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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.