数独小游戏——Python实现


前言

使用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。😦
看一下效果:

在这里插入图片描述

  • 上面乍一看没有问题,但是实际上忽略了两个关键问题:
    1️⃣ 输入框验证函数需要需要允许空字符情况的存在,否则无法进行删除操作
    2️⃣ 设置成“可读状态”后会对下一步“高亮显示”操作带来困难,需要作另外的处理。因此可以考虑对于已经给出的数字的输入框验证函数保持不变,和上面一样。而待填入的数字的验证函数,添加一个空字符条件判断即可。

更改后代码如下:

# 输入框验证函数 ——针对已给出的数字
def validate_input1(new_value):
    if new_value.isdigit() and int(new_value) >= 1 and int(new_value) <= 9:
        return True
    return False



# 输入框验证函数 ——针对待填入的数字
def validate_input2(new_value):
    
    if new_value == "":
        return True
    
    if len(new_value) == 1 and new_value.isdigit():
        if 1 <= int(new_value) <= 9:
            return True
    return False


validate_cmd1 = frame.register(validate_input1)
validate_cmd2 = frame.register(validate_input2)


# 绘制九宫格
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_cmd2, "%P"))
                else:
                    label = tk.Entry(frame,width=4,font=('TimesNewom',15,'bold'), validate="key", validatecommand=(validate_cmd1, "%P"))     # 注意这里的参数要和上面的label一致,否则会出奇怪的bug~
                    label.insert(-1,str(board[i][j]))
          
                label.grid(row=i, column=j,padx=5,pady=5)
  • 填入数字时,界面需要有个反馈,那就是待填入的格子所在的行、列和小九宫格需要高亮显示,并且填入数字之后当前棋盘上所有与之相同的数字也要高亮显示,这样方便玩家进行判定是否正确填入。

通过3个绑定事件来实现这部分功能。
label.bind('<FocusIn>', highlight) )——聚焦到空白格时进行高亮显示
label.bind('<Leave>', unhighlight) ——焦点离开时进行高亮显示
label.bind('<KeyRelease>', update_board)——按下键盘时高亮显示相同数字

代码如下:

# 绘制九宫格
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_cmd2, "%P"))
                else:
                    label = tk.Entry(frame,width=4,font=('TimesNewom',15,'bold'), validate="key", validatecommand=(validate_cmd1, "%P"))     # 注意这里的参数要和上面的label一致,否则会出奇怪的bug~
                    label.insert(-1,str(board[i][j]))
          
                label.grid(row=i, column=j,padx=5,pady=5)


                # 添加高亮显示
                label.row = i
                label.col = j
                label.bind('<FocusIn>', highlight)
                label.bind('<Leave>', unhighlight)
                label.bind('<KeyRelease>', update_board)

# 高亮显示当前格子所在的行、列和小九宫格
def highlight(event):
    row, col = event.widget.row, event.widget.col
    for i in range(9):
        # 高亮行
        if i != row:
            label = event.widget.master.grid_slaves(row=i, column=col)[0]
            label.config(bg='yellow',font=('times',15))
        # 高亮列
        if i != col:
            label = event.widget.master.grid_slaves(row=row, column=i)[0]
            label.config(bg='yellow',font=('times',15))
        # 高亮小九宫格
        r = row // 3 * 3 + i // 3
        c = col // 3 * 3 + i % 3
        if (r, c) != (row, col):
            label = event.widget.master.grid_slaves(row=r, column=c)[0]
            label.config(bg='yellow',font=('times',15))

        
        
    # 高亮当前格子
    label = event.widget
    label.config(bg='yellow')

# 取消高亮显示
def unhighlight(event):
    row, col = event.widget.row, event.widget.col
    for i in range(9):
        # 取消高亮行
        if i != row:
            label = event.widget.master.grid_slaves(row=i, column=col)[0]
            label.config(bg='white',font=('times',15))
        # 取消高亮列
        if i != col:
            label = event.widget.master.grid_slaves(row=row, column=i)[0]
            label.config(bg='white',font=('times',15))
        # 取消高亮小九宫格
        r = row // 3 * 3 + i // 3
        c = col // 3 * 3 + i % 3
        if (r, c) != (row, col):
            label = event.widget.master.grid_slaves(row=r, column=c)[0]
            label.config(bg='white',font=('times',15))
        
        
        # 取消高亮显示相同数字的格子
        for i in range(9):
            for j in range(9):
                if board[i][j] == board[row][col]:
                    label = event.widget.master.grid_slaves(row=i, column=j)[0]
                    label.config(fg='black',font=('times',15))

    # 取消高亮当前格子
    label = event.widget
    label.config(bg='white')

