前言

使用python简单实现一下数独小游戏,并且使用tkinter进行界面展示。


一、题目生成

1.数独规则

  • 在 9x9 的棋盘网格中将数字 1 ~ 9 填入空白格
  • 每一列只能包含数字 1 到 9,不能重复
  • 每一行只能包含数字 1 到 9,不能重复
  • 每个 3×3 的小九宫格只能包含数字 1 到 9,每一列或每一行中的每个数字只能使用一次

2.生成初始题目

先直接得到若干个数独游戏的答案,然后再随机让一些数字变成待填入的空白格就OK了,而随机变成空白格的数量就决定着游戏难度的大小。
使用递归的方法实现,并且在create_board函数返回答案和题目,来看代码:

# 生成题库
import random
import copy
def generate_sudoku_board():
    # 创建一个9x9的二维列表,表示数独棋盘
    board = [[0] * 9 for _ in range(9)]

    # 递归函数,用于填充数独棋盘的每个单元格
    def filling_board(row, col):
        # 检查是否填充完成整个数独棋盘
        if row == 9:
            return True
        
        # 计算下一个单元格的行和列索引
        next_row = row if col < 8 else row + 1
        next_col = (col + 1) % 9

        # 获取当前单元格在小九宫格中的索引
        box_row = row // 3
        box_col = col // 3

        # 随机生成1到9的数字
        numbers = random.sample(range(1, 10), 9)

        for num in numbers:
            # 检查行、列、小九宫格是否已经存在相同的数字
            if num not in board[row] and all(board[i][col] != num for i in range(9)) and all(num != board[i][j] for i in range(box_row*3, box_row*3+3) for j in range(box_col*3, box_col*3+3)):
                board[row][col] = num

                # 递归填充下一个单元格
                if filling_board(next_row, next_col):
                    return True

                # 回溯,将当前单元格重置为0
                board[row][col] = 0

        return False

    # 填充数独棋盘
    filling_board(0, 0)

    return board
 
def create_board(level): # level数字越大代表游戏难度越大
        """
        生成一个随机的数独棋盘,空白格少
        """
        board = generate_sudoku_board()
        board1 =  copy.deepcopy(board)
        for i in range(81):
            row = i // 9
            col = i % 9
            if random.randint(0, 9) < level:
                board1[row][col] = 0
        return (board,board1)

打印一下结果

v = create_board(5)[1]  
print(v)

>>>
[[1, 0, 0, 8, 0, 6, 0, 0, 4],
 [5, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 6, 0, 7, 0, 2, 0, 0, 1],
 [2, 0, 0, 3, 7, 9, 0, 0, 0],
 [7, 0, 0, 6, 8, 0, 0, 3, 2],
 [0, 0, 5, 4, 0, 0, 7, 6, 9],
 [6, 0, 7, 0, 0, 8, 9, 4, 0],
 [3, 0, 1, 0, 4, 0, 0, 0, 0],
 [9, 0, 4, 5, 6, 0, 0, 2, 7]]

二、界面设计

这里选择使用tkinter进行界面展示。
希望设计的界面效果如下:

UI部分的代码:


import tkinter as tk
import ctypes


root = tk.Tk()
# 界面优化代码---------------------------------
# 调用api设置成由应用程序缩放
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# 调用api获得当前的缩放因子
ScaleFactor=ctypes.windll.shcore.GetScaleFactorForDevice(0)
# 设置缩放因子
root.tk.call('tk', 'scaling', ScaleFactor/75)
#--------------------------------------------

root.title('数独游戏')
root.geometry('900x1000')
frame = tk.Frame(root,width=500,height=500)
frame.pack()

# 绘制九宫格
def print_board(frame,board):
        """
        在界面上显示数独棋盘
        """
        for i in range(9):
            for j in range(9):
                if board[i][j] == 0:
                    label = tk.Label(frame, text="", width=3, height=2, font=("Arial", 16), bg="white")
                else:
                    label = tk.Label(frame, text=str(board[i][j]), width=3, height=2, font=("Arial", 16), bg="white")
                label.grid(row=i, column=j)



def XX(level):
    global aa
    aa = create_board(level)
    return aa

board = XX(5)[1]
print_board(frame,board)


# 添加按键组件
def Button(root,level1,level2):
    

    button1 = tk.Button(root, text="出题:难度1", command=lambda:print_board(frame,XX(level1)[1])) #注意加上lambda!
    button1.pack(side=tk.LEFT, padx=10, pady=10)

    button2 = tk.Button(root, text="出题:难度2", command=lambda:print_board(frame,XX(level2)[1]))
    button2.pack(side=tk.LEFT, padx=10, pady=10)
    
    button3 = tk.Button(root, text="解题",command=lambda:print_board(frame,aa[0]))
    button3.pack(side=tk.LEFT, padx=10, pady=10)
    
    button_quit = tk.Button(root,text='退出',command=root.quit)
    button_quit.pack(side=tk.RIGHT, padx=10, pady=10)
    

Button(root,5,7)


root.mainloop()

最终效果:

三、升级优化

以上版本称之为1.0版本,只有出题和解题两个最基本的功能,没有交互的体验感,离一个真正的“数独”游戏还差的很远,下面对代码升级一下,加入用户交互的方式。

  • 既然需要交互式设计,自然会想到采用tk.Entry()组件。因此绘制九宫格就从之前的文本显示修改为输入框显示,然后将已经出现的数字设置为“可读状态”,避免被修改;而对于待填入的空白格,将输入内容做个限制:只能填入1个数字,而且是1~9,输入其他内容时无效。思路有了,修改代码如下:
# 输入框验证函数
def validate_input(new_value):
    if new_value.isdigit() and int(new_value) >= 1 and int(new_value) <= 9:
        return True
    return False

validate_cmd = frame.register(validate_input)
# 绘制九宫格
def print_board(frame,board):
        """
        在界面上显示数独棋盘
        """
        for i in range(9):
            for j in range(9):
                if board[i][j] == 0:
                    label = tk.Entry(frame,width=4,font=('TimesNewom',15,'bold'), validate="key", validatecommand=(validate_cmd, "%P"))
                else:
                    label = tk.Entry(frame,width=4,font=('TimesNewom',15,'bold'), validate="key", validatecommand=(validate_cmd, "%P"))     # 注意这里的参数要和上面的label一致,否则会出奇怪的bug~
                    label.insert(-1,str(board[i][j]))
                    label.config(state='readonly')
          
                label.grid(row=i, column=j,padx=5,pady=5)

在上面的代码中,Entry组件中的validatevalidatecommand方法用于设置验证类型和验证函数。validate_input函数用于验证输入内容,只有当输入内容是1到9的数字时才返回True,否则返回False。root.register方法用于将这个函数注册为一个验证函数,并返回一个函数名,这个函数名可以用于validatecommand方法中。%P表示当前输入框中的内容。
注意:两个entry组件中的参数必须要一致,否则界面就会出现奇怪的bug。