数独游戏难度模式解析
在数独游戏中,难度通常由已知数字(提示数)的数量决定。难度越高,已知数字越少,玩家需要推理的步骤越多。以下是不同模式下的算法区别和核心代码解析。
文章目录
1. 难度模式配置
我们定义了三种难度模式:
- 简单模式:提示数字较多,玩家容易完成。
- 中等模式:提示数字适中,需要一定的推理能力。
- 困难模式:提示数字较少,需要较强的逻辑推理。
1.1 难度与移除数量的关系
- 简单模式:移除较少数字,保留较多提示。
- 中等模式:移除中等数量数字,提示适中。
- 困难模式:移除较多数字,提示较少。
MODE_CONFIG = {
"9x9": {"base": 3, "remove_count": {"easy": 30, "medium": 40, "hard": 50}, "size": 9}, # 9x9棋盘,数字范围1-9
"6x6": {"base": 2, "remove_count": {"easy": 15, "medium": 20, "hard": 25}, "size": 6}, # 6x6棋盘,数字范围1-6
"4x4": {"base": 2, "remove_count": {"easy": 5, "medium": 8, "hard": 10}, "size": 4} # 4x4棋盘,数字范围1-4
}
1.2 难度选择界面
我们为难度模式添加了选择界面,用户可以选择不同的难度:
def create_difficulty_selection(self):
# 创建难度选择区域
self.difficulty_frame = tk.Frame(self.root)
self.difficulty_frame.grid(row=0, column=1, pady=10)
tk.Label(self.difficulty_frame, text="选择难度:").pack(side="left", padx=5)
self.difficulty_var = tk.StringVar(value="easy")
for difficulty in ["easy", "medium", "hard"]:
tk.Radiobutton(self.difficulty_frame, text=self.DIFFICULTY_LABELS[difficulty],
variable=self.difficulty_var, value=difficulty,
command=self.update_difficulty).pack(side="left", padx=5)
2. 核心算法解析
2.1 数独生成算法
数独生成算法基于 拉丁方阵(Latin Square) 的概念,通过以下步骤生成:
- 模式函数:用于生成数独的初始填充。
- 随机化:通过随机化行、列和数字,确保生成的数独是随机的。
- 移除数字:根据难度设置移除一定数量的数字,生成题目。
def pattern(r, c): return (base * (r % base) + r // base + c) % side
from random import sample
def shuffle(s): return sample(s, len(s))
rBase = range(base)
rows = [g * base + r for g in shuffle(rBase) for r in shuffle(rBase)]
cols = [g * base + c for g in shuffle(rBase) for c in shuffle(rBase)]
nums = shuffle(range(1, size + 1)) # 数字范围根据棋盘大小调整
# 创建棋盘时使用正确的尺寸
board = [[0 for _ in range(side)] for _ in range(side)]
# 填充棋盘
for r in range(side):
for c in range(side):
board[r][c] = nums[pattern(r, c)]
2.2 数字移除算法
数字移除是根据难度设置的参数进行的,确保不会移除所有数字,并且不会超出棋盘范围:
# 移除部分数字,生成题目
squares = side * side
# 确保不会尝试移除超过可用数量的数字
remove_count = min(remove_count, squares - self.MIN_REMOVE_COUNT) # 至少保留一个数字
# 安全地随机移除数字
available_positions = [(r, c) for r in range(side) for c in range(side) if r < side and c < side]
if available_positions:
# 确保至少保留一个数字
safe_remove_count = min(remove_count, len(available_positions))
for r, c in sample(available_positions, safe_remove_count):
if r < side and c < side: # 再次检查边界
board[r][c] = 0
3. 完整代码在资源中可以下载
以下是包含难度模式的部分代码:
class SudokuGame:
# 常量定义
CELL_WIDTH = 3
CELL_FONT = ('Arial', 18)
ORIGINAL_COLOR = 'black'
PLAYER_COLOR = 'blue'
BG_COLOR = 'white'
READONLY_BG_COLOR = 'lightgray'
MIN_REMOVE_COUNT = 1 # 至少保留一个数字
MODE_CONFIG = {
"9x9": {"base": 3, "remove_count": {"easy": 30, "medium": 40, "hard": 50}, "size": 9}, # 9x9棋盘,数字范围1-9
"6x6": {"base": 2, "remove_count": {"easy": 15, "medium": 20, "hard": 25}, "size": 6}, # 6x6棋盘,数字范围1-6
"4x4": {"base": 2, "remove_count": {"easy": 5, "medium": 8, "hard": 10}, "size": 4} # 4x4棋盘,数字范围1-4
}
DIFFICULTY_LABELS = {
"easy": "简单",
"medium": "中等",
"hard": "困难"
}
def __init__(self, root):
self.root = root
self.root.title("数独游戏")
self.board = []
self.original = []
self.entries = [[None for _ in range(9)] for _ in range(9)]
self.mode = "9x9" # 默认模式改为9x9
self.difficulty = "easy" # 默认难度
self.create_mode_selection()
self.create_difficulty_selection()
self.create_widgets()
self.generate_sudoku() # 默认生成9x9棋盘和棋局
def create_mode_selection(self):
# 创建模式选择区域
self.mode_frame = tk.Frame(self.root)
self.mode_frame.grid(row=0, column=0, pady=10)
tk.Label(self.mode_frame, text="选择模式:").pack(side="left", padx=5)
self.mode_var = tk.StringVar(value="9x9")
modes = ["4x4", "6x6", "9x9"]
for mode in modes:
tk.Radiobutton(self.mode_frame, text=mode, variable=self.mode_var,
value=mode, command=self.update_mode).pack(side="left", padx=5)
def create_difficulty_selection(self):
# 创建难度选择区域
self.difficulty_frame = tk.Frame(self.root)
self.difficulty_frame.grid(row=0, column=1, pady=10)
tk.Label(self.difficulty_frame, text="选择难度:").pack(side="left", padx=5)
self.difficulty_var = tk.StringVar(value="easy")
for difficulty in ["easy", "medium", "hard"]:
tk.Radiobutton(self.difficulty_frame, text=self.DIFFICULTY_LABELS[difficulty],
variable=self.difficulty_var, value=difficulty,
command=self.update_difficulty).pack(side="left", padx=5)
def update_mode(self):
# 更新模式
self.mode = self.mode_var.get()
def update_difficulty(self):
# 更新难度
self.difficulty = self.difficulty_var.get()
def clear_board(self):
# 清理现有的输入框
for row in range(9):
for col in range(9):
if self.entries[row][col]:
self.entries[row][col].destroy()
self.entries[row][col] = None
def create_widgets(self):
# 创建数独棋盘
self.frame = tk.Frame(self.root)
self.frame.grid(row=1, column=0, padx=10, pady=10, columnspan=2)
# 根据模式设置棋盘大小
config = self.MODE_CONFIG.get(self.mode, self.MODE_CONFIG["9x9"])
size = config["size"]
for row in range(size):
for col in range(size):
entry = tk.Entry(self.frame, width=self.CELL_WIDTH, font=self.CELL_FONT, justify='center')
entry.grid(row=row, column=col, padx=(0 if col % 3 != 2 else 5),
pady=(0 if row % 3 != 2 else 5))
self.entries[row][col] = entry
# 按钮
self.btn_frame = tk.Frame(self.root)
self.btn_frame.grid(row=2, column=0, pady=10, columnspan=2)
self.reset_button = tk.Button(self.btn_frame, text="重新开始本局", command=self.reset_board)
self.reset_button.pack(side="left", padx=10)
self.new_button = tk.Button(self.btn_frame, text="生成新棋局", command=self.generate_sudoku)
self.new_button.pack(side="left", padx=10)
def generate_sudoku(self):
# 根据选择的模式和难度生成数独
config = self.MODE_CONFIG.get(self.mode, self.MODE_CONFIG["9x9"])
base = config["base"]
remove_count = config["remove_count"][self.difficulty]
size = config["size"]
side = size # 棋盘边长等于size
def pattern(r, c): return (base * (r % base) + r // base + c) % side
from random import sample
def shuffle(s): return sample(s, len(s))
rBase = range(base)
rows = [g * base + r for g in shuffle(rBase) for r in shuffle(rBase)]
cols = [g * base + c for g in shuffle(rBase) for c in shuffle(rBase)]
nums = shuffle(range(1, size + 1)) # 数字范围根据棋盘大小调整
# 创建棋盘时使用正确的尺寸
board = [[0 for _ in range(side)] for _ in range(side)]
# 填充棋盘
for r in range(side):
for c in range(side):
board[r][c] = nums[pattern(r, c)]
# 移除部分数字,生成题目
squares = side * side
# 确保不会尝试移除超过可用数量的数字
remove_count = min(remove_count, squares - self.MIN_REMOVE_COUNT) # 至少保留一个数字
# 安全地随机移除数字
available_positions = [(r, c) for r in range(side) for c in range(side) if r < side and c < side]
if available_positions:
# 确保至少保留一个数字
safe_remove_count = min(remove_count, len(available_positions))
for r, c in sample(available_positions, safe_remove_count):
if r < side and c < side: # 再次检查边界
board[r][c] = 0
# 清空未使用区域
self.clear_board()
# 重新创建对应当前模式的输入框
# 使用安全的size值
current_size = config["size"]
for row in range(current_size):
for col in range(current_size):
# 确保不会越界访问
if row < len(self.entries) and col < len(self.entries[row]):
entry = tk.Entry(self.frame, width=self.CELL_WIDTH, font=self.CELL_FONT, justify='center')
entry.grid(row=row, column=col, padx=(0 if col % 3 != 2 else 5),
pady=(0 if row % 3 != 2 else 5))
entry.bind("<Key>", self.on_key)
self.entries[row][col] = entry
self.board = board
self.original = copy.deepcopy(board)
self.update_gui()
def update_gui(self):
# 根据模式更新GUI
config = self.MODE_CONFIG.get(self.mode, self.MODE_CONFIG["9x9"])
size = config["base"] ** 2
# 确保board已初始化
if not self.board:
return
for row in range(size):
for col in range(size):
val = 0
# 安全访问board元素
if row < len(self.board) and col < len(self.board[row]):
val = self.board[row][col]
entry = self.entries[row][col]
if entry:
entry.delete(0, tk.END)
# 安全访问board元素
if row < len(self.board) and col < len(self.board[row]):
val = self.board[row][col]
if val != 0:
entry.insert(0, str(val))
entry.config(fg=self.ORIGINAL_COLOR, readonlybackground=self.READONLY_BG_COLOR)
entry.bind("<Key>", lambda e: "break") # 不可编辑原始数字
else:
entry.config(fg=self.PLAYER_COLOR, bg=self.BG_COLOR)
entry.bind("<Key>", self.on_key)
def on_key(self, event):
widget = event.widget
if event.char.isdigit() or event.keysym in ('BackSpace', 'Delete'):
# 允许输入数字或删除
pass
else:
return "break"
def reset_board(self):
self.board = copy.deepcopy(self.original)
self.update_gui()
4. 总结
- 难度模式:通过配置文件定义不同模式下的难度参数,确保代码的可维护性。
- 数独生成算法:基于拉丁方阵生成数独,并通过随机化确保生成的数独是随机的。
- 数字移除算法:根据难度设置移除数字的数量,并确保不会移除所有数字。