通过网盘分享的文件: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;
}

这样选中一个格子时,相关的行、列、宫格都会有浅色高亮。

高亮的作用

高亮相关区域有两个作用:

  1. 辅助排除:数独规则是行、列、宫格不能重复。高亮这些区域,玩家一眼就能看到哪些数字已经用过了。

  2. 视觉引导:让玩家的注意力集中在相关区域,减少视觉搜索的负担。

多层高亮

可以用不同深度的颜色区分:

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 提交检查

有两种错误检查方式:

  1. 实时检查:填入数字后立即检查,错误立即提示。对新手友好,但降低了难度。

  2. 提交检查:填完所有格子后点击"检查"按钮,一次性显示所有错误。更有挑战性。

当前实现用的是实时检查(和答案比较),简化了逻辑。

候选数字

高级数独玩家会在格子里标记候选数字(可能填入的数字)。

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个空,是中等难度。

挖空的策略

随机挖空可能产生多解或无解的数独。更好的方法是:

  1. 生成完整的数独解
  2. 随机挖一个空
  3. 检查是否仍然唯一解
  4. 如果是,继续挖;如果不是,恢复这个空
  5. 重复直到达到目标数量

这个算法比较复杂,当前实现简化了,直接随机挖空。

计时功能

可以加个计时器,记录玩家用了多长时间:

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