Java Swing桌面版数独小游戏(2)
·
1、演示视频
Java Swing桌面版数独小游戏
2、项目截图







设计说明
3.1 整体架构设计
本项目采用单窗体架构,所有功能均集中在一个JFrame窗体中实现,主要分为三个界面区域和核心功能模块:
- 顶部难度选择区域:包含简单、中等、困难三个按钮,用于切换游戏难度;
- 中间数独棋盘区域:采用9x9的网格布局,每个单元格为一个JTextField组件,用于显示原始数字和接收用户输入;
- 底部功能按钮区域:包含提示和重置两个按钮,用于触发对应功能;
- 核心功能模块:包括数独生成模块、输入处理模块、数据校验模块、界面更新模块等,负责实现游戏的核心逻辑。
3.2 类结构设计
项目主要包含两个类:主类SudokuGame和内部类SudokuNumericDocument,具体职责如下:
3.2.1 主类 SudokuGame
作为项目的入口类,继承自JFrame,负责窗体的初始化、界面组件的创建、事件监听的绑定以及核心业务逻辑的实现。主要包含以下成员变量和方法:
- 成员变量:存储数独解决方案的二维数组
solution、存储用户可见数独数据的二维数组board、存储输入框组件的二维数组cellFields、标记当前难度的枚举变量currentDifficulty、标记数独是否完成的布尔变量isSudokuCompleted等; - 核心方法:数独生成方法
generateSudoku、回溯填充方法fillBoard、挖空优化方法removeCellsOptimized、界面强制更新方法updateBoardUIForce、输入验证方法validateInput、数独完成校验方法checkSudokuCompleted、提示方法showHint等。
3.2.2 内部类 SudokuNumericDocument
继承自PlainDocument,作为自定义文档模型,负责过滤用户的输入内容,仅允许输入1-9的单个数字,避免使用DocumentListener导致的锁冲突问题。主要包含以下成员变量和方法:
- 成员变量:存储单元格行号的
row、列号的col、关联的输入框组件field; - 核心方法:重写的
insertString方法,实现输入内容的过滤和单个数字的插入逻辑。
3.3 数据设计
项目使用两个9x9的二维整型数组存储数独数据:
solution数组:存储完整的数独解决方案,由回溯算法生成,作为用户输入验证的依据,在游戏过程中不会被修改;board数组:存储用户可见的数独数据,是solution数组的副本,经过挖空处理后得到,会随着用户输入正确数字或点击提示按钮而更新,用于记录数独的填充状态。
3.4 界面设计
界面采用简洁的设计风格,注重实用性和视觉区分度:
- 数独棋盘的每个单元格采用不同粗细的边框,区分3x3的宫格,提升视觉层次感;
- 不同类型的数字采用不同的颜色:原始数字为灰色,用户正确输入数字为蓝色,用户错误输入数字为红色,提示数字为橙色;
- 输入框采用居中对齐,字体大小为20号,保证数字显示清晰;
- 窗体大小固定为500x600,居中显示,适配主流屏幕分辨率。
四、算法说明
4.1 数独生成算法(回溯算法)
数独的生成核心依赖于回溯算法,该算法是一种深度优先搜索算法,通过尝试填充数字并回溯来找到可行的解。本项目中,回溯算法不仅用于生成完整的数独解决方案,还保证了数独的合法性(每行、每列、每个3x3宫格内数字不重复)。
4.1.1 算法流程
- 从数独的第一个单元格(0,0)开始,依次遍历每个单元格;
- 对于当前空单元格,生成1-9的随机顺序数字列表,避免每次生成的数独相同;
- 遍历随机数字列表,判断当前数字是否符合数独规则(行、列、宫格内无重复);
- 若符合规则,将该数字填入单元格,并递归处理下一个单元格;
- 若递归处理下一个单元格返回成功,则说明数独已填充完成,返回true;
- 若递归处理下一个单元格返回失败,则清空当前单元格的数字,尝试下一个数字;
- 若所有数字都尝试失败,返回false,触发回溯。
4.1.2 数独合法性校验
在填充数字时,需要校验该数字是否符合数独规则,校验逻辑如下:
- 校验当前行:遍历当前行的所有单元格,判断是否存在相同数字;
- 校验当前列:遍历当前列的所有单元格,判断是否存在相同数字;
- 校验当前3x3宫格:计算当前单元格所在宫格的起始坐标,遍历宫格内的所有单元格,判断是否存在相同数字;
- 若以上三项校验均通过,则数字合法,否则不合法。
4.2 挖空优化算法
生成完整的数独解决方案后,需要进行挖空处理,生成不同难度的数独谜题。为避免过度挖空导致数字缺失或解谜难度过高,本项目采用了优化的挖空算法:
- 收集数独中所有非空单元格的坐标,存入列表并打乱顺序,保证挖空的随机性;
- 遍历打乱后的坐标列表,依次移除单元格的数字,直到达到指定的挖空数量;
- 在移除数字后,校验该单元格所在的3x3宫格内的数字数量,若少于2个,则恢复该数字,避免宫格内数字过少导致视觉缺失或解谜困难;
- 最终得到的数独谜题既满足难度要求,又保证了视觉完整性和解谜的可行性。
4.3 输入过滤算法
输入过滤算法集成在自定义文档模型的insertString方法中,负责过滤非法输入,仅保留合法的单个数字:
- 判断输入的字符串是否为空或输入框是否不可编辑,若是则直接返回;
- 遍历输入字符串的每个字符,提取第一个1-9的数字字符,忽略其他字符;
- 若未提取到合法数字,直接返回;
- 清空输入框原有内容,插入提取到的合法数字,确保输入框只有一个数字;
- 异步调用验证方法,判断数字是否正确并标记颜色。
五、测试说明
5.1 测试环境
| 测试项 | 配置/环境 |
|---|---|
| 操作系统 | Windows 10/11、Linux Ubuntu 20.04、Mac OS 12 |
| JDK版本 | JDK 8、JDK 11、JDK 17 |
| 运行方式 | 命令行编译运行、IDE(Eclipse/IntelliJ IDEA)运行 |
5.2 功能测试用例
| 测试功能 | 测试步骤 | 预期结果 | 测试结果 |
|---|---|---|---|
| 难度选择功能 | 1. 启动程序,默认显示简单难度数独; 2. 点击“中等”按钮; 3. 点击“困难”按钮; 4. 再次点击“简单”按钮。 |
1. 界面显示简单难度数独(20个空缺); 2. 界面刷新为中等难度数独(30个空缺); 3. 界面刷新为困难难度数独(40个空缺); 4. 界面刷新为简单难度数独。 |
通过 |
| 输入过滤功能 | 1. 点击空缺单元格,输入字母“a”; 2. 输入符号“@”; 3. 输入数字“123”; 4. 输入数字“5”。 |
1. 输入框无内容; 2. 输入框无内容; 3. 输入框仅显示“1”; 4. 输入框显示“5”。 |
通过 |
| 输入验证功能 | 1. 点击空缺单元格,输入正确数字; 2. 输入错误数字。 |
1. 数字显示为蓝色; 2. 数字显示为红色。 |
通过 |
| 提示功能 | 1. 点击“提示”按钮; 2. 多次点击“提示”按钮,直到数独填满; 3. 再次点击“提示”按钮。 |
1. 随机空缺单元格填充橙色数字,变为不可编辑; 2. 所有空缺单元格被填充; 3. 弹出“数独已经完成,无需提示”提示框。 |
通过 |
| 重置功能 | 1. 输入若干数字,点击“提示”按钮; 2. 点击“重置”按钮。 |
1. 数独包含用户输入和提示数字; 2. 界面刷新为当前难度的新数独,所有用户输入和提示数字被清空。 |
通过 |
| 数独完成提示功能 | 1. 填满所有空缺单元格且数字均正确; 2. 点击提示按钮填满所有空缺单元格。 |
1. 弹出“恭喜!你已成功完成数独!”提示框; 2. 弹出“恭喜!你已成功完成数独!”提示框。 |
通过 |
5.3 边界测试
- 空输入测试:输入空字符串或仅按空格键,输入框无变化,符合预期;
- 最大数字测试:输入数字“9”,输入框正常显示,验证正确后显示蓝色,符合预期;
- 重复提示测试:多次点击提示按钮,不会重复填充同一单元格,符合预期;
- 重复完成提示测试:数独完成后,再次输入数字或点击提示按钮,不会重复弹出完成提示框,符合预期。
5.4 兼容性测试
在不同操作系统(Windows 10、Linux Ubuntu 20.04、Mac OS 12)和不同JDK版本(JDK 8、JDK 11、JDK 17)下,程序均能正常编译运行,界面显示正常,所有功能均可正常使用,无兼容性问题。
六、关键代码
6.1 数独生成核心代码(回溯算法)
/**
* 生成完整的数独解决方案(使用回溯算法)
* @return 9x9的数独数组
*/
private int[][] generateSudoku() {
int[][] board = new int[SIZE][SIZE];
// 从(0,0)开始填充数独
fillBoard(board, 0, 0);
return board;
}
/**
* 回溯算法填充数独棋盘
* @param board 数独棋盘
* @param row 当前行
* @param col 当前列
* @return 是否填充成功
*/
private boolean fillBoard(int[][] board, int row, int col) {
// 到达最后一列,切换到下一行第一列
if (col == SIZE) {
row++;
col = 0;
// 所有行都填充完毕,返回成功
if (row == SIZE) {
return true;
}
}
// 如果当前位置已有数字(递归过程中填充的),直接处理下一个位置
if (board[row][col] != 0) {
return fillBoard(board, row, col + 1);
}
// 生成1-9的随机顺序数字,避免每次生成的数独都一样
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= SIZE; i++) {
numbers.add(i);
}
Collections.shuffle(numbers);
// 尝试填充每个数字
for (int num : numbers) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
// 递归处理下一个位置,如果成功则返回true
if (fillBoard(board, row, col + 1)) {
return true;
}
// 回溯:如果下一个位置填充失败,清空当前位置
board[row][col] = 0;
}
}
// 所有数字都尝试过,填充失败
return false;
}
/**
* 验证数字是否可以填入指定位置(行、列、3x3宫格无重复)
* @param board 数独棋盘
* @param row 行号
* @param col 列号
* @param num 要填入的数字
* @return 是否有效
*/
private boolean isValid(int[][] board, int row, int col, int num) {
// 检查行:当前行是否已有该数字
for (int c = 0; c < SIZE; c++) {
if (board[row][c] == num) {
return false;
}
}
// 检查列:当前列是否已有该数字
for (int r = 0; r < SIZE; r++) {
if (board[r][col] == num) {
return false;
}
}
// 检查3x3宫格:当前宫格是否已有该数字
int gridRowStart = (row / GRID) * GRID;
int gridColStart = (col / GRID) * GRID;
for (int r = gridRowStart; r < gridRowStart + GRID; r++) {
for (int c = gridColStart; c < gridColStart + GRID; c++) {
if (board[r][c] == num) {
return false;
}
}
}
// 所有检查通过,数字有效
return true;
}
6.2 挖空优化核心代码
/**
* 优化的挖空逻辑:避免过度挖空,确保每个3x3宫格都有足够的数字
* @param board 数独棋盘
* @param count 要移除的单元格数量
*/
private void removeCellsOptimized(int[][] board, int count) {
Random random = new Random();
int removed = 0;
// 存储所有非空单元格的坐标,确保挖空时只从这些位置选择
List<Point> filledCells = new ArrayList<>();
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
filledCells.add(new Point(row, col));
}
}
// 打乱顺序,随机挖空
Collections.shuffle(filledCells);
// 循环直到移除指定数量的单元格,或没有可移除的单元格
for (Point p : filledCells) {
if (removed >= count) {
break;
}
int row = p.x;
int col = p.y;
// 确保当前单元格有数字,才进行挖空
if (board[row][col] != 0) {
// 临时保存数字,用于校验挖空后数独是否仍有解(简单校验:至少每个宫格保留2个数字)
int temp = board[row][col];
board[row][col] = 0;
removed++;
// 简单校验:如果当前3x3宫格的数字少于2个,恢复数字(避免宫格数字过少)
if (getGridFilledCount(board, row, col) < 2) {
board[row][col] = temp;
removed--;
}
}
}
}
/**
* 获取指定单元格所在3x3宫格的已填充数字数量
* @param board 数独棋盘
* @param row 行号
* @param col 列号
* @return 宫格内已填充的数字数量
*/
private int getGridFilledCount(int[][] board, int row, int col) {
int gridRowStart = (row / GRID) * GRID;
int gridColStart = (col / GRID) * GRID;
int count = 0;
for (int r = gridRowStart; r < gridRowStart + GRID; r++) {
for (int c = gridColStart; c < gridColStart + GRID; c++) {
if (board[r][c] != 0) {
count++;
}
}
}
return count;
}
6.3 自定义输入过滤文档模型核心代码
/**
* 自定义文档模型:仅过滤非1-9的字符和单数字限制,确保输入流畅
*/
private class SudokuNumericDocument extends PlainDocument {
private final int row; // 单元格行号
private final int col; // 单元格列号
private final JTextField field; // 关联的文本框
/**
* 构造方法:直接传入文本框
* @param row 行号
* @param col 列号
* @param field 关联的文本框
*/
public SudokuNumericDocument(int row, int col, JTextField field) {
this.row = row;
this.col = col;
this.field = field;
}
/**
* 重写插入字符串方法,仅保留必要的过滤逻辑
* @param offs 插入位置
* @param str 插入的字符串
* @param a 属性集
* @throws BadLocationException 位置异常
*/
@Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
// 1. 如果字符串为空或文本框不可编辑,直接返回
if (str == null || str.isEmpty() || !field.isEditable()) {
return;
}
// 2. 只保留第一个1-9的数字字符
char validChar = 0;
for (char c : str.toCharArray()) {
if (c >= '1' && c <= '9') {
validChar = c;
break;
}
}
// 3. 没有有效数字,直接返回
if (validChar == 0) {
return;
}
// 4. 先清空原有内容,再插入新数字(确保单个数字)
super.remove(0, getLength());
super.insertString(0, String.valueOf(validChar), a);
// 5. 异步验证输入的数字并标记颜色
int num = Character.getNumericValue(validChar);
SwingUtilities.invokeLater(() -> validateInput(field, row, col, num));
}
}
6.4 数独完成校验核心代码
/**
* 验证用户输入的数字是否正确,并通过颜色标记结果
* @param field 输入的文本框
* @param row 行号
* @param col 列号
* @param num 输入的数字
*/
private void validateInput(JTextField field, int row, int col, int num) {
// 增加空值判断:如果field为null或不可编辑,直接返回
if (field == null || !field.isEditable()) {
return;
}
// 检查数字是否符合数独规则(是否和解决方案一致)
if (num == solution[row][col]) {
field.setForeground(Color.BLUE); // 正确:蓝色
// 更新棋盘数据(记录用户正确的输入)
board[row][col] = num;
// 验证后检查数独是否完成
checkSudokuCompleted();
} else {
field.setForeground(Color.RED); // 错误:红色
}
}
/**
* 检查数独是否完成:所有单元格都有数字且与解决方案一致
*/
private void checkSudokuCompleted() {
// 如果已经标记为完成,直接返回(避免重复弹窗)
if (isSudokuCompleted) {
return;
}
// 遍历所有单元格,检查是否都有正确的数字
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
// 有单元格为空,或数字与解决方案不一致,说明未完成
if (board[row][col] == 0 || board[row][col] != solution[row][col]) {
return;
}
}
}
// 标记为完成
isSudokuCompleted = true;
// 弹出完成提示
JOptionPane.showMessageDialog(this, "恭喜!你已成功完成数独!", "数独完成", JOptionPane.INFORMATION_MESSAGE);
}
6.5 提示功能核心代码
/**
* 提示功能:随机选择一个空缺的单元格,填充正确的数字(标记为橙色,区分原始数字)
*/
private void showHint() {
List<Point> emptyCells = new ArrayList<>();
// 收集所有空缺的单元格坐标
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (board[row][col] == 0) {
emptyCells.add(new Point(row, col));
}
}
}
// 如果没有空缺单元格,提示游戏完成(兜底逻辑)
if (emptyCells.isEmpty()) {
JOptionPane.showMessageDialog(this, "数独已经完成,无需提示!", "提示", JOptionPane.INFORMATION_MESSAGE);
return;
}
// 随机选择一个空缺单元格
Random random = new Random();
Point point = emptyCells.get(random.nextInt(emptyCells.size()));
int row = point.x;
int col = point.y;
// 填充正确的数字,并设为不可编辑(提示的数字)
JTextField field = cellFields[row][col];
field.setText(String.valueOf(solution[row][col]));
field.setEditable(false);
field.setForeground(Color.ORANGE); // 提示数字为橙色,区分原始灰色数字
// 更新棋盘数据,标记该位置已填充(避免重复提示)
board[row][col] = solution[row][col];
}更多推荐

所有评论(0)