---
# System prepended metadata

title: the 2048's code

---

# 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()
    
```