从0到1:用Python的PyQt打造专属数独游戏

liftword1个月前 (04-22)技术文章10

一、引言

数独,作为一款风靡全球的数字益智游戏,凭借其简单易懂的规则和极具挑战性的玩法,吸引了无数玩家为之痴迷。从纸质谜题到电子游戏,数独的身影无处不在,无论是在闲暇时光放松大脑,还是在锻炼逻辑思维能力时,数独都是绝佳之选。你是否曾好奇,这些充满趣味的数独游戏是如何开发出来的呢?今天,就让我们一起走进 Python 的世界,利用强大的 PyQt 库来打造属于自己的数独游戏。在这个过程中,我们不仅能深入了解数独游戏的内部机制,还能掌握 PyQt 的使用技巧,感受 Python 编程的魅力。无论你是编程新手,还是渴望提升技能的开发者,都能在这次的开发之旅中收获满满。

二、准备工作

(一)安装 PyQt

在开始使用 PyQt 开发数独游戏之前,我们首先需要将其安装到我们的开发环境中。PyQt 的安装方式较为简单,最常用的方法是使用 pip 命令进行安装 。pip 是 Python 的包管理工具,它能帮助我们快速、便捷地获取和安装各种 Python 库。打开命令行工具(在 Windows 系统中通常是命令提示符或 PowerShell,在 Linux 和 macOS 系统中是终端),输入以下命令:

pip install PyQt5

上述命令会自动从 Python Package Index(PyPI)下载并安装最新版本的 PyQt5 库。不过,由于 PyPI 的服务器位于国外,在网络状况不佳的情况下,下载速度可能会非常缓慢,甚至导致安装失败。为了解决这个问题,我们可以使用国内的镜像源来加速下载。例如,使用豆瓣的镜像源,安装命令如下:

pip install PyQt5 -i https://pypi.douban.com/simple

除了 pip 安装,如果你使用的是 Anaconda 这样的包管理工具,也可以通过它来安装 PyQt5 。在 Anaconda Prompt 中输入:

conda install -c anaconda pyqt

在安装过程中,可能会遇到一些问题。比如,提示缺少某些依赖项,这通常是因为系统中缺少必要的开发工具或库。在 Windows 系统中,如果遇到此类问题,可以尝试安装 Microsoft Visual C++ Redistributable,它提供了 C++ 运行时库,许多 Python 库的安装都依赖于此。在 Linux 系统中,可能需要安装一些基础的开发包,如 GCC、make 等,可以使用系统自带的包管理器进行安装,例如在 Ubuntu 系统中,使用以下命令安装:

sudo apt-get install build-essential

另外,如果出现版本冲突的问题,比如已经安装了旧版本的 PyQt,而新版本的安装与之冲突,可以先尝试卸载旧版本,再进行安装。卸载命令如下:

pip uninstall PyQt5

安装完成后,可以在 Python 交互环境中输入import PyQt5进行测试,如果没有报错,说明安装成功。

(二)了解数独规则与核心功能

在深入开发数独游戏之前,我们必须对数独的规则有清晰的理解。数独的基本结构是一个 9x9 的方格,这个大的方格又被划分为 9 个 3x3 的小方格,我们称之为 “宫” 。游戏的目标是在每个空格中填入数字 1 到 9,同时要满足以下规则:

  • 行规则:每一行中的数字 1 到 9 都只能出现一次。
  • 列规则:每一列中的数字 1 到 9 也都只能出现一次。
  • 宫规则:每个 3x3 的宫中,数字 1 到 9 同样只能出现一次。

理解这些规则是开发数独游戏的基础,它将指导我们如何生成有效的数独谜题,以及如何判断玩家的填入是否正确。

对于数独游戏的开发而言,有两个核心功能是必不可少的:生成谜题和解决数独。生成谜题功能需要能够创建一个初始的数独棋盘,这个棋盘既要满足数独的规则,又要有一定的难度级别,不能过于简单或过于复杂,以保证游戏的趣味性和挑战性 。解决数独功能则是实现一个算法,能够根据给定的数独棋盘,找到符合规则的完整解。这个算法是数独游戏的核心逻辑,它需要运用各种逻辑推理和搜索策略,例如回溯法、唯一候选数法等。有了这两个核心功能,我们就可以搭建起数独游戏的基本框架,再结合 PyQt 强大的图形界面开发能力,就能将数独游戏以直观、友好的方式呈现给玩家。

三、生成数独谜题

(一)核心算法思路

生成数独谜题的关键在于回溯算法,这是一种经典的深度优先搜索策略。想象一下,我们站在数独棋盘的左上角,面前是一片等待填充数字的空格。回溯算法就像是一位耐心的探索者,从这个起点开始,一个格子一个格子地前进,尝试填入数字 1 到 9。每填入一个数字,它都会仔细检查,确保这个数字符合数独的规则,即在同一行、同一列和同一个 3x3 的宫中都没有重复。

当遇到一个空格时,算法会随机选择一个数字填入,然后递归地进入下一个空格继续尝试。如果在某一步发现没有合适的数字可以填入,也就是说,无论选择哪个数字都会违反数独规则,这时算法就会 “回溯”,回到上一个空格,撤销之前填入的数字,尝试其他的可能性 。就好比我们在迷宫中走到了死胡同,只能退回到上一个路口,选择另一条路继续探索。

