在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、游戏概述

九宫数独是一款经典的数字逻辑推理游戏,也是本项目的终极挑战。玩家需要在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 })
}

布局特点

  1. 顶部导航栏:返回按钮、标题、解锁按钮
  2. 可折叠分类卡片:点击展开/收起关卡网格
  3. 5×5关卡网格:每类25个关卡,金色标记已完成
  4. 深蓝色背景:统一的主题风格

2.3 关卡启动流程

private startLevel(level: number): void {
  appState.startNewGameByLevel(level);
  router.pushUrl({ url: 'pages/SudokuPage' });
}

关卡启动时,通过AppState生成对应难度的数独谜题,然后跳转到游戏页面。

三、数独生成算法

3.1 回溯算法原理

数独生成的核心是回溯算法(Backtracking),这是一种经典的暴力搜索算法,通过递归尝试所有可能的解决方案。

算法流程

  1. 从第一个空格开始(索引0)
  2. 尝试填入1-n的数字(随机打乱顺序以生成不同的谜题)
  3. 检查当前数字是否有效(行、列、宫格不重复)
  4. 如果有效,继续填充下一个空格
  5. 如果无效,尝试下一个数字
  6. 如果所有数字都无效,回溯到上一个空格
  7. 如果所有空格都填满,返回成功

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;
}

算法详解

  1. 终止条件:当index等于总格子数时,说明所有格子都已填满,返回true
  2. 坐标计算:通过index计算当前格子的行和列
  3. 随机数字序列:使用Fisher-Yates算法打乱数字顺序,确保每次生成的数独不同
  4. 递归尝试:尝试每个数字,有效则递归填充下一格
  5. 回溯:如果当前路径失败,将当前格子置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;
}

三层验证

  1. 行验证:检查当前行是否已有相同数字
  2. 列验证:检查当前列是否已有相同数字
  3. 宫格验证:检查当前宫格是否已有相同数字

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 谜题生成流程

完整数独解生成 → 随机打乱位置 → 根据难度移除格子 → 返回谜题

关键设计

  1. 基于完整解:先生成完整的数独解,再移除格子,确保谜题有唯一解
  2. 随机移除:使用洗牌算法随机选择要移除的位置
  3. 难度递进:移除比例越高,难度越大

五、游戏主页面实现

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();
  }
}

初始化流程

  1. AppState获取当前游戏配置
  2. 初始化所有状态数组
  3. 从游戏配置中加载单元格数据
  4. 启动计时器

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())

渲染特点

  1. 动态字体大小:根据网格尺寸调整字体(4宫格28sp,6宫格22sp,9宫格18sp)
  2. 颜色区分
    • 原始数字:金色#FFD700
    • 用户输入:天蓝色#87CEEB
    • 错误数字:红色#FF6B6B
  3. 选中高亮:选中格子显示金色半透明背景
  4. 动态边框:宫格边界显示较粗的金色边框

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;
    }
  }
}

选中效果

  1. 清除之前的选中和高亮状态
  2. 标记当前选中的格子
  3. 高亮同行、同列的所有格子
  4. 高亮所有与选中格子数值相同的格子

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();
  }
}

输入流程

  1. 检查是否选中了格子
  2. 检查选中的是否为原始数字(原始数字不可修改)
  3. 保存当前状态到历史记录
  4. 填入数字
  5. 检查错误
  6. 检查是否通关

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;
}

验证逻辑

  1. 遍历所有非空格子
  2. 检查每个格子的数值是否在同行、同列、同宫格中重复
  3. 如果重复,标记为错误

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();
    }
  });
}

通关流程

  1. 停止计时器
  2. 显示通关对话框
  3. 用户选择"下一关"或"返回首页"

七、撤销/重做功能

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;
  }
}

设计说明

  • 每条记录保存当前的cellValuescellIsError状态
  • 使用深拷贝确保历史记录不会被后续修改影响

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);
}

保存逻辑

  1. 增加历史索引
  2. 如果索引在历史数组范围内,删除后续的记录(截断重做栈)
  3. 创建新的历史记录并添加到数组

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);
  }
}

