js 生成数独游戏算法

1,816 阅读3分钟

这篇文章讲的是如何使用 JavaScript 生成数独游戏的算法思路

需要一定的编程基础 完整代码在文章末尾

数独规则

  1. 数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复。
  2. 数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。

数独的组成

组成解释
方格水平方向有九横行,垂直方向有九纵列的矩形,画分八十一个小正方形,称为九宫格(Grid),如图一所示,是数独(Sudoku)的作用范围。
水平方向的每一横行有九格,每一横行称为行(Row),如图二所示。
垂直方向的每一纵列有九格,每一纵列称为列(Column),如图三所示。
三行与三列相交之处有九格,每一单元称为小九宫(Box、Block),简称宫,如图四所示(在杀手数独中,宫往往用单词Nonet表示)。

以上介绍摘抄百度百科(数独)

Snipaste_20220522_115222.png

算法思考

看完游戏规则和组成规则,下面就可以开发生成数独的算法了。

  1. 生成一个 9 * 9 的宫格,这里我使用的是二维数组来保存数据 [ [], [], [], ...]

  2. 创建随机数,在一个嵌套的循环中依次添加至九宫格数组中

    此时添加的数字很多都是重复的,下面我们来处理重复的数

  3. 已知一行中的数字 1到9 是不可以重复的,此时只需要将二维数组中对应的一维数据,也就是数组中的第n个元素,与当前随机数做查重判断,如果重复生成新的随机数

  4. 按照数独规则,每列中的数字也是不可以重复的,此时需要将列的数据进行处理,取出对应编号的列数据,同理与的操作一致

  5. 的操作同上

上诉就是数独的算法思考,如果自己动手的话会遇到很多坑,比如随机数太多导致循环次数多影响性能,或者校验规则卡死等问题。

随机数太多问题

处理方式:创建一个数组,数组中有 1-9 的数字,每次添加成功的数字将从数组中剔除,直到下一行重置

这里会出现一种 bug,有几率死循环,因为数独的不确定性,规则在校验时会出现只有一个数但是校验不通过的情况

校验规则卡死

处理方式:回退数据,每次校验不成功记录次数,次数过多回退数据,然后重置记录

处理方式均可在代码中根据注释查看详细操作。

效果图

Snipaste_20220522_120402.png

Snipaste_2022-05-22_12-10-58.png


在线数独游戏

塞北孤舟 数独 (guanxl.top)

Snipaste_2022-05-22_12-13-03.png

完整代码

<!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>