# the 2048's code
程式碼已經長到我手機沒辦法一次複製全部了,所以我拆成了兩個part貼上,如果你們想要玩,要記得貼在同一個檔案裡面,part 2直接接在part 1下面就可以了。
如果嫌麻煩也可以前往我的Githib直接下載檔案,如果會用的話。
我所有物件都是pygame直接畫出來的,所以不用額外下載任何檔案、照片lol,只要終端機⬇️
```bash=
pip install pygame
```
就可以玩
## 遊玩方法
1. 繼續往下看、自己複製 -> [程式碼](##程式碼)
2. 前往專屬Github(更新較快) -> [2048-made-with-pygame](https://github.com/honzhe05/2048-made-with-pygame)
## What's new
> [name=hzzz]
> [time=Sat, Nov 1, 2025]
1. 新增滑動、合併及新增方塊時的動畫。
2. 可調整螢幕尺寸300, 400, 500, 600px。
3. 重新開始及死亡畫面。
>
> [time=Fir, Oct 31, 2025]
1. 新增遊戲。
## 程式碼
快速移動到 [Part2](###Part2)
### Part1
```python=
import pygame
import sys
import os
import json
import random
import math
from collections import deque
# part 1
class block_body(pygame.sprite.Sprite):
def __init__(self, Color, X, Y, Value, anim):
pygame.sprite.Sprite.__init__(self)
self.anim = anim
self.color = Color
self.value = Value
self.base_image = get_tile_surface(self.value, self.color)
self.image = self.base_image.copy()
pygame.draw.rect(
self.base_image, self.color,
pygame.Rect(0, 0, block, block),
border_radius=int(screen_size / 53)
)
font_sm = pygame.font.SysFont(
"comingsoon", int(block*0.5)
)
font_sm.set_bold(True)
text = font_sm.render(str(self.value), True, (60, 58, 50))
text_rect = text.get_rect(center=(block/2, block/2))
self.base_image.blit(text, text_rect)
self.rect = self.base_image.get_rect()
self.X = grid + X * block_width
self.Y = block_width + block + Y * block_width
self.start_pos = pygame.Vector2(self.X, self.Y)
self.target_pos = pygame.Vector2(self.X, self.Y)
self.move_progress = 1.0
self.move_speed = 0.48
self.anim_size = int(block * 0.4)
self.font_anim = 0.71
self.board_pos = (X, Y)
sprite_map[(X, Y)] = self
def update(self):
moved = False
for start, end in list(path_dict.items()):
sprite = find_sprite_at(*start)
sprite2 = find_sprite_at(*end)
if sprite is None:
del path_dict[start]
continue
if sprite2:
del path_dict[start]
value = sprite2.value * 2
color = tile_colors.get(value, (60, 58, 50))
sprite.kill()
all.remove(sprite)
board[end[0]][end[1]] = value
sprite2.color = color
sprite2.value = value
sprite2.anim_size = int(block * 0.4)
sprite2.font_anim = 0.71
sprite2.anim = True
sprite2.board_pos = (end[0], end[1])
empty_positions.append((start[0], start[1]))
sprite2.base_image = get_tile_surface(
sprite2.value, sprite2.color
)
del sprite_map[(start[0], start[1])]
sprite_map[(end[0], end[1])] = sprite2
moved = True
continue
board[start[0]][start[1]] = 0
board[end[0]][end[1]] = sprite.value
tpx = grid + end[0] * block_width
tpy = block_width + block + end[1] * block_width
sprite.board_pos = (end[0], end[1])
sprite.prev_X = sprite.X
sprite.prev_Y = sprite.Y
sprite.move_to(tpx, tpy)
del sprite_map[(start[0], start[1])]
sprite_map[(end[0], end[1])] = sprite
empty_positions.append((start[0], start[1]))
empty_positions.remove((end[0], end[1]))
moved = True
del path_dict[start]
if moved:
global pending_new_tile
pending_new_tile = True
save_data()
if self.move_progress < 1.0:
self.move_progress += self.move_speed
if self.move_progress > 1.0:
self.move_progress = 1.0
now_pos = self.start_pos.lerp(self.target_pos, self.move_progress)
else:
now_pos = self.target_pos
self.X, self.Y = now_pos.x, now_pos.y
if self.anim:
if self.anim_size < block:
self.anim_size += 30
if self.anim_size >= block:
self.anim_size = block
self.anim = False
scaled_image = pygame.transform.smoothscale(
self.base_image,
(self.anim_size, self.anim_size)
)
self.image = scaled_image
self.rect = self.image.get_rect(
center=(
self.X + block / 2, self.Y + block / 2
)
)
else:
self.image = self.base_image
self.rect = self.image.get_rect(
topleft=(self.X, self.Y)
)
def move_to(self, new_x, new_y):
self.start_pos = pygame.Vector2(self.X, self.Y)
self.target_pos = pygame.Vector2(new_x, new_y)
self.move_progress = 0.0
class arrow_keys(pygame.sprite.Sprite):
def __init__(self, Dir):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((asize, asize), pygame.SRCALPHA)
self.dir = Dir
self.rect = self.image.get_rect()
self.rect.x = dir_pos[self.dir][0]
self.rect.y = dir_pos[self.dir][1]
def update(self):
self.image.fill((0, 0, 0, 0))
color = (180, 180, 180)
mouse_pos = pygame.mouse.get_pos()
if self.rect.collidepoint(mouse_pos):
color = (150, 150, 150)
if mouse_pressed:
color = (160, 160, 160)
elif mouse_released:
color = (180, 180, 180)
moving(self.dir)
pygame.draw.rect(
self.image, color,
self.image.get_rect(), border_radius=8
)
pygame.draw.polygon(
self.image, (30, 30, 30), pos[self.dir]
)
pygame.init()
clock = pygame.time.Clock()
def find_sprite_at(board_x, board_y):
return sprite_map.get((board_x, board_y))
def resize(set_row, set_screen):
global row, screen_size, block, grid
global block_width, sc_w, sc_h, screen, asize
global t, agrid, apos, a, b, b1, c, d, pos
global dir_pos, font_size_small, font
arrow.empty()
row = set_row
screen_size = set_screen
block = int(screen_size / row)
grid = int(block / 7)
block_width = block + grid
sc_w = row * block_width + grid
sc_h = sc_w + 2 * block
screen = pygame.display.set_mode((sc_w, sc_h))
asize = screen_size / 5
t = asize * 0.6
agrid = asize / 10
apos = sc_h + 2 * grid
a = t / 2 * math.sqrt(3)
b = (asize - a) / 2.0 + (asize / 50)
b1 = (asize - a) / 2.0 - (asize / 50)
c = (asize - t) / 2
d = c + t / 2
pos = [
[(b, c), (a + b, d), (b, t + c)],
[(c, b), (t + c, b), (d, a + b)],
[(a + b1, c), (a + b1, t + c), (b1, d)],
[(c, a + b1), (t + c, a + b1), (d, b1)]
]
dir_pos = [
(3 * asize + 2 * agrid, apos + asize + agrid),
(2 * asize + agrid, apos + asize + agrid),
(asize, apos + asize + agrid),
(2 * asize + agrid, apos)
]
font_size_small = int(screen_size / 20)
font = pygame.font.SysFont("comingsoon", font_size_small)
font.set_bold(True)
all.empty()
sprite_map.clear()
anim_list.clear()
path_dict.clear()
empty_positions.clear()
text_cache.clear()
empty_positions.extend(
(x, y) for x in range(row)
for y in range(row) if board[x][y] == 0
)
for i in range(row):
for j in range(row):
if board[i][j] != 0:
color = tile_colors.get(board[i][j], (60, 58, 50))
p1 = block_body(color, i, j, board[i][j], False)
all.add(p1)
sprite_map[(i, j)] = p1
for i in range(4):
p2 = arrow_keys(i)
arrow.add(p2)
background = (155, 135, 118)
block_back = (189, 172, 152)
tile_colors = {
2: (238, 228, 218),
4: (237, 224, 200),
8: (242, 177, 121),
16: (245, 149, 99),
32: (246, 124, 95),
64: (246, 94, 59),
128: (237, 207, 114),
256: (237, 204, 97),
512: (237, 200, 80),
1024: (237, 197, 63),
2048: (237, 194, 46),
}
dx = [1, 0, -1, 0]
dy = [0, 1, 0, -1]
mouse_released, mouse_pressed = False, False
row, screen_size, block, grid = 4, 0, 0, 0
block_width, sc_w, sc_h = 0, 0, 0
asize, t, agrid, apos = 0, 0, 0, 0
a, b, b1, c, d = 0, 0, 0, 0, 0
pos = []
dir_pos = []
board = [[0]*row for _ in range(row)]
font_size_small = 0
font = pygame.font.SysFont(None, 0)
score = 0
best_score = 0
first_moved = False
game_over = False
pending_new_tile = False
all = pygame.sprite.Group()
arrow = pygame.sprite.Group()
empty_positions = [(x, y) for x in range(row) for y in range(row)]
anim_list = deque()
path_dict = {}
sprite_map = {}
text_cache = {}
def get_tile_surface(value, color):
if value in text_cache:
return text_cache[value]
surf = pygame.Surface((block, block), pygame.SRCALPHA)
pygame.draw.rect(
surf, color, pygame.Rect(0, 0, block, block),
border_radius=int(screen_size / 53)
)
font_sm = pygame.font.SysFont("comingsoon", int(block * 0.5))
font_sm.set_bold(True)
text = font_sm.render(str(value), True, (60, 58, 50))
text_rect = text.get_rect(center=(block / 2, block / 2))
surf.blit(text, text_rect)
text_cache[value] = surf
return surf
def generate_block(anim=False):
x, y = random.choice(empty_positions)
empty_positions.remove((x, y))
num = 4 if random.randint(0, 10) == 0 else 2
board[x][y] = num
color = tile_colors.get(num, (60, 58, 50))
if find_sprite_at(x, y) is None:
p1 = block_body(color, x, y, num, anim)
all.add(p1)
anim_list.append((x, y))
def any_block_moving():
for sprite in all:
if isinstance(sprite, block_body) and sprite.move_progress < 1.0:
return True
return False
def check_death():
place = False
for x in range(row):
for y in range(row):
if board[x][y] == 0:
place = True
if not place:
death()
def death():
global game_over
for i in range(row):
for j in range(row):
for k in range(4):
x, y = i + dx[k], j + dy[k]
if 0 <= x < row and 0 <= y < row:
if board[i][j] == board[x][y]:
return
game_over = True
def moving(step):
global first_moved
first_moved = True
q = deque()
visited = [[False]*row for _ in range(row)]
for i in get_scan_order(step):
for j in range(row):
x, y = (i, j) if step in [0, 2] else (j, i)
if board[x][y] != 0:
q.append((x, y))
while q:
x, y = q.popleft()
nx, ny = x + dx[step], y + dy[step]
global score
if 0 <= nx < row and 0 <= ny < row:
if board[nx][ny] == 0:
board[nx][ny] = board[x][y]
board[x][y] = 0
start = (x, y)
end = (nx, ny)
find = False
for k, v in path_dict.items():
if v == start:
path_dict[k] = end
find = True
break
if not find:
path_dict[start] = end
q.append((nx, ny))
elif (
board[nx][ny] == board[x][y] and
not visited[nx][ny]
):
board[nx][ny] *= 2
board[x][y] = 0
start = (x, y)
end = (nx, ny)
find = False
for k, v in path_dict.items():
if v == start:
path_dict[k] = end
find = True
break
if not find:
path_dict[start] = end
score += board[nx][ny]
visited[nx][ny] = True
def get_scan_order(step):
if step in [0, 1]:
return range(row-1, -1, -1)
else:
return range(row)
```
### Part2
```python=
# part 2
def update_score_label(text, text1, kind):
text = font.render(text, True, (151, 138, 118))
text_sc = font.render(text1, True, (151, 138, 118))
popup = pygame.Rect(
grid + sc_w / 2 * kind, block * 1.3,
sc_w / 2 - 2 * grid,
block / 2, border_radius=8
)
pygame.draw.rect(
screen, (233, 231, 217),
popup, width=3 * kind,
border_radius=10
)
text_rect = text.get_rect()
text_sc_rect = text_sc.get_rect()
text_rect.right = popup.right - 10
text_rect.centery = popup.centery
text_sc_rect.left = popup.left + 10
text_sc_rect.centery = popup.centery
screen.blit(text, text_rect)
screen.blit(text_sc, text_sc_rect)
def update_screen():
global best_score
screen.fill((252, 248, 240))
rect = pygame.Rect(0, 2 * block, sc_w, sc_w)
pygame.draw.rect(
screen, background, rect,
border_radius=int(screen_size / 16)
)
best_score = max(score, best_score)
update_score_label(f"{score}", "SCORE", 0)
update_score_label(f"{best_score}", "BEST", 1)
r2 = int(screen_size / 11)
reload_rect = pygame.Rect(
sc_w * 0.88, block / 1.8, r2, r2
)
draw_reload_icon(reload_rect.center, r2 / 2)
resize_rect = pygame.Rect(
sc_w * 0.05, block / 1.9, r2, r2
)
draw_resize_icon(resize_rect.center, r2)
draw_grid()
mouse_pos = pygame.mouse.get_pos()
mouse_click = pygame.mouse.get_pressed()[0]
if (
reload_rect.collidepoint(mouse_pos) and
mouse_click and
not game_over
):
restart_game()
elif (
resize_rect.collidepoint(mouse_pos) and
mouse_click and
not game_over
):
s = choose_screen_size()
resize(4, s)
def restart_game():
global board, score, first_moved
global empty_positions
if first_moved:
board = [[0]*row for _ in range(row)]
empty_positions = [(x, y) for x in range(row) for y in range(row)]
sprite_map.clear()
anim_list.clear()
path_dict.clear()
all.empty()
score = 0
for _ in range(2):
generate_block(True)
first_moved = False
def get_save_path(filename="2048_data.json"):
folder = os.path.dirname(__file__)
return os.path.join(folder, filename)
def save_data():
file = get_save_path()
game_data = {
"board": board,
"score": score,
"best score": best_score,
"screen_size": screen_size,
"first_moved": first_moved
}
try:
with open(file, "w", encoding="utf-8") as f:
json.dump(
game_data, f, ensure_ascii=False)
print("Game data saved!")
except Exception as e:
print(f"Failed to save game data: {e}")
def load_data():
file = get_save_path()
global board, score, best_score, screen_size
global first_moved
if os.path.exists(file) and os.path.getsize(file) > 0:
with open(file, "r", encoding="utf-8") as f:
try:
data = json.load(f)
board = data.get("board", 0)
score = data.get("score", 0)
best_score = data.get("best score", 0)
screen_size = data.get("screen_size", 0)
first_moved = data.get("first_moved", 0)
except json.JSONDecodeError:
board = [[0]*4 for _ in range(4)]
score = 0
best_score = 0
screen_size = 0
first_moved = False
else:
board = [[0]*4 for _ in range(4)]
score = 0
best_score = 0
screen_size = 0
first_moved = False
def draw_resize_icon(center, size, color=(117, 100, 82)):
x, y = center
box_rect = pygame.Rect(0, 0, size, size)
box_rect.center = center
line = int(screen_size / 100)
pygame.draw.rect(
screen, color, box_rect,
width=line, border_radius=8
)
ax = box_rect.right - 2 * line
ay = box_rect.bottom - 2 * line
pygame.draw.line(screen, color, (ax - line, ay), (ax, ay - 8), line)
bx = box_rect.left + 2 * line
by = box_rect.top + 2 * line
pygame.draw.line(screen, color, (bx + 8, by), (bx, by + 8), line)
def draw_game_over_overlay():
overlay = pygame.Surface((sc_w, sc_h), pygame.SRCALPHA)
overlay.fill((0, 0, 0, 100))
screen.blit(overlay, (0, 0))
r2 = int(screen_size / 8)
reload_rect = pygame.Rect(sc_w / 2 - r2 / 2, sc_h / 1.8, r2, r2)
draw_reload_icon(reload_rect.center, r2 / 2, (255, 255, 255))
keys = pygame.key.get_pressed()
mouse_pos = pygame.mouse.get_pos()
mouse_click = pygame.mouse.get_pressed()[0]
if (
keys[pygame.K_RETURN] or (
reload_rect.collidepoint(mouse_pos) and
mouse_click
)
):
restart_game()
global game_over
game_over = False
font_big = pygame.font.SysFont(
"comingsoon", int(screen_size / 7)
)
font_big.set_bold(True)
text = font_big.render(
"GAME OVER", True, (255, 255, 255)
)
screen.blit(
text, text.get_rect(
center=(sc_w / 2, sc_h / 2 - r2))
)
def draw_reload_icon(center, r, color=(117, 100, 82)):
arc = pygame.Rect(0, 0, r * 2, r * 2)
arc.center = center
line = int(screen_size / 100)
a1, a2 = math.radians(30), math.radians(330)
pygame.draw.arc(screen, color, arc, a1, a2, line)
tip = (
center[0] + r * math.cos(a2),
center[1] + r * math.sin(a2)
)
for s in [-1, 1]:
a = a2 + s * math.radians(75)
dx = r * 0.25 * math.cos(a)
dy = r * 0.25 * math.sin(a)
end = (
tip[0] - dx * 1.5 - 5,
tip[1] + dy * 1.1 - 5
)
pygame.draw.line(screen, color, tip, end, line)
def draw_grid():
for i in range(block_width + block, sc_h - grid, block_width):
for j in range(grid, sc_w - grid, block_width):
rect_block = pygame.Rect(j, i, block, block)
pygame.draw.rect(
screen, block_back, rect_block,
border_radius=int(screen_size / 53)
)
def choose_screen_size():
sizes = [300, 400, 500, 600]
word = [
"For mobile phones, recommend 500px;",
"for computers, 300 or 400px."
]
label_y = [270, 300]
cols = 2
spacing = 20
btn_w, btn_h = 240, 100
margin_x, margin_y = 60, 60
screen = pygame.display.set_mode((620, 380))
font = pygame.font.SysFont("cutivemono", 50)
font.set_bold(True)
font_small = pygame.font.SysFont("cutivemono", 18)
font.set_bold(True)
last = pygame.time.get_ticks()
while True:
screen.fill((252, 248, 240))
for i, s in enumerate(sizes):
row = i // cols
col = i % cols
x = margin_x + col * (btn_w + spacing)
y = margin_y + row * (btn_h + spacing)
rect = pygame.Rect(x, y, btn_w, btn_h)
now = pygame.time.get_ticks()
label = font.render(
f"{s} px", True, (60, 58, 50)
)
label_rect = label.get_rect(center=rect.center)
screen.blit(label, label_rect)
if i in [0, 1]:
rect1 = pygame.Rect(
0, label_y[i], 620, 100
)
rect1.centerx = label_y[i] * 0.78 + 20 * i
label1 = font_small.render(
word[i], True, (60, 58, 50)
)
label_rect1 = label.get_rect(
center=rect1.center
)
screen.blit(label1, label_rect1)
mouse_pos = pygame.mouse.get_pos()
if rect.collidepoint(mouse_pos):
pygame.draw.rect(screen, (200, 200, 200), rect, 2)
if (
now - last > 300 and
pygame.mouse.get_pressed()[0]
):
return s
else:
pygame.draw.rect(screen, (180, 180, 180), rect, 1)
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.display.update()
load_data()
if screen_size == 0:
s = choose_screen_size()
for _ in range(2):
generate_block(True)
else:
s = screen_size
check_death()
resize(4, s)
while True:
mouse_released, mouse_pressed = False, False
clock.tick(60)
for e in pygame.event.get():
if e.type == pygame.QUIT:
save_data()
pygame.quit()
sys.exit()
elif e.type == pygame.KEYDOWN:
if any_block_moving():
break
if e.key in [pygame.K_RIGHT, pygame.K_d]:
moving(0)
elif e.key in [pygame.K_DOWN, pygame.K_s]:
moving(1)
elif e.key in [pygame.K_LEFT, pygame.K_a]:
moving(2)
elif e.key in [pygame.K_UP, pygame.K_w]:
moving(3)
mouse_click = pygame.mouse.get_pressed()[0]
if mouse_click:
mouse_pressed = True
else:
mouse_released = True
update_screen()
all.update()
all.draw(screen)
arrow.update()
arrow.draw(screen)
if game_over:
draw_game_over_overlay()
if pending_new_tile:
generate_block(True)
pending_new_tile = False
else:
check_death()
save_data()
pygame.display.update()
```