文章目录
手把手教你用HTML5 Canvas开发数独(Sudoku)小游戏
本文将详细介绍如何使用HTML5 Canvas从头开始开发一个完整的数独小游戏。我们将逐步分解开发过程,从基础设置到高级功能实现。

1. 项目概述与准备工作
1.1 数独游戏规则
数独是一个9×9的网格游戏,被划分为9个3×3的小宫格。游戏目标是在每个单元格中填入1-9的数字,使得:
- 每行包含1-9的所有数字且不重复
- 每列包含1-9的所有数字且不重复
- 每个3×3宫格包含1-9的所有数字且不重复
1.2 技术选型
- HTML5 Canvas:用于绘制游戏界面
- JavaScript:实现游戏逻辑
- CSS:美化界面和布局
1.3 项目结构规划
- 游戏状态管理
- 数独生成算法
- 用户交互处理
- 游戏界面绘制
- 辅助功能实现
2. 创建基础HTML结构与样式
首先创建基本的HTML结构,包含游戏区域、控制面板和信息显示区域。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas数独小游戏</title>
<style>
/* 基础样式设置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.container {
max-width: 800px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
/* 游戏信息面板样式 */
.game-info {
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
}
/* 游戏主区域样式 */
.sudoku-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
align-items: center;
}
/* Canvas样式 */
#sudokuCanvas {
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
/* 控制按钮样式 */
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
width: 100%;
}
button {
background: linear-gradient(135deg, #4facfe, #00f2fe);
border: none;
color: white;
padding: 12px 25px;
border-radius: 50px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.game-info {
flex-direction: column;
gap: 10px;
}
.controls {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Canvas数独小游戏</h1>
<p>使用Canvas实现的9宫格数独游戏</p>
</header>
<div class="game-info">
<div class="info-item">
<div class="info-label">难度</div>
<div class="info-value" id="difficulty">中等</div>
</div>
<div class="info-item">
<div class="info-label">用时</div>
<div class="info-value" id="timer">00:00</div>
</div>
<div class="info-item">
<div class="info-label">错误</div>
<div class="info-value" id="errors">0</div>
</div>
</div>
<div class="sudoku-container">
<canvas id="sudokuCanvas" width="450" height="450"></canvas>
<div class="number-pad">
<!-- 数字按钮将在这里动态生成 -->
</div>
</div>
<div class="controls">
<button id="newGameBtn">新游戏</button>
<button id="hintBtn">提示 (3)</button>
<button id="checkBtn">检查答案</button>
</div>
</div>
<script>
// 游戏逻辑将在这里实现
</script>
</body>
</html>
3. 初始化Canvas与游戏状态
接下来,我们将设置Canvas并初始化游戏状态。
// 获取Canvas元素和上下文
const sudokuCanvas = document.getElementById('sudokuCanvas');
const ctx = sudokuCanvas.getContext('2d');
// 游戏状态和配置
const gameState = {
difficulty: 'medium', // 默认难度
time: 0, // 游戏时间
timerInterval: null, // 计时器引用
errors: 0, // 错误计数
hints: 3, // 提示次数
selectedCell: null, // 当前选中的单元格
isCompleted: false // 游戏是否完成
};
// 难度配置
const difficulties = {
easy: { name: '简单', emptyCells: 35 },
medium: { name: '中等', emptyCells: 45 },
hard: { name: '困难', emptyCells: 55 }
};
// 数独相关变量
let solution = []; // 完整解决方案
let puzzle = []; // 当前谜题(包含空白)
let userInput = []; // 用户输入的数字
let initialPuzzle = []; // 初始谜题状态
// 初始化游戏
function initGame() {
// 重置游戏状态
clearInterval(gameState.timerInterval);
gameState.time = 0;
gameState.errors = 0;
gameState.hints = 3;
gameState.selectedCell = null;
gameState.isCompleted = false;
// 更新UI
updateUI();
// 生成数独谜题
generateSudoku();
// 绘制数独网格
drawSudoku();
// 启动计时器
startTimer();
}
4. 实现数独生成算法
数独生成是游戏的核心,我们将使用回溯算法生成有效的数独谜题。
// 生成数独谜题
function generateSudoku() {
// 创建一个完整的数独解决方案
solution = createSolvedSudoku();
// 根据难度移除一些数字
puzzle = JSON.parse(JSON.stringify(solution)); // 深拷贝
userInput = Array(9).fill().map(() => Array(9).fill(0));
initialPuzzle = JSON.parse(JSON.stringify(puzzle));
const difficulty = difficulties[gameState.difficulty];
removeNumbers(puzzle, difficulty.emptyCells);
}
// 创建已解决的数独
function createSolvedSudoku() {
// 创建一个9x9的空数组
const grid = Array(9).fill().map(() => Array(9).fill(0));
// 使用回溯算法填充数独
solveSudoku(grid);
return grid;
}
// 回溯算法解决数独
function solveSudoku(grid) {
const emptyCell = findEmptyCell(grid);
if (!emptyCell) return true; // 没有空单元格,解决完成
const [row, col] = emptyCell;
// 随机打乱数字顺序,增加生成谜题的随机性
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
shuffleArray(numbers);
for (let num of numbers) {
if (isValidPlacement(grid, row, col, num)) {
grid[row][col] = num;
if (solveSudoku(grid)) {
return true;
}
grid[row][col] = 0; // 回溯
}
}
return false; // 无解
}
// 查找空单元格
function findEmptyCell(grid) {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (grid[row][col] === 0) {
return [row, col];
}
}
}
return null;
}
// 检查数字放置是否有效
function isValidPlacement(grid, row, col, num) {
// 检查行
for (let x = 0; x < 9; x++) {
if (grid[row][x] === num) return false;
}
// 检查列
for (let x = 0; x < 9; x++) {
if (grid[x][col] === num) return false;
}
// 检查3x3宫格
const startRow = Math.floor(row / 3) * 3;
const startCol = Math.floor(col / 3) * 3;
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
if (grid[startRow + r][startCol + c] === num) return false;
}
}
return true;
}
// 随机移除数字创建谜题
function removeNumbers(grid, count) {
let removed = 0;
while (removed < count) {
const row = Math.floor(Math.random() * 9);
const col = Math.floor(Math.random() * 9);
if (grid[row][col] !== 0) {
grid[row][col] = 0;
removed++;
}
}
}
// 工具函数:打乱数组
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
5. 绘制数独网格与数字
使用Canvas API绘制数独网格、数字和选中效果。
// 绘制数独网格
function drawSudoku() {
const cellSize = sudokuCanvas.width / 9;
// 清除画布
ctx.clearRect(0, 0, sudokuCanvas.width, sudokuCanvas.height);
// 绘制单元格
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
const x = col * cellSize;
const y = row * cellSize;
// 绘制单元格背景
ctx.fillStyle = '#ffffff';
ctx.fillRect(x, y, cellSize, cellSize);
// 如果单元格被选中,添加背景色
if (gameState.selectedCell &&
gameState.selectedCell[0] === row &&
gameState.selectedCell[1] === col) {
ctx.fillStyle = 'rgba(79, 172, 254, 0.2)';
ctx.fillRect(x, y, cellSize, cellSize);
}
// 绘制边框
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, cellSize, cellSize);
// 绘制数字
const initialValue = puzzle[row][col];
const userValue = userInput[row][col];
if (initialValue !== 0 || userValue !== 0) {
ctx.font = `bold ${cellSize * 0.6}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 初始谜题数字为黑色,用户输入为蓝色
if (initialValue !== 0) {
ctx.fillStyle = '#000000';
ctx.fillText(
initialValue.toString(),
x + cellSize / 2,
y + cellSize / 2
);
} else {
// 用户输入的数字
ctx.fillStyle = '#4facfe';
// 检查用户输入是否正确
if (userValue !== solution[row][col]) {
ctx.fillStyle = '#ff6b6b';
}
ctx.fillText(
userValue.toString(),
x + cellSize / 2,
y + cellSize / 2
);
}
}
}
}
// 绘制粗线分隔3x3宫格
ctx.strokeStyle = '#000000';
ctx.lineWidth = 3;
for (let i = 0; i <= 9; i += 3) {
// 垂直线
ctx.beginPath();
ctx.moveTo(i * cellSize, 0);
ctx.lineTo(i * cellSize, sudokuCanvas.height);
ctx.stroke();
// 水平线
ctx.beginPath();
ctx.moveTo(0, i * cellSize);
ctx.lineTo(sudokuCanvas.width, i * cellSize);
ctx.stroke();
}
// 绘制选中的单元格边框
if (gameState.selectedCell) {
const [row, col] = gameState.selectedCell;
const x = col * cellSize;
const y = row * cellSize;
ctx.strokeStyle = '#4facfe';
ctx.lineWidth = 3;
ctx.strokeRect(x, y, cellSize, cellSize);
}
}
6. 实现用户交互功能
处理用户点击、键盘输入和数字按钮操作。
// 处理单元格点击
function handleCanvasClick(event) {
if (gameState.isCompleted) return;
const rect = sudokuCanvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const cellSize = sudokuCanvas.width / 9;
const col = Math.floor(x / cellSize);
const row = Math.floor(y / cellSize);
// 只有当单元格是空的(初始谜题中没有数字)时才能选择
if (puzzle[row][col] === 0) {
gameState.selectedCell = [row, col];
drawSudoku();
}
}
// 输入数字
function inputNumber(number) {
if (!gameState.selectedCell || gameState.isCompleted) return;
const [row, col] = gameState.selectedCell;
// 只有当单元格是空的(初始谜题中没有数字)时才能输入
if (puzzle[row][col] === 0) {
userInput[row][col] = number;
// 检查输入是否正确
if (number !== solution[row][col]) {
gameState.errors++;
updateUI();
}
drawSudoku();
checkCompletion();
}
}
// 删除数字
function deleteNumber() {
if (!gameState.selectedCell || gameState.isCompleted) return;
const [row, col] = gameState.selectedCell;
// 只有当单元格是空的(初始谜题中没有数字)时才能删除
if (puzzle[row][col] === 0) {
userInput[row][col] = 0;
drawSudoku();
}
}
// 键盘事件处理
document.addEventListener('keydown', (event) => {
if (gameState.isCompleted) return;
const key = event.key;
// 数字键1-9
if (key >= '1' && key <= '9') {
inputNumber(parseInt(key));
}
// 删除键或退格键
if (key === 'Delete' || key === 'Backspace') {
deleteNumber();
}
// 方向键移动选择
if (gameState.selectedCell) {
const [row, col] = gameState.selectedCell;
switch(key) {
case 'ArrowUp':
if (row > 0) gameState.selectedCell = [row - 1, col];
break;
case 'ArrowDown':
if (row < 8) gameState.selectedCell = [row + 1, col];
break;
case 'ArrowLeft':
if (col > 0) gameState.selectedCell = [row, col - 1];
break;
case 'ArrowRight':
if (col < 8) gameState.selectedCell = [row, col + 1];
break;
}
drawSudoku();
}
});
// 添加Canvas点击事件监听
sudokuCanvas.addEventListener('click', handleCanvasClick);
7. 实现游戏辅助功能
添加计时器、提示系统和游戏完成检测。
// 开始计时器
function startTimer() {
gameState.timerInterval = setInterval(() => {
gameState.time++;
updateTimer();
}, 1000);
}
// 更新计时器显示
function updateTimer() {
const minutes = Math.floor(gameState.time / 60);
const seconds = gameState.time % 60;
timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
// 更新UI
function updateUI() {
difficultyElement.textContent = difficulties[gameState.difficulty].name;
errorsElement.textContent = gameState.errors;
hintsElement.textContent = gameState.hints;
hintBtn.textContent = `提示 (${gameState.hints})`;
}
// 检查是否完成游戏
function checkCompletion() {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
// 如果有任何单元格为空或用户输入错误
if ((puzzle[row][col] === 0 && userInput[row][col] === 0) ||
(userInput[row][col] !== 0 && userInput[row][col] !== solution[row][col])) {
return false;
}
}
}
// 游戏完成
gameState.isCompleted = true;
clearInterval(gameState.timerInterval);
// 显示完成消息
completionTime.textContent = timerElement.textContent;
completionErrors.textContent = gameState.errors;
completionMessage.style.display = 'block';
return true;
}
// 使用提示
function useHint() {
if (gameState.hints <= 0 || gameState.isCompleted) return;
// 找到一个空的但已解决的单元格
let emptyCells = [];
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (puzzle[row][col] === 0 && userInput[row][col] === 0) {
emptyCells.push([row, col]);
}
}
}
if (emptyCells.length > 0) {
// 随机选择一个空单元格
const randomIndex = Math.floor(Math.random() * emptyCells.length);
const [row, col] = emptyCells[randomIndex];
// 填入正确答案
userInput[row][col] = solution[row][col];
gameState.hints--;
updateUI();
drawSudoku();
checkCompletion();
}
}
// 检查答案
function checkAnswer() {
let hasErrors = false;
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
// 检查用户输入是否正确
if (userInput[row][col] !== 0 && userInput[row][col] !== solution[row][col]) {
hasErrors = true;
}
}
}
if (hasErrors) {
alert('您的答案中有错误,请检查!');
} else {
alert('恭喜!您的答案目前全部正确,继续完成剩余部分!');
}
}
8. 添加控制功能与事件监听
最后,添加按钮事件监听器和数字键盘。
// 获取DOM元素
const difficultyElement = document.getElementById('difficulty');
const timerElement = document.getElementById('timer');
const errorsElement = document.getElementById('errors');
const hintsElement = document.getElementById('hints');
const newGameBtn = document.getElementById('newGameBtn');
const hintBtn = document.getElementById('hintBtn');
const checkBtn = document.getElementById('checkBtn');
const deleteBtn = document.getElementById('deleteBtn');
const difficultyButtons = document.querySelectorAll('.difficulty-btn');
const numberButtons = document.querySelectorAll('.number-btn');
// 创建数字键盘
function createNumberPad() {
const numberPad = document.querySelector('.number-pad');
numberPad.innerHTML = '';
// 创建数字按钮1-9
for (let i = 1; i <= 9; i++) {
const button = document.createElement('button');
button.className = 'number-btn';
button.dataset.number = i;
button.textContent = i;
button.addEventListener('click', () => {
inputNumber(i);
});
numberPad.appendChild(button);
}
// 创建删除按钮
const deleteButton = document.createElement('button');
deleteButton.className = 'number-btn delete';
deleteButton.id = 'deleteBtn';
deleteButton.textContent = '删除';
deleteButton.addEventListener('click', deleteNumber);
numberPad.appendChild(deleteButton);
}
// 事件监听器
newGameBtn.addEventListener('click', initGame);
hintBtn.addEventListener('click', useHint);
checkBtn.addEventListener('click', checkAnswer);
// 难度选择按钮
difficultyButtons.forEach(btn => {
btn.addEventListener('click', () => {
gameState.difficulty = btn.dataset.difficulty;
initGame();
});
});
// 初始化数字键盘
createNumberPad();
// 初始化游戏
initGame();
9. 完整代码整合与优化
将以上所有代码片段整合到一个完整的HTML文件中,并添加额外的优化和错误处理。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas数独小游戏</title>
<style>
/* 完整样式代码 - 见前面的样式部分 */
</style>
</head>
<body>
<div class="container">
<!-- 完整HTML结构 - 见前面的HTML部分 -->
</div>
<script>
// 完整的JavaScript代码 - 整合前面所有代码片段
</script>
</body>
</html>
10. 总结与扩展思路
10.1 开发总结
通过本教程,我们成功实现了一个功能完整的Canvas数独游戏,包括:
- 使用回溯算法生成有效的数独谜题
- 利用Canvas API绘制游戏界面
- 实现用户交互和输入验证
- 添加计时器、提示系统等辅助功能
10.2 性能优化建议
- 缓存计算结果:对于频繁使用的计算(如单元格位置),可以缓存结果
- 避免频繁重绘:只在必要时重绘Canvas,而不是每次交互都重绘整个画布
- 使用离屏Canvas:对于复杂的静态元素,可以使用离屏Canvas预先绘制
10.3 功能扩展思路
- 保存游戏进度:使用localStorage保存游戏状态
- 添加更多难度级别:增加专家、大师等级别
- 实现自动求解功能:添加自动求解算法
- 添加音效和动画:增强游戏体验
- 支持触摸设备:优化移动端体验
通过这个教程,您不仅学会了如何使用Canvas开发数独游戏,还掌握了游戏开发的核心概念和技术。希望这个教程对您的学习有所帮助!

1887

被折叠的 条评论
为什么被折叠?