# 更新当前格子的值,并高亮相同数字
def update_board(event):
    row, col = event.widget.row, event.widget.col
    val = event.widget.get()
    if len(val) > 0:
        if val.isdigit() and 1 <= int(val) <= 9:
            aa[1][row][col] = int(val)
        else:
            event.widget.delete(0, 'end')
            event.widget.insert(0, str(aa[1][row][col]))

    # 高亮相同数字的格子
    for i in range(9):
        for j in range(9):
            if aa[1][i][j] == aa[1][row][col]:
                label = event.widget.master.grid_slaves(row=i, column=j)[0]
                label.config(fg='red',font=('times',15,'bold'))

👁‍🗨来看一下优化后的效果:
在这里插入图片描述

  • 最后对整个界面做一些美观的优化,就可以实现一个简单的数独小游戏了。🎮

整个代码:

# 生成题库
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)


import tkinter as tk
import ctypes
from tkinter import messagebox

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 validate_input1(new_value):
    if new_value.isdigit() and int(new_value) >= 1 and int(new_value) <= 9:
        return True
    return False



# 输入框验证函数 ——针对待填入的数字
def validate_input2(new_value):
    
    if new_value == "":
        return True
    
    if len(new_value) == 1 and new_value.isdigit():
        if 1 <= int(new_value) <= 9:
            return True
    return False


validate_cmd1 = frame.register(validate_input1)
validate_cmd2 = frame.register(validate_input2)


# 绘制九宫格
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_cmd2, "%P"))
                else:
                    label = tk.Entry(frame,width=4,font=('TimesNewom',15,'bold'), validate="key", validatecommand=(validate_cmd1, "%P"))     # 注意这里的参数要和上面的label一致,否则会出奇怪的bug~
                    label.insert(-1,str(board[i][j]))
          
                label.grid(row=i, column=j,padx=10,pady=10)


                # 添加高亮显示
                label.row = i
                label.col = j
                label.bind('<FocusIn>', highlight)
                label.bind('<Leave>', unhighlight)
                label.bind('<KeyRelease>', update_board)

# 高亮显示当前格子所在的行、列和小九宫格
def highlight(event):
    row, col = event.widget.row, event.widget.col
    for i in range(9):
        # 高亮行
        if i != row:
            label = event.widget.master.grid_slaves(row=i, column=col)[0]
            label.config(bg='yellow',font=('times',15))
        # 高亮列
        if i != col:
            label = event.widget.master.grid_slaves(row=row, column=i)[0]
            label.config(bg='yellow',font=('times',15))
        # 高亮小九宫格
        r = row // 3 * 3 + i // 3
        c = col // 3 * 3 + i % 3
        if (r, c) != (row, col):
            label = event.widget.master.grid_slaves(row=r, column=c)[0]
            label.config(bg='yellow',font=('times',15))

        
        
    # 高亮当前格子
    label = event.widget
    label.config(bg='yellow')

# 取消高亮显示
def unhighlight(event):
    row, col = event.widget.row, event.widget.col
    for i in range(9):
        # 取消高亮行
        if i != row:
            label = event.widget.master.grid_slaves(row=i, column=col)[0]
            label.config(bg='white',font=('times',15))
        # 取消高亮列
        if i != col:
            label = event.widget.master.grid_slaves(row=row, column=i)[0]
            label.config(bg='white',font=('times',15))
        # 取消高亮小九宫格
        r = row // 3 * 3 + i // 3
        c = col // 3 * 3 + i % 3
        if (r, c) != (row, col):
            label = event.widget.master.grid_slaves(row=r, column=c)[0]
            label.config(bg='white',font=('times',15))
        
        
        # 取消高亮显示相同数字的格子
        for i in range(9):
            for j in range(9):
                if board[i][j] == board[row][col]:
                    label = event.widget.master.grid_slaves(row=i, column=j)[0]
                    label.config(fg='black',font=('times',15))

    # 取消高亮当前格子
    label = event.widget
    label.config(bg='white')

