用 JS 做一个数独游戏(一)

数独的棋盘由 9x9 的方格组成,每一行的数字包含 1 ~ 9 九个数字,并且每一列包含 1 ~ 9 这 9 个不重复的数字,另外,整个棋盘分为 9 个 3x3 的块,每个块中包含的数字也是 1 ~ 9。数独棋盘是非常对称的,所以行和列实际上通过旋转一定的角度就可以相互转换。

数独终盘

生成步骤

生成数独终盘有多种方法,其中一种是挖洞法:先生成一个随机的数独终盘,然后随机隐藏某几个位置的数字,让用户进行填空。这里我们用到的方法就是挖洞法,以行为单位进行数字的填充。

初始的挖洞法步骤:


  1. 生成的一个空的数独棋盘,棋盘中每个格子的数字都是 0;

数独棋盘用 Board 对象表示,该对象中有 81 个 Grid 对象,表示 9x9 的方格。

// class Board
for( let i = 0; i < 9; i++ ) {
for( let j = 0; j < 9; j++ ) {
this.grids[i * 9 + j] = new Grid( i, j );
}
}
  1. 生成 1~9 的乱序排列并填充第一行(填充第一列的效果和填充第一行再进行旋转其实是一样的效果,由于填充第一行元素时,其它各行都没有填充过数字,因此第一行的约束条件是最少的);
// populate the first row
let row0 = this.getRowGrids(0);
let randomArray = Utils.getRandomValue();
randomArray.forEach( (element, index ) => {
row0[index].setValue( element );
});

填充完第一行的数独棋盘:

  1. 填充第 2 行时,计算第 1 个格子可以使用的数字集合;

这个集合是从一个数组 [1, 2, 3, 4, 5, 6, 7, 8, 9] (把它称为 基本数组)中剔除掉已经被用过的数字。这些用过的数字是从这个格子所在的行、列及块中有效的数字。例如上图中,第 2 行,第 1 列(在数组中序号分别是 1,0)已经用过的数字是 2, 4, 7,那么第 2 行第 1 列可以使用的数字就只能在剩下未使用过的数字中随机选择一个;

  1. 从 (2, 1) 格子处选择可用数字集合中的一个,填充至该位置;

  2. 重复 3~4 步,填充完第 2 行;

  3. 重复第 3~5 步,依次填充完其它行;

  4. 完成数独终盘。


错误的示例

然而上面的算法步骤有一个致命的缺陷:在某些位置(第一个出现的位置是第二行最后一列)可能出现 1~9 所有数字都被该格子所在的行、列和块使用过了,那么这个格子就没有可用的数字。如下图:

可以看到,第 2 行第 9 列没有可用的数字,无法进行填充。其它行也有可能出现这种情况。在这个数独棋盘中,所有为 0 的格子都是无法满足约束条件的。

实际上,出现 0 的位置是因为下面的函数在调用 getRandomValueArray 时返回的数组位空,或者说 used 数组已经是包含 1~9 这 9 个数字。

getRandomValidValue(pos) {
let used = this.getUsedValueArrayAt(pos);
let valueArray = this.getRandomValueArray(used);
return valueArray[0] === undefined ? 0 : valueArray[0];
}

在 github 上存有 错误示例的代码

修正挖洞法

直接采用挖洞法生成的数独终盘不一定是有效的,如果把填充过程当作一棵树,填充的数字为树的节点,那么有些树枝的路径是无法满足数独的要求的。当填充过程中发现某个位置没有可选的数字时,应该返回上一个填充的节点,在上一个节点处选择其他的数字进行填充。也就是回溯方法。

回溯方法需要记录每个格子可用的数字集合,以及填充时用到的数字,以便在算法失败时(某个位置没有可用数字集合)进行回溯。回溯时已经尝试过的数字将被标记为不可用。有两点需要注意:

  • 对每一行的格子而言,从左往右的顺序实际上构成了一个栈,因此不需要额外构建一个堆栈来存储填充时的情况。

  • 另外需要注意的一点是,若在 (i, 1) 位置的格子没有可用的数字时,需要回溯到上一行的第 1 列重新选择。

为了记录每次分支时选择的数字,引入一个用于记录当前选择的对象 Choice

