闲来无事,用HTML+JS+CSS制作了一个数独游戏消遣。
1、游戏的界面:

2、游戏的玩法:

3、游戏结束时弹出提示框

下面是由戏的全部代码。其中HTML负责UI构造,CSS负责UI的显示,JS包含了游戏的全部逻辑。
1、HTML
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>数独游戏</title>
<link rel="stylesheet" href="sudoku_style.css" />
</head>
<body>
<h1>数独游戏</h1>
<div class="sudoku-container">
<table id="sudoku-board"></table>
<div class="buttons">
<button id="solve-btn">新题</button>
<button id="reset-btn">重置</button>
<button id="answer-btn">答案</button>
</div>
</div>
<script src="sudoku_script.js"></script>
</body>
</html>
2、CSS
css
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f4f4f4;
}
h1 {
margin-top: 30px;
}
.sudoku-container {
display: inline-block;
margin-top: 20px;
padding: 20px;
background: beige;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
}
#sudoku-board {
border-collapse: collapse;
margin: 0 auto;
}
#sudoku-board td {
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
border: 1px solid #999;
font-size: 18px;
cursor: pointer;
transition: background 0.2s ease;
position: relative;
padding: 0;
}
.sudoku-mini-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
width: 100%;
height: 100%;
}
.mini-cell {
font-size: 11px;
color: #222;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
user-select: none;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.mini-cell.gray {
color: #bbb;
cursor: not-allowed;
}
.mini-cell.black {
color: #222;
}
.mini-cell.yellow {
color: #ff0;
}
.mini-cell:hover:not(.gray) {
background: #e0e0e0;
}
.sudoku-cell-fixed {
font-size: 24px;
font-weight: bold;
color: #1976d2;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.sudoku-cell-user {
font-size: 24px;
font-weight: bold;
color: #388e3c;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.sudoku-mini-grid {
width: 100%;
height: 100%;
pointer-events: auto;
}
#sudoku-board tr:nth-child(3n) td {
border-bottom: 2px solid #000;
}
#sudoku-board td:nth-child(3n) {
border-right: 2px solid #000;
}
#sudoku-board tr:first-child td {
border-top: 2px solid #000;
}
#sudoku-board td:first-child {
border-left: 2px solid #000;
}
#sudoku-board .preset {
background-color: #e0e0e0;
font-weight: bold;
}
#sudoku-board .user-input {
background-color: #fff;
}
#sudoku-board .error {
color: red;
}
.buttons {
margin-top: 20px;
}
.buttons button {
margin: 0 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
transition: background 0.2s ease;
}
.buttons button:hover {
background-color: #45a049;
}
.buttons button:active {
background-color: #3e8e41;
}
.sudoku-cell-fixed.red, .sudoku-cell-user.red {
color: red;
}
3、JS
javascript
const boardElement = document.getElementById("sudoku-board");
const solveBtn = document.getElementById("solve-btn");
const resetBtn = document.getElementById("reset-btn");
const answerBtn = document.getElementById("answer-btn");
let originalBoard = [
[5, 3, null, null, 7, null, null, null, null],
[6, null, null, 1, 9, 5, null, null, null],
[null, 9, 8, null, null, null, null, 6, null],
[8, null, null, null, 6, null, null, null, 3],
[4, null, null, 8, null, 3, null, null, 1],
[7, null, null, null, 2, null, null, null, 6],
[null, 6, null, null, null, null, 2, 8, null],
[null, null, null, 4, 1, 9, null, null, 5],
[null, null, null, null, 8, null, null, 7, 9]
];
let currentBoard = JSON.parse(JSON.stringify(originalBoard));
// 保存被标红的格子,格式如:'row,col'
let redCells = new Set();
// 计时相关
let timerStart = null;
let timerUsed = 0;
function getCandidates(row, col) {
if (currentBoard[row][col]) return [];
let candidates = [];
for (let num = 1; num <= 9; num++) {
if (isValidCell(row, col, num)) candidates.push(num);
}
return candidates;
}
function drawBoard() {
boardElement.innerHTML = "";
for (let row = 0; row < 9; row++) {
const tr = document.createElement("tr");
for (let col = 0; col < 9; col++) {
const td = document.createElement("td");
td.dataset.row = row;
td.dataset.col = col;
// 已确定数字
if (currentBoard[row][col]) {
const isPreset = originalBoard[row][col];
let cellClass = isPreset ? 'sudoku-cell-fixed' : 'sudoku-cell-user';
if (redCells.has(row + ',' + col)) {
cellClass += ' red';
}
td.innerHTML = `<div class="${cellClass}">${currentBoard[row][col]}</div>`;
// 右键取消
td.oncontextmenu = function(e) {
e.preventDefault();
if (!isPreset) {
currentBoard[row][col] = null;
redCells.delete(row + ',' + col);
drawBoard();
}
};
// 左键标红
td.onclick = function(e) {
if (e.button === 0) {
if (!redCells.has(row + ',' + col)) {
redCells.add(row + ',' + col);
drawBoard();
}
}
};
} else {
// 未确定,渲染9小格
const miniGrid = document.createElement('div');
miniGrid.className = 'sudoku-mini-grid';
for (let k = 1; k <= 9; k++) {
const miniCell = document.createElement('div');
miniCell.className = 'mini-cell';
miniCell.textContent = k;
// 判断是否可选
if (isValidCell(row, col, k)) {
miniCell.classList.add('black');
// 中键点击确定该数字
miniCell.onmousedown = function(e) {
if (e.button === 1) { // 中键
e.preventDefault();
// 仅在首次确定未确定格时启动计时
if (currentBoard[row][col] === null && timerStart === null) {
timerStart = Date.now();
}
currentBoard[row][col] = k;
drawBoard();
}
};
} else {
miniCell.classList.add('gray');
}
miniGrid.appendChild(miniCell);
}
td.appendChild(miniGrid);
}
tr.appendChild(td);
}
boardElement.appendChild(tr);
}
// 检查是否全部填满
let allFilled = true;
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (!currentBoard[row][col]) allFilled = false;
}
}
if (allFilled) {
if (timerStart !== null) {
timerUsed = Math.round((Date.now() - timerStart) / 1000);
setTimeout(() => { alert(`恭喜您解决了本题,共计耗时${timerUsed}秒!`); timerStart = null; }, 100);
}
}
}
// 生成唯一解数独新题
solveBtn.addEventListener("click", async () => {
// 生成唯一解数独
let puzzle;
do {
puzzle = generateSudokuPuzzle();
} while (!puzzle || countSolutions(puzzle) !== 1);
originalBoard = puzzle;
currentBoard = JSON.parse(JSON.stringify(originalBoard));
redCells.clear();
timerStart = null;
drawBoard();
});
// 生成完整解
// 随机生成一个完整的数独解(9x9的填满且合法的盘面)
function generateFullSolution() {
// 创建一个9x9的空棋盘,所有格子初始为null
let board = Array.from({ length: 9 }, () => Array(9).fill(null));
// 递归回溯填充函数,从左上角(0,0)开始
function fill(row, col) {
// 如果行号越界,说明已填满整盘,返回true
if (row === 9) return true;
// 计算下一个要填的格子的行列号
let nextRow = col === 8 ? row + 1 : row;
let nextCol = col === 8 ? 0 : col + 1;
// 1~9随机顺序尝试,增加解的多样性
let nums = [1,2,3,4,5,6,7,8,9].sort(() => Math.random() - 0.5);
for (let num of nums) {
// 判断num是否可以填入当前格(不违反数独规则)
if (isValidForBoard(board, row, col, num)) {
board[row][col] = num; // 填入数字
// 递归填下一个格子,若成功则整盘可解
if (fill(nextRow, nextCol)) return true;
board[row][col] = null; // 回溯,撤销填入
}
}
// 1~9都不行,说明此路不通,返回false
return false;
}
// 从(0,0)开始填盘
fill(0,0);
return board; // 返回填好的完整解
}
// 随机挖空,生成题目
function generateSudokuPuzzle() {
let solution = generateFullSolution();
let puzzle = JSON.parse(JSON.stringify(solution));
// 随机顺序挖空
let cells = [];
for (let r = 0; r < 9; r++) for (let c = 0; c < 9; c++) cells.push([r,c]);
cells = cells.sort(() => Math.random() - 0.5);
for (let i = 0; i < 60; i++) { // 最多挖60个空
let [r,c] = cells[i];
let backup = puzzle[r][c];
puzzle[r][c] = null;
// 挖空后如果解不唯一,撤回
if (countSolutions(puzzle) !== 1) puzzle[r][c] = backup;
}
return puzzle;
}
// 判断唯一解
function countSolutions(board) {
let count = 0;
let b = JSON.parse(JSON.stringify(board));
function dfs() {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
if (b[r][c] === null) {
for (let num = 1; num <= 9; num++) {
if (isValidForBoard(b, r, c, num)) {
b[r][c] = num;
dfs();
b[r][c] = null;
if (count > 1) return;
}
}
return;
}
}
}
count++;
}
dfs();
return count;
}
function isValidForBoard(board, row, col, num) {
for (let i = 0; i < 9; i++) {
if (board[row][i] === num || board[i][col] === num) return false;
}
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
if (board[boxRow + r][boxCol + c] === num) return false;
}
}
return true;
}
function markErrors() {
clearErrorMarks();
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const cell = boardElement.rows[r].cells[c];
if (!isValidCell(r, c, currentBoard[r][c])) {
cell.classList.add("error");
}
}
}
}
function isValidCell(row, col, num) {
for (let i = 0; i < 9; i++) {
if ((i !== col && currentBoard[row][i] === num) ||
(i !== row && currentBoard[i][col] === num)) {
return false;
}
}
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const x = boxRow + r;
const y = boxCol + c;
if (x !== row && y !== col && currentBoard[x][y] === num) {
return false;
}
}
}
return true;
}
function clearErrorMarks() {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const cell = boardElement.rows[r].cells[c];
cell.classList.remove("error");
}
}
}
resetBtn.addEventListener("click", () => {
currentBoard = JSON.parse(JSON.stringify(originalBoard));
redCells.clear();
drawBoard();
});
answerBtn.addEventListener("click", () => {
let solution = solveSudoku(JSON.parse(JSON.stringify(originalBoard)));
if (!solution) {
return;
}
currentBoard = solution;
redCells.clear();
timerStart = null;
drawBoard();
});
function updateBoardUI(board) {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const cell = boardElement.rows[r].cells[c];
cell.textContent = board[r][c];
}
}
}
// 求解器部分(回溯算法)
function solveSudoku(board) {
function isValid(row, col, num) {
for (let i = 0; i < 9; i++) {
if (board[row][i] === num || board[i][col] === num) return false;
}
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
if (board[boxRow + r][boxCol + c] === num) return false;
}
}
return true;
}
function backtrack() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === null) {
for (let num = 1; num <= 9; num++) {
if (isValid(row, col, num)) {
board[row][col] = num;
if (backtrack()) return true;
board[row][col] = null;
}
}
return false;
}
}
}
return true;
}
if (backtrack()) {
return board;
}
return false;
}
function isValidSudoku(board) {
const rows = Array.from({ length: 9 }, () => new Set());
const cols = Array.from({ length: 9 }, () => new Set());
const boxes = Array.from({ length: 9 }, () => new Set());
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
const val = board[r][c];
if (val === null) continue;
const boxIndex = Math.floor(r / 3) * 3 + Math.floor(c / 3);
if (rows[r].has(val) || cols[c].has(val) || boxes[boxIndex].has(val)) {
return false;
}
rows[r].add(val);
cols[c].add(val);
boxes[boxIndex].add(val);
}
}
return true;
}
drawBoard();
本文代码在CSDN的C知道生成的代码框架基础上改进和增加功能而成。