这个过程会一直持续,直到整个棋盘都被成功填满,每一行、每一列和每一个宫中的数字都符合规则,这样就生成了一个完整的数独谜题。回溯算法的巧妙之处在于,它通过不断地尝试和撤销,能够在庞大的解空间中找到符合数独规则的解,为我们呈现出一个个充满挑战的数独谜题。

(二)Python 代码实现

import random


def is_valid(board, row, col, num):
    # 检查行
    for i in range(9):
        if board[row][i] == num:
            return False
    # 检查列
    for i in range(9):
        if board[i][col] == num:
            return False
    # 检查3x3宫
    start_row, start_col = 3 * (row // 3), 3 * (col // 3)
    for i in range(3):
        for j in range(3):
            if board[start_row + i][start_col + j] == num:
                return False
    return True


def fill_board(board):
    for i in range(9):
        for j in range(9):
            if board[i][j] == 0:
                numbers = list(range(1, 10))
                random.shuffle(numbers)  # 随机打乱数字顺序,增加谜题的多样性
                for num in numbers:
                    if is_valid(board, i, j, num):
                        board[i][j] = num
                        if fill_board(board):
                            return True
                        board[i][j] = 0  # 回溯
                return False
    return True


def create_sudoku():
    board = [[0] * 9 for _ in range(9)]
    fill_board(board)
    return board

在上述代码中,is_valid函数是数独规则的守护者,它仔细检查每一个可能填入的数字,确保其在行、列和宫中的唯一性。当fill_board函数遍历到一个空格时,它会从 1 到 9 中随机选择一个数字,调用is_valid函数进行检查,如果符合规则就填入,并递归地处理下一个空格。如果所有数字都无法填入,就会将当前空格重置为 0,回溯到上一个空格 。create_sudoku函数则是谜题的创造者,它初始化一个空的数独棋盘,调用fill_board函数进行填充,最终生成一个完整的数独谜题。

四、解决数独问题

(一)判断数字放置的安全性

在解决数独问题的过程中,判断在给定位置放置数字是否安全是至关重要的一步。这一步骤是确保数独解的合法性的基础,需要仔细检查行、列和小九宫格,以保证每个数字在这些区域内的唯一性。

首先是行的检查。对于一个 9x9 的数独棋盘,每一行都应该包含数字 1 到 9 且不重复。当我们尝试在某个位置(row, col)放置数字num时,需要遍历该行的所有列,即从第 0 列到第 8 列。如果在这一行中发现已经存在数字num,那么在该位置放置num就是不安全的,因为这违反了数独的行规则。例如,在某一行中已经有数字 5,那么当我们考虑在该行的其他位置放置 5 时,就必须拒绝这个操作 。

列的检查原理与行类似。每一列同样需要包含数字 1 到 9 且无重复。当检查在位置(row, col)放置num的安全性时,我们遍历整个列,从第 0 行到第 8 行。如果在这一列中找到了与num相同的数字,那么该位置不能放置num。例如,在某一列中已经存在数字 3,那么在该列的任何其他位置放置 3 都是不符合数独规则的。

小九宫格的检查相对复杂一些。数独棋盘被划分为 9 个 3x3 的小九宫格,每个小九宫格内也必须包含数字 1 到 9 且不重复。要确定某个位置(row, col)所在的小九宫格,我们可以通过数学计算得到小九宫格的起始行和起始列。具体来说,起始行start_row为3 * (row // 3),起始列start_col为3 * (col // 3) 。然后,我们在这个 3x3 的小九宫格内进行遍历,检查是否已经存在数字num。如果存在,则该位置不能放置num。例如,在某个小九宫格中已经有了数字 7,那么在该小九宫格内的其他位置就不能再放置 7。通过这样细致的行、列和小九宫格的检查,我们能够准确判断在给定位置放置数字是否安全,为后续的回溯法求解数独问题提供坚实的保障。

(二)回溯法求解

回溯法是解决数独问题的核心算法,它就像是一位耐心且机智的探索者,在数独的数字迷宫中寻找正确的路径。回溯法的基本原理是通过不断地尝试和撤销来找到问题的解。在数独中,我们从棋盘的第一个空格开始,尝试填入数字 1 到 9。每填入一个数字,都要检查这个数字是否满足数独的规则,即是否在同一行、同一列和同一个 3x3 的小九宫格中唯一 。

如果填入的数字满足规则,我们就继续处理下一个空格;如果不满足规则,就撤销当前的选择,尝试其他数字。这个过程会一直持续,直到所有空格都被成功填满,或者发现无法找到有效的解。例如,当我们在某个空格尝试填入数字 5,检查后发现它在同一行已经存在,那么就将 5 移除,尝试填入 6,以此类推。

下面是用 Python 实现回溯法解决数独问题的代码:

def solve_sudoku(board):
    for i in range(9):
        for j in range(9):
            if board[i][j] == 0:
                for num in range(1, 10):
                    if is_valid(board, i, j, num):
                        board[i][j] = num
                        if solve_sudoku(board):
                            return True
                        board[i][j] = 0
                return False
    return True


在这段代码中,solve_sudoku函数负责整个求解过程。它通过两层循环遍历数独棋盘的每一个格子。当遇到一个值为 0(表示空格)的格子时,就尝试从 1 到 9 填入数字。这里调用了前面提到的is_valid函数来检查填入的数字是否合法。如果合法,就将数字填入,并递归调用solve_sudoku函数继续处理下一个空格 。如果在递归过程中,所有的空格都能成功填入数字,那么就找到了数独的解,返回True;如果在某个空格处,尝试了所有数字都无法满足规则,就将该空格重置为 0(回溯),并返回False,表示当前的尝试失败,需要重新选择数字。当整个棋盘都遍历完且没有遇到无法解决的空格时,说明找到了完整的解,返回True 。回溯法通过这种不断尝试、检查和回溯的方式,能够在复杂的数独解空间中找到正确的答案,是解决数独问题的有效方法。

五、使用 PyQt 搭建图形界面

(一)创建主窗口与布局

在使用 PyQt 搭建数独游戏的图形界面时,首先要创建主窗口。主窗口就像是游戏的舞台,所有的游戏元素都将在这个舞台上呈现。在 Python 中,我们使用QApplication和QMainWindow类来实现这一功能 。QApplication是 PyQt 应用程序的基础,它管理着应用程序的控制流和主要设置,就像一个总指挥,协调着整个应用程序的运行。而QMainWindow则为我们提供了一个主窗口的框架,我们可以在这个框架上添加各种组件,如菜单栏、工具栏、状态栏等。

下面是创建主窗口的代码示例:

from PyQt5.QtWidgets import QApplication, QMainWindow


app = QApplication([])
window = QMainWindow()
window.setWindowTitle('数独游戏')
window.setGeometry(100, 100, 400, 500)  # 设置窗口位置和大小

在这段代码中,我们首先创建了一个QApplication对象app,它是应用程序的入口点,负责处理初始化、事件循环等重要任务 。接着,创建了QMainWindow对象window,并使用setWindowTitle方法为窗口设置了标题 “数独游戏”,这个标题会显示在窗口的顶部栏,让玩家一眼就能知道这是一个数独游戏。setGeometry方法则用于设置窗口的位置和大小,它接受四个参数,前两个参数100, 100表示窗口左上角在屏幕上的坐标(以像素为单位),后两个参数400, 500表示窗口的宽度和高度(同样以像素为单位)。通过这样的设置,我们就创建了一个位置在屏幕 (100, 100) 处,大小为 400x500 像素的主窗口。

布局的设计是让界面元素合理排列的关键。PyQt 提供了多种布局管理器,如QVBoxLayout(垂直布局)、QHBoxLayout(水平布局)和QGridLayout(网格布局)等 。对于数独游戏,我们可以使用QGridLayout来创建一个 9x9 的网格布局,以便放置数独的格子。QGridLayout就像是一个网格状的架子,我们可以将各种组件按照行列的方式放置在这个架子上,使它们整齐有序地排列。下面是使用QGridLayout创建布局的代码:

from PyQt5.QtWidgets import QWidget, QGridLayout


central_widget = QWidget()
window.setCentralWidget(central_widget)
layout = QGridLayout(central_widget)

在这段代码中,首先创建了一个QWidget对象central_widget,它将作为主窗口的中心部件,承载所有的游戏界面元素 。然后,使用setCentralWidget方法将这个中心部件设置到主窗口window上。接着,创建了QGridLayout对象layout,并将其应用到中心部件central_widget上。这样,我们就为游戏界面搭建好了一个基本的网格布局框架,后续可以在这个框架上添加数独网格和其他交互元素。

(二)添加数独网格与交互元素

在主窗口和布局搭建完成后,接下来就是在窗口中添加数独网格。数独网格是数独游戏的核心区域,玩家在这里进行数字的填写和游戏的操作。实现数独网格有两种常见的方式,一种是使用QTableWidget,它是 PyQt 提供的一个表格组件,非常适合展示和编辑表格数据,就像一个电子表格软件中的表格一样 。另一种方式是自定义QWidget,通过重写绘制和事件处理方法来实现更加灵活的界面效果。

使用QTableWidget实现数独网格的代码如下:

from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem


sudoku_table = QTableWidget(9, 9)
for i in range(9):
    for j in range(9):
        item = QTableWidgetItem()
        item.setFlags(item.flags() & ~Qt.ItemIsEditable)  # 初始设置为不可编辑
        sudoku_table.setItem(i, j, item)
layout.addWidget(sudoku_table, 0, 0)

在这段代码中,首先创建了一个 9x9 的QTableWidget对象sudoku_table,它将作为数独网格展示在界面上 。然后,通过两层循环遍历表格的每一个单元格,为每个单元格创建一个QTableWidgetItem对象item。QTableWidgetItem是QTableWidget中的单元格数据项,它可以存储各种类型的数据,如文本、图片等。在这里,我们将每个单元格的初始状态设置为不可编辑,通过setFlags方法将Qt.ItemIsEditable标志位去除,这样玩家在游戏开始时就不能随意修改单元格中的内容 。最后,使用setItem方法将创建好的item添加到表格的相应位置,并通过addWidget方法将sudoku_table添加到之前创建的网格布局layout中,位置在第 0 行第 0 列。

如果选择自定义QWidget来实现数独网格,代码会相对复杂一些,但可以实现更个性化的效果。首先需要继承QWidget类,并重写paintEvent方法来绘制网格和数字,重写mousePressEvent方法来处理用户的点击事件。以下是一个简单的自定义QWidget实现数独网格的示例代码:

import sys
import math
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtCore import Qt


class SudokuWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.board = [[0] * 9 for _ in range(9)]  # 初始化数独棋盘

    def paintEvent(self, event):
        painter = QPainter(self)
        pen = QPen(Qt.black, 2, Qt.SolidLine)
        painter.setPen(pen)

        # 绘制9x9网格
        cell_width = self.width() / 9
        cell_height = self.height() / 9
        for i in range(10):
            x = i * cell_width
            y = i * cell_height
            if i % 3 == 0:
                pen.setWidth(3)
            else:
                pen.setWidth(1)
            painter.setPen(pen)
            painter.drawLine(x, 0, x, self.height())
            painter.drawLine(0, y, self.width(), y)

        # 绘制数字
        font = painter.font()
        font.setPointSize(20)
        painter.setFont(font)
        for i in range(9):
            for j in range(9):
                if self.board[i][j] != 0:
                    text = str(self.board[i][j])
                    x = j * cell_width + cell_width / 2 - painter.fontMetrics().width(text) / 2
                    y = i * cell_height + cell_height / 2 + painter.fontMetrics().height() / 4
                    painter.drawText(x, y, text)

    def mousePressEvent(self, event):
        cell_width = self.width() / 9
        cell_height = self.height() / 9
        col = int(event.x() / cell_width)
        row = int(event.y() / cell_height)
        print(f"点击了第{row + 1}行,第{col + 1}列")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = QWidget()
    sudoku_widget = SudokuWidget()
    layout = QGridLayout(window)
    layout.addWidget(sudoku_widget, 0, 0)
    window.show()
    sys.exit(app.exec_())

在这个自定义QWidget的示例中,SudokuWidget类继承自QWidget。在__init__方法中,初始化了一个 9x9 的数独棋盘self.board,用于存储数独的数字 。paintEvent方法负责绘制数独网格和数字。首先,创建了一个QPainter对象painter,它就像是一个画家,负责在窗口上绘制各种图形和文本。设置了画笔pen的颜色为黑色,宽度为 2,线条样式为实线 。

通过循环绘制 9x9 的网格线,对于每 3 条线,将画笔宽度设置为 3,以突出显示大九宫格的边界。然后,设置字体大小为 20,用于绘制数字。遍历数独棋盘,如果单元格中的数字不为 0,则计算数字的绘制位置,并使用drawText方法将数字绘制在相应的单元格中 。mousePressEvent方法用于处理鼠标点击事件,通过计算点击位置的坐标,确定点击的单元格所在的行和列,并打印出来。在main函数中,创建了SudokuWidget对象,并将其添加到网格布局中,最后显示窗口。

为了让玩家能够与数独游戏进行交互,还需要添加输入框用于填写数字。可以在每个单元格中添加QLineEdit输入框,让玩家直接在输入框中输入数字 。以下是添加输入框的代码:

from PyQt5.QtWidgets import QLineEdit


for i in range(9):
    for j in range(9):
        input_box = QLineEdit()
        input_box.setFixedSize(30, 30)  # 设置输入框大小
        input_box.setAlignment(Qt.AlignCenter)  # 文本居中对齐
        layout.addWidget(input_box, i, j)

在这段代码中,通过两层循环遍历数独网格的每一个单元格,为每个单元格创建一个QLineEdit输入框input_box。使用setFixedSize方法设置输入框的大小为 30x30 像素,使其大小合适,既不会太大影响界面美观,也不会太小导致输入不便 。setAlignment方法用于设置输入框中文本的对齐方式,这里设置为居中对齐,让输入的数字在输入框中看起来更加整齐。最后,通过addWidget方法将输入框添加到网格布局layout中的相应位置,实现了在数独网格中添加输入框的功能,方便玩家进行数字输入。

(三)实现按钮功能

在数独游戏中,“解决” 和 “新游戏” 按钮是非常重要的交互元素,它们为玩家提供了核心的游戏操作功能。实现这些按钮的功能,能够极大地提升玩家的游戏体验,让玩家能够方便地进行游戏的各种操作。

首先是创建 “解决” 按钮。在 PyQt 中,使用QPushButton类来创建按钮,这个类就像是一个虚拟的按钮组件,我们可以设置它的各种属性和行为。以下是创建 “解决” 按钮的代码:

from PyQt5.QtWidgets import QPushButton


solve_button = QPushButton('解决')
layout.addWidget(solve_button, 9, 0, 1, 3)  # 将按钮添加到布局中,跨1行3列

在这段代码中,创建了一个QPushButton对象solve_button,并将其文本设置为 “解决”,这样按钮在界面上显示时,玩家就能清楚地知道它的功能 。然后,使用addWidget方法将按钮添加到之前创建的网格布局layout中。这里的参数9, 0, 1, 3表示按钮放置在第 9 行第 0 列,并且跨越 1 行 3 列,这样可以让按钮在界面上有足够的显示空间,同时也能保证布局的合理性。

接下来是为 “解决” 按钮绑定事件处理函数。当玩家点击 “解决” 按钮时,我们希望程序能够调用之前实现的数独求解算法,找到数独的解并显示在界面上。以下是绑定事件处理函数的代码:

def solve_sudoku_and_display():
    board = []
    for i in range(9):
        row = []
        for j in range(9):
            item = sudoku_table.item(i, j)
            if item is not None and item.text():
                row.append(int(item.text()))
            else:
                row.append(0)
        board.append(row)
    if solve_sudoku(board):
        for i in range(9):
            for j in range(9):
                sudoku_table.item(i, j).setText(str(board[i][j]))


solve_button.clicked.connect(solve_sudoku_and_display)


在这段代码中,定义了一个名为solve_sudoku_and_display的函数,它就是 “解决” 按钮的事件处理函数。在这个函数中,首先从数独表格sudoku_table中获取当前显示的数独棋盘状态,将其转换为一个二维数组board 。这里通过两层循环遍历表格的每一个单元格,获取单元格中的文本内容,如果文本内容不为空,则将其转换为整数添加到row列表中,否则添加 0 表示该单元格为空 。然后,调用之前实现的solve_sudoku函数尝试求解数独。如果求解成功,再次通过两层循环遍历数独棋盘,将求解得到的数字设置回数独表格sudoku_table的相应单元格中,这样就实现了点击 “解决” 按钮后,程序自动求解数独并显示结果的功能。最后,使用clicked.connect方法将按钮的点击事件与事件处理函数solve_sudoku_and_display绑定起来,当按钮被点击时,就会自动调用这个函数。

创建 “新游戏” 按钮的过程与 “解决” 按钮类似。以下是创建 “新游戏” 按钮并绑定事件处理函数的代码:

new_game_button = QPushButton('新游戏')
layout.addWidget(new_game_button, 9, 3, 1, 3)  # 将按钮添加到布局中,跨1行3列


def generate_and_display_new_sudoku():
    board = create_sudoku()
    for i in range(9):
        for j in range(9):
            item = sudoku_table.item(i, j)
            if item is not None:
                item.setText(str(board[i][j]))


new_game_button.clicked.connect(generate_and_display_new_sudoku)

在这段代码中,创建了 “新游戏” 按钮new_game_button,并将其文本设置为 “新游戏”,然后将其添加到网格布局layout中的第 9 行第 3 列,同样跨越 1 行 3 列 。定义了事件处理函数
generate_and_display_new_sudoku,在这个函数中,首先调用之前实现的create_sudoku函数生成一个新的数独谜题,得到一个二维数组board 。

然后,通过两层循环遍历数独表格sudoku_table的每一个单元格,将新生成的数独谜题中的数字设置到相应的单元格中,实现了点击 “新游戏” 按钮后,程序生成新的数独谜题并显示在界面上的功能。最后,使用clicked.connect方法将 “新游戏” 按钮的点击事件与事件处理函数
generate_and_display_new_sudoku绑定起来,使按钮能够正常响应玩家的点击操作。通过这样的方式,我们成功实现了 “解决” 和 “新游戏” 按钮的功能,为玩家提供了完整的数独游戏交互体验。

六、整合与优化

(一)将核心逻辑与界面结合

现在,我们已经分别实现了数独游戏的核心逻辑,包括生成谜题和解决数独,以及基于 PyQt 的图形界面。接下来的关键步骤是将这两部分紧密结合起来,让它们能够协同工作,为玩家呈现一个完整、流畅的游戏体验。

在 PyQt 界面中,我们之前创建了数独表格(如使用QTableWidget实现的sudoku_table)和各种交互按钮(如 “解决” 按钮和 “新游戏” 按钮)。而核心逻辑部分则包含了生成数独谜题的create_sudoku函数和解决数独的solve_sudoku函数。

当玩家点击 “新游戏” 按钮时,我们需要调用create_sudoku函数生成一个新的数独谜题,然后将谜题中的数字填充到数独表格中显示给玩家。具体实现如下:

def generate_and_display_new_sudoku():
    board = create_sudoku()
    for i in range(9):
        for j in range(9):
            item = sudoku_table.item(i, j)
            if item is not None:
                item.setText(str(board[i][j]))


new_game_button.clicked.connect(generate_and_display_new_sudoku)


在这段代码中,
generate_and_display_new_sudoku函数首先调用create_sudoku函数生成一个新的数独谜题,得到一个二维数组board。然后通过两层循环遍历数独表格sudoku_table的每一个单元格,将新生成的数独谜题中的数字设置到相应的单元格中。最后,使用clicked.connect方法将 “新游戏” 按钮的点击事件与
generate_and_display_new_sudoku函数绑定起来,这样当玩家点击 “新游戏” 按钮时,就会触发这个函数,实现生成并显示新数独谜题的功能。

当玩家点击 “解决” 按钮时,我们要从数独表格中获取当前玩家看到的数独状态,将其转换为核心逻辑中能处理的二维数组形式,然后调用solve_sudoku函数求解数独,最后将求解结果再显示回数独表格。具体代码如下:

def solve_sudoku_and_display():
    board = []
    for i in range(9):
        row = []
        for j in range(9):
            item = sudoku_table.item(i, j)
            if item is not None and item.text():
                row.append(int(item.text()))
            else:
                row.append(0)
        board.append(row)
    if solve_sudoku(board):
        for i in range(9):
            for j in range(9):
                sudoku_table.item(i, j).setText(str(board[i][j]))


solve_button.clicked.connect(solve_sudoku_and_display)


在solve_sudoku_and_display函数中,首先通过两层循环从数独表格sudoku_table中获取当前显示的数独棋盘状态,将其转换为一个二维数组board。这里会检查每个单元格中的文本内容,如果文本内容不为空,则将其转换为整数添加到row列表中,否则添加 0 表示该单元格为空。然后调用solve_sudoku函数尝试求解数独。如果求解成功,再次通过两层循环遍历数独棋盘,将求解得到的数字设置回数独表格sudoku_table的相应单元格中,实现点击 “解决” 按钮后自动求解数独并显示结果的功能。最后,将 “解决” 按钮的点击事件与solve_sudoku_and_display函数绑定。通过这样的方式,我们成功地将数独游戏的核心逻辑与 PyQt 界面进行了整合,实现了数据在两者之间的交互,为玩家提供了完整的游戏功能。

(二)代码优化与细节完善

在完成数独游戏的基本功能实现后,我们可以从多个方面对代码进行优化,以提升游戏的性能和用户体验,同时完善界面细节,让游戏更加美观和易用。

在算法效率方面,虽然回溯算法是解决数独问题的经典方法,但在处理复杂数独时,其时间复杂度较高。我们可以考虑对回溯算法进行优化,例如使用位运算来加速判断某个数字在某行、某列或某个 3x3 宫格中是否可用。位运算利用二进制位来表示数字的存在与否,通过位与、位或等操作,可以快速地进行集合的交并运算,从而减少循环判断的次数,提高算法的执行速度。

以判断某行是否可以放置数字num为例,原本的方法是通过循环遍历该行的所有元素来检查是否存在num,而使用位运算时,可以预先将每行已存在的数字用一个二进制数表示,假设row_bitmap表示某行已存在数字的位图,那么判断num是否可用可以通过(row_bitmap & (1 << num)) == 0来实现,其中1 << num表示将 1 左移num位,得到一个只有第num位为 1,其他位为 0 的二进制数,通过与row_bitmap进行位与运算,如果结果为 0,则表示num在该行可用,这种方式比传统的循环判断要快得多。

代码的可读性也非常重要,它直接影响到代码的维护和扩展。我们可以对代码进行重构,将一些功能独立的代码块封装成函数,给函数和变量取更具描述性的名字。例如,在之前的代码中,判断数字是否可以放置在某个位置的代码逻辑分散在多个地方,我们可以将其封装成一个函数is_number_available,函数接收数独棋盘、行号、列号和要判断的数字作为参数,返回一个布尔值表示该数字是否可以放置在指定位置。这样,在其他需要判断数字可用性的地方,直接调用这个函数即可,使代码结构更加清晰,易于理解和维护。

在界面细节方面,设置合适的背景颜色可以营造出更好的视觉氛围。使用 PyQt 的setStyleSheet方法来设置背景颜色,例如将整个数独游戏界面的背景设置为淡蓝色,可以给玩家一种清新、舒适的感觉 。代码如下:

window.setStyleSheet('background-color: lightblue;')

字体样式的选择也能提升界面的美观度和可读性。可以设置数独表格中数字的字体大小、字体类型和颜色。例如,将字体设置为微软雅黑,大小为 16,颜色为深蓝色,使数字更加清晰易读。代码如下:

from PyQt5.QtGui import QFont, QPalette, QColor


font = QFont('微软雅黑', 16)
sudoku_table.setFont(font)
palette = QPalette()
palette.setColor(QPalette.WindowText, QColor('darkblue'))
sudoku_table.setPalette(palette)

在这段代码中,首先创建了一个QFont对象font,设置字体为微软雅黑,大小为 16。然后将这个字体应用到数独表格sudoku_table上,使表格中的文本使用该字体显示 。接着创建了一个QPalette对象palette,设置其WindowText颜色为深蓝色,WindowText颜色用于表示文本的颜色。最后将这个调色板应用到数独表格sudoku_table上,实现了设置表格中数字颜色为深蓝色的效果。通过这些代码优化和界面细节的完善,我们可以让数独游戏更加高效、美观和易用,为玩家带来更好的游戏体验。

七、游戏源码

# -*- coding: utf-8 -*-

import random
import sys

from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QTableWidget,
                             QTableWidgetItem, QPushButton, QVBoxLayout, QHBoxLayout,
                             QMessageBox)


class SudokuGame(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.new_game()

    def initUI(self):
        # 窗口设置
        self.setWindowTitle('Python数独游戏')
        self.setFixedSize(600, 600)

        # 主部件和布局
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # 数独表格
        self.table = QTableWidget(9, 9)
        self.table.setFixedSize(450, 450)
        self.table.setFont(QFont("Arial", 16))
        self.table.verticalHeader().setVisible(False)
        self.table.horizontalHeader().setVisible(False)
        self.table.setShowGrid(False)

        # 设置单元格大小和样式
        for i in range(9):
            self.table.setColumnWidth(i, 50)
            self.table.setRowHeight(i, 50)
            for j in range(9):
                item = QTableWidgetItem()
                item.setTextAlignment(Qt.AlignCenter)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                self.table.setItem(i, j, item)

        # 按钮布局
        button_layout = QHBoxLayout()
        self.new_btn = QPushButton("新游戏", self)
        self.solve_btn = QPushButton("解决", self)
        self.check_btn = QPushButton("检查", self)

        # 设置按钮样式
        for btn in [self.new_btn, self.solve_btn, self.check_btn]:
            btn.setFixedSize(100, 40)
            btn.setFont(QFont("微软雅黑", 10))

        button_layout.addWidget(self.new_btn)
        button_layout.addWidget(self.solve_btn)
        button_layout.addWidget(self.check_btn)

        # 组合布局
        main_layout.addWidget(self.table, alignment=Qt.AlignCenter)
        main_layout.addLayout(button_layout)

        # 连接信号
        self.new_btn.clicked.connect(self.new_game)
        self.solve_btn.clicked.connect(self.solve)
        self.check_btn.clicked.connect(self.check)
        self.table.cellChanged.connect(self.cell_changed)

    # 核心游戏逻辑
    def is_valid(self, board, row, col, num):
        # 检查行
        for x in range(9):
            if board[row][x] == num:
                return False
        # 检查列
        for x in range(9):
            if board[x][col] == num:
                return False
        # 检查3x3宫
        start_row = row - row % 3
        start_col = col - col % 3
        for i in range(3):
            for j in range(3):
                if board[i + start_row][j + start_col] == num:
                    return False
        return True

    def fill_board(self, board):
        for i in range(9):
            for j in range(9):
                if board[i][j] == 0:
                    numbers = list(range(1, 10))
                    random.shuffle(numbers)
                    for num in numbers:
                        if self.is_valid(board, i, j, num):
                            board[i][j] = num
                            if self.fill_board(board):
                                return True
                            board[i][j] = 0
                    return False
        return True

    def create_sudoku(self, difficulty=30):
        # 生成完整数独
        board = [[0] * 9 for _ in range(9)]
        self.fill_board(board)

        # 挖空生成谜题
        for _ in range(difficulty):
            row = random.randint(0, 8)
            col = random.randint(0, 8)
            while board[row][col] == 0:
                row = random.randint(0, 8)
                col = random.randint(0, 8)
            board[row][col] = 0
        return board

    # 界面交互逻辑
    def new_game(self):
        self.solution = self.create_sudoku()
        self.current_board = [row.copy() for row in self.solution]

        # 挖空答案生成题目
        for i in range(9):
            for j in range(9):
                if random.random() < 0.5:  # 50%概率挖空
                    self.current_board[i][j] = 0

        self.update_display()

    def update_display(self):
        self.table.blockSignals(True)
        for i in range(9):
            for j in range(9):
                value = self.current_board[i][j]
                item = self.table.item(i, j)
                if value == 0:
                    item.setText("")
                    item.setFlags(item.flags() | Qt.ItemIsEditable)
                    item.setBackground(QColor(240, 240, 240))
                else:
                    item.setText(str(value))
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                    item.setBackground(QColor(255, 255, 255))
        self.table.blockSignals(False)

    def cell_changed(self, row, col):
        item = self.table.item(row, col)
        text = item.text()
        if text.isdigit() and 1 <= int(text) <= 9:
            self.current_board[row][col] = int(text)
            item.setForeground(QColor(0, 0, 255))
        else:
            item.setText("")
            self.current_board[row][col] = 0

    def solve(self):
        # 使用回溯法求解
        def backtrack(board):
            for i in range(9):
                for j in range(9):
                    if board[i][j] == 0:
                        for num in range(1, 10):
                            if self.is_valid(board, i, j, num):
                                board[i][j] = num
                                if backtrack(board):
                                    return True
                                board[i][j] = 0
                        return False
            return True

        if backtrack(self.current_board):
            self.update_display()
        else:
            QMessageBox.warning(self, "无解", "当前数独无解!")

    def check(self):
        errors = []
        for i in range(9):
            for j in range(9):
                num = self.current_board[i][j]
                if num != 0:
                    temp = self.current_board[i][j]
                    self.current_board[i][j] = 0
                    if not self.is_valid(self.current_board, i, j, temp):
                        errors.append((i, j))
                    self.current_board[i][j] = temp

        if not errors:
            QMessageBox.information(self, "检查通过", "当前填写正确!")
        else:
            for row, col in errors:
                self.table.item(row, col).setBackground(QColor(255, 200, 200))
            QMessageBox.warning(self, "错误", f"发现{len(errors)}处错误!")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    game = SudokuGame()
    game.show()
    sys.exit(app.exec_())

八、总结与展望

(一)回顾开发过程

在这次用 PyQt 开发数独游戏的旅程中,我们从最基础的准备工作出发,一步步搭建起了一个完整的数独游戏。首先,成功安装 PyQt 库是我们进入图形界面开发的敲门砖,在这个过程中,我们可能遇到了网络问题导致下载缓慢,或是依赖项缺失等状况,但通过更换镜像源、安装相应的开发工具等方法,最终顺利解决,为后续的开发奠定了基础。

接着,深入理解数独规则是核心,它就像是游戏的灵魂,指引着我们编写生成谜题和解决数独的算法。回溯算法在其中扮演了关键角色,无论是生成谜题时从一个空格开始不断尝试数字,还是解决数独时通过递归和回溯找到正确的解,每一步都充满了挑战与乐趣。在这个过程中,判断数字放置的安全性至关重要,通过仔细检查行、列和小九宫格,确保每个数字都符合数独规则,这是整个算法能够正确运行的保障。

搭建图形界面时,我们运用 PyQt 的各种组件和布局管理器,创建了主窗口、数独网格以及交互按钮。在这个过程中,为每个组件设置合适的属性和事件处理函数,让它们能够与玩家进行有效的交互。例如,“新游戏” 按钮点击后生成新谜题,“解决” 按钮点击后求解数独,这些功能的实现让游戏变得生动起来。

最后,将核心逻辑与界面进行整合,使得游戏能够根据玩家的操作,如点击按钮,正确地调用生成谜题或解决数独的算法,并将结果显示在界面上。同时,对代码进行优化和细节完善,提升了游戏的性能和用户体验,让整个游戏更加流畅和美观。

(二)拓展与改进方向

这个数独游戏已经具备了基本的功能,但仍有许多可以拓展和改进的方向,等待着我们去探索。在难度级别选择方面,目前的数独谜题难度相对单一,我们可以设计更复杂的算法,根据空格数量、解题所需的逻辑技巧等因素,生成不同难度级别的数独谜题 。例如,简单难度的谜题可以有较多的已知数字,主要考验玩家对基本规则的熟悉程度;而困难难度的谜题则减少已知数字,需要玩家运用更多的高级逻辑推理技巧,如候选数法、XY-wing 等。

计时功能的添加能为游戏增添紧张刺激的氛围,让玩家在规定时间内完成数独挑战,增加游戏的竞技性。可以使用 Python 的time模块或 PyQt 提供的定时器功能来实现这一功能。当玩家点击 “新游戏” 按钮时,开始计时,在游戏界面上实时显示已用时间,当玩家完成数独或点击 “解决” 按钮时,停止计时并显示最终用时。

提示功能对于新手玩家来说非常友好,当玩家遇到困难时,可以点击提示按钮,程序根据当前数独的状态,分析出最容易确定的数字,并在界面上进行提示。实现这一功能需要在解决数独的算法基础上进行扩展,找到当前棋盘上可以通过简单逻辑推理确定的数字位置和值。希望大家能够在这个基础上继续探索,为这个数独游戏增添更多精彩的功能,让它成为一个更加完善、有趣的益智游戏。

相关文章

Qt for Python—Qt Designer 概览

前言本系列第三篇文章(Qt for Python学习笔记—应用程序初探 )、第四篇文章(Qt for Python学习笔记—应用程序再探 )中均是使用纯代码方式来开发 PySide6 GUI 应用程序...

PySide:基于 Qt 框架的 Python 高级 UI 库

PySide 是 Python 的官方 Qt 库绑定,由 Qt for Python 提供支持。它允许开发者使用 Python 编写基于 Qt 框架的图形用户界面(GUI)应用。作为一个功能强大的跨平...

使用PySide2做窗体,到底是怎么个事?看这个能不能搞懂

PySide2 是 Qt 框架的 Python 绑定,允许你使用 Python 创建功能强大的跨平台 GUI 应用程序。PySide2 的基本使用方法:安装 PySide2pip install Py...

118.Python——PyQt窗体上显示监控视频画面

在做计算机视觉项目时,经常需要打开和显示监控视频画面,对画面进行分析处理。使用OpenCV打开摄像头显示视频画面,视频可以参看:1.3 OpenCV打开本地摄像头并显示视频画面。本文主要实现在PyQt...

Python Qt GUI设计:将UI文件转换Python文件三种妙招(基础篇—2)

在开始本文之前提醒各位朋友,Python记得安装PyQt5库文件,Python语言功能很强,但是Python自带的GUI开发库Tkinter功能很弱,难以开发出专业的GUI。好在Python语言的开放...

进入Python的世界02外篇-Pycharm配置Pyqt6

为什么这样配置,要开发带UI的python也只能这样了,安装过程如下:一 安装工具打开终端:pip install PyQt6 PyQt6-tools二 打开设置并汉化点击plugin,安装汉化插件,...