class Choice {
/**
*
* @param {Array<Number>} choiceSet
*/
constructor(choiceSet) {
this.choiceSet = choiceSet;
this.attemptIndex = -1;
} /**
* 将索引移至下一个位置,并返回该位置的数字
*/
next() {
this.attemptIndex++;
return this.choiceSet[this.attemptIndex];
}
}

修正的算法步骤:


  1. 生成的一个空的数独棋盘,棋盘中每个格子的数字都是 0;

  2. 生成 1~9 的乱序排列并填充第一行;

  3. 填充第 2 行时,计算第 1 个格子,其位置为 (2,1),可以使用的数字集合;

  4. 从 (2, 1) 格子处选择可用数字集合中的一个,填充至该位置,将用过的数字标记为不可用;

  5. 若某个位置 (i, j) 的格子已无可用的数字集合,若该格子位于第 1 列,则返回上一行,从第一列进行回溯,否则只需返回到上一列进行回溯。

  6. 重复 3~5 步,填充完第 2 行;

  7. 重复第 3~6 步,依次填充完其它行;

  8. 完成数独终盘。


修正后的算法实现:

整体的填充步骤没有变化,在填充时首先判断 grid 对象的 choice 属性,若为 undefined,则需要计算 grid 所在的位置处可能的选择:

for(let i = 1; i < 9; i++) {
for(let j = 0; j < 9; j++) {
grid = this.grids[i*9+j];
// process.stdout.write( `i=${i}, j=${j}. `);
if( grid.choice === undefined ) {
used = this.getUsedValueArrayAt( {row: i, col: j} );
unused = Utils.getRandomValue( {exclude: used} );
grid.choice = new Choice( unused );
}
index = this.populateGrid(grid);
i = index.i;
j = index.j;
}
}

填充方法 populateGrid 完成了回溯部分:

/**
* 填充方格,并返回修正过的 i,j索引
*
* 该函数属于回溯部分,在方格没有可选数字时,修正索引,以便进行回溯
* @param {Grid} grid
* @returns {{i: Number, j: Number}}
*/
populateGrid(grid) {
let i = grid.row, j = grid.col;
let value = grid.choice.next(); if( value !== undefined ) {
// 有可以选择的数字
grid.setValue(value);
}
else {
// 没有可选的数字
// Utils.printRow(this, i);
grid.value = 0;
grid.choice = undefined;
if( j == 0 ) {
// i = (i > 0 ? i - 1 : i );
i -= 1;
j -= 1;
this.resetPartialGrids({rowStart: i, rowEnd: i+1, colStart: 1, colEnd: 9}); // 返回上一行后,清空该行其它列的数据
}
else {
j -= 2;
}
}
return {i, j};
}

需要回溯时,populateGrid 会返回修改后的索引,以便重入特定位置,进行回溯。

完成数独终盘

通过修正后的挖洞法,最终可以生成一个有效的数独终盘。生成 1000 种数独终盘所用的时间如下:

其他

在使用 JavaScript 编写数独时,使用了 QUnit 进行代码测试,在编写测试代码时不仅提高了代码的准确性,也帮助我在测试时完善逻辑代码,用更好的函数封装组织逻辑。

可以直接在 Node 中使用 QUnit,也可以通过浏览器执行测试。用浏览器执行时,视觉效果更好:

引入的 html 文件也比较简单,主要内容如下:

<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-2.6.1.js"></script>
<script src="./NumberPlaceGame.js"></script>
<script src="./TestNumberPlaceGame.js"></script>
</body>

代码

能够正确生成数独终盘的 代码

测试代码

参考资料

https://www.cnblogs.com/wangqinze/p/7501716.html

