这篇文章讲的是如何使用 JavaScript 生成数独游戏的算法思路
需要一定的编程基础 完整代码在文章末尾
数独规则
- 数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。
- 数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。
数独的组成
| 组成 | 解释 |
|---|---|
| 方格 | 水平方向有九横行,垂直方向有九纵列的矩形,画分八十一个小正方形,称为九宫格(Grid),如图一所示,是数独(Sudoku)的作用范围。 |
| 行 | 水平方向的每一横行有九格,每一横行称为行(Row),如图二所示。 |
| 列 | 垂直方向的每一纵列有九格,每一纵列称为列(Column),如图三所示。 |
| 宫 | 三行与三列相交之处有九格,每一单元称为小九宫(Box、Block),简称宫,如图四所示(在杀手数独中,宫往往用单词Nonet表示)。 |
以上介绍摘抄百度百科(数独)
算法思考
看完游戏规则和组成规则,下面就可以开发生成数独的算法了。
-
生成一个 9 * 9 的宫格,这里我使用的是二维数组来保存数据
[ [], [], [], ...] -
创建随机数,在一个嵌套的循环中依次添加至九宫格数组中
此时添加的数字很多都是重复的,下面我们来处理重复的数
-
行 已知一行中的数字 1到9 是不可以重复的,此时只需要将二维数组中对应的一维数据,也就是数组中的第n个元素,与当前随机数做查重判断,如果重复生成新的随机数
-
列 按照数独规则,每列中的数字也是不可以重复的,此时需要将列的数据进行处理,取出对应编号的列数据,同理与行的操作一致
-
宫 的操作同上
上诉就是数独的算法思考,如果自己动手的话会遇到很多坑,比如随机数太多导致循环次数多影响性能,或者校验规则卡死等问题。
随机数太多问题
处理方式:创建一个数组,数组中有 1-9 的数字,每次添加成功的数字将从数组中剔除,直到下一行重置
这里会出现一种 bug,有几率死循环,因为数独的不确定性,规则在校验时会出现只有一个数但是校验不通过的情况
校验规则卡死
处理方式:回退数据,每次校验不成功记录次数,次数过多回退数据,然后重置记录
处理方式均可在代码中根据注释查看详细操作。
效果图
在线数独游戏
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.sudoku-box {
display: flex;
align-items: center;
justify-content: center;
}
.sudoku-box>div>div {
display: flex;
justify-content: center;
align-items: center;
width: 25px;
height: 25px;
background-color: #f5f5f5;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div class="main">
<div class="sudoku-box"></div>
</div>
<button>重置</button>
</body>
<script>
const createSudoku = {
//总行数
rowNum: 9,
//总列数
colNum: 9,
//错误次数,过多表示死胡同
errorNum: 0,
//块级错误次数,过多需要向上回滚
blockError: 0,
//初始化方法
init() {
let sudoku = [];
for (let i = 0; i < this.rowNum; i++) {
sudoku[i] = []
}
for (let x = 0; x < sudoku.length; x++) {
let repeat = this.randomNum()
for (let y = 0; y < this.colNum; y++) {
if (this.blockError > 10) {
//回退
this.blockError = 0
this.errorNum = 0;
sudoku[x] = []
sudoku[x - 1] = []
x -= 2
break
}
if (this.errorNum > 10) {
//重置
y = -1
this.errorNum = 0;
repeat = this.randomNum()
sudoku[x] = []
continue
}
//随机数
let num = this.random(repeat, 0, repeat.length - 1)
//通过状态
let status = this.isTrue(num, sudoku, x, y);
if (status) {
this.errorNum = 0
this.blockError = 0
let index = repeat.indexOf(num);
repeat.splice(index, 1)
sudoku[x].push(num)
} else {
y--
}
}
}
return sudoku;
},
/**
* 根据所传数组,生成依据数组长度的随机数,返回数组下标数据
* @param {Array} arr 依据数组
* @param {Number} min 最小值
* @param {Number} max 最大值
* @returns {Number} 返回随机数
**/
random(arr, min, max) {
if (arr.length === 1) return arr[0]
let num = Math.floor(Math.random() * (max - min + 1)) + min;
return arr[num];
},
/**
* 返回1到9的数组
*/
randomNum() {
return [1, 2, 3, 4, 5, 6, 7, 8, 9];
},
/**
* 检查数在二维数组中是否满足条件,返回布尔
* @param {Number} num 要检查的数
* @param {Array} sudoku 二维数组
* @param {Number} x 行数,x轴
* @param {Number} y 列数,y轴
* @returns {Boolean} 返回布尔
*/
isTrue(num, sudoku, x, y) {
// console.log(x, y);
if (sudoku[x].includes(num)) {
// console.log('%c行重复', 'color: red');
return false;
}
let y_data = [];
for (let i = 0; i < sudoku.length; i++) {
for (let j = 0; j < sudoku[i].length; j++) {
if (j === y) {
y_data.push(sudoku[i][j])
break;
}
}
}
if (y_data.includes(num)) {
this.errorNum++
// console.log(JSON.stringify(y_data) + '%c列重复', 'color: red');
return false;
}
let block_data = this.returnBlock(sudoku, x, y);
if (block_data.includes(num)) {
this.blockError++
// console.log(JSON.stringify(block_data) + '%c块重复', 'color: red');
return false;
}
return true;
// console.log(x_data.toString())
// console.log(y_data.toString())
// console.log('----------------')
},
/**
* 根据x轴和y轴在二维数组中获取3*3区域数据并返回
* @param {Array} sudoku 二维数组
* @param {Number} x 行数,x轴
* @param {Number} y 列数,y轴
* @returns {Array} 返回3*3区域数组
*/
returnBlock(sudoku, x, y) {
if (x === 0) return []
let block = [];
if (x < 3) {
x = 3;
} else if (x < 6) {
x = 6;
} else if (x < 9) {
x = 9;
}
if (y < 3) {
y = 3;
} else if (y < 6) {
y = 6;
} else if (y < 9) {
y = 9;
}
for (let i = x - 3; i < x; i++) {
for (let j = y - 3; j < y; j++) {
if (sudoku[i][j]) block.push(sudoku[i][j])
}
}
// console.log(JSON.stringify(block));
return block
},
/**
* 将所传的数独棋盘根据难度等级挖孔并返回
* @param {Array} sudoku 二维数组
* @param {Number} difficulty 难度等级
* @returns {Boolean} 返回数组
*/
digHole(sudoku, difficulty = 1) {
//难度等级
difficulty = difficulty > 6 ? 6 : difficulty;
//浮动区间
const interval = this.random([-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5], 0, 10)
const level = difficulty * 10 + interval;
let arr = [];
for (let i = 0; i < 81; i++) {
arr.push(i);
}
//产生随机去除的数位置
let digArr = [];
for (let i = 0; i < level; i++) {
let num = this.random(arr, 0, arr.length - 1);
arr.splice(arr.indexOf(num), 1);
digArr.push(num);
}
//挖去相应位置的数
for (let i = 0; i < digArr.length; i++) {
let x = parseInt(digArr[i] / this.rowNum);
let y = digArr[i] - x * this.rowNum;
sudoku[x][y] = '';
}
return sudoku;
}
}
function main() {
let startDate = new Date()
let sudoku = createSudoku.init()
sudoku = createSudoku.digHole(sudoku, 1)
renderSudoku(sudoku)
console.log(sudoku);
console.log('耗时' + (new Date() - startDate) + 'ms');
}
// for (let i = 0; i < 50; i++) {
main();
// }
//渲染数独
function renderSudoku(sudoku) {
let str = '';
for (let i = 0; i < sudoku.length; i++) {
str += '<div>';
for (let j = 0; j < sudoku[i].length; j++) {
str += '<div>' + sudoku[i][j] + '</div>';
}
str += '</div>';
}
document.querySelector('.sudoku-box').innerHTML = str;
}
document.querySelector('button').addEventListener('click', function (e) {
main()
})
</script>
</html>