vue3+ts简单实现数独

393 阅读2分钟

前几天在刷算法的时候,遇到了这么一个题目——36. 有效的数独 - 力扣(LeetCode),做完之后就有了一个想法:这不就可以当做数独游戏的判断条件(当然还必须判断数独矩阵是否被填满)吗?以前写个人博客的时候还想着写一下小游戏,正好也有挺久没接触vue3了,就当练练手写了这个小游戏。

预览:数独小游戏(手机和电脑都能玩)

首先我们需要解决的是数独初始化问题,我的想法是:先生成一个随机的完整的数独,再挖几个空

随机生成一个完整的数独:

我最开始的想法是:先建一个9 * 9的二维数组arr,arr的每一行都是1到9,然后再将每一行随机打乱(就相当于洗牌嘛),就硬去碰运气嘛,我就不信了。结果就是不行,我跑了将近2分钟,都没一个是符合数独的要求的,我还跑了5次,应该不是运气的原因。

既然运气不行就想别的办法呗,然后我又想到一个,二维数组每一项就是一个数组,我再将每个田字格也当成一个数组,每随机填一个数前就用indexof判断是否有重复的,有重复的就再换个数嘛。其实这还是碰运气,因为我没去管竖的是否满足不重复的条件,我想着如果要去检测竖的是否满足要求(感觉会很复杂),那我还不如去碰运气。结果还是不行。然后我想着既然竖的不好判断,那我就按竖的填数字不就好了嘛!

完成完整数独的大概代码:

function randNine() {
    return [1, 2, 3, 4, 5, 6, 7, 8, 9].sort(() => Math.random() - 0.5)
}
function makeMatrix(n) {
    return Array.from(Array(n), () => [])
}
//如果当前这个数字不符合要求,就将这个数字及以后的移一下
function moveArrItem(arr, start) {
    const first = arr[start]
    for (let i = start; i < 8; i++) {
        arr[i] = arr[i + 1]
    }
    arr[8] = first
}
function makeSudoku() {
    while (1) {
        let x =-1
        const digits = makeMatrix(9)
        let randNineArr = randNine()
        for (let i = 0; i < 9; i++) {
            digits[i].push(randNineArr[i])
        }
        let box = makeMatrix(3)
        box[0] = [...randNineArr.slice(0, 3)]
        box[1] = [...randNineArr.slice(3, 6)]
        box[2] = [...randNineArr.slice(6)]
        let flag = 0
        for (let i = 1; i < 9; i++) {
            randNineArr = randNine()
            //每三列,就初始化九宫格
            if (i % 3 === 0) {
                box = makeMatrix(3)
            }
            let count = 0
            for (let j = 0; j < 9; j++) {
            //判断横向和九宫格里是否重复
                if (box[Math.floor(j / 3)].indexOf(randNineArr[j]) === -1 && digits[j].indexOf(randNineArr[j]) === -1) {
                    x = box[Math.floor(j / 3)].indexOf(randNineArr[j])
                    digits[j].push(randNineArr[j])
                    box[Math.floor(j / 3)].push(randNineArr[j])
                    count = 0
                } else {
                    count++
                    moveArrItem(randNineArr, j)
                    j--
                    if (count > 8 - j) {
                        flag = 1
                        break
                    }
                }
            }
            if (flag) break
        }
        if (!flag) {
            return digits
        }
    }
}

那之后也就简单很多了,无非是调一调样式了

布局还是用flex布局(其实最开始是用的grid,但我不是特别熟练,遇到一些问题后就直接用永远滴神--flex了)

还有就是挖空了,我是随机选出48个坐标(当然你可以改一下,我一直疑惑,到底是空越多越难还是越少越难?),将其在数独上的数字改为-1,然后再html中判断(v-if)是否为-1,如果是-1就不显示

HTML部分:

<template>
  <div class="box1">
    <div class="box_grid">
      <div class="row" v-for="(item1, index1) in arr2" :key="item1">
        <div
          class="row_item_outer"
          v-for="(item2, index2) in item1"
          :key="item2"
        >
          <div
            :class="{
              row_item_inner_active: index1 + '' + index2 === divActive,
            }"
            class="row_item_inner"
            @click="divClick(index1, index2)"
          >
            <template v-if="item2 !== -1">
              <template v-if="unChanged.indexOf(index1 + '' + index2) === -1">
                <span>{{ item2 }}</span>
              </template>
              <template v-else
                ><span
                  style="color: blue; font-size: 18px; font-weight: bolder"
                  >{{ item2 }}</span
                ></template
              >
            </template>
          </div>
        </div>
      </div>
      <div class="numberBtns">
        <button v-for="item in 9" :key="item" @click="btnClick(item)">
          {{ item }}
        </button>
      </div>
      <div class="gameSetBtns">
        <button @click="restart()">重新开始</button>
        <button @click="reset()">复原</button>
        <el-button @click="finish()">完成</el-button>
      </div>
    </div>
  </div>
</template>

ts部分:

<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import type { Action } from "element-plus";

