HarmonyOS ArkTS 九宫数独游戏实现技术



一、游戏概述
九宫数独是一款经典的数字逻辑推理游戏,也是本项目的终极挑战。玩家需要在9×9的网格中填入数字1-9,使得每行、每列和每个3×3宫格内的数字都不重复。本项目基于HarmonyOS ArkTS技术栈实现,支持4×4、6×6和9×9三种网格尺寸,包含100个难度递进的关卡。
1.1 数独规则
数独游戏的核心规则非常简单:
- 在一个n×n的网格中填入数字1-n
- 每行的数字不能重复
- 每列的数字不能重复
- 每个宫格(Box)内的数字不能重复
对于标准9×9数独:
- 每行9个数字1-9不重复
- 每列9个数字1-9不重复
- 每个3×3宫格9个数字1-9不重复
1.2 网格尺寸支持
本项目支持三种网格尺寸:
| 尺寸 | 宫格划分 | 难度定位 | 适用关卡 |
|---|---|---|---|
| 4×4 | 2×2宫格 | 入门 | 1-25关 |
| 6×6 | 2×3宫格 | 简单 | 26-50关 |
| 9×9 | 3×3宫格 | 进阶/困难 | 51-100关 |
1.3 技术架构
┌─────────────────────────────────────────────────────────────┐
│ 九宫数独游戏架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ SudokuLevelPage │──────▶│ SudokuPage │ │
│ │ (关卡选择页面) │ │ (游戏主页面) │ │
│ └──────────────────┘ └──────────┬────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ AppState.ets │ │
│ │ - completedLevels: Array<number> │ │
│ │ - levelTimes: Array<number> │ │
│ │ - currentGame: GameState │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ SudokuGenerator.ets │ │
│ │ - generate(): 生成完整数独解 │ │
│ │ - generatePuzzle(): 生成谜题 │ │
│ │ - solve(): 回溯算法求解 │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ 数据模型 │ │
│ │ - SudokuBoard: 数独棋盘 │ │
│ │ - CellData: 单元格数据 │ │
│ │ - Difficulty: 难度枚举 │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
1.4 功能特性
- 多种网格尺寸:4×4、6×6、9×9三种规格
- 100个关卡:从入门到困难的完整难度梯度
- 实时验证:输入时自动检测错误
- 撤销/重做:支持无限步操作回退
- 提示功能:自动填入一个正确数字
- 暂停功能:随时暂停计时
- 通关提示:完成关卡后显示恭喜界面
- 进度保存:自动保存已完成关卡和最佳时间
二、关卡系统设计
2.1 关卡分类结构
数独关卡系统采用四级分类,每类25关,总计100关:
@State categories: Array<LevelCategory> = [
new LevelCategory('entry', '入门', '4宫格', 1, 25, 4, '#FFD700'),
new LevelCategory('easy', '简单', '6宫格', 26, 50, 6, '#FFA500'),
new LevelCategory('advanced', '进阶', '9宫格', 51, 75, 9, '#FF69B4'),
new LevelCategory('difficult', '困难', '9宫格', 76, 100, 9, '#FF6347')
];
分类设计说明:
| 分类 | 网格尺寸 | 宫格划分 | 难度特点 |
|---|---|---|---|
| 入门 | 4×4 | 2×2 | 数字范围小,规则简单,适合初学者 |
| 简单 | 6×6 | 2×3 | 数字范围扩大,宫格形状不规则,增加难度 |
| 进阶 | 9×9 | 3×3 | 标准数独,中等难度,移除45%的数字 |
| 困难 | 9×9 | 3×3 | 标准数独,高难度,移除70%的数字 |
2.2 关卡选择页面布局
关卡选择页面采用可折叠的分类卡片设计:
build() {
Column() {
Row() {
Button('← 返回')
.fontSize(14)
.fontColor('#FFD700')
.backgroundColor('transparent')
.borderColor('#FFD700')
.borderWidth(1)
.padding({ left: 12, right: 12 })
.onClick(() => {
router.back();
})
Text('数独关卡')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD700')
Button('解锁')
.fontSize(12)
.fontColor('#FFD700')
.backgroundColor('rgba(255, 215, 0, 0.1)')
.borderColor('#FFD700')
.borderWidth(1)
.borderRadius(8)
.padding({ left: 10, right: 10 })
.onClick(() => {
this.unlockAllLevels();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })
.margin({ top: 60 })
Scroll() {
Column({ space: 12 }) {
ForEach(this.categories, (category: LevelCategory) => {
Column() {
Button() {
Row() {
Column({ space: 4 }) {
Text(category.name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(category.color)
Text(category.description)
.fontSize(12)
.fontColor('#87CEEB')
}
Blank()
Text(category.isExpanded ? '▼' : '▶')
.fontSize(16)
.fontColor('#FFD700')
}
.width('100%')
}
.width('90%')
.height(60)
.backgroundColor('rgba(255, 255, 255, 0.05)')
.borderColor(category.color + '40')
.borderWidth(1)
.borderRadius(12)
.onClick(() => {
this.toggleCategory(category);
})
if (category.isExpanded) {
Column({ space: 8 }) {
Grid() {
ForEach(this.getLevels(category), (level: number) => {
GridItem() {
Column() {
Text(level.toString())
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isLevelCompleted(level) ? '#FFD700' : '#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor(this.isLevelCompleted(level) ? 'rgba(255, 215, 0, 0.2)' : 'rgba(255, 255, 255, 0.05)')
.borderColor(category.color + '60')
.borderWidth(1)
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => {
this.startLevel(level);
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr')
.width('85%')
.height(300)
.backgroundColor('rgba(10, 25, 47, 0.8)')
.borderRadius(12)
}
}
}
})
}
.padding({ top: 16, bottom: 200 })
}
.width('100%')
.flexGrow(1)
.backgroundColor('#0A192F')
}
.width('100%')
.height('100%')
.backgroundColor('#0A192F')
.padding({ bottom: 60 })
}
布局特点:
- 顶部导航栏:返回按钮、标题、解锁按钮
- 可折叠分类卡片:点击展开/收起关卡网格
- 5×5关卡网格:每类25个关卡,金色标记已完成
- 深蓝色背景:统一的主题风格
2.3 关卡启动流程
private startLevel(level: number): void {
appState.startNewGameByLevel(level);
router.pushUrl({ url: 'pages/SudokuPage' });
}
关卡启动时,通过AppState生成对应难度的数独谜题,然后跳转到游戏页面。
三、数独生成算法
3.1 回溯算法原理
数独生成的核心是回溯算法(Backtracking),这是一种经典的暴力搜索算法,通过递归尝试所有可能的解决方案。
算法流程:
- 从第一个空格开始(索引0)
- 尝试填入1-n的数字(随机打乱顺序以生成不同的谜题)
- 检查当前数字是否有效(行、列、宫格不重复)
- 如果有效,继续填充下一个空格
- 如果无效,尝试下一个数字
- 如果所有数字都无效,回溯到上一个空格
- 如果所有空格都填满,返回成功
3.2 数独生成器实现
export class SudokuGenerator {
private board: Array<number> = [];
private size: number = 9;
generate(size: number): Array<number> {
this.size = size;
this.board = new Array<number>(size * size);
for (let i = 0; i < size * size; i++) {
this.board[i] = 0;
}
this.solve(0);
return this.board;
}
设计说明:
board:存储数独棋盘的一维数组size:网格尺寸(4、6或9)generate():公共入口方法,初始化空棋盘并调用求解方法
3.3 回溯求解核心
private solve(index: number): boolean {
let totalSize: number = this.size * this.size;
if (index === totalSize) {
return true;
}
let row: number = Math.floor(index / this.size);
let col: number = index % this.size;
let numbers: Array<number> = this.shuffle(this.getNumberArray());
for (let i = 0; i < numbers.length; i++) {
let num: number = numbers[i];
if (this.isValid(row, col, num)) {
this.board[index] = num;
if (this.solve(index + 1)) {
return true;
}
this.board[index] = 0;
}
}
return false;
}
算法详解:
- 终止条件:当
index等于总格子数时,说明所有格子都已填满,返回true - 坐标计算:通过
index计算当前格子的行和列 - 随机数字序列:使用Fisher-Yates算法打乱数字顺序,确保每次生成的数独不同
- 递归尝试:尝试每个数字,有效则递归填充下一格
- 回溯:如果当前路径失败,将当前格子置0并尝试下一个数字
3.4 有效性检查
private isValid(row: number, col: number, num: number): boolean {
for (let i = 0; i < this.size; i++) {
if (this.board[row * this.size + i] === num) return false;
if (this.board[i * this.size + col] === num) return false;
}
let boxRowSize: number = this.getBoxRowSize(this.size);
let boxColSize: number = this.getBoxColSize(this.size);
let boxRow: number = Math.floor(row / boxRowSize) * boxRowSize;
let boxCol: number = Math.floor(col / boxColSize) * boxColSize;
for (let r = 0; r < boxRowSize; r++) {
for (let c = 0; c < boxColSize; c++) {
if (this.board[(boxRow + r) * this.size + boxCol + c] === num) return false;
}
}
return true;
}
三层验证:
- 行验证:检查当前行是否已有相同数字
- 列验证:检查当前列是否已有相同数字
- 宫格验证:检查当前宫格是否已有相同数字
3.5 宫格尺寸计算
不同网格尺寸的宫格划分不同:
private getBoxRowSize(size: number): number {
if (size === 4) return 2;
if (size === 6) return 2;
return 3;
}
private getBoxColSize(size: number): number {
if (size === 4) return 2;
if (size === 6) return 3;
return 3;
}
| 网格尺寸 | 宫格行数 | 宫格列数 | 宫格数量 |
|---|---|---|---|
| 4×4 | 2 | 2 | 4个2×2宫格 |
| 6×6 | 2 | 3 | 6个2×3宫格 |
| 9×9 | 3 | 3 | 9个3×3宫格 |
3.6 Fisher-Yates洗牌算法
private shuffle(array: Array<number>): Array<number> {
let result: Array<number> = [...array];
for (let i = result.length - 1; i > 0; i--) {
let j: number = Math.floor(Math.random() * (i + 1));
let temp: number = result[i];
result[i] = result[j];
result[j] = temp;
}
return result;
}
算法优势:
- O(n)时间复杂度,效率高
- 原地交换,空间复杂度O(1)
- 真正的随机排列,每个排列出现的概率相等
四、谜题生成算法
4.1 难度控制策略
生成完整的数独解后,需要根据难度移除一定数量的格子来生成谜题:
generatePuzzle(solution: Array<number>, difficulty: Difficulty, size: number): Array<number> {
let puzzle: Array<number> = [...solution];
let removeCount: number = 0;
let totalCells: number = size * size;
switch (difficulty) {
case Difficulty.EASY:
removeCount = Math.floor(totalCells * 0.4);
break;
case Difficulty.MEDIUM:
removeCount = Math.floor(totalCells * 0.55);
break;
case Difficulty.HARD:
removeCount = Math.floor(totalCells * 0.7);
break;
}
let positions: Array<number> = [];
for (let i = 0; i < totalCells; i++) {
positions.push(i);
}
positions = this.shuffle(positions);
for (let i = 0; i < removeCount && i < positions.length; i++) {
puzzle[positions[i]] = 0;
}
return puzzle;
}
难度参数:
| 难度 | 移除比例 | 剩余格子数(9×9) | 剩余格子数(6×6) | 剩余格子数(4×4) |
|---|---|---|---|---|
| 简单 | 40% | 49个 | 22个 | 10个 |
| 中等 | 55% | 37个 | 17个 | 7个 |
| 困难 | 70% | 24个 | 11个 | 5个 |
4.2 谜题生成流程
完整数独解生成 → 随机打乱位置 → 根据难度移除格子 → 返回谜题
关键设计:
- 基于完整解:先生成完整的数独解,再移除格子,确保谜题有唯一解
- 随机移除:使用洗牌算法随机选择要移除的位置
- 难度递进:移除比例越高,难度越大
五、游戏主页面实现
5.1 状态管理设计
@Entry
@Component
struct SudokuPage {
@State cellValues: Array<number> = [];
@State cellIsOriginal: Array<boolean> = [];
@State cellIsSelected: Array<boolean> = [];
@State cellIsError: Array<boolean> = [];
@State cellIsHighlighted: Array<boolean> = [];
@State timerSeconds: number = 0;
@State selectedRow: number = -1;
@State selectedCol: number = -1;
@State isCompleted: boolean = false;
@State currentLevel: number = 1;
@State boardSize: number = 9;
private solution: Array<number> = [];
private difficulty: Difficulty = Difficulty.EASY;
private history: Array<SudokuHistoryRecord> = [];
private historyIndex: number = -1;
private timerId: number = 0;
状态变量说明:
| 变量名 | 类型 | 作用 |
|---|---|---|
cellValues |
Array<number> | 存储每个格子的数值 |
cellIsOriginal |
Array<boolean> | 标记是否为原始数字(不可修改) |
cellIsSelected |
Array<boolean> | 标记当前选中的格子 |
cellIsError |
Array<boolean> | 标记错误的格子 |
cellIsHighlighted |
Array<boolean> | 标记高亮的格子(同行、同列、同值) |
timerSeconds |
number | 计时秒数 |
selectedRow |
number | 当前选中行 |
selectedCol |
number | 当前选中列 |
isCompleted |
boolean | 是否完成 |
currentLevel |
number | 当前关卡 |
boardSize |
number | 网格尺寸 |
solution |
Array<number> | 数独答案 |
history |
Array<SudokuHistoryRecord> | 操作历史记录 |
historyIndex |
number | 当前历史位置 |
5.2 页面初始化
onPageShow() {
let game = appState.currentGame;
if (game.level !== 0) {
this.currentLevel = game.level;
this.boardSize = game.board.size;
this.difficulty = game.difficulty;
this.solution = game.solution;
let totalCells: number = this.boardSize * this.boardSize;
this.cellValues = new Array<number>(totalCells);
this.cellIsOriginal = new Array<boolean>(totalCells);
this.cellIsSelected = new Array<boolean>(totalCells);
this.cellIsError = new Array<boolean>(totalCells);
this.cellIsHighlighted = new Array<boolean>(totalCells);
for (let i = 0; i < totalCells; i++) {
this.cellValues[i] = game.board.cells[i].value;
this.cellIsOriginal[i] = game.board.cells[i].isOriginal;
this.cellIsSelected[i] = false;
this.cellIsError[i] = false;
this.cellIsHighlighted[i] = false;
}
this.startTimer();
} else {
appState.startNewGameByLevel(1);
this.onPageShow();
}
}
初始化流程:
- 从
AppState获取当前游戏配置 - 初始化所有状态数组
- 从游戏配置中加载单元格数据
- 启动计时器
5.3 网格渲染
Grid() {
ForEach(this.cellValues, (value: number, index: number) => {
GridItem() {
Column() {
if (value !== 0) {
Text(value.toString())
.fontSize(this.boardSize === 4 ? 28 : (this.boardSize === 6 ? 22 : 18))
.fontWeight(FontWeight.Bold)
.fontColor(this.cellIsError[index] ? '#FF6B6B' : (this.cellIsOriginal[index] ? '#FFD700' : '#87CEEB'))
}
}
.width('100%')
.height('100%')
.backgroundColor(this.cellIsSelected[index] ? 'rgba(255, 215, 0, 0.25)' : 'rgba(10, 25, 47, 0.9)')
.borderWidth(this.getBorderWidth(index))
.borderColor(this.getBorderColor(index))
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => this.selectCell(index))
}
})
}
.width('80%')
.aspectRatio(1)
.backgroundColor('#0A192F')
.borderWidth(3)
.borderColor('#FFD700')
.borderRadius(16)
.columnsTemplate(this.getColumnsTemplate())
.rowsTemplate(this.getRowsTemplate())
渲染特点:
- 动态字体大小:根据网格尺寸调整字体(4宫格28sp,6宫格22sp,9宫格18sp)
- 颜色区分:
- 原始数字:金色
#FFD700 - 用户输入:天蓝色
#87CEEB - 错误数字:红色
#FF6B6B
- 原始数字:金色
- 选中高亮:选中格子显示金色半透明背景
- 动态边框:宫格边界显示较粗的金色边框
5.4 动态边框计算
数独网格需要显示宫格边界,这是通过动态计算每个格子的边框宽度和颜色实现的:
private getBorderWidth(index: number): BorderWidth {
let col: number = index % this.boardSize;
let row: number = Math.floor(index / this.boardSize);
let boxRowSize: number = this.getBoxRowSize();
let boxColSize: number = this.getBoxColSize();
let right: number = (col % boxColSize === boxColSize - 1) ? 2 : 1;
let bottom: number = (row % boxRowSize === boxRowSize - 1) ? 2 : 1;
return new BorderWidth(1, 1, right, bottom);
}
private getBorderColor(index: number): BorderColor {
let col: number = index % this.boardSize;
let row: number = Math.floor(index / this.boardSize);
let boxRowSize: number = this.getBoxRowSize();
let boxColSize: number = this.getBoxColSize();
let thick: string = '#FFD700';
let thin: string = '#2D4A6F';
let right: string = (col % boxColSize === boxColSize - 1) ? thick : thin;
let bottom: string = (row % boxRowSize === boxRowSize - 1) ? thick : thin;
return new BorderColor(thin, thin, right, bottom);
}
边框设计原理:
- 宫格边界使用较粗(2px)的金色
#FFD700 - 普通格子边界使用较细(1px)的深蓝色
#2D4A6F - 通过取模运算判断格子是否位于宫格边界
5.5 数字输入键盘
Grid() {
ForEach(this.getNumberList(), (num: number) => {
GridItem() {
Button(num.toString())
.fontSize(this.boardSize === 4 ? 24 : (this.boardSize === 6 ? 20 : 16))
.fontColor('#FFD700')
.backgroundColor('rgba(255, 215, 0, 0.1)')
.borderColor('#FFD700')
.borderWidth(1)
.borderRadius(8)
.width('100%')
.height('100%')
.onClick(() => this.inputNumber(num))
}
})
}
.width('90%')
.height(this.boardSize === 9 ? 160 : 100)
.columnsTemplate(this.boardSize === 4 ? '1fr 1fr' : '1fr 1fr 1fr')
.rowsTemplate(this.boardSize === 4 ? '1fr 1fr' : (this.boardSize === 6 ? '1fr 1fr' : '1fr 1fr 1fr'))
键盘布局:
- 4宫格:2×2布局(4个数字)
- 6宫格:3×2布局(6个数字)
- 9宫格:3×3布局(9个数字)
5.6 功能按钮
Row({ space: 16 }) {
Button('❌ 擦除')
.fontSize(14)
.fontColor('#FF6B6B')
.backgroundColor('rgba(255, 107, 107, 0.2)')
.borderColor('#FF6B6B')
.borderWidth(1)
.borderRadius(8)
.flexGrow(1)
.height(40)
.onClick(() => this.clearCell())
Button('↪ 重做')
.fontSize(14)
.fontColor('#FFD700')
.backgroundColor('transparent')
.borderColor('#FFD700')
.borderWidth(1)
.borderRadius(8)
.flexGrow(1)
.height(40)
.onClick(() => this.redo())
}
.width('90%')
.padding({ top: 8 })
Row({ space: 16 }) {
Button('💡 提示')
.fontSize(14)
.fontColor('#0A192F')
.backgroundColor('#FFD700')
.borderRadius(8)
.flexGrow(1)
.height(40)
.onClick(() => this.showHint())
Button('↩ 撤销')
.fontSize(14)
.fontColor('#FFD700')
.backgroundColor('transparent')
.borderColor('#FFD700')
.borderWidth(1)
.borderRadius(8)
.flexGrow(1)
.height(40)
.onClick(() => this.undo())
}
.width('90%')
.padding({ top: 8 })
功能按钮说明:
| 按钮 | 功能 | 颜色 |
|---|---|---|
| 擦除 | 清除选中格子的数字 | 红色主题 |
| 重做 | 重做上一次撤销的操作 | 金色边框 |
| 提示 | 自动填入一个正确数字 | 金色背景 |
| 撤销 | 撤销上一次操作 | 金色边框 |
六、核心游戏逻辑
6.1 格子选中处理
private selectCell(index: number): void {
let totalCells: number = this.boardSize * this.boardSize;
for (let i = 0; i < totalCells; i++) {
this.cellIsSelected[i] = false;
this.cellIsHighlighted[i] = false;
}
this.cellIsSelected[index] = true;
this.selectedRow = Math.floor(index / this.boardSize);
this.selectedCol = index % this.boardSize;
let selectedValue: number = this.cellValues[index];
for (let i = 0; i < totalCells; i++) {
let row: number = Math.floor(i / this.boardSize);
let col: number = i % this.boardSize;
if (row === this.selectedRow || col === this.selectedCol) {
this.cellIsHighlighted[i] = true;
}
if (selectedValue !== 0 && this.cellValues[i] === selectedValue) {
this.cellIsHighlighted[i] = true;
}
}
}
选中效果:
- 清除之前的选中和高亮状态
- 标记当前选中的格子
- 高亮同行、同列的所有格子
- 高亮所有与选中格子数值相同的格子
6.2 数字输入
private inputNumber(num: number): void {
if (this.selectedRow === -1 || this.selectedCol === -1) {
return;
}
let index: number = this.selectedRow * this.boardSize + this.selectedCol;
if (this.cellIsOriginal[index]) {
return;
}
this.saveHistory();
this.cellValues[index] = num;
this.checkErrors();
if (this.checkWin()) {
this.handleWin();
}
}
输入流程:
- 检查是否选中了格子
- 检查选中的是否为原始数字(原始数字不可修改)
- 保存当前状态到历史记录
- 填入数字
- 检查错误
- 检查是否通关
6.3 错误检查
private checkErrors(): void {
let totalCells: number = this.boardSize * this.boardSize;
for (let i = 0; i < totalCells; i++) {
this.cellIsError[i] = false;
}
for (let i = 0; i < totalCells; i++) {
let value: number = this.cellValues[i];
if (value !== 0) {
let row: number = Math.floor(i / this.boardSize);
let col: number = i % this.boardSize;
if (!this.isCellValid(row, col, value)) {
this.cellIsError[i] = true;
}
}
}
}
private isCellValid(row: number, col: number, value: number): boolean {
for (let i = 0; i < this.boardSize; i++) {
if (i !== col && this.cellValues[row * this.boardSize + i] === value) return false;
if (i !== row && this.cellValues[i * this.boardSize + col] === value) return false;
}
let boxRowSize: number = this.getBoxRowSize();
let boxColSize: number = this.getBoxColSize();
let boxRow: number = Math.floor(row / boxRowSize) * boxRowSize;
let boxCol: number = Math.floor(col / boxColSize) * boxColSize;
for (let r = 0; r < boxRowSize; r++) {
for (let c = 0; c < boxColSize; c++) {
let currentRow: number = boxRow + r;
let currentCol: number = boxCol + c;
if (!(currentRow === row && currentCol === col)) {
if (this.cellValues[currentRow * this.boardSize + currentCol] === value) return false;
}
}
}
return true;
}
验证逻辑:
- 遍历所有非空格子
- 检查每个格子的数值是否在同行、同列、同宫格中重复
- 如果重复,标记为错误
6.4 通关检查
private checkWin(): boolean {
let totalCells: number = this.boardSize * this.boardSize;
for (let i = 0; i < totalCells; i++) {
if (this.cellValues[i] !== this.solution[i]) {
return false;
}
}
return true;
}
通关条件:所有格子的数值与答案完全一致。
6.5 通关处理
private handleWin(): void {
this.isCompleted = true;
this.stopTimer();
promptAction.showDialog({
title: '恭喜通关!',
message: '用时: ' + this.formatTime(this.timerSeconds),
buttons: [
new DialogButton('下一关', '#FFD700'),
new DialogButton('返回首页', '#87CEEB')
]
}).then((result: DialogResult) => {
if (result.index === 0) {
this.nextLevel();
} else {
router.back();
}
});
}
通关流程:
- 停止计时器
- 显示通关对话框
- 用户选择"下一关"或"返回首页"
七、撤销/重做功能
7.1 历史记录数据结构
class SudokuHistoryRecord {
cellValues: Array<number> = [];
cellIsError: Array<boolean> = [];
constructor(cellValues: Array<number>, cellIsError: Array<boolean>) {
this.cellValues = this.copyArray(cellValues);
this.cellIsError = this.copyArray(cellIsError);
}
private copyArray<T>(source: Array<T>): Array<T> {
let result: Array<T> = [];
for (let i = 0; i < source.length; i++) {
result.push(source[i]);
}
return result;
}
}
设计说明:
- 每条记录保存当前的
cellValues和cellIsError状态 - 使用深拷贝确保历史记录不会被后续修改影响
7.2 保存历史
private saveHistory(): void {
this.historyIndex++;
if (this.historyIndex < this.history.length) {
this.history = this.history.slice(0, this.historyIndex);
}
let record: SudokuHistoryRecord = new SudokuHistoryRecord(this.cellValues, this.cellIsError);
this.history.push(record);
}
保存逻辑:
- 增加历史索引
- 如果索引在历史数组范围内,删除后续的记录(截断重做栈)
- 创建新的历史记录并添加到数组
7.3 撤销操作
private undo(): void {
if (this.historyIndex > 0) {
this.historyIndex--;
let record: SudokuHistoryRecord = this.history[this.historyIndex];
this.cellValues = this.copyArray(record.cellValues);
this.cellIsError = this.copyArray(record.cellIsError);
}
}
撤销逻辑:
- 检查是否有可撤销的记录
- 减少历史索引
- 恢复到上一条记录的状态
7.4 重做操作
private redo(): void {
if (this.historyIndex < this.history.length - 1) {
this.historyIndex++;
let record: SudokuHistoryRecord = this.history[this.historyIndex];
this.cellValues = this.copyArray(record.cellValues);
this.cellIsError = this.copyArray(record.cellIsError);
}
}
重做逻辑:
- 检查是否有可重做的记录
- 增加历史索引
- 恢复到下一条记录的状态
八、提示功能
private showHint(): void {
let totalCells: number = this.boardSize * this.boardSize;
for (let i = 0; i < totalCells; i++) {
if (!this.cellIsOriginal[i] && this.cellValues[i] === 0) {
this.saveHistory();
this.cellValues[i] = this.solution[i];
this.checkErrors();
let row: number = Math.floor(i / this.boardSize);
let col: number = i % this.boardSize;
this.selectCell(i);
if (this.checkWin()) {
this.handleWin();
}
return;
}
}
}
提示逻辑:
- 查找第一个空白的非原始格子
- 保存当前状态(允许撤销提示)
- 填入正确答案
- 选中填入的格子
- 检查是否因此通关
九、数据模型设计
9.1 CellData类
export class CellData {
value: number = 0;
isOriginal: boolean = false;
constructor(value: number, isOriginal: boolean) {
this.value = value;
this.isOriginal = isOriginal;
}
}
字段说明:
value:格子的数字值(0表示空白)isOriginal:是否为原始数字(不可修改)
9.2 SudokuBoard类
export class SudokuBoard {
cells: Array<CellData> = [];
size: number = 9;
constructor(size: number = 9) {
this.size = size;
this.initEmpty();
}
initEmpty(): void {
this.cells = [];
let totalSize: number = this.size * this.size;
for (let i = 0; i < totalSize; i++) {
this.cells.push(new CellData(0, false));
}
}
getCell(row: number, col: number): CellData {
return this.cells[row * this.size + col];
}
setCell(row: number, col: number, value: number): void {
let index: number = row * this.size + col;
this.cells[index].value = value;
}
getRow(row: number): Array<CellData> {
let result: Array<CellData> = [];
for (let col = 0; col < this.size; col++) {
result.push(this.getCell(row, col));
}
return result;
}
getCol(col: number): Array<CellData> {
let result: Array<CellData> = [];
for (let row = 0; row < this.size; row++) {
result.push(this.getCell(row, col));
}
return result;
}
getBox(row: number, col: number): Array<CellData> {
let result: Array<CellData> = [];
let boxSize: number = this.getBoxSize();
let boxRow: number = Math.floor(row / boxSize) * boxSize;
let boxCol: number = Math.floor(col / boxSize) * boxSize;
for (let r = 0; r < boxSize; r++) {
for (let c = 0; c < boxSize; c++) {
result.push(this.getCell(boxRow + r, boxCol + c));
}
}
return result;
}
getBoxSize(): number {
if (this.size === 4) return 2;
if (this.size === 6) return 3;
return 3;
}
}
设计特点:
- 统一的棋盘表示:使用一维数组存储二维棋盘
- 便捷的访问方法:提供按行、列、宫格获取数据的方法
- 支持多种尺寸:通过
size字段支持4×4、6×6、9×9三种规格
9.3 Difficulty枚举
export enum Difficulty {
EASY,
MEDIUM,
HARD
}
十、性能优化考虑
10.1 数组操作优化
ArkTS中数组的直接修改不会触发UI更新,必须创建新数组引用:
// 正确方式:创建新数组触发更新
this.cellValues[index] = num;
this.cellValues = this.cellValues.slice();
10.2 回溯算法优化
数独生成的回溯算法虽然时间复杂度为O(n!),但实际使用中有以下优化:
- 随机顺序尝试:打乱数字顺序,避免每次生成相同的数独
- 提前剪枝:一旦发现冲突立即回溯,不需要尝试所有可能
- 规模限制:最大9×9网格,总格子数81个,现代计算机可以快速处理
10.3 计时器优化
使用递归setTimeout而非setInterval,避免累积误差:
private tick(): void {
if (this.timerId !== 0) {
this.timerSeconds++;
setTimeout(() => {
this.tick();
}, 1000);
}
}
10.4 渲染优化
- 动态字体大小:根据网格尺寸调整字体,避免小网格中字体过大
- 条件渲染:只渲染非空格子的数字,减少渲染负担
- 渲染分组:使用
renderGroup(true)减少渲染批次(如果支持)
十一、扩展功能建议
11.1 游戏增强功能
- 笔记功能:在格子中标记可能的数字
- 错误提示:提示具体的冲突位置
- 难度选择:允许玩家自定义难度
- 多人模式:支持多人协作完成数独
- 每日挑战:每天生成一个新的挑战数独
11.2 技术优化方向
- 唯一解验证:确保生成的谜题只有唯一解
- 难度算法优化:基于解题步数评估难度,而非简单的移除比例
- 动画效果:添加填入数字的动画效果
- 手势支持:支持滑动选择多个格子
11.3 UI/UX改进
- 深色/浅色主题切换:支持系统主题切换
- 字体自定义:允许玩家调整字体大小
- 音效反馈:添加填入数字和完成关卡的音效
- 成就系统:添加完成特定目标的成就
十二、总结
九宫数独游戏是本项目的终极挑战,也是技术复杂度最高的游戏。本项目基于HarmonyOS ArkTS技术栈实现了完整的数独游戏功能:
- 核心算法:回溯算法生成数独解,随机移除格子生成谜题
- 多种尺寸:支持4×4、6×6、9×9三种网格规格
- 难度系统:100个关卡,从入门到困难的完整难度梯度
- 交互功能:实时验证、撤销/重做、提示、暂停
- 进度管理:全局状态管理通关进度和最佳时间
- 视觉设计:深蓝色背景配金色主题,清晰的宫格边界
游戏的核心难点在于:
- 实现高效的回溯算法生成数独解
- 设计合理的难度递进曲线
- 处理多种网格尺寸的统一逻辑
- 实现流畅的撤销/重做功能
- 动态计算宫格边界的视觉效果
通过本项目的实践,可以深入理解ArkTS的状态管理机制、复杂UI组件的动态布局、以及经典算法在移动端的实现方法。九宫数独不仅是一款有趣的益智游戏,也是学习回溯算法和状态管理的绝佳案例。
数独游戏的开发过程体现了以下技术要点:
- 算法设计:回溯算法的实现与优化
- 状态管理:复杂状态的组织与历史记录
- UI渲染:动态边框和条件渲染
- 用户交互:选中、输入、验证的完整流程
- 数据模型:面向对象的数据结构设计
这些技术要点不仅适用于数独游戏,也可以应用于其他需要复杂逻辑和状态管理的HarmonyOS应用开发中。
更多推荐

所有评论(0)