当前位置: 首页 > news >正文

HTML+JS+CSS制作一个数独游戏

闲来无事,用HTML+JS+CSS制作了一个数独游戏消遣。

1、游戏的界面:

2、游戏的玩法:

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

下面是由戏的全部代码。其中HTML负责UI构造,CSS负责UI的显示,JS包含了游戏的全部逻辑。

1、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

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

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的空棋盘,所有格子初始为nulllet board = Array.from({ length: 9 }, () => Array(9).fill(null));// 递归回溯填充函数,从左上角(0,0)开始function fill(row, col) {// 如果行号越界,说明已填满整盘,返回trueif (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都不行,说明此路不通,返回falsereturn 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知道生成的代码框架基础上改进和增加功能而成。

http://www.lryc.cn/news/582981.html

相关文章:

  • 原生屏幕旋转算法(AccelSensor)
  • 力扣-31.下一个排列
  • Python打卡:Day47
  • 【排序】插入排序
  • 单调栈通关指南:从力扣 84 到力扣 42
  • eslint扁平化配置
  • IoTDB:专为物联网场景设计的高性能时序数据库
  • 深圳凭物联网软件开发构建智慧‘城市大脑‘
  • c语言学习_函数递归
  • 「Java案例」求n1-n2内的素数
  • 使用Node.js搭建Web应用有哪些注意事项?
  • 在 Vue2 与 Vue3 中,面对 **大数据量交互体验优化** 和 **ECharts 大数据渲染性能优化**
  • 萌新赛第(一)场
  • EfficientVMamba: Atrous Selective Scan for Light Weight Visual Mamba论文精读(逐段解析)
  • 华为泰山服务器重启后出现 XFS 文件系统磁盘“不识别”(无法挂载或访问),但挂载点目录仍在且无数据
  • Nginx完全指南 - 从入门到精通(加强版)
  • 【深度学习入门 鱼书学习笔记(1)感知机】
  • Java常用加密算法详解与实战代码 - 附可直接运行的测试示例
  • Spring Boot 多数据源切换:AbstractRoutingDataSource
  • 语言模型 RLHF 实践指南(一):策略网络、价值网络与 PPO 损失函数
  • MySQL索引面试问题梳理
  • 【Android】组件及布局介绍
  • Flutter基础(前端教程②-卡片列表)
  • 【牛客刷题】小红的v三元组
  • 从单体到微服务:Spring Cloud 开篇与微服务设计
  • 音频主动降噪技术
  • 暑假算法日记第四天
  • Spring AI:检索增强生成(RAG)
  • 工作中的思考
  • Java教程:【程序调试技巧】入门