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) 将数独规则表示为约束

为了求解这个优化问题,我们需要将数独的规则转换为数学约束条件:

  1. 单元格约束
    [
    \sum_{k=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } i, j \in {1, \ldots, 9}
    ]
    这表示每个单元格(i, j)中只能填入一个数字。

  2. 行约束
    [
    \sum_{j=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } i, k \in {1, \ldots, 9}
    ]
    这表示在每一行i中,每个数字k只能出现一次。

  3. 列约束
    [
    \sum_{i=1}^{9} x_{ijk} = 1, \quad \text{对于所有 } j, k \in {1, \ldots, 9}
    ]
    这表示在每一列j中,每个数字k只能出现一次。

  4. 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)随机填充数字

这个过程包括以下几个关键步骤:

  1. 随机选择位置和数字:随机选取一个小于81的数n,代表数独矩阵中的位置,以及一个1到9之间的数t,代表要填入的数字。

  2. 检查并填入数字:检查所选位置n是否已经有数字。如果有,则n=(n+1)%81,然后重复检查。如果没有,则进一步检查是否可以将数字t填入位置n。这一步需要使用前面提到的辅助函数 cheak_mat 来判断填入的数字是否违反数独的规则。

  3. 填入数字:如果确定可以填入,那么将数字t填入位置n,并且计数器count加1。

  4. 达到特定条件后求解数独:当计数器count的值达到某一特定值K时,执行数独求解。这里使用的求解方法是 SolveSudoku 函数,它能够检查当前数独是否有解,是否唯一。

  5. 处理多解情况:如果存在多个解,那么选择两个解中不同的一个数字,填入数独矩阵中,然后再次执行求解。重复这个过程直到找到唯一解。

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,"请输入19的整数","警告");
        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的小格子(也称为'宫')中必须填入19的数字。" + 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,"请输入19的整数","警告");
                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的小格子(也称为'宫')中必须填入19的数字。" + 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
Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