# 更新当前格子的值,并高亮相同数字
def update_board(event):
    row, col = event.widget.row, event.widget.col
    val = event.widget.get()
    if len(val) > 0:
        if val.isdigit() and 1 <= int(val) <= 9:
            aa[1][row][col] = int(val)
        else:
            event.widget.delete(0, 'end')
            event.widget.insert(0, str(aa[1][row][col]))

    # 高亮相同数字的格子
    for i in range(9):
        for j in range(9):
            if aa[1][i][j] == aa[1][row][col]:
                label = event.widget.master.grid_slaves(row=i, column=j)[0]
                label.config(fg='red',font=('times',15,'bold'))



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]), relief="raised",height=2,font=('楷体',10,'bold')) #注意加上lambda!
    button1.pack(side=tk.LEFT, padx=10, pady=10)

    button2 = tk.Button(root, text="出题:难度2", command=lambda:print_board(frame,XX(level2)[1]), relief="raised",height=2,font=('楷体',10,'bold'))
    button2.pack(side=tk.LEFT, padx=10, pady=10)
    
    button3 = tk.Button(root, text="解题",command=lambda:print_board(frame,aa[0]), relief="raised",height=2,font=('楷体',10,'bold'))
    button3.pack(side=tk.LEFT, padx=10, pady=10)
    
    button4 = tk.Button(root,text="验证",command = lambda:check(aa[1]), relief="raised",height=2,font=('楷体',10,'bold'))
    button4.pack(side=tk.LEFT, padx=10, pady=10) 
    
    button_quit = tk.Button(root,text='退出',command=root.quit,bg='red', relief="raised",height=2,font=('楷体',10,'bold'))
    button_quit.pack(side=tk.RIGHT, padx=10, pady=10)
    

Button(root,5,7)


# 判断输赢
def check_victory(sudo):
    # 检查每行是否有重复数字
    for row in sudo:
        if len(set(row)) != 9:
            return False
    
    # 检查每列是否有重复数字
    for col in range(9):
        column = [sudo[row][col] for row in range(9)]
        if len(set(column)) != 9:
            return False
    
    # 检查每个3x3的小九宫格内是否有重复数字
    for box_row in range(3):
        for box_col in range(3):
            box = [sudo[box_row*3 + i][box_col*3 + j] for i in range(3) for j in range(3)]
            if len(set(box)) != 9:
                return False
    return True
def check(sudo):
    if sudo == aa[1]:
        if check_victory(sudo) :
            messagebox.showinfo("提示", "恭喜你,你赢了!")
        else:
            messagebox.showinfo("提示", "很遗憾,你输了!")
root.mainloop()

最终效果:

在这里插入图片描述


总结

  • 这里将每个功能用函数的形式去实现,而如果整个代码写成的形式封装在一起会更好。
  • 鼠标在聚焦输入框和离开输入框时存在一些问题,界面会稍微浮动变化。
  • 不足之处,欢迎指正!🔍
  • 16
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,下面是一个简单的数独游戏的 Python 代码实现: ```python import random # 初始化 9x9 的数独盘面 def init_board(): board = [[0 for i in range(9)] for j in range(9)] return board # 打印数独盘面 def print_board(board): for i in range(9): for j in range(9): print(board[i][j], end=" ") if j % 3 == 2: print("|", end=" ") print() if i % 3 == 2: print("- " * 11) # 检查行是否合法 def check_row(board, row, num): for i in range(9): if board[row][i] == num: return False return True # 检查列是否合法 def check_col(board, col, num): for i in range(9): if board[i][col] == num: return False return True # 检查小九宫格是否合法 def check_square(board, row, col, num): for i in range(3): for j in range(3): if board[row+i][col+j] == num: return False return True # 检查当前位置是否可以填入数字 def check_num(board, row, col, num): return check_row(board, row, num) and check_col(board, col, num) and check_square(board, row-row%3, col-col%3, num) # 随机生成数独盘面 def generate_board(board, num): for i in random.sample(range(81), num): row = i // 9 col = i % 9 n = random.randint(1, 9) if check_num(board, row, col, n): board[row][col] = n # 主函数 def main(): board = init_board() generate_board(board, 30) print_board(board) if __name__ == '__main__': main() ``` 上述代码通过 `init_board` 函数初始化一个 9x9 的数独盘面,然后通过 `generate_board` 函数随机生成指定数量的数独题目。最后通过 `print_board` 函数将生成的数独盘面打印出来。 需要注意的是,这只是一个简单的数独游戏实现,可能存在一些小问题。如果需要实现更高级的数独游戏,需要考虑更多的细节和算法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值