Flutter for OpenHarmony游戏集合App实战之数独格子高亮
本文介绍了数独游戏的格子高亮与选中逻辑实现。通过selectedRow和selectedCol记录选中位置,点击时更新状态并触发UI重绘。选中格子显示浅蓝色背景,未选中格子采用棋盘格配色区分宫格。通过粗细边框划分3x3宫格边界,并用不同颜色区分题目固定数字与玩家输入。文章还探讨了错误计数机制、数字按钮布局,以及扩展高亮功能(高亮关联行、列和宫格)以辅助玩家排除数字。整体实现简洁高效,提供了良好的数
通过网盘分享的文件:game_flutter_openharmony.zip
链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip
前言
数独是一个9x9的数字填充游戏,每行、每列、每个3x3宫格都要填入1-9不重复。
玩数独时,你需要选中一个格子,然后填入数字。选中的格子要有明显的高亮,让玩家知道自己在操作哪个位置。
这篇来聊聊数独的格子高亮和选中逻辑。
选中状态
int? selectedRow, selectedCol;
用两个可空整数记录选中的行和列。
int?表示可以是null,游戏开始时没有选中任何格子。
点击选中
GestureDetector(
onTap: () => setState(() { selectedRow = r; selectedCol = c; }),
点击格子时,把该格子的行列坐标存起来。
setState触发重建,格子会根据新的选中状态重新渲染。
判断是否选中
bool selected = r == selectedRow && c == selectedCol;
当前格子的行列和选中的行列相同,就是被选中的格子。
高亮颜色
color: selected ? Colors.blue[100] : (((r ~/ 3) + (c ~/ 3)) % 2 == 0 ? Colors.grey[100] : Colors.white),
这行代码有点长,拆开看:
选中时
selected ? Colors.blue[100]
选中的格子用浅蓝色背景,很醒目。
未选中时
(((r ~/ 3) + (c ~/ 3)) % 2 == 0 ? Colors.grey[100] : Colors.white)
未选中的格子用棋盘格配色,区分不同的3x3宫格。
宫格配色
(r ~/ 3) + (c ~/ 3)
r ~/ 3得到宫格的行号(0、1、2),c ~/ 3得到宫格的列号。
两者相加,奇偶交替:
0+0=0(偶) 0+1=1(奇) 0+2=2(偶)
1+0=1(奇) 1+1=2(偶) 1+2=3(奇)
2+0=2(偶) 2+1=3(奇) 2+2=4(偶)
偶数宫格用浅灰色,奇数宫格用白色,形成棋盘格效果。
💡 这个配色是为了区分宫格。数独的规则涉及3x3宫格,用不同颜色让玩家更容易看出宫格边界。
宫格边框
border: Border(
right: BorderSide(width: (c + 1) % 3 == 0 ? 2 : 0.5),
bottom: BorderSide(width: (r + 1) % 3 == 0 ? 2 : 0.5),
),
除了颜色,还用粗边框区分宫格。
右边框
(c + 1) % 3 == 0 ? 2 : 0.5
列号+1能被3整除时(第3、6、9列),右边框粗(2像素),否则细(0.5像素)。
下边框
(r + 1) % 3 == 0 ? 2 : 0.5
同样的逻辑,行号+1能被3整除时下边框粗。
这样9x9的格子被分成9个3x3的宫格,边界清晰。
外边框
Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(border: Border.all(color: Colors.black, width: 2)),
整个棋盘外面有一圈2像素的黑色边框。
数字颜色区分
Text(
board[r][c] == 0 ? '' : '${board[r][c]}',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: fixed[r][c] ? Colors.black : Colors.blue),
),
空格子
board[r][c] == 0 ? ''
值为0时显示空字符串,不显示数字。
固定数字 vs 填入数字
color: fixed[r][c] ? Colors.black : Colors.blue
- 固定数字(题目给的):黑色
- 填入数字(玩家填的):蓝色
这样玩家能区分哪些是题目,哪些是自己填的。
fixed数组
late List<List<bool>> fixed;
9x9的布尔数组,记录每个格子是否是固定的。
初始化
fixed = List.generate(9, (_) => List.filled(9, true));
一开始全部设为true。
挖空
int toRemove = 40;
while (toRemove > 0) {
int r = random.nextInt(9), c = random.nextInt(9);
if (board[r][c] != 0) {
board[r][c] = 0;
fixed[r][c] = false;
toRemove--;
}
}
随机挖掉40个格子,这些格子的fixed设为false,需要玩家填。
填入数字
void _placeNumber(int num) {
if (selectedRow == null || fixed[selectedRow!][selectedCol!]) return;
前置检查
if (selectedRow == null || fixed[selectedRow!][selectedCol!]) return;
两种情况不能填:
- 没有选中任何格子
- 选中的是固定格子(题目给的)
正确与错误
setState(() {
if (num == solution[selectedRow!][selectedCol!]) {
board[selectedRow!][selectedCol!] = num;
if (_checkWin()) _showWinDialog();
} else {
errors++;
if (errors >= 3) _showGameOverDialog();
}
});
填入的数字和答案比较:
- 正确:写入board,检查是否完成
- 错误:错误次数加1,3次错误游戏结束
数字按钮
Wrap(
spacing: 8, runSpacing: 8,
children: List.generate(9, (i) => SizedBox(
width: 40, height: 40,
child: ElevatedButton(
onPressed: () => _placeNumber(i + 1),
style: ElevatedButton.styleFrom(padding: EdgeInsets.zero),
child: Text('${i + 1}', style: const TextStyle(fontSize: 18)),
),
)),
),
9个数字按钮,1-9。
Wrap布局
Wrap(
spacing: 8, runSpacing: 8,
Wrap会自动换行,spacing是水平间距,runSpacing是行间距。
按钮大小
SizedBox(
width: 40, height: 40,
固定40x40像素,正方形按钮。
padding清零
style: ElevatedButton.styleFrom(padding: EdgeInsets.zero),
默认按钮有内边距,清零后数字能居中显示。
错误计数
Padding(padding: const EdgeInsets.all(8), child: Text('错误: $errors/3', style: const TextStyle(fontSize: 18))),
显示当前错误次数和上限。
3次错误游戏结束,给玩家一定的容错空间。
GridView配置
GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 9),
itemCount: 81,
9x9网格
crossAxisCount: 9,
每行9个格子。
81个格子
itemCount: 81,
9x9=81。
索引转换
int r = i ~/ 9, c = i % 9;
线性索引转行列坐标。
高亮的扩展
当前只高亮选中的格子。可以扩展成高亮整行、整列、整个宫格:
bool isHighlighted = r == selectedRow || c == selectedCol ||
(r ~/ 3 == selectedRow! ~/ 3 && c ~/ 3 == selectedCol! ~/ 3);
这样选中一个格子时,相关的行、列、宫格都会有浅色高亮,帮助玩家排除数字。
但当前实现简化了,只高亮选中格子。
扩展高亮
当前只高亮选中的格子。可以扩展成高亮整行、整列、整个宫格:
bool isHighlighted(int r, int c) {
if (selectedRow == null) return false;
// 同一行
if (r == selectedRow) return true;
// 同一列
if (c == selectedCol) return true;
// 同一宫格
if (r ~/ 3 == selectedRow! ~/ 3 && c ~/ 3 == selectedCol! ~/ 3) return true;
return false;
}
这样选中一个格子时,相关的行、列、宫格都会有浅色高亮。
高亮的作用
高亮相关区域有两个作用:
-
辅助排除:数独规则是行、列、宫格不能重复。高亮这些区域,玩家一眼就能看到哪些数字已经用过了。
-
视觉引导:让玩家的注意力集中在相关区域,减少视觉搜索的负担。
多层高亮
可以用不同深度的颜色区分:
Color getCellColor(int r, int c) {
if (r == selectedRow && c == selectedCol) {
return Colors.blue[200]!; // 选中格子,最深
}
if (r == selectedRow || c == selectedCol) {
return Colors.blue[50]!; // 同行同列,中等
}
if (r ~/ 3 == selectedRow! ~/ 3 && c ~/ 3 == selectedCol! ~/ 3) {
return Colors.blue[50]!; // 同宫格,中等
}
// 默认颜色
return ((r ~/ 3) + (c ~/ 3)) % 2 == 0 ? Colors.grey[100]! : Colors.white;
}
选中格子最深,相关区域次之,其他格子最浅。层次分明。
相同数字高亮
另一个有用的高亮是:高亮所有相同数字的格子。
int? highlightNumber;
// 点击格子时
onTap: () => setState(() {
selectedRow = r;
selectedCol = c;
highlightNumber = board[r][c] != 0 ? board[r][c] : null;
}),
// 判断是否高亮
bool isNumberHighlighted = highlightNumber != null && board[r][c] == highlightNumber;
选中一个有数字的格子,所有相同数字的格子都会高亮。
这帮助玩家快速找到某个数字在棋盘上的分布,判断哪里还缺这个数字。
错误高亮
填入错误数字时,可以用红色高亮提示:
bool isError(int r, int c) {
int value = board[r][c];
if (value == 0) return false;
// 检查行
for (int i = 0; i < 9; i++) {
if (i != c && board[r][i] == value) return true;
}
// 检查列
for (int i = 0; i < 9; i++) {
if (i != r && board[i][c] == value) return true;
}
// 检查宫格
int boxR = (r ~/ 3) * 3, boxC = (c ~/ 3) * 3;
for (int i = boxR; i < boxR + 3; i++) {
for (int j = boxC; j < boxC + 3; j++) {
if ((i != r || j != c) && board[i][j] == value) return true;
}
}
return false;
}
如果某个数字和同行、同列、同宫格的其他数字重复,就是错误的。
错误的格子用红色背景或红色文字提示,让玩家知道哪里填错了。
实时检查 vs 提交检查
有两种错误检查方式:
-
实时检查:填入数字后立即检查,错误立即提示。对新手友好,但降低了难度。
-
提交检查:填完所有格子后点击"检查"按钮,一次性显示所有错误。更有挑战性。
当前实现用的是实时检查(和答案比较),简化了逻辑。
候选数字
高级数独玩家会在格子里标记候选数字(可能填入的数字)。
late List<List<Set<int>>> candidates;
// 初始化
candidates = List.generate(9, (_) => List.generate(9, (_) => <int>{}));
// 切换候选数字
void toggleCandidate(int r, int c, int num) {
if (fixed[r][c] || board[r][c] != 0) return;
setState(() {
if (candidates[r][c].contains(num)) {
candidates[r][c].remove(num);
} else {
candidates[r][c].add(num);
}
});
}
用Set存储每个格子的候选数字,可以添加或删除。
显示候选数字
Widget buildCandidates(Set<int> nums) {
return GridView.count(
crossAxisCount: 3,
children: List.generate(9, (i) {
int num = i + 1;
return Center(
child: Text(
nums.contains(num) ? '$num' : '',
style: const TextStyle(fontSize: 8, color: Colors.grey),
),
);
}),
);
}
在格子里用3x3的小网格显示候选数字,字号很小(8),不会遮挡正式填入的数字。
这个功能增加了复杂度,当前实现简化了。
难度级别
数独的难度取决于挖空的数量和位置:
enum Difficulty { easy, medium, hard }
int getRemoveCount(Difficulty difficulty) {
switch (difficulty) {
case Difficulty.easy: return 30;
case Difficulty.medium: return 40;
case Difficulty.hard: return 50;
}
}
- 简单:挖30个空,剩51个数字,线索多
- 中等:挖40个空,剩41个数字
- 困难:挖50个空,剩31个数字,需要更多推理
当前实现固定挖40个空,是中等难度。
挖空的策略
随机挖空可能产生多解或无解的数独。更好的方法是:
- 生成完整的数独解
- 随机挖一个空
- 检查是否仍然唯一解
- 如果是,继续挖;如果不是,恢复这个空
- 重复直到达到目标数量
这个算法比较复杂,当前实现简化了,直接随机挖空。
计时功能
可以加个计时器,记录玩家用了多长时间:
Stopwatch stopwatch = Stopwatch();
String get timeString {
int seconds = stopwatch.elapsed.inSeconds;
int minutes = seconds ~/ 60;
seconds = seconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
Stopwatch是Dart的计时器类,可以开始、暂停、重置。
padLeft(2, '0')把数字补齐成两位,比如5变成"05"。
显示格式是"MM:SS",比如"03:45"表示3分45秒。
提示功能
可以加个提示按钮,自动填入一个正确的数字:
void giveHint() {
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
if (board[r][c] == 0) {
setState(() {
board[r][c] = solution[r][c];
// 可以用特殊颜色标记提示填入的数字
});
return;
}
}
}
}
找到第一个空格子,填入正确答案。
可以限制提示次数,比如每局只能用3次提示。
小结
这篇讲了数独的格子高亮,核心知识点:
- 可空类型:int?存储选中状态,null表示未选中
- 点击选中:GestureDetector + setState,响应用户交互
- 条件颜色:三元运算符根据选中状态设置背景色
- 宫格配色:(r~/3 + c~/3) % 2区分不同宫格,棋盘格效果
- 粗细边框:(c+1)%3==0判断宫格边界,视觉分隔
- fixed数组:区分固定数字和可填数字,保护题目
- 颜色区分:黑色固定,蓝色填入,一目了然
- Wrap布局:数字按钮自动换行,适应不同屏幕
- 扩展高亮:高亮行、列、宫格,辅助推理
- 相同数字高亮:快速定位数字分布
- 错误高亮:实时提示填错的数字
高亮是数独交互的基础,让玩家清楚地知道自己在操作哪个格子,也能辅助推理和排除。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)