Flutter for OpenHarmony数独游戏App实战:笔记功能
笔记功能的关键设计要点:数据结构使用Set存储每个单元格的候选数字;模式切换通过notesMode控制输入行为;视觉呈现使用3x3网格显示笔记;智能辅助提供自动填充和自动清理功能;冲突检测帮助玩家发现无效笔记。笔记功能是解决困难数独的必备工具。通过记录候选数字,玩家可以系统地排除不可能的选项,逐步缩小答案范围。良好的笔记功能设计可以显著提升游戏体验。欢迎加入开源鸿蒙跨平台社区:https://op
笔记功能是数独游戏的重要辅助工具。当玩家不确定某个单元格应该填什么数字时,可以先记录几个候选数字作为笔记。这个功能对于解决困难级别的数独谜题尤为重要。今天我们来详细实现数独游戏的笔记功能。
笔记数据结构
首先在GameController中定义笔记的数据结构。
class GameController extends GetxController {
List<List<Set<int>>> notes = [];
bool notesMode = false;
void generateNewGame(String diff) {
// 其他初始化代码...
notes = List.generate(9, (_) => List.generate(9, (_) => <int>{}));
notesMode = false;
}
notes是一个9x9的二维数组,每个元素是一个Set,存储该单元格的候选数字。使用Set可以自动处理重复添加的情况,并且查找效率高。notesMode标记当前是否处于笔记输入模式。
切换笔记模式
void toggleNotesMode() {
notesMode = !notesMode;
update();
}
toggleNotesMode简单地切换notesMode的值并更新UI。当notesMode为true时,点击数字键盘会添加笔记而不是填入数字。UI上的笔记按钮也会根据这个状态显示不同的样式。
添加笔记方法
void addNote(int number) {
if (selectedRow < 0 || selectedCol < 0) return;
if (isFixed[selectedRow][selectedCol]) return;
if (board[selectedRow][selectedCol] != 0) return;
int row = selectedRow;
int col = selectedCol;
Set<int> currentNotes = notes[row][col];
Set<int> previousNotes = Set.from(currentNotes);
addNote首先进行多项检查:是否有选中单元格、是否是固定数字、单元格是否已有数字。笔记只能添加到空单元格。然后获取当前笔记集合,保存副本用于撤销。
切换笔记数字
if (currentNotes.contains(number)) {
currentNotes.remove(number);
} else {
currentNotes.add(number);
}
如果笔记中已有这个数字则移除,否则添加。这种切换式的交互让操作更简便,玩家不需要区分添加和删除操作。
记录笔记操作历史
moveHistory.add(GameMove(
row: row,
col: col,
previousNotes: previousNotes,
newNotes: Set.from(currentNotes),
));
update();
}
笔记操作也记录到历史中,这样撤销功能可以恢复笔记的变化。使用Set.from创建副本,避免直接引用导致的问题。update()触发UI更新。
显示笔记的Widget
Widget _buildNotes(Set<int> notes) {
if (notes.isEmpty) return const SizedBox();
return GridView.count(
crossAxisCount: 3,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(2.w),
_buildNotes构建笔记显示组件。如果没有笔记返回空组件。GridView.count创建3x3的网格,physics禁用滚动因为笔记区域很小。
笔记数字显示
children: List.generate(9, (index) {
int num = index + 1;
return Center(
child: Text(
notes.contains(num) ? num.toString() : '',
style: TextStyle(fontSize: 8.sp, color: Colors.grey),
),
);
}),
);
}
生成9个子组件对应数字1-9。如果玩家标记了某个数字,对应位置就显示这个数字,否则显示空。笔记使用较小的字体和灰色,与正式填入的数字形成视觉区分。
在单元格中使用笔记
Widget _buildCell(GameController controller, int row, int col) {
int value = controller.board[row][col];
Set<int> cellNotes = controller.notes[row][col];
return Container(
decoration: BoxDecoration(
color: _getCellColor(controller, row, col),
),
构建单元格时获取该位置的数字和笔记。Container设置背景色,根据选中、高亮等状态决定颜色。
单元格内容显示
child: Center(
child: value != 0
? Text(
value.toString(),
style: TextStyle(
fontSize: 20.sp,
fontWeight: controller.isFixed[row][col] ? FontWeight.bold : FontWeight.normal,
color: _getTextColor(controller, row, col),
),
)
: _buildNotes(cellNotes),
),
);
}
如果单元格有数字则显示数字,否则显示笔记。这种条件渲染确保数字和笔记不会同时显示。固定数字使用粗体,玩家填入的数字使用普通字重。
填入数字时清空笔记
void enterNumber(int number) {
if (selectedRow < 0 || selectedCol < 0) return;
if (isFixed[selectedRow][selectedCol]) return;
int row = selectedRow;
int col = selectedCol;
if (notesMode) {
addNote(number);
} else {
enterNumber根据当前模式决定是添加笔记还是填入数字。notesMode为true时调用addNote,否则执行填入数字的逻辑。
记录填入操作
int previousValue = board[row][col];
Set<int> previousNotes = Set.from(notes[row][col]);
moveHistory.add(GameMove(
row: row,
col: col,
previousValue: previousValue,
newValue: number,
previousNotes: previousNotes,
newNotes: {},
));
保存旧值和旧笔记用于撤销。GameMove记录完整的状态变化,包括数字和笔记。newNotes设为空集合,因为填入数字后笔记会被清空。
执行填入并清空笔记
board[row][col] = number;
notes[row][col] = {};
update();
_checkCompletion();
}
}
填入正式数字时,同时清空该单元格的笔记。这是合理的行为,因为一旦确定了数字,候选数字就没有意义了。_checkCompletion检查游戏是否完成。
智能笔记功能
void autoFillNotes() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == 0 && !isFixed[i][j]) {
notes[i][j] = _getPossibleNumbers(i, j);
}
}
}
update();
}
autoFillNotes自动为所有空单元格填充可能的候选数字。遍历棋盘,对每个空的非固定单元格调用_getPossibleNumbers计算可能的数字。
计算可能的数字
Set<int> _getPossibleNumbers(int row, int col) {
Set<int> possible = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 排除同行的数字
for (int i = 0; i < 9; i++) {
possible.remove(board[row][i]);
}
_getPossibleNumbers计算某个位置可以填入的数字。从1-9开始,逐步排除不可能的数字。首先排除同一行已有的数字。
排除列和宫格数字
// 排除同列的数字
for (int i = 0; i < 9; i++) {
possible.remove(board[i][col]);
}
// 排除同宫格的数字
int boxRow = (row ~/ 3) * 3;
int boxCol = (col ~/ 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
possible.remove(board[boxRow + i][boxCol + j]);
}
}
return possible;
}
继续排除同列和同宫格已有的数字。boxRow和boxCol计算宫格左上角坐标。返回的集合就是该位置所有可能的候选数字。
清除所有笔记
void clearAllNotes() {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
notes[i][j] = {};
}
}
update();
}
clearAllNotes清空所有单元格的笔记。这个功能在玩家想要重新开始记笔记时很有用。遍历整个棋盘,将每个笔记集合设为空。
笔记高亮显示
Widget _buildNotes(Set<int> notes, int? highlightNumber) {
if (notes.isEmpty) return const SizedBox();
return GridView.count(
crossAxisCount: 3,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(2.w),
增强版的_buildNotes接收highlightNumber参数,用于高亮显示特定数字。当玩家选中某个有数字的单元格时,其他单元格笔记中的相同数字可以高亮。
高亮笔记数字
children: List.generate(9, (index) {
int num = index + 1;
bool isHighlighted = highlightNumber != null && num == highlightNumber;
return Center(
child: Text(
notes.contains(num) ? num.toString() : '',
style: TextStyle(
fontSize: 8.sp,
color: isHighlighted ? Colors.blue : Colors.grey,
fontWeight: isHighlighted ? FontWeight.bold : FontWeight.normal,
),
),
);
}),
);
}
如果数字与highlightNumber匹配,使用蓝色和粗体显示。这帮助玩家追踪某个数字在笔记中的分布情况,是解题的重要辅助。
笔记按钮UI
Widget _buildNotesButton(GameController controller) {
return GestureDetector(
onTap: controller.toggleNotesMode,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: controller.notesMode ? Colors.blue.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
border: controller.notesMode ? Border.all(color: Colors.blue, width: 2) : null,
),
笔记按钮在激活时有明显的视觉变化。notesMode为true时使用蓝色背景和边框,否则使用灰色背景。
按钮内容
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
controller.notesMode ? Icons.edit : Icons.edit_outlined,
size: 24.sp,
color: controller.notesMode ? Colors.blue : Colors.grey.shade700,
),
SizedBox(height: 4.h),
Text(
controller.notesMode ? '笔记开' : '笔记',
style: TextStyle(
fontSize: 12.sp,
color: controller.notesMode ? Colors.blue : Colors.grey.shade700,
),
),
],
),
),
);
}
激活时使用实心图标和"笔记开"文字,未激活时使用空心图标和"笔记"文字。这些变化让玩家清楚地知道当前处于笔记模式。
笔记冲突分析
class NoteAnalyzer {
static Set<int> getConflictingNotes(
List<List<int>> board,
List<List<Set<int>>> notes,
int row,
int col,
) {
Set<int> conflicts = {};
Set<int> cellNotes = notes[row][col];
NoteAnalyzer分析笔记中的冲突。getConflictingNotes检查笔记中的数字是否与已填入的数字冲突。
检查行列冲突
for (int note in cellNotes) {
// 检查同行是否有这个数字
for (int i = 0; i < 9; i++) {
if (board[row][i] == note) {
conflicts.add(note);
break;
}
}
// 检查同列是否有这个数字
for (int i = 0; i < 9; i++) {
if (board[i][col] == note) {
conflicts.add(note);
break;
}
}
遍历笔记中的每个数字,检查同行和同列是否已有这个数字。如果有,说明这个笔记是无效的,添加到冲突集合。
检查宫格冲突
// 检查同宫格是否有这个数字
int boxRow = (row ~/ 3) * 3;
int boxCol = (col ~/ 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] == note) {
conflicts.add(note);
break;
}
}
}
}
return conflicts;
}
}
继续检查同宫格是否有冲突。返回的集合包含所有无效的笔记数字。这个功能帮助玩家发现错误的笔记。
显示冲突笔记
Widget _buildNotesWithConflicts(Set<int> notes, Set<int> conflicts, int? highlightNumber) {
if (notes.isEmpty) return const SizedBox();
return GridView.count(
crossAxisCount: 3,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(2.w),
_buildNotesWithConflicts增加了冲突显示功能。接收conflicts参数标记哪些笔记是无效的。
冲突笔记样式
children: List.generate(9, (index) {
int num = index + 1;
bool hasNote = notes.contains(num);
bool isConflict = conflicts.contains(num);
bool isHighlighted = highlightNumber != null && num == highlightNumber;
Color textColor = Colors.grey;
if (isConflict) {
textColor = Colors.red;
} else if (isHighlighted) {
textColor = Colors.blue;
}
根据状态决定文字颜色。冲突的笔记用红色显示,高亮的用蓝色,普通的用灰色。
冲突笔记删除线
return Center(
child: Text(
hasNote ? num.toString() : '',
style: TextStyle(
fontSize: 8.sp,
color: textColor,
fontWeight: isHighlighted ? FontWeight.bold : FontWeight.normal,
decoration: isConflict ? TextDecoration.lineThrough : null,
),
),
);
}),
);
}
冲突的笔记除了红色还加上删除线,让玩家一眼就能看出哪些笔记是无效的。这种视觉区分帮助玩家快速识别问题。
自动清理无效笔记
void cleanInvalidNotes() {
bool hasChanges = false;
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (notes[row][col].isEmpty) continue;
Set<int> validNotes = {};
for (int note in notes[row][col]) {
if (_isNoteValid(row, col, note)) {
validNotes.add(note);
}
}
cleanInvalidNotes遍历所有单元格,移除与已填数字冲突的笔记。对每个笔记调用_isNoteValid检查是否有效。
更新笔记
if (validNotes.length != notes[row][col].length) {
notes[row][col] = validNotes;
hasChanges = true;
}
}
}
if (hasChanges) {
update();
}
}
如果有效笔记数量与原笔记不同,说明有无效笔记被移除。只在有变化时调用update(),避免不必要的UI更新。
检查笔记有效性
bool _isNoteValid(int row, int col, int note) {
// 检查同行
for (int i = 0; i < 9; i++) {
if (board[row][i] == note) return false;
}
// 检查同列
for (int i = 0; i < 9; i++) {
if (board[i][col] == note) return false;
}
_isNoteValid检查单个笔记是否有效。如果同行或同列已有这个数字,返回false。
检查宫格
// 检查同宫格
int boxRow = (row ~/ 3) * 3;
int boxCol = (col ~/ 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] == note) return false;
}
}
return true;
}
继续检查同宫格。如果三项检查都通过,返回true表示笔记有效。这个方法可以在玩家填入数字后自动调用。
填入数字时更新相关笔记
void _removeNoteFromRelatedCells(int row, int col, int number) {
// 清理同行的笔记
for (int i = 0; i < 9; i++) {
if (i != col) {
notes[row][i].remove(number);
}
}
当玩家填入一个数字时,自动从同行其他单元格的笔记中移除这个数字。这大大减少了玩家手动维护笔记的工作量。
清理列和宫格笔记
// 清理同列的笔记
for (int i = 0; i < 9; i++) {
if (i != row) {
notes[i][col].remove(number);
}
}
// 清理同宫格的笔记
int boxRow = (row ~/ 3) * 3;
int boxCol = (col ~/ 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
int r = boxRow + i;
int c = boxCol + j;
if (r != row || c != col) {
notes[r][c].remove(number);
}
}
}
}
同样清理同列和同宫格的笔记。排除当前单元格本身。这个功能让笔记始终保持最新状态。
为单个单元格填充笔记
void fillNotesForCell(int row, int col) {
if (board[row][col] != 0) return;
if (isFixed[row][col]) return;
Set<int> possible = _getPossibleNumbers(row, col);
moveHistory.add(GameMove(
row: row,
col: col,
previousNotes: Set.from(notes[row][col]),
newNotes: Set.from(possible),
));
notes[row][col] = possible;
update();
}
fillNotesForCell为单个单元格填充所有可能的候选数字。先检查单元格是否为空且非固定。记录到历史支持撤销,然后设置笔记并更新UI。
清空单个单元格笔记
void clearNotesForCell(int row, int col) {
if (notes[row][col].isEmpty) return;
moveHistory.add(GameMove(
row: row,
col: col,
previousNotes: Set.from(notes[row][col]),
newNotes: {},
));
notes[row][col] = {};
update();
}
clearNotesForCell清空单个单元格的笔记。如果已经为空则直接返回。记录到历史支持撤销操作。
长按显示笔记选项
Widget _buildCellWithGestures(GameController controller, int row, int col) {
return GestureDetector(
onTap: () => controller.selectCell(row, col),
onLongPress: () => _showNoteOptions(controller, row, col),
child: _buildCell(controller, row, col),
);
}
为单元格添加长按手势。onTap选中单元格,onLongPress显示笔记操作菜单。这种交互方式让高级功能不会干扰基本操作。
笔记选项菜单
void _showNoteOptions(GameController controller, int row, int col) {
if (controller.board[row][col] != 0) return;
if (controller.isFixed[row][col]) return;
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: EdgeInsets.all(16.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_showNoteOptions显示底部弹出菜单。先检查单元格是否可以添加笔记。mainAxisSize.min让菜单高度自适应内容。
菜单选项
ListTile(
leading: const Icon(Icons.auto_fix_high),
title: const Text('自动填充笔记'),
onTap: () {
controller.fillNotesForCell(row, col);
Navigator.pop(context);
},
),
ListTile(
leading: const Icon(Icons.clear),
title: const Text('清空笔记'),
onTap: () {
controller.clearNotesForCell(row, col);
Navigator.pop(context);
},
),
],
),
),
);
}
提供两个选项:自动填充笔记和清空笔记。点击后执行对应操作并关闭菜单。ListTile提供统一的菜单项样式。
笔记统计
class NoteStatistics {
final int totalNotes;
final int cellsWithNotes;
final Map<int, int> noteDistribution;
NoteStatistics({
required this.totalNotes,
required this.cellsWithNotes,
required this.noteDistribution,
});
NoteStatistics统计笔记的分布情况。totalNotes是笔记总数,cellsWithNotes是有笔记的单元格数,noteDistribution是每个数字在笔记中出现的次数。
计算统计数据
static NoteStatistics calculate(List<List<Set<int>>> notes) {
int totalNotes = 0;
int cellsWithNotes = 0;
Map<int, int> distribution = {};
for (int i = 1; i <= 9; i++) {
distribution[i] = 0;
}
calculate静态方法计算统计数据。初始化计数器和分布Map,为1-9每个数字设置初始计数为0。
遍历统计
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
Set<int> cellNotes = notes[row][col];
if (cellNotes.isNotEmpty) {
cellsWithNotes++;
totalNotes += cellNotes.length;
for (int note in cellNotes) {
distribution[note] = (distribution[note] ?? 0) + 1;
}
}
}
}
return NoteStatistics(
totalNotes: totalNotes,
cellsWithNotes: cellsWithNotes,
noteDistribution: distribution,
);
}
}
遍历所有单元格,统计有笔记的单元格数、笔记总数和每个数字的出现次数。这些统计可以帮助玩家了解解题进度。
显示笔记统计
Widget _buildNoteStats(GameController controller) {
NoteStatistics stats = NoteStatistics.calculate(controller.notes);
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8.r),
),
_buildNoteStats构建笔记统计面板。调用calculate获取统计数据,使用Container设置背景和圆角。
统计内容
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('笔记统计', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
Text('总笔记数:${stats.totalNotes}'),
Text('有笔记的单元格:${stats.cellsWithNotes}'),
SizedBox(height: 8.h),
显示标题和基本统计数据。使用Column垂直排列,crossAxisAlignment.start让内容左对齐。
数字分布
Wrap(
spacing: 8.w,
runSpacing: 4.h,
children: List.generate(9, (index) {
int num = index + 1;
int count = stats.noteDistribution[num] ?? 0;
return Chip(
label: Text('$num: $count'),
backgroundColor: count > 0 ? Colors.blue.shade50 : Colors.grey.shade200,
);
}),
),
],
),
);
}
使用Wrap和Chip展示每个数字的分布。有笔记的数字用蓝色背景突出显示,没有的用灰色。这种可视化帮助玩家快速了解笔记情况。
总结
笔记功能的关键设计要点:数据结构使用Set存储每个单元格的候选数字;模式切换通过notesMode控制输入行为;视觉呈现使用3x3网格显示笔记;智能辅助提供自动填充和自动清理功能;冲突检测帮助玩家发现无效笔记。
笔记功能是解决困难数独的必备工具。通过记录候选数字,玩家可以系统地排除不可能的选项,逐步缩小答案范围。良好的笔记功能设计可以显著提升游戏体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)