撤销逻辑

  1. 检查是否有可撤销的记录
  2. 减少历史索引
  3. 恢复到上一条记录的状态

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);
  }
}

重做逻辑

  1. 检查是否有可重做的记录
  2. 增加历史索引
  3. 恢复到下一条记录的状态

八、提示功能

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;
    }
  }
}

提示逻辑

  1. 查找第一个空白的非原始格子
  2. 保存当前状态(允许撤销提示)
  3. 填入正确答案
  4. 选中填入的格子
  5. 检查是否因此通关

九、数据模型设计

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;
  }
}

设计特点

  1. 统一的棋盘表示:使用一维数组存储二维棋盘
  2. 便捷的访问方法:提供按行、列、宫格获取数据的方法
  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!),但实际使用中有以下优化:

  1. 随机顺序尝试:打乱数字顺序,避免每次生成相同的数独
  2. 提前剪枝:一旦发现冲突立即回溯,不需要尝试所有可能
  3. 规模限制:最大9×9网格,总格子数81个,现代计算机可以快速处理

10.3 计时器优化

使用递归setTimeout而非setInterval,避免累积误差:

private tick(): void {
  if (this.timerId !== 0) {
    this.timerSeconds++;
    setTimeout(() => {
      this.tick();
    }, 1000);
  }
}

10.4 渲染优化

  1. 动态字体大小:根据网格尺寸调整字体,避免小网格中字体过大
  2. 条件渲染:只渲染非空格子的数字,减少渲染负担
  3. 渲染分组:使用renderGroup(true)减少渲染批次(如果支持)

十一、扩展功能建议

11.1 游戏增强功能

  1. 笔记功能:在格子中标记可能的数字
  2. 错误提示:提示具体的冲突位置
  3. 难度选择:允许玩家自定义难度
  4. 多人模式:支持多人协作完成数独
  5. 每日挑战:每天生成一个新的挑战数独

11.2 技术优化方向

  1. 唯一解验证:确保生成的谜题只有唯一解
  2. 难度算法优化:基于解题步数评估难度,而非简单的移除比例
  3. 动画效果:添加填入数字的动画效果
  4. 手势支持:支持滑动选择多个格子

11.3 UI/UX改进

  1. 深色/浅色主题切换:支持系统主题切换
  2. 字体自定义:允许玩家调整字体大小
  3. 音效反馈:添加填入数字和完成关卡的音效
  4. 成就系统:添加完成特定目标的成就

十二、总结

九宫数独游戏是本项目的终极挑战,也是技术复杂度最高的游戏。本项目基于HarmonyOS ArkTS技术栈实现了完整的数独游戏功能:

  1. 核心算法:回溯算法生成数独解,随机移除格子生成谜题
  2. 多种尺寸:支持4×4、6×6、9×9三种网格规格
  3. 难度系统:100个关卡,从入门到困难的完整难度梯度
  4. 交互功能:实时验证、撤销/重做、提示、暂停
  5. 进度管理:全局状态管理通关进度和最佳时间
  6. 视觉设计:深蓝色背景配金色主题,清晰的宫格边界

游戏的核心难点在于:

  • 实现高效的回溯算法生成数独解
  • 设计合理的难度递进曲线
  • 处理多种网格尺寸的统一逻辑
  • 实现流畅的撤销/重做功能
  • 动态计算宫格边界的视觉效果

通过本项目的实践,可以深入理解ArkTS的状态管理机制、复杂UI组件的动态布局、以及经典算法在移动端的实现方法。九宫数独不仅是一款有趣的益智游戏,也是学习回溯算法和状态管理的绝佳案例。

数独游戏的开发过程体现了以下技术要点:

  1. 算法设计:回溯算法的实现与优化
  2. 状态管理:复杂状态的组织与历史记录
  3. UI渲染:动态边框和条件渲染
  4. 用户交互:选中、输入、验证的完整流程
  5. 数据模型:面向对象的数据结构设计

这些技术要点不仅适用于数独游戏,也可以应用于其他需要复杂逻辑和状态管理的HarmonyOS应用开发中。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