class Sudoku {
  digits: number[][];
  unChanged: string[];
  unChangedCount: number;
  constructor() {
    this.digits = this.makeMatrix(9);
    this.unChangedCount = 48;//挖掉48个空
    this.unChanged = [];//挖掉的空
  }
  makeSudoku(): boolean {
    const temp = this.makeMatrix(9);
    let randNineArr = this.randNine();
    for (let i = 0; i < 9; i++) {
      temp[i].push(randNineArr[i]);
    }
    let box = this.makeMatrix(3);
    box[0] = [...randNineArr.slice(0, 3)];
    box[1] = [...randNineArr.slice(3, 6)];
    box[2] = [...randNineArr.slice(6)];
    for (let i = 1; i < 9; i++) {
      randNineArr = this.randNine();
      if (i % 3 === 0) {
        box = this.makeMatrix(3);
      }
      let count = 0;
      for (let j = 0; j < 9; j++) {
        if (
          box[Math.floor(j / 3)].indexOf(randNineArr[j]) === -1 &&
          temp[j].indexOf(randNineArr[j]) === -1
        ) {
          temp[j].push(randNineArr[j]);
          box[Math.floor(j / 3)].push(randNineArr[j]);
          count = 0;
        } else {
          count++;
          this.moveArrItem(randNineArr, j);
          j--;
          if (count > 8 - j) {
            return false;
          }
        }
      }
    }
    this.digits = temp;
    return true;
  }
  randNine() {
    return [1, 2, 3, 4, 5, 6, 7, 8, 9].sort(() => Math.random() - 0.5);
  }
  makeMatrix(n: number): number[][] {
    return Array.from(Array(n), (): number[] => []);
  }
  moveArrItem(arr: number[], start: number) {
    const first = arr[start];
    for (let i = start; i < 8; i++) {
      arr[i] = arr[i + 1];
    }
    arr[8] = first;
  }
  shuffle() {
    this.unChanged = [];
    while (this.unChanged.length < this.unChangedCount) {
      const x = Math.floor(Math.random() * 9);
      const y = Math.floor(Math.random() * 9);
      if (this.unChanged.indexOf(x + "" + y) !== -1) {
        continue;
      } else {
        this.unChanged.push(`${x}${y}`);
        this.digits[x][y] = -1;
      }
    }
  }
}

const indexs = ref([12, 12]);
const sudoku = new Sudoku();
while (!sudoku.makeSudoku()) {}
sudoku.shuffle();
const arr2 = ref(copy(sudoku.digits));
const divActive = ref("");
const unChanged = ref([...sudoku.unChanged]);
function btnClick(x: number) {
  arr2.value[indexs.value[0]][indexs.value[1]] = x;
}
function divClick(index1: number, index2: number) {
  if (sudoku.unChanged.indexOf(index1 + "" + index2) !== -1) {
    divActive.value = index1 + "" + index2;
    indexs.value = [];
    indexs.value.push(index1, index2);
  }
}
function restart() {
  while (!sudoku.makeSudoku()) {}
  sudoku.shuffle();
  unChanged.value = [...sudoku.unChanged];
  arr2.value = copy(sudoku.digits);
}
function reset() {
  arr2.value = copy(sudoku.digits);
}
function isValidSudoku(board: number[][]): boolean {
  const col = {};
  const box = {};
  for (let i = 0; i < 9; i++) {
    for (let j = 0; j < 9; j++) {
      const x = board[i][j];
      const boxIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);
      if (col[j + "" + x] || box[boxIndex + "" + x]) {
        return false;
      }
      col[j + "" + x] = 1;
      box[boxIndex + "" + x] = 1;
    }
  }
  return true;
}
function copy(arr: number[][]): number[][] {
  const re: number[][] = [];
  for (let i = 0; i < arr.length; i++) {
    const temp = arr[i].concat();
    re.push(temp);
  }
  return re;
}
function finish() {
  for (const item of arr2.value) {
    for (const x of item) {
      if (x === -1) {
        ElMessageBox.alert("还未完成", "Title", {
          confirmButtonText: "OK",
          callback: (action: Action) => {
            ElMessage({
              type: "info",
              message: `action: ${action}`,
            });
          },
        });
        return;
      }
    }
  }
  if (isValidSudoku(arr2.value)) {
    ElMessageBox.alert("You Win!!!", "Title", {
      // if you want to disable its autofocus
      // autofocus: false,
      confirmButtonText: "OK",
      callback: (action: Action) => {
        ElMessage({
          type: "info",
          message: `action: ${action}`,
        });
      },
    });
  } else {
    ElMessageBox.alert("失败", "Title", {
      // if you want to disable its autofocus
      // autofocus: false,
      confirmButtonText: "OK",
      callback: (action: Action) => {
        ElMessage({
          type: "info",
          message: `action: ${action}`,
        });
      },
    });
  }
}
</script>

css部分:

<style scoped lang="less">
.box1 {
  padding: 20px;

  .box_grid {
    .row {
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;

      .row_item_outer {
        width: 30px;
        height: 30px;
        border: 0.2px solid rgba(0, 0, 0, 0.2);
      }

      .row_item_inner {
        width: 27px;
        height: 27px;
      }

      .row_item_inner_active {
        background-color: yellow;
      }

      .row_item_outer:first-child {
        border-left: 2px solid rgba(0, 0, 0, 1);
      }

      .row_item_outer:nth-child(3n) {
        border-right: 2px solid rgba(0, 0, 0, 1);
      }
    }

    .row:nth-child(1) {
      border-top: 2px solid rgba(0, 0, 0, 1);

      div {
        border-top: none;
      }
    }

    .row:nth-child(3n) {
      border-bottom: 2px solid rgba(0, 0, 0, 1);

      div {
        border-bottom: none;
      }
    }

    .numberBtns {
      margin-top: 15px;

      button {
        width: 30px;
        height: 30px;
      }
    }

    .gameSetBtns {
      margin-top: 15px;

      button {
        margin-right: 15px;
      }
    }
  }
}
</style>