MATLAB GUI游戏设计——数独
MATLAB GUI游戏设计——数独
本教程旨在笔者学习使用MATLAB App Designer 工具设计和实现一个数独游戏的过程。
一、求解数独
数独是一种流行的逻辑谜题,其基本规则要求玩家在9x9的网格中填入数字,使得每行、每列以及每个3x3的子网格中的数字1到9各出现一次。标准的数独谜题设计有一个唯一的解决方案。
数独的求解方法有非常多,笔者这里提供两种求解方法。
1、深度优先搜索算法
深度优先搜索(DFS)是一种递归回溯的方法,用于探索所有可能的数字填充方式,直到找到一个有效的解决方案或确定数独无解为止。这种算法的核心在于两个主要步骤:检查插入数字的合法性和递归求解。
(1) 检查插入数的合法性
在数独中插入数字前,我们需要验证该数字是否符合数独的基本规则。以下函数 cheak_mat
检查在给定位置放置特定数字是否违反了数独的规则。
function result = cheak_mat(matrix, i, j, num)
% cheak_mat函数:检查在数独矩阵中特定位置放置特定数字是否符合数独规则。
% 初始化返回结果为true。假定开始时放置数字是有效的。
result = true;
% 检查同一列是否有相同的数字。
for row = 1:9
if matrix(row, j) == num
% 如果找到相同的数字,设置结果为false,并退出循环。
result = false;
break;
end
end
% 检查同一行是否有相同的数字。
for column = 1:9
if matrix(i, column) == num
% 如果找到相同的数字,设置结果为false,并退出循环。
result = false;
break;
end
end
% 计算该位置所在的3x3子网格的起始坐标。
I = floor((i - 1) / 3) + 1;
J = floor((j - 1) / 3) + 1;
% 检查同一个3x3子网格内是否有相同的数字。
for row = (3 * I - 2):(3 * I)
for column = (3 * J - 2):(3 * J)
if matrix(row, column) == num
% 如果找到相同的数字,设置结果为false,并退出循环。
result = false;
break;
end
end
end
end
此函数分别检查所选数字在相应的行、列和3x3子网格中是否已存在。如果发现重复,函数返回 false
,表示该位置不能放置该数字。
(2) 深度优先求解
下面的 solve_mat
函数使用DFS算法遍历数独的每个空格,尝试所有可能的数字,并递归地进行下一步。
function [found, matrix] = solve_mat(matrix, id)
% solve_mat函数:使用深度优先搜索算法解决数独问题。
% matrix:当前数独矩阵
% id:当前处理的单元格编号(1到81)
% 初始化found标志为false。假定开始时没有找到解决方案。
found = false;
% 如果id超过81,意味着所有的格子都已经被处理过,找到了一个解决方案。
if id > 81
% 显示解决方案。
disp(matrix);
% 将found标志设置为true,表示找到了一个解决方案。
found = true;
return;
end
% 如果当前格子已经有数字,递归处理下一个格子。
if matrix(id) ~= 0
[found, matrix] = solve_mat(matrix, id + 1);
else
% 如果当前格子为空,尝试放置1到9的每一个数字。
for num = 1:9
% 如果还没有找到解决方案,检查当前数字是否可以放置在当前格子。
if ~found && cheak_mat(matrix, mod(id - 1, 9) + 1, floor((id - 1) / 9) + 1, num)
% 如果可以放置,将数字放入格子。
matrix(id) = num;
% 递归处理下一个格子。
[found, matrix] = solve_mat(matrix, id + 1);
end
end
end
% 如果尝试了所有数字都无法找到解决方案,将当前格子重置为0,并返回。
if ~found
matrix(id) = 0;
end
end
该函数逐格检查,如果发现当前路径不可行(即无法放置合法数字),它会回溯到前一个步骤并尝试其他可能性。通过这种方式,函数可以找到数独的一个或所有解决方案。
为了找到所有解决方案,我们对 solve_mat
函数进行了修改,使其在找到一个解决方案后不立即返回,而是继续探索其他可能性。
function [solutions, matrix] = solve_mat(matrix, id, solutions)
% 如果没有提供解决方案列表,初始化一个空列表
if nargin < 3
solutions = {};
end
% 检查是否处理完所有格子
if id > 81
% 添加当前解决方案到解决方案列表
solutions{end + 1} = matrix;
return;
end
% 如果当前格子已经填充,递归调用下一个格子
if matrix(id) ~= 0
[solutions, matrix] = solve_mat(matrix, id + 1, solutions);
return;
end
% 尝试在当前格子填入1到9的每个数字
for num = 1:9
if cheak_mat(matrix, mod(id - 1, 9) + 1, floor((id - 1) / 9) + 1, num)
matrix(id) = num; % 放置数字
[solutions, matrix] = solve_mat(matrix, id + 1, solutions); % 递归调用
end
end
% 恢复当前格子为0,以便回溯
matrix(id) = 0;
end
这种修改使得函数能够收集数独的所有可能解决方案,但相应地,这也会大幅增加计算量和运行时间。特别是对于有多个解的数独问题,计算可能变得非常耗时。
2、线性优化算法
MATLAB的官方文档给出了一种相当巧妙的求救方法,下面笔者介绍这种算法。
(1) 初始化
我们先给出一个初始的谜题,下面的数独是2012年英国《每日邮报》报道的,据说是世界上难度最大的数独游戏。
现在我们开始着手解开这个数独。
首先,我们可以把已知的数独谜题写成以下矩阵的形式:
B = [1,1,8;
2,3,3;
2,4,6;
3,2,7;
3,5,9;
3,7,2;
4,2,5;
4,6,7;
5,5,4;
5,6,5;
5,7,7;
6,4,1;
6,8,3;
7,3,1;
7,8,6;
7,9,8;
8,3,8;
8,4,5;
8,8,1;
9,2,9;
9,7,4];
这个矩阵表示的含义是:第一行 B(1,1,8) 表示第 1 行第 1 列的整数提示为 8。第二行 B(2,3,3) 表示第 2 行第 3 列的整数提示为 3,以此类推。
您的文档开头部分已经非常清晰地描述了数独问题和初始设置。我将根据您的风格继续完善后续部分。
(2) 二元整数规划方法
在这种方法中,我们使用一个三维的二元数组 x
来表示数独的解。这个数组的大小为 9×9×9,其中 x(i,j,k)
表示数独第 i
行、第 j
列是否填入数字 k
(填入则为1,否则为0)。
这种表示方式将数独的每个单元格转换成一个包含1到9的层,每一层代表一个可能的数字。如果某一格的解是数字 k
,那么对应的 x(i,j,k)
将被设置为1,其他层的 x(i,j,*)
则为0。
(3) 将数独规则表示为约束
为了求解这个优化问题,我们需要将数独的规则转换为数学约束条件:
-
单元格约束:
[
\sum_{k=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } i, j \in {1, \ldots, 9}
]
这表示每个单元格(i, j)中只能填入一个数字。 -
行约束:
[
\sum_{j=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } i, k \in {1, \ldots, 9}
]
这表示在每一行i中,每个数字k只能出现一次。 -
列约束:
[
\sum_{i=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } j, k \in {1, \ldots, 9}
]
这表示在每一列j中,每个数字k只能出现一次。 -
3x3子网格约束:
[
\sum_{i=U+1}^{U+3} \sum_{j=V+1}^{V+3} x_{ijk} = 1, \quad \text{对于所有 } U, V \in {0, 3, 6} \text{ 和 } k \in {1, \ldots, 9}
]
这表示在每个3x3的子网格中,每个数字k只能出现一次。
(4) 以优化问题的形式求解数独
首先,我们创建一个9×9×9的二元优化变量 x
:
x = optimvar('x',9,9,9,'Type','integer','LowerBound',0,'UpperBound',1);
然后,我们创建一个优化问题 sudpuzzle
,其目标函数可以是任意的,因为我们只关心满足约束的可行解。不过,为了帮助求解器更快地找到解,我们可以选择一个有助于打破问题对称性的目标函数。
sudpuzzle = optimproblem;
mul = ones(1,1,9);
mul = cumsum(mul,3);
sudpuzzle.Objective = sum(sum(sum(x,1),2).*mul);
接下来,我们添加前面提到的约束条件:
% 单元格约束
sudpuzzle.Constraints.consx = sum(x,1) == 1;
% 行约束
sudpuzzle.Constraints.consy = sum(x,2) == 1;
% 列约束
sudpuzzle.Constraints.consz = sum(x,3) == 1;
并为3x3子网格添加约束:
% 3x3子网格约束
majorg = optimconstr(3,3,9);
for u = 1:3
for v = 1:3
arr = x(3*(u-1)+1:3*(u-1)+3,3*(v-1)+1:3*(v-1)+3,:);
majorg(u,v,:) = sum(sum(arr,1),2) == ones(1,1,9);
end
end
sudpuzzle.Constraints
.majorg = majorg;
最后,将数独谜题的提示值转换为约束条件,固定相应的 x
值为1:
% 将提示转换为约束
for u = 1:size(B,1)
x.LowerBound(B(u,1),B(u,2),B(u,3)) = 1;
end
现在,我们可以使用 MATLAB 的优化工具箱求解这个问题:
sudsoln = solve(sudpuzzle);
求解完成后,我们可以通过以下代码来提取并显示解决方案:
% 提取并显示解决方案
sudsoln.x = round(sudsoln.x);
y = ones(size(sudsoln.x));
for k = 2:9
y(:,:,k) = k;
end
S = sudsoln.x.*y;
S = sum(S,3);
drawSudoku(S);
通过上述步骤,我们不仅能够求解数独,还能深入理解二元整数规划方法在解决此类逻辑问题中的应用。这种方法的优点在于它对于任何难度级别的数独都是通用的,并且能保证找到数独的唯一解(如果存在的话)。
(5)以优化问题求多解
MATLAB的优化工具箱在遇到第一个可行解就结束计算了,为了求出多解,我们需要再加上一个不能等于第一个解的约束。
% 设第一个解为firstSolution
% 添加额外约束以排除第一个解
% 创建一个表示是否与第一个解至少有一个不同的单元格的逻辑变量
different = optimvar('different', 9, 9, 9, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1);
% 确保'different'至少有一个是1(与第一个解不同)
sudpuzzle.Constraints.differentAtLeastOne = sum(different, 'all') >= 1;
% 对于每个单元格,如果第一个解在那里有数字,则'different'对应的位置必须为1
for i = 1:9
for j = 1:9
for k = 1:9
if firstSolution(i,j,k) == 1
sudpuzzle.Constraints.(['different', num2str(i), num2str(j), num2str(k)]) = different(i,j,k) + x(i,j,k) == 1;
end
end
end
end
% 再次求解
[sudsoln2, ~, ~, ~] = solve(sudpuzzle);
这仅仅这得到第二个解,我们可以通过添加额外的约束来得到第三个解,以此类推。
二、生成数独
在设计数独游戏时,生成初步的数独谜题是一个关键步骤。这里我们主要探讨两种方法:一种是生成一个完整的数独并适当“挖洞”以形成谜题;另一种是从一个已知的不完整但具有唯一解的数独谜题开始,并根据需要进一步处理。
1、生成完整数独
生成一个完整的数独谜题可以通过递归回溯法来实现。这种方法首先生成一个空白的9x9网格,然后尝试逐个填充数字,确保每一步都遵循数独的规则。如果在某一步无法合法填入数字,算法将回溯到之前的步骤,尝试其他数字。以下是基本实现:
function sudoku_matrix = generateFullSudoku()
% 初始化空的数独矩阵
sudoku_matrix = zeros(9, 9);
% 递归填充数独的单个单元格
function success = fillCell(i, j)
if i > 9 % 如果超出最后一行,表示数独填充完成
success = true;
return;
end
% 计算下一个单元格的坐标
next_i = i + (j == 9);
next_j = mod(j, 9) + 1;
nums = randperm(9); % 生成随机数字序列
for k = 1:9
num = nums(k);
if cheak_mat(sudoku_matrix, i, j, num)
sudoku_matrix(i, j) = num;
if fillCell(next_i, next_j)
success = true;
return;
end
sudoku_matrix(i, j) = 0; % 回溯
end
end
success = false; % 无法找到合适的数字填充
end
% 开始填充数独
fillCell(1, 1);
end
2、生成数独初盘
生成数独初盘的过程是数独游戏设计中的核心部分。它涉及到从一个完全空白的数独矩阵开始,逐步添加数字,最终形成一个具有一定难度的数独游戏。这个过程需要细致的算法来保证数独谜题既有趣又具有挑战性。下面是实现这一目标的步骤:
(1)初始化数据
开始时,我们有一个完全空白的9x9的数独矩阵。这个矩阵将作为我们添加数字的基础。
A = zeros(9, 9); % 数独矩阵
count = 0; % 计数器
(2)求多解函数
按照之前的描述,可以写成:
function [ans1, ans2] = SolveSudoku(A)
B = [];
for i = 1:9
for j = 1:9
if A(i,j) ~= 0
B = [B;[i,j,A(i,j)]];
end
end
end
% 创建优化变量
x = optimvar('x', 9, 9, 9, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1);
% 创建优化问题
sudpuzzle = optimproblem;
% 添加约束:行、列、宫格内数字唯一
sudpuzzle.Constraints.consx = sum(x,1) == 1;
sudpuzzle.Constraints.consy = sum(x,2) == 1;
sudpuzzle.Constraints.consz = sum(x,3) == 1;
% 添加宫格约束
for u = 1:3
for v = 1:3
arr = x(3*(u-1)+1:3*u, 3*(v-1)+1:3*v, :);
sudpuzzle.Constraints.(['box', num2str(u), num2str(v)]) = sum(sum(arr,1),2) == 1;
end
end
% 设置初始值
for u = 1:size(B,1)
x.LowerBound(B(u,1),B(u,2),B(u,3)) = 1;
end
% 求解
[sudsoln1, ~, exitflag, ~] = solve(sudpuzzle);
% 检查第一次求解是否成功
if exitflag ~= 1
ans1 = [];
ans2 = [];
return;
end
% 提取第一个解
firstSolution = round(sudsoln1.x);
% 添加额外约束以排除第一个解
% 创建一个表示是否与第一个解至少有一个不同的单元格的逻辑变量
different = optimvar('different', 9, 9, 9, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1);
% 确保'different'至少有一个是1(与第一个解不同)
sudpuzzle.Constraints.differentAtLeastOne = sum(different, 'all') >= 1;
% 对于每个单元格,如果第一个解在那里有数字,则'different'对应的位置必须为1
for i = 1:9
for j = 1:9
for k = 1:9
if firstSolution(i,j,k) == 1
sudpuzzle.Constraints.(['different', num2str(i), num2str(j), num2str(k)]) = different(i,j,k) + x(i,j,k) == 1;
end
end
end
end
% 再次求解
[sudsoln2, ~, ~, ~] = solve(sudpuzzle);
y = ones(size(firstSolution));
for k = 2:9
y(:,:,k) = k;
end
ans1 = firstSolution.*y;
ans1 = sum(ans1,3);
y = ones(size(sudsoln2.x));
for k = 2:9
y(:,:,k) = k;
end
ans2 = sudsoln2.x.*y;
ans2 = sum(ans2,3);
end
(3)随机填充数字
这个过程包括以下几个关键步骤:
-
随机选择位置和数字:随机选取一个小于81的数n,代表数独矩阵中的位置,以及一个1到9之间的数t,代表要填入的数字。
-
检查并填入数字:检查所选位置n是否已经有数字。如果有,则n=(n+1)%81,然后重复检查。如果没有,则进一步检查是否可以将数字t填入位置n。这一步需要使用前面提到的辅助函数
cheak_mat
来判断填入的数字是否违反数独的规则。 -
填入数字:如果确定可以填入,那么将数字t填入位置n,并且计数器count加1。
-
达到特定条件后求解数独:当计数器count的值达到某一特定值K时,执行数独求解。这里使用的求解方法是
SolveSudoku
函数,它能够检查当前数独是否有解,是否唯一。 -
处理多解情况:如果存在多个解,那么选择两个解中不同的一个数字,填入数独矩阵中,然后再次执行求解。重复这个过程直到找到唯一解。
function A = generateSudokuPuzzle(K)
% 初始化数据
A = zeros(9, 9); % 数独矩阵
count = 0; % 计数器
while count < K
n = randi(81);
t = randi(9);
% 检查位置是否已填数字
while A(n) ~= 0
n = mod(n+1, 81) + 1;
end
% 计算行、列、宫索引
i = ceil(n / 9);
j = mod(n - 1, 9) + 1;
% 检查是否可以填入数字
if cheak_mat(A, i, j, t)
A(i,j) = t;
count = count + 1;
end
end
% 求解数独
[ans1, ans2] = SolveSudoku(A);
if isempty(ans1)
disp('无解,重新开始');
A = generateSudokuPuzzle(K); % 无解时重新开始
elseif isequal(ans1, ans2)
disp('找到唯一解');
else
disp('存在多个解,处理多解情况');
% 处理多解情况
while ~isequal(ans1, ans2)
% 选择两个解中相异的位置
diffPos = find(ans1 ~= ans2, 1);
A(diffPos) = ans1(diffPos); % 选择一个解中的数字填入
% 再次求解数独
[ans1, ans2] = SolveSudoku(A);
end
disp('找到唯一解');
end
end
K的取值相当的重要,如果太少,多解的情况过多,则需要更多次数的数独求解,如果K值过大,无解的情况过多,又需要过多的次数进行验证。统计表明,K=25左右合适。
(3)调整提示数字的数量
根据游戏的难度级别(简单、中等、困难),调整数独矩阵中提示数字的总量。这一步是通过随机删除或添加一些数字来实现的,同时确保数独仍有唯一解。
以下是相关的MATLAB代码,实现了上述步骤:
% 生成数独初盘的函数
function [puzzleSudoku, solvedSudoku] = SudokuPuzzle(difficulty)
% 初始化数据
puzzleSudoku = generateSudokuPuzzle(25);
solvedSudoku = SolveSudoku(puzzleSudoku);
% 根据难度设置总提示数字的数量
switch lower(difficulty)
case '简单'
totalHints = 35 + randi(9); % 简单难度,36~45
case '中等'
totalHints = 26 + randi(9); % 中等难度,27~36
case '困难'
totalHints = 16 + randi(10); % 困难难度,17~27
end
% 调整提示数字的数量
if nnz(puzzleSudoku) == totalHints
% 如果数量相等,则保留
elseif nnz(puzzleSudoku) > totalHints
% 如果提示数过多,则随机删除一些数字
positions = randperm(81);
for idx = positions
if puzzleSudoku(idx) ~= 0
originalValue = puzzleSudoku(idx);
puzzleSudoku(idx) = 0;
[ans1, ans2] = SolveSudoku(puzzleSudoku);
if isempty(ans1) || ~isequal(ans1, ans2)
puzzleSudoku(idx) = originalValue;
end
if nnz(puzzleSudoku) == totalHints
break;
end
end
end
else
% 如果提示数不足,则随机增加数字
positions = randperm(81);
for idx = positions
if puzzleSudoku(idx) ~= 0
puzzleSudoku(idx) = solvedSudoku(idx);
if nnz(puzzleSudoku) == totalHints
break;
end
end
end
end
solvedSudoku = SolveSudoku(puzzleSudoku);
% 转化成稀疏矩阵
B = [];
for i = 1:9
for j = 1:9
if puzzleSudoku(i,j) ~= 0
B = [B;[i,j,puzzleSudoku(i,j)]];
end
end
end
puzzleSudoku = B;
end
在generateSudokuPuzzle
函数的实现中,我们通常生成的数独初盘大约包含30个提示数字。这一数字是经过精心设计的,以确保谜题既有挑战性又不至于过于复杂。值得注意的是,在添加数字的过程中,我们能够精确地控制填入的数字数量,以满足设定的初始条件。然而,在减少提示数字的阶段,尽管我们会尝试遍历并删除尽可能多的数字,但有时可能无法精确达到预期的数量。这主要是因为在确保数独仍有唯一解的前提下,某些数字可能不适宜被删除。因此,在这个过程中,我们的目标是尽可能地接近预设的提示数,以达到一个平衡点,既保证了数独谜题的解决难度,又保持了其解决的可行性。
三、设计数独游戏
在数独游戏的设计中,不仅需要关注数独谜题的生成和解决算法,还要精心设计游戏的用户界面和交互逻辑。一个直观、易操作的界面和流畅、合理的交互回调机制是提升用户体验的关键。
1、设计游戏界面
游戏界面是玩家与数独游戏互动的主要平台。在MATLAB App Designer中设计界面时,我们重点关注以下几个方面:
-
清晰的布局:在9x9的数独网格中,每个小格都应清晰可见,大小适中,以方便玩家观察和思考。
-
直观的操作:界面应包含必要的控制元素,如数字输入按钮、难度选择按钮、检查答案按钮等,确保玩家可以轻松地进行游戏操作。
-
友好的提示信息:在玩家操作过程中,界面应能提供即时的反馈信息,如错误提示、游戏成功信息等,增强游戏的互动性。
于是我们可以添加开始回调函数startupFcn
:
function startupFcn(app)
% 创建一个面板,用于存放400个文本框
p1 = uipanel('Parent', app.UIFigure, 'Position', [30, 30, 380, 380]);
% 初始化属性
app.ButtonS = cell(9, 9);
app.chosenDifficulty = '中等';
app.flg = 0;
app.Button_4.Enable = "off";
app.Button_5.Enable = "off";
% 创建按钮的过程中添加回调函数
for row = 1:9
for col = 1:9
position = [(col - 1) * 40 + 10, (row - 1) * 40 + 10, 40, 40];
app.ButtonS{row, col} = uibutton(p1, ...
'Position', position, ...
'FontSize', 20,...
'Text', '', ...
'BackgroundColor', [1, 1, 1], ...
'ButtonPushedFcn', @(btn,event) sudokuButtonPushed(app, btn, row, col) ...
);
end
end
% 更改特定子块的背景颜色
subblocks = [1,2; 2,1; 2,3; 3,2]; % 定义需要更改颜色的子块
for idx = 1:size(subblocks, 1)
blockRow = subblocks(idx, 1);
blockCol = subblocks(idx, 2);
for row = (blockRow-1)*3+1 : blockRow*3
for col = (blockCol-1)*3+1 : blockCol*3
app.ButtonS{row, col}.BackgroundColor = [0.96, 0.96, 0.96]; % 设置为红色
end
end
end
end
2、设计相关回调
回调函数是游戏动态交互的核心,它定义了玩家操作与游戏响应之间的逻辑关系。在数独游戏中,主要的回调函数包括:
-
数字填入回调:当玩家选择一个格子并输入数字时,该回调函数负责更新数独矩阵,并进行必要的合法性检查。
-
难度选择回调:玩家可以选择不同的难度级别,该回调函数根据选择调整数独谜题的难度。
-
查看答案回调:玩家在无法解决数独时,该回调函数能给出答案。
-
检查答案回调:玩家在完成数独填写后,可以使用此功能检查答案的正确性。该函数将玩家的答案与正确答案进行比对,并给出相应的反馈。
-
游戏帮助和提示:对于初学者或在游戏中遇到困难的玩家,提供帮助和提示是非常必要的。这可以通过一个专门的帮助按钮来实现,点击后弹出游戏规则说明或提示信息。
(1)数字填入回调
这个回调函数在玩家选择一个空格并输入数字时被触发。它的主要作用是更新数独矩阵中相应位置的数字,并进行必要的合法性检查。如果玩家输入的数字违反数独的规则,例如在同一行、列或3x3宫内重复,系统将提供相应的错误提示。此功能确保了玩家可以安全地尝试不同的数字,同时保持数独规则的完整性。
function sudokuButtonPushed(app, btn, row, col)
% 检查游戏是否开始
if app.flg == 0
return;
end
% 检查是否是提示数;
if isequal(btn.FontColor, [0,0,1])
uialert(app.UIFigure,"无法修改","警告");
return;
end
% 弹出对话框让用户输入数字
inputNumber = inputdlg(['请输入数字,行: ', num2str(row), ', 列: ', num2str(col)], ...
'输入数字', [1 50]);
% 检查用户是否输入了值并更新按钮文本
if ~isempty(inputNumber)
num = str2double(inputNumber{1});
if num >= 1 && num <= 9
btn.Text = inputNumber{1};
elseif ~isnan(num)
uialert(app.UIFigure,"请输入1到9的整数","警告");
else
uialert(app.UIFigure,"无效字符","警告");
end
end
end
(2)难度选择回调
这个回调函数允许玩家选择数独的难度等级,例如“简单”、“中等”或“困难”。玩家的选择将决定数独谜题中预填数字的数量,从而影响游戏的整体难度。通过这个功能,数独游戏可以满足不同玩家的需求,从初学者到经验丰富的高手都能找到合适的挑战。
function Button_3Pushed(app, event)
% 弹出确认对话框,让用户选择难度
choice = uiconfirm(app.UIFigure, '请选择数独的难度等级', ...
'选择难度', ...
'Options', {'简单', '中等', '困难'}, ...
'DefaultOption', 2, 'CancelOption', 2);
% 根据用户选择更新难度
app.chosenDifficulty = choice;
end
(3)查看答案回调
当玩家在解决数独时遇到困难,可以通过这个功能查看数独的正确答案。这个回调函数会展示整个数独谜题的解决方案,帮助玩家学习和理解数独解题的策略和技巧。这是一个非常有用的学习工具,特别是对于初学者来说。
function Button_4Pushed(app, event)
for row = 1:9
for col = 1:9
app.ButtonS{row, col}.Text = num2str(app.solvedSudoku(row, col));
end
end
app.Button_5.Enable = 'off';
end
(4)检查答案回调
这个功能允许玩家在完成数独填写后验证答案的正确性。当玩家认为已经解决了数独,可以使用这个功能来检查答案。系统会将玩家的答案与正确答案进行比较,并提供相应的反馈。如果答案正确,玩家会收到成功的提示;如果有错误,系统会提示需要重新检查。
function Button_5Pushed(app, event)
isCorrect = true; % 假设玩家填写正确
for row = 1:9
for col = 1:9
% 获取按钮上的文本并转换为数字
userInput = str2double(app.ButtonS{row, col}.Text);
% 检查用户输入是否与答案一致
if userInput ~= app.solvedSudoku(row, col)
isCorrect = false; % 如果有不一致的地方,标记为错误
break; % 退出循环
end
end
if ~isCorrect
break; % 如果已经发现错误,则不需要继续检查
end
end
% 根据检查结果弹出对话框
if isCorrect
% 如果答案正确
uialert(app.UIFigure, '恭喜你,答案正确!', '成功', 'Icon', 'success');
else
% 如果答案错误
uialert(app.UIFigure, '答案有误,请再次检查。', '错误', 'Icon', 'warning');
end
end
(5)游戏帮助和提示
为了帮助初学者或在解题过程中遇到难题的玩家,游戏提供了一个专门的帮助按钮。点击后,会弹出包含游戏规则说明和解题提示的信息框。这个功能对于提高玩家的解题技巧和游戏体验至关重要,尤其是对那些刚开始接触数独游戏的新玩家来说更是如此
function Button_2Pushed(app, event)
% 游戏帮助信息
helpMessage = "数独游戏帮助:" + newline + ...
"1. 数独是一个逻辑游戏,目标是填满9x9的网格。" + newline + ...
"2. 每行、每列以及每个3x3的小格子(也称为'宫')中必须填入1到9的数字。" + newline + ...
"3. 每个数字在每行、每列和每个小格子中只能出现一次。" + newline + ...
"4. 游戏开始时,部分格子已经填好数字,玩家需要根据这些数字来推断出剩余格子的数字。" + newline + ...
"5. 选择难度按钮可以改变游戏的难度级别。" + newline + ...
"6. 创建数独按钮会生成一个新的数独谜题。" + newline + ...
"7. 检查数独按钮用来验证您的答案是否正确。" + newline + ...
"祝您游戏愉快!";
% 显示帮助信息
uialert(app.UIFigure, helpMessage, '游戏帮助', 'Icon', 'info');
end
总结与反思
总的来说,这个项目在数独游戏设计和求解算法的有一定创新,但是数独生成时间长的问题仍然存在。
最后附上源代码
classdef SudukuGame < matlab.apps.AppBase
% Properties that correspond to app components
properties (Access = public)
UIFigure matlab.ui.Figure
Panel matlab.ui.container.Panel
Button_2 matlab.ui.control.Button
Button_5 matlab.ui.control.Button
Button_4 matlab.ui.control.Button
Button_3 matlab.ui.control.Button
Button matlab.ui.control.Button
Label matlab.ui.control.Label
end
properties (Access = public)
ButtonS % 按钮
chosenDifficulty % 难度
puzzleSudoku % 数独题目
solvedSudoku % 数独答案
flg % 游戏开始的标志
end
methods (Access = public)
function [puzzleSudoku,solvedSudoku] = SudokuPuzzle(app,difficulty)
% 输入参数:
% difficulty - 字符串,表示难度级别 ('简单', '中等', '困难')
% 首先生成一个解决的数独和最初的数独谜题
puzzleSudoku = app.generateSudokuPuzzle(25);
solvedSudoku = app.SolveSudoku(puzzleSudoku);
puzzleTotalHints = nnz(puzzleSudoku);
% 根据难度设置总提示数字的数量
switch lower(difficulty)
case '简单'
totalHints = 35 + randi(9); % 简单难度,36~45
case '中等'
totalHints = 26 + randi(9); % 中等难度,27~36
case '困难'
totalHints = 16 + randi(10); % 难度难度,17~27
end
if puzzleTotalHints == totalHints
% 如果相等,则保留结果
elseif puzzleTotalHints >= totalHints
% 如果多余,则删除提示数
% 随机排列所有81个位置
positions = randperm(81);
for idx = positions
if puzzleSudoku(idx) ~= 0
% 保存原始数值
originalValue = puzzleSudoku(idx);
puzzleSudoku(idx) = 0; % 将选中的位置置为0
% 进行数独求解
[ans1, ans2] = app.SolveSudoku(puzzleSudoku);
% 判断是否仍有唯一解
if ~isempty(ans1) && isequal(ans1, ans2)
% 如果有唯一解,则保持当前位置为0
% 否则,恢复原始数值
else
puzzleSudoku(idx) = originalValue;
end
% 检查当前数独的提示数是否在难度范围内
if nnz(puzzleSudoku) == totalHints
break;
end
end
end
else
% 如果多余,则随机增加数字
% 随机排列所有81个位置
positions = randperm(81);
for idx = positions
if puzzleSudoku(idx) ~= 0
% 选中的位置填入数字
puzzleSudoku(idx) = solvedSudoku(idx);
% 检查当前数独的提示数是否在难度范围内
if nnz(puzzleSudoku) == totalHints
break;
end
end
end
end
solvedSudoku = app.SolveSudoku(puzzleSudoku);
puzzleSudoku = app.Convert(puzzleSudoku);
end
function B = Convert(~,puzzleSudoku)
% 转化成稀疏矩阵
B = [];
for i = 1:9
for j = 1:9
if puzzleSudoku(i,j) ~= 0
B = [B;[i,j,puzzleSudoku(i,j)]];
end
end
end
end
% 回调函数定义
function sudokuButtonPushed(app, btn, row, col)
% 检查游戏是否开始
if app.flg == 0
return;
end
% 检查是否是提示数;
if isequal(btn.FontColor, [0,0,1])
uialert(app.UIFigure,"无法修改","警告");
return;
end
% 弹出对话框让用户输入数字
inputNumber = inputdlg(['请输入数字,行: ', num2str(row), ', 列: ', num2str(col)], ...
'输入数字', [1 50]);
% 检查用户是否输入了值并更新按钮文本
if ~isempty(inputNumber)
num = str2double(inputNumber{1});
if num >= 1 && num <= 9
btn.Text = inputNumber{1};
elseif ~isnan(num)
uialert(app.UIFigure,"请输入1到9的整数","警告");
else
uialert(app.UIFigure,"无效字符","警告");
end
end
end
function result = cheak_mat(~,matrix,i,j,num)
result=true;
for row=1:9
if matrix(row,j) == num
result=false;
break;
end
end
for column=1:9
if matrix(i,column) == num
result=false;
break;
end
end
I=floor((i-1)/3)+1;
J=floor((j-1)/3)+1;
for row=(3*I-2):(3*I)
for column=(3*J-2):(3*J)
if matrix(row,column) == num
result=false;
break;
end
end
end
end
function [ans1, ans2] = SolveSudoku(~,A)
B = [];
for i = 1:9
for j = 1:9
if A(i,j) ~= 0
B = [B;[i,j,A(i,j)]];
end
end
end
% 创建优化变量
x = optimvar('x', 9, 9, 9, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1);
% 创建优化问题
sudpuzzle = optimproblem;
% 添加约束:行、列、宫格内数字唯一
sudpuzzle.Constraints.consx = sum(x,1) == 1;
sudpuzzle.Constraints.consy = sum(x,2) == 1;
sudpuzzle.Constraints.consz = sum(x,3) == 1;
% 添加宫格约束
for u = 1:3
for v = 1:3
arr = x(3*(u-1)+1:3*u, 3*(v-1)+1:3*v, :);
sudpuzzle.Constraints.(['box', num2str(u), num2str(v)]) = sum(sum(arr,1),2) == 1;
end
end
% 设置初始值
for u = 1:size(B,1)
x.LowerBound(B(u,1),B(u,2),B(u,3)) = 1;
end
% 求解
[sudsoln1, ~, exitflag, ~] = solve(sudpuzzle);
% 检查第一次求解是否成功
if exitflag ~= 1
ans1 = [];
ans2 = [];
return;
end
% 提取第一个解
firstSolution = round(sudsoln1.x);
% 添加额外约束以排除第一个解
% 创建一个表示是否与第一个解至少有一个不同的单元格的逻辑变量
different = optimvar('different', 9, 9, 9, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1);
% 确保'different'至少有一个是1(与第一个解不同)
sudpuzzle.Constraints.differentAtLeastOne = sum(different, 'all') >= 1;
% 对于每个单元格,如果第一个解在那里有数字,则'different'对应的位置必须为1
for i = 1:9
for j = 1:9
for k = 1:9
if firstSolution(i,j,k) == 1
sudpuzzle.Constraints.(['different', num2str(i), num2str(j), num2str(k)]) = different(i,j,k) + x(i,j,k) == 1;
end
end
end
end
% 再次求解
[sudsoln2, ~, ~, ~] = solve(sudpuzzle);
y = ones(size(firstSolution));
for k = 2:9
y(:,:,k) = k;
end
ans1 = firstSolution.*y;
ans1 = sum(ans1,3);
y = ones(size(sudsoln2.x));
for k = 2:9
y(:,:,k) = k;
end
ans2 = sudsoln2.x.*y;
ans2 = sum(ans2,3);
end
function A = generateSudokuPuzzle(app,K)
% 初始化数据
A = zeros(9, 9); % 数独矩阵
count = 0; % 计数器
while count < K
n = randi(81);
t = randi(9);
% 检查位置是否已填数字
while A(n) ~= 0
n = mod(n+1, 81) + 1;
end
% 计算行、列、宫索引
i = ceil(n / 9);
j = mod(n - 1, 9) + 1;
% 检查是否可以填入数字
if app.cheak_mat(A, i, j, t)
A(i,j) = t;
count = count + 1;
end
end
% 求解数独
[ans1, ans2] = app.SolveSudoku(A);
if isempty(ans1)
disp('无解,重新开始');
A = app.generateSudokuPuzzle(K); % 无解时重新开始
elseif isequal(ans1, ans2)
disp('找到唯一解');
else
disp('存在多个解,处理多解情况');
% 处理多解情况
while ~isequal(ans1, ans2)
% 选择两个解中相异的位置
diffPos = find(ans1 ~= ans2, 1);
A(diffPos) = ans1(diffPos); % 选择一个解中的数字填入
% 再次求解数独
[ans1, ans2] = app.SolveSudoku(A);
end
disp('找到唯一解');
end
end
end
% Callbacks that handle component events
methods (Access = private)
% Code that executes after component creation
function startupFcn(app)
% 创建一个面板,用于存放400个文本框
p1 = uipanel('Parent', app.UIFigure, 'Position', [30, 30, 380, 380]);
% 初始化属性
app.ButtonS = cell(9, 9);
app.chosenDifficulty = '中等';
app.flg = 0;
app.Button_4.Enable = "off";
app.Button_5.Enable = "off";
% 创建按钮的过程中添加回调函数
for row = 1:9
for col = 1:9
position = [(col - 1) * 40 + 10, (row - 1) * 40 + 10, 40, 40];
app.ButtonS{row, col} = uibutton(p1, ...
'Position', position, ...
'FontSize', 20,...
'Text', '', ...
'BackgroundColor', [1, 1, 1], ...
'ButtonPushedFcn', @(btn,event) sudokuButtonPushed(app, btn, row, col) ...
);
end
end
% 更改特定子块的背景颜色
subblocks = [1,2; 2,1; 2,3; 3,2]; % 定义需要更改颜色的子块
for idx = 1:size(subblocks, 1)
blockRow = subblocks(idx, 1);
blockCol = subblocks(idx, 2);
for row = (blockRow-1)*3+1 : blockRow*3
for col = (blockCol-1)*3+1 : blockCol*3
app.ButtonS{row, col}.BackgroundColor = [0.96, 0.96, 0.96]; % 设置为红色
end
end
end
end
% Button pushed function: Button_3
function Button_3Pushed(app, event)
% 弹出确认对话框,让用户选择难度
choice = uiconfirm(app.UIFigure, '请选择数独的难度等级', ...
'选择难度', ...
'Options', {'简单', '中等', '困难'}, ...
'DefaultOption', 2, 'CancelOption', 2);
% 根据用户选择更新难度
app.chosenDifficulty = choice;
end
% Button pushed function: Button
function ButtonPushed(app, event)
app.flg = 0;
for row = 1:9
for col = 1:9
app.ButtonS{row,col}.Text = '';
app.ButtonS{row,col}.FontColor = [0,0,0];
end
end
[app.puzzleSudoku,app.solvedSudoku] = app.SudokuPuzzle(app.chosenDifficulty);
for idx = 1:size(app.puzzleSudoku, 1)
row = app.puzzleSudoku(idx, 1); % 获取行号
col = app.puzzleSudoku(idx, 2); % 获取列号
value = app.puzzleSudoku(idx, 3); % 获取值
% 在数独矩阵中填入相应的值
app.ButtonS{row, col}.Text = num2str(value);
app.ButtonS{row, col}.FontColor = [0,0,1];
end
app.flg = 1;
app.Button_4.Enable = "on";
app.Button_5.Enable = "on";
end
% Button pushed function: Button_4
function Button_4Pushed(app, event)
for row = 1:9
for col = 1:9
app.ButtonS{row, col}.Text = num2str(app.solvedSudoku(row, col));
end
end
app.Button_5.Enable = 'off';
end
% Button pushed function: Button_5
function Button_5Pushed(app, event)
isCorrect = true; % 假设玩家填写正确
for row = 1:9
for col = 1:9
% 获取按钮上的文本并转换为数字
userInput = str2double(app.ButtonS{row, col}.Text);
% 检查用户输入是否与答案一致
if userInput ~= app.solvedSudoku(row, col)
isCorrect = false; % 如果有不一致的地方,标记为错误
break; % 退出循环
end
end
if ~isCorrect
break; % 如果已经发现错误,则不需要继续检查
end
end
% 根据检查结果弹出对话框
if isCorrect
% 如果答案正确
uialert(app.UIFigure, '恭喜你,答案正确!', '成功', 'Icon', 'success');
else
% 如果答案错误
uialert(app.UIFigure, '答案有误,请再次检查。', '错误', 'Icon', 'warning');
end
end
% Button pushed function: Button_2
function Button_2Pushed(app, event)
% 游戏帮助信息
helpMessage = "数独游戏帮助:" + newline + ...
"1. 数独是一个逻辑游戏,目标是填满9x9的网格。" + newline + ...
"2. 每行、每列以及每个3x3的小格子(也称为'宫')中必须填入1到9的数字。" + newline + ...
"3. 每个数字在每行、每列和每个小格子中只能出现一次。" + newline + ...
"4. 游戏开始时,部分格子已经填好数字,玩家需要根据这些数字来推断出剩余格子的数字。" + newline + ...
"5. 选择难度按钮可以改变游戏的难度级别。" + newline + ...
"6. 创建数独按钮会生成一个新的数独谜题。" + newline + ...
"7. 检查数独按钮用来验证您的答案是否正确。" + newline + ...
"祝您游戏愉快!";
% 显示帮助信息
uialert(app.UIFigure, helpMessage, '游戏帮助', 'Icon', 'info');
end
end
% Component initialization
methods (Access = private)
% Create UIFigure and components
function createComponents(app)
% Create UIFigure and hide until all components are created
app.UIFigure = uifigure('Visible', 'off');
app.UIFigure.Position = [325 110 630 500];
app.UIFigure.Name = 'MATLAB App';
app.UIFigure.Resize = 'off';
app.UIFigure.WindowStyle = 'modal';
% Create Label
app.Label = uilabel(app.UIFigure);
app.Label.BackgroundColor = [0 0.4471 0.7412];
app.Label.HorizontalAlignment = 'center';
app.Label.FontName = '楷体';
app.Label.FontSize = 30;
app.Label.FontColor = [1 1 1];
app.Label.Position = [1 441 630 60];
app.Label.Text = '数独';
% Create Panel
app.Panel = uipanel(app.UIFigure);
app.Panel.TitlePosition = 'centertop';
app.Panel.Title = '控制面板';
app.Panel.FontName = '楷体';
app.Panel.FontSize = 20;
app.Panel.Position = [440 30 160 380];
% Create Button
app.Button = uibutton(app.Panel, 'push');
app.Button.ButtonPushedFcn = createCallbackFcn(app, @ButtonPushed, true);
app.Button.FontName = '楷体';
app.Button.FontSize = 20;
app.Button.Position = [30 285 100 34];
app.Button.Text = '创建数独';
% Create Button_3
app.Button_3 = uibutton(app.Panel, 'push');
app.Button_3.ButtonPushedFcn = createCallbackFcn(app, @Button_3Pushed, true);
app.Button_3.FontName = '楷体';
app.Button_3.FontSize = 20;
app.Button_3.Position = [30 225 100 34];
app.Button_3.Text = '难度选择';
% Create Button_4
app.Button_4 = uibutton(app.Panel, 'push');
app.Button_4.ButtonPushedFcn = createCallbackFcn(app, @Button_4Pushed, true);
app.Button_4.FontName = '楷体';
app.Button_4.FontSize = 20;
app.Button_4.Position = [30 165 100 34];
app.Button_4.Text = '数独答案';
% Create Button_5
app.Button_5 = uibutton(app.Panel, 'push');
app.Button_5.ButtonPushedFcn = createCallbackFcn(app, @Button_5Pushed, true);
app.Button_5.FontName = '楷体';
app.Button_5.FontSize = 20;
app.Button_5.Position = [30 105 100 34];
app.Button_5.Text = '检查数独';
% Create Button_2
app.Button_2 = uibutton(app.Panel, 'push');
app.Button_2.ButtonPushedFcn = createCallbackFcn(app, @Button_2Pushed, true);
app.Button_2.FontName = '楷体';
app.Button_2.FontSize = 20;
app.Button_2.Position = [30 41 100 34];
app.Button_2.Text = '游戏帮助';
% Show the figure after all components are created
app.UIFigure.Visible = 'on';
end
end
% App creation and deletion
methods (Access = public)
% Construct app
function app = SudukuGame
% Create UIFigure and components
createComponents(app)
% Register the app with App Designer
registerApp(app, app.UIFigure)
% Execute the startup function
runStartupFcn(app, @startupFcn)
if nargout == 0
clear app
end
end
% Code that executes before app deletion
function delete(app)
% Delete UIFigure when app is deleted
delete(app.UIFigure)
end
end
end