手把手教你用HTML5 Canvas开发数独(Sudoku)小游戏

文章目录

手把手教你用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 性能优化建议

  1. 缓存计算结果:对于频繁使用的计算(如单元格位置),可以缓存结果
  2. 避免频繁重绘:只在必要时重绘Canvas,而不是每次交互都重绘整个画布
  3. 使用离屏Canvas:对于复杂的静态元素,可以使用离屏Canvas预先绘制

10.3 功能扩展思路

  1. 保存游戏进度:使用localStorage保存游戏状态
  2. 添加更多难度级别:增加专家、大师等级别
  3. 实现自动求解功能:添加自动求解算法
  4. 添加音效和动画:增强游戏体验
  5. 支持触摸设备:优化移动端体验

通过这个教程,您不仅学会了如何使用Canvas开发数独游戏,还掌握了游戏开发的核心概念和技术。希望这个教程对您的学习有所帮助!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值