用 JS 做一个数独游戏(一)的更多相关文章

  1. 用 JS 做一个数独游戏(二)

    用 JS 做一个数独游戏(二) 在 上一篇博客 中,我们通过 Node 运行了我们的 JavaScript 代码,在控制台中打印出来生成好的数独终盘.为了让我们的数独游戏能有良好的体验,这篇博客将会为 ...

  2. 如何用纯js做一个大富翁游戏

    下面这张是效果图: 先立个flag,一个星期内把这个坑填了

  3. 使用PixiJS做一个小游戏

    PixiJS PixiJS使用WebGL,是一个超快的HTML5 2D渲染引擎.作为一个Javascript的2D渲染器,Pixi.js的目标是提供一个快速的.轻量级而且是兼任所有设备的2D库. 官方 ...

  4. js实现一个小游戏(飞翔的jj)

    js实现一个小游戏(飞翔的jj) 源代码+素材图片在我的仓库 <!DOCTYPE html> <html lang="en"> <head> & ...

  5. python做一个数独小游戏

    最近看了下python的一些知识,在这里记载一下. 1.首先是安装,在官网下载最新的版本3.6,安装的时候要注意在下面勾选上ADD TO PATH,安装的时候会自动写入到环境变量里面,如果没有勾选,可 ...

  6. 用html5 canvas和JS写个数独游戏

    为啥要写这个游戏? 因为我儿子二年级数字下册最后一章讲到了数独.他想玩儿. 因为我也想玩有提示功能的数独. 因为我也正想决定要把HTML5和JS搞搞熟.熟悉一个编程平台,最好的办法,就是了解其原理与思 ...

  7. 用JS做一个简单的电商产品放大镜功能

    使用js制作一个简单的产品放大图 购物网站的产品页经常会放有一个产品展示图区.该图区有一个功能就是产品图的放大功能,移动左侧的焦点区域,可以放大细节部分观看,详情如下图.实现该功能的方法也非常简单. ...

  8. 如何使用impress.js做一个网页版本的PPT

    blockquote{font-size: 18px;line-height:1.5;margin:0;}line-height: 1.5; 要做一个网站制作规范培训,之前村长做过一次培训,但是后来一 ...

  9. 使用node.js做一个自用的天气插件

    var request = require('request') var url = 'http://www.baidu.com/home/xman/data/superload' var cooki ...

随机推荐

  1. C#数据类型及差异(复习专用)

    一.数据类型 值类型 类型 描述 范围 默认值 bool 布尔值 True 或 False False byte 8 位无符号整数 0 到 255 0 char 16 位 Unicode 字符 U + ...

  2. symbol访问法及symbor注册表

    symbol主要作用是防止对象属性名冲突 在ES6之前,对象的属性名只能是字符串,这样很容易造成字符串的冲突. 例:假设person对象是从外部库引入的一个对象 let person = { name ...

  3. 几款Web服务器性能压力测试工具

    一.http_load 程序非常小,解压后也不到100K http_load以并行复用的方式运行,用以测试web服务器的吞吐量与负载. 但是它不同于大多数压力测试工具,它可以以一个单一的进程运行,一般 ...

  4. Nginx+ISS+Redis实现完美负载均衡

    前篇文章讲到nginx是使网站采用分布式,对用户的请求采用分布式,分配到不同的服务器上,然后进行同一站点的访问,保证了访问的高效,使用率高,生命期长. 说到ISS,这里重点介绍tomcat,Tomca ...

  5. CI框架源码学习笔记2——Common.php

    上一节我们最后说到了CodeIgniter.php,可是这一节的标题是Common.php,有的朋友可能会觉得很奇怪.事实上,CodeIgniter.php其实包含了ci框架启动的整个流程. 里面引入 ...

  6. 基本css拼图形

    关闭按钮: <em class="close"></em> .close { width: 16px; height: 16px; text-align: ...

  7. python3查询mysql数据

    python3不支持MySQLdb,代替的是import pymysql 连接数据库查表: import pymysqlconn= pymysql.connect( host='xx.xx.xx.xx ...

  8. luogu2257 YY的GCD--莫比乌斯反演

    link 给定N, M,求1<=x<=N, 1<=y<=M且gcd(x, y)为质数的(x, y)有多少对 多组数据T = 10000 N, M <= 10000000 ...

  9. [USACO08NOV]安慰奶牛Cheering up the Cow BZOJ 1232 Kruskal

    Farmer John变得非常懒, 他不想再继续维护供奶牛之间供通行的道路. 道路被用来连接N (5 <= N <= 10,000)个牧场, 牧场被连续地编号为1..N. 每一个牧场都是一 ...

  10. Kibana6.2.2源码入口

    后端入口 \kibana-6.2.2\src\server\kbn_server.js 前端入口 kibana-6.2.2\src\ui\public\chrome\chrome.js 页面框架模板 ...