54 道题、一次遍历、一个 JSON:纯端侧鸿蒙数独的三个设计取舍

举报
yd_227460417 发表于 2026/11/06 15:49:38 2026/11/06
【摘要】 不联网、不登录,54 道分难度题库,填错即时标红,通关进度写进沙箱文件。本文拆解一个纯端侧鸿蒙数独里题库生成、冲突校验与进度持久化的实现思路,以及“不保证唯一解”这类刻意保留的边界。

一、为什么做一个“什么都没有”的数独

最近在鸿蒙实践案例中心看到一个纯端侧的数独案例。它没有后端、没有账号、没有网络权限,连登录页都没有,只做三件事:

  1. 提供一套 54 道的分难度题库;
  2. 填数后实时做冲突校验;
  3. 把通关进度保存到本地。

功能看起来朴素,但它把 ArkUI 最基础、也最容易写乱的三块——声明式 UI、状态管理、端侧持久化——完整地串联了一遍。如果你已经能写 ArkUI 页面,但还在纠结“状态放哪、数据存哪、规则写哪”,这个案例的结构比堆功能的项目更值得读。

环境很轻:DevEco Studio 官网最新稳定版,compatibleSdkVersion 为 6.1.0(23),compileSdkVersion 与 targetSdkVersion 为 6.1.1(24),模拟器或真机都能跑。


二、题库:54 道题不是手写的

手写 54 道数独既不现实,也容易出错。案例的思路是:先准备一个合法终盘,再对它做不破坏合法性的变换,最后按难度挖空。

不破坏合法性的变换有三类,都是数独的经典性质:

  • 数字重映射:把 1~9 做一个随机置换,整盘仍然合法;
  • 转置:沿主对角线翻转,行列互换,仍然合法;
  • 宫行 / 宫列内交换:同一组三行(或三列)内部交换,仍然合法。

先准备一个基准终盘:

const BASE_SOLUTION: number[] = [
  5, 3, 4, 6, 7, 8, 9, 1, 2,
  6, 7, 2, 1, 9, 5, 3, 4, 8,
  1, 9, 8, 3, 4, 2, 5, 6, 7,
  8, 5, 9, 7, 6, 1, 4, 2, 3,
  4, 2, 6, 8, 5, 3, 7, 9, 1,
  7, 1, 3, 9, 2, 4, 8, 5, 6,
  9, 6, 1, 5, 3, 7, 2, 8, 4,
  2, 8, 7, 4, 1, 9, 6, 3, 5,
  3, 4, 5, 2, 8, 6, 1, 7, 9
];

再做数字置换和转置:

function shuffledDigits(): number[] {
  const d: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
  for (let i = d.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const t = d[i]; d[i] = d[j]; d[j] = t;
  }
  return d;
}

function transpose(board: number[]): number[] {
  const out: number[] = [];
  for (let i = 0; i < 81; i++) {
    out.push(0);
  }
  for (let r = 0; r < 9; r++) {
    for (let c = 0; c < 9; c++) {
      out[c * 9 + r] = board[r * 9 + c];
    }
  }
  return out;
}

function applyDigitMap(board: number[], map: number[]): number[] {
  const out: number[] = [];
  for (const v of board) {
    out.push(v === 0 ? 0 : map[v - 1]);
  }
  return out;
}

挖空决定难度:每个 3×3 宫保留的提示数越少,题目越难。

function digHoles(solved: number[], blanksPerBox: number): number[] {
  const puzzle: number[] = solved.slice();
  for (let b = 0; b < 9; b++) {
    const br = Math.floor(b / 3) * 3;
    const bc = (b % 3) * 3;
    const cells: number[] = [];
    for (let r = 0; r < 3; r++) {
      for (let c = 0; c < 3; c++) {
        cells.push((br + r) * 9 + bc + c);
      }
    }
    // 洗牌后挖掉前 blanksPerBox 个
    for (let i = cells.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      const t = cells[i]; cells[i] = cells[j]; cells[j] = t;
    }
    for (let k = 0; k < blanksPerBox; k++) {
      puzzle[cells[k]] = 0;
    }
  }
  return puzzle;
}

这里有第一个值得说的取舍:挖空只保证“有解”,不保证“唯一解”。

要证明一道数独只有唯一解,通常需要反复求解、统计解的个数,端侧做这件事成本不低。案例干脆不做唯一性校验,并把通关判定设为「填满且无行列宫冲突」,不与内置答案比对。所以同一道题可能存在多套合法终盘,玩家填出任意一套都算通关。

这不是漏洞,而是一个被写进案例边界说明的设计决定。项目里能把“我故意没做什么、为什么不做”写清楚,比悄悄留下一个坑要好得多。


三、冲突校验:81 格一次扫完

最容易写歪的做法是:每

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。