用 Pygame 实现 15 puzzle

背景

用 Tkinter 实现简单的 15 puzzle 一文中,我们用 Tkinter\text{Tkinter} Tkinter 实现了简单的 15 puzzle\text{15 puzzle} 15 puzzle。我想到如果用 Pygame\text{Pygame} Pygame 应该可以让界面更美观,于是我决定在那篇文章的基础上用 Pygame\text{Pygame} Pygame 来实现 15 puzzle\text{15 puzzle} 15 puzzle。

正文

代码

用 Tkinter 实现简单的 15 puzzle 一文中,提到了 15 puzzle\text{15 puzzle} 15 puzzle 的介绍以及需要解决的问题,这里不赘述。由于我刚开始学习 Pygame\text{Pygame} Pygame,对它的了解很有限,再加上现在人工智能的能力非常强大,我就想到先用 trae 来完成 Tkinter→Pygame\text{Tkinter}\to \text{Pygame} Tkinter→Pygame 的转化,而我只需要在它的基础上,再做调整即可。转化的过程如下图所示(trae 回答的内容有点长,下图只截取了一部分内容)

转化完的代码虽然并非完全准确,但也算质量很高了。我又调整了些细节,最终的代码如下

python 复制代码
import pygame
import random

class FifteenPuzzle:
    CELL_SIZE = 100
    CELL_GAP = 5
    N = 4
    WINDOW_SIZE = N * CELL_SIZE + (N + 1) * CELL_GAP + 150
    FONT_SIZE = 50

    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)
    GRAY = (200, 200, 200)
    BLUE = (100, 149, 237)
    GREEN = (60, 179, 113)
    DARK_BLUE = (70, 130, 180)

    VALUE_FOR_EMPTY_POS = N * N

    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((self.WINDOW_SIZE, self.WINDOW_SIZE))
        pygame.display.set_caption("15 Puzzle")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.Font(None, self.FONT_SIZE)
        self.button_font = pygame.font.Font(None, 36)

        self.board = []
        self.empty_pos = (self.N - 1, self.N - 1)
        self.click_cnt = 0
        self.game_won = False

        self.new_game_rect = pygame.Rect(
            self.WINDOW_SIZE // 2 - 80,
            self.WINDOW_SIZE - 60,
            160,
            40
        )

        self.create_board()
        self.shuffle_board()

    def create_board(self):
        self.board = []
        for row in range(self.N):
            self.board.append([])
            for col in range(self.N):
                value = row * self.N + col + 1
                self.board[row].append(value)
    
    def is_inside_board(self, pos):
        row, col = pos
        return 0 <= row < self.N and 0 <= col < self.N

    def shuffle_board(self):
        while True:
            for _ in range(100):
                row, col = self.empty_pos
                candidates = [(row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)]
                while True:
                    candidate = random.choice(candidates)
                    if self.is_inside_board(candidate):
                        break
                r, c = candidate
                self.board[self.empty_pos[0]][self.empty_pos[1]] = self.board[r][c]
                self.board[r][c] = self.VALUE_FOR_EMPTY_POS
                self.empty_pos = candidate
            if not self.all_at_original_position():
                break

    def all_at_original_position(self):
        for row in range(self.N):
            for col in range(self.N):
                target = row * self.N + col + 1
                if self.board[row][col] != target:
                    return False
        return True

    def get_cell_rect(self, row, col):
        x = self.CELL_GAP + col * (self.CELL_SIZE + self.CELL_GAP)
        y = self.CELL_GAP + row * (self.CELL_SIZE + self.CELL_GAP)
        return pygame.Rect(x, y, self.CELL_SIZE, self.CELL_SIZE)

    def draw_cell(self, row, col, color, text_color=BLACK):
        rect = self.get_cell_rect(row, col)
        pygame.draw.rect(self.screen, color, rect, border_radius=8)
        if (row, col) != self.empty_pos:
            text = self.font.render(str(self.board[row][col]), True, text_color)
            text_rect = text.get_rect(center=rect.center)
            self.screen.blit(text, text_rect)

    def draw_button(self, text, rect, color, hover_color):
        mouse_pos = pygame.mouse.get_pos()
        if rect.collidepoint(mouse_pos):
            pygame.draw.rect(self.screen, hover_color, rect, border_radius=8)
        else:
            pygame.draw.rect(self.screen, color, rect, border_radius=8)
        text_surf = self.button_font.render(text, True, self.WHITE)
        text_rect = text_surf.get_rect(center=rect.center)
        self.screen.blit(text_surf, text_rect)

    def draw(self):
        self.screen.fill(self.GRAY)

        for row in range(self.N):
            for col in range(self.N):
                if self.game_won:
                    self.draw_cell(row, col, self.GREEN)
                elif (row, col) == self.empty_pos:
                    self.draw_cell(row, col, self.WHITE)
                else:
                    self.draw_cell(row, col, self.BLUE)

        if self.game_won:
            msg = f"You won after {self.click_cnt} clicks!"
        else:
            msg = f"Clicks: {self.click_cnt}"
        click_text = self.button_font.render(msg, True, self.BLACK)
        click_rect = click_text.get_rect(center=(self.WINDOW_SIZE // 2, self.WINDOW_SIZE - 105))
        self.screen.blit(click_text, click_rect)

        self.draw_button("New Game", self.new_game_rect, self.BLUE, self.DARK_BLUE)

        pygame.display.flip()

    def handle_click(self, pos):
        if self.new_game_rect.collidepoint(pos):
            self.create_board()
            self.shuffle_board()
            self.click_cnt = 0
            self.game_won = False
            return

        if self.game_won:
            return

        for row in range(self.N):
            for col in range(self.N):
                if self.get_cell_rect(row, col).collidepoint(pos):
                    row_diff = abs(row - self.empty_pos[0])
                    col_diff = abs(col - self.empty_pos[1])
                    if (row_diff == 1 and col_diff == 0) or (col_diff == 1 and row_diff == 0):
                        self.board[self.empty_pos[0]][self.empty_pos[1]] = self.board[row][col]
                        self.board[row][col] = self.VALUE_FOR_EMPTY_POS
                        self.empty_pos = (row, col)
                        
                        self.click_cnt += 1
                        if self.all_at_original_position():
                            self.game_won = True
                    break

    def run(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                elif event.type == pygame.MOUSEBUTTONDOWN:
                    if event.button == 1:
                        self.handle_click(event.pos)

            self.draw()
            self.clock.tick(60)

        pygame.quit()

if __name__ == "__main__":
    game = FifteenPuzzle()
    game.run()

运行效果

请将完整的代码(上一小节已提供)保存为 fifteen.py。使用下方的命令可以运行 fifteen.py

复制代码
python3 fifteen.py

运行效果如下图所示 ⬇️ (您在自己电脑上运行该程序得到的开局很可能和下图展示的局面不同)

此时可以点击空格旁边的 1,7,131,7,13 1,7,13 中的任意一个数字。

如果点击 11 1,会看到如下的效果("空位置" 和 11 1 发生了交换)

我玩了一会儿,终于得到了预期的局面 ⬇️ (一共有 104104 104 次有效的点击)

参考资料

相关推荐
黄忠7 小时前
大模型之LangGraph技术体系
python·llm
hboot20 小时前
AI工程师第二课 - 数据处理
人工智能·python·数据分析
用户8356290780511 天前
使用 Python 自动化 PowerPoint 形状布局与格式设置
后端·python
用户8356290780511 天前
用 Python 自动化 PowerPoint 演讲者备注添加
后端·python
黄忠1 天前
01-系统架构设计-LangGraph状态机与多源异构RAG
python
zzzzzz3101 天前
假如我是掘金管理员,我先给评论区装个'代码审查'系统
python·程序员·机器人
砍材农夫1 天前
python环境|conda安装和使用(2)
后端·python
程序员龙叔2 天前
编写高质量 Skill 系列 -- 如何设计需求分析与用例生成的 SKILL
自动化测试·软件测试·python·软件测试工程师·接口测试·性能测试·skill·ai测试
用户8356290780512 天前
使用 Python 操作 Word 内容控件
后端·python