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

从零到一通过Web技术开发一个五子棋

文章目录

    • 1 你的第一个Web互动游戏——五子棋
    • 2 核心技术剖析
      • 2.1 棋盘的绘制与渲染
        • 2.1.1 HTML结构:棋盘的骨架
        • 2.1.2 CSS美化:让棋盘看起来像个样
        • 2.1.3 JavaScript动态生成:让棋盘“活”起来
      • 2.2 落子逻辑与交互
        • 2.2.1 事件监听:捕获用户的“点击”
        • 2.2.2 落子判断与棋子渲染
      • 2.3 胜负判断算法
        • 2.3.1 判断方向:五种连珠可能
        • 2.3.2 算法实现细节
      • 2.4 (进阶)多人联机:WebSocket的魔力
        • 2.4.1 为什么需要WebSocket?
        • 2.4.2 WebSocket基本原理
        • 2.4.3 前后端实现思路
    • 3 实践
      • 3.1 代码结构与模块化
      • 3.2 错误处理与用户提示
      • 3.3 性能优化
      • 3.4 交互体验提升
    • 4 最后
      • 4.1 回顾
      • 4.2 不同游戏难度在实现上有什么不同?
      • 4.3 你的Web开发之旅才刚刚开始!

近期文章

  • 动手用Web技术开发一个数独游戏
  • 动手用 Web 实现一个 2048 游戏
  • 【前端练手必备】从零到一,教你用JS写出风靡全球的“贪吃蛇”!
  • Google Search Console 做SEO分析之“已发现未编入” 与 “已抓取未编入” 有什么区别?
  • 如何通过 noindex 阻止网页被搜索引擎编入索引?
  • 建站SEO优化之站点地图sitemap
  • 个人建站做SEO网站外链这一点需要注意,做错了可能受到Google惩罚
  • 一文搞懂SEO优化之站点robots.txt
  • 实现篇:二叉树遍历收藏版
  • 实现篇:LRU算法的几种实现
  • Nginx Upstream了解一下
  • 一文搞懂 Markdown 文档规则

五子棋的规则简单易懂,但其背后的Web开发原理却能涵盖前端交互、游戏状态管理,甚至还能拓展到实时通信。它不仅是前端入门的绝佳实践项目,更能让你体验到亲手创造一个互动产品的巨大成就感。通过这个项目,你将不再是只会切图的“页面仔”,而是能让页面“活”起来的魔法师!

1 你的第一个Web互动游戏——五子棋

是不是经常羡慕那些能做出炫酷小游戏的开发者?今天,就来一起迈出第一步,用最纯粹的Web技术(HTML、CSS、JavaScript)打造一个经典小游戏——五子棋! 五子棋在线体验

  • 绘制精美的五子棋盘:用HTML和CSS搭骨架,用JavaScript赋予生命。
  • 实现核心落子逻辑:响应用户点击,更新游戏状态。
  • 掌握经典的胜负判断算法:让你的五子棋能真正判断输赢。
  • (进阶) 探索多人联机的奥秘:了解WebSocket如何打破实时通信的壁垒。

准备好了吗?让我们开始这场充满乐趣的Web游戏开发之旅!

2 核心技术剖析

2.1 棋盘的绘制与渲染

五子棋的第一步,当然是把棋盘画出来!我们有两种主要方案:基于DOM元素基于Canvas

2.1.1 HTML结构:棋盘的骨架

我们以DOM元素为例,创建一个容器来承载棋盘。

<div id="chessboard"></div>

干货:DOM vs Canvas?

  • DOM方案:每个棋格或棋子都是一个独立的HTML元素(<div>)。优点是上手快,调试方便,可以使用CSS丰富的样式特性。缺点是当棋盘很大或元素很多时,DOM操作可能导致性能问题。
  • Canvas方案:通过<canvas>元素在JavaScript中进行像素级绘图。优点是性能好,适合绘制大量图形,动画流畅。缺点是学习曲线稍高,调试不如DOM直观。

如何选择? 对于五子棋这种棋盘大小相对固定、元素数量可控的游戏,DOM方案足以应对,且更易于理解和实现。但如果你想做更复杂的策略游戏或动画,Canvas会是更好的选择。本文我们主要基于DOM来实现。

2.1.2 CSS美化:让棋盘看起来像个样

现在,我们给棋盘容器加上样式,并巧妙地利用CSS伪元素来绘制棋盘线。

#chessboard {display: grid;/* 15x15棋盘,每个格子大小为30px */grid-template-columns: repeat(15, 30px);grid-template-rows: repeat(15, 30px);width: 450px; /* 15 * 30px */height: 450px; /* 15 * 30px */border: 1px solid #333;background-color: #f7d6a2; /* 棋盘背景色 */position: relative; /* 用于棋子定位 */
}/* 使用伪元素绘制棋盘线 - 垂直线 */
#chessboard::before {content: '';position: absolute;top: 0;left: 0;width: 100%;height: 100%;background:linear-gradient(to right, #333 1px, transparent 1px),linear-gradient(to bottom, #333 1px, transparent 1px);background-size: 30px 30px; /* 每个格子的宽度 */pointer-events: none; /* 让点击事件穿透 */
}/* 棋子样式 */
.piece {position: absolute;width: 28px; /* 略小于格子大小,留出空隙 */height: 28px;border-radius: 50%;transform: translate(-50%, -50%); /* 居中显示 */transition: transform 0.1s ease-out; /* 增加落子动画 */
}.black { background-color: #000; }
.white { background-color: #fff; border: 1px solid #ccc; }

干货:CSS伪元素绘制棋盘线

这里我们用linear-gradientbackground-size::before伪元素上创建了网格背景,它模拟了棋盘线。这种方法比创建225个<div>元素来充当格子更高效,因为它减少了DOM元素的数量,从而优化了渲染性能。pointer-events: none;确保伪元素不会阻挡鼠标事件,让点击能穿透到棋盘本身。

2.1.3 JavaScript动态生成:让棋盘“活”起来

棋盘画好了,但它还是静态的。我们需要用JavaScript来动态管理棋盘的状态。

const BOARD_SIZE = 15;
const CELL_SIZE = 30; // 和CSS中的格子大小一致
let board = []; // 二维数组表示棋盘状态:0:空, 1:黑棋, 2:白棋
let currentPlayer = 1; // 1:黑棋, 2:白棋function initBoard() {const chessboardDiv = document.getElementById('chessboard');chessboardDiv.innerHTML = ''; // 清空可能存在的旧棋子board = Array(BOARD_SIZE).fill(0).map(() => Array(BOARD_SIZE).fill(0));currentPlayer = 1; // 默认黑棋先手// 干货:使用DocumentFragment批量操作DOM// 创建一个文档片段,所有DOM操作都在这里进行,最后一次性添加到DOM树// 避免频繁重排重绘,提高性能const fragment = document.createDocumentFragment();for (let r = 0; r < BOARD_SIZE; r++) {for (let c = 0; c < BOARD_SIZE; c++) {const intersection = document.createElement('div');intersection.className = 'intersection'; // 作为一个可点击的落子区域intersection.dataset.row = r;intersection.dataset.col = c;// 定位每个交点(棋子实际落点)intersection.style.left = `${c * CELL_SIZE + CELL_SIZE / 2}px`;intersection.style.top = `${r * CELL_SIZE + CELL_SIZE / 2}px`;fragment.appendChild(intersection);}}chessboardDiv.appendChild(fragment);
}initBoard();

代码速览: initBoard函数初始化了board二维数组,所有值都设为0(空)。接着,它动态创建了代表棋盘交点的div元素,并设置了data-rowdata-col属性,方便后续获取点击位置。DocumentFragment的使用是一个性能优化的干货,它能将多次DOM操作合并为一次,显著减少浏览器的重排重绘,让初始化过程更流畅。

2.2 落子逻辑与交互

现在,棋盘有了,我们得让它能响应玩家的点击。

2.2.1 事件监听:捕获用户的“点击”
const chessboardDiv = document.getElementById('chessboard');
chessboardDiv.addEventListener('click', handleBoardClick);function handleBoardClick(event) {// 干货:事件委托(Event Delegation)// 通过判断event.target来确定点击的是否是棋盘交点// 避免给每个intersection元素都绑定事件监听器,减少内存消耗if (!event.target.classList.contains('intersection')) {return; // 如果点击的不是交点,则忽略}const row = parseInt(event.target.dataset.row);const col = parseInt(event.target.dataset.col);placePiece(row, col);
}

代码速览: 我们只给chessboardDiv这个父元素绑定了一个点击事件监听器handleBoardClick。在handleBoardClick中,通过event.target判断实际点击的是哪个交点,并获取其rowcol。这正是事件委托的妙用,它能大大减少事件监听器的数量,优化性能。

2.2.2 落子判断与棋子渲染

点击事件捕获后,我们需要判断是否可以落子,并渲染棋子。

function placePiece(row, col) {// 1. 判断是否已存在棋子if (board[row][col] !== 0) {console.log('此处已有棋子,请重新选择!');return;}// 2. 更新棋盘状态数组board[row][col] = currentPlayer;// 3. 渲染棋子const piece = document.createElement('div');piece.className = `piece ${currentPlayer === 1 ? 'black' : 'white'}`;// 定位棋子到对应的交点piece.style.left = `${col * CELL_SIZE + CELL_SIZE / 2}px`;piece.style.top = `${row * CELL_SIZE + CELL_SIZE / 2}px`;// 干货:优化渲染:直接添加到被点击的intersection的父级chessboardDiv// 这样避免了重新查询DOMdocument.getElementById('chessboard').appendChild(piece);// 4. 判断胜负if (checkWin(row, col, currentPlayer)) {alert(`${currentPlayer === 1 ? '黑棋' : '白棋'}胜利!`);// 游戏结束处理,例如禁用落子,显示重置按钮chessboardDiv.removeEventListener('click', handleBoardClick);return;}// 5. 切换当前玩家currentPlayer = currentPlayer === 1 ? 2 : 1;
}

代码速览: placePiece函数是落子逻辑的核心。它首先检查点击位置是否为空,然后更新board数组并动态创建div元素来表示棋子,添加相应的CSS类名(blackwhite)来改变颜色。定位棋子时要根据格子大小和交点中心进行计算。最后,它调用checkWin判断胜负,并切换当前玩家。

2.3 胜负判断算法

这是五子棋最核心的算法之一。我们需要检查当前落子点在水平、垂直、两个对角线方向上是否有连续的五颗同色棋子。

2.3.1 判断方向:五种连珠可能
  • 水平:左右方向
  • 垂直:上下方向
  • 主对角线:左上到右下
  • 副对角线:右上到左下
2.3.2 算法实现细节

干货:只判断新落子点周围

一个重要的优化是:我们不需要每次落子都遍历整个棋盘来判断胜负。我们只需要检查新落子点的八个方向(或四个方向的两端延伸),看是否有连续五子。这样可以大大减少计算量。

function checkWin(row, col, player) {// 定义8个方向的偏移量:水平、垂直、对角线const directions = [[0, 1],   // 水平右[1, 0],   // 垂直下[1, 1],   // 主对角线右下[1, -1]   // 副对角线左下];// 对于每个方向,我们检查正反两个方向的连珠数for (let i = 0; i < directions.length; i++) {const dr = directions[i][0];const dc = directions[i][1];let count = 1; // 初始连珠数为1(包括当前落子)// 向一个方向延伸for (let step = 1; step < 5; step++) {const newRow = row + dr * step;const newCol = col + dc * step;if (newRow >= 0 && newRow < BOARD_SIZE &&newCol >= 0 && newCol < BOARD_SIZE &&board[newRow][newCol] === player) {count++;} else {break; // 遇到边界或不同色棋子,停止延伸}}// 向相反方向延伸for (let step = 1; step < 5; step++) {const newRow = row - dr * step;const newCol = col - dc * step;if (newRow >= 0 && newRow < BOARD_SIZE &&newCol >= 0 && newCol < BOARD_SIZE &&board[newRow][newCol] === player) {count++;} else {break;}}if (count >= 5) {return true; // 发现五子连珠}}return false; // 没有五子连珠
}

代码速览: checkWin函数遍历directions数组,每个元素代表一个主方向的偏移量。对于每个主方向,它会分别向正方向反方向延伸,计数连续同色棋子的数量。只要有一个方向上的计数达到或超过5,就说明赢了。边界条件检查newRow >= 0 && newRow < BOARD_SIZE等)是必不可少的,防止数组越界。

2.4 (进阶)多人联机:WebSocket的魔力

如果你的五子棋只能自己玩,那多无聊?实现联机对战才是游戏的灵魂!这里,我们需要WebSocket

2.4.1 为什么需要WebSocket?

传统的HTTP协议是短连接、请求-响应模式。客户端发送请求,服务器响应后连接就关闭了。这种模式不适合实时互动游戏,因为服务器无法主动推送信息给客户端(例如对手的落子)。

WebSocket提供了全双工通信的持久连接,一旦建立,客户端和服务器可以双向自由发送消息,且延迟极低,完美适用于实时游戏、聊天室等场景。

2.4.2 WebSocket基本原理

客户端通过特殊的HTTP握手请求升级到WebSocket协议。握手成功后,双方就建立了一条TCP连接上的“管道”,可以不受HTTP请求/响应模式的限制,直接发送消息帧。

2.4.3 前后端实现思路
  • 前端(JavaScript)
let ws;
function connectWebSocket() {ws = new WebSocket('ws://localhost:3000'); // 替换成你的服务器地址ws.onopen = () => {console.log('WebSocket连接成功!');// 可以发送加入房间的消息ws.send(JSON.stringify({ type: 'joinGame', gameId: 'gomoku123' }));};ws.onmessage = (event) => {const message = JSON.parse(event.data);if (message.type === 'placePiece') {// 收到对手落子信息,在本地棋盘渲染并更新状态console.log('收到对手落子:', message.row, message.col);// 模拟placePiece逻辑,但不切换玩家,因为是对方的落子// 你需要一个单独的函数来处理接收到的落子renderOpponentPiece(message.row, message.col, message.player);} else if (message.type === 'gameStart') {console.log('游戏开始!你是玩家', message.playerRole);// 根据playerRole决定你是黑棋还是白棋}// 处理其他消息,如胜利、聊天等};ws.onclose = () => console.log('WebSocket连接关闭');ws.onerror = (error) => console.error('WebSocket错误:', error);
}// 玩家落子后,发送消息给服务器
function sendMove(row, col) {if (ws && ws.readyState === WebSocket.OPEN) {ws.send(JSON.stringify({type: 'placePiece',row: row,col: col,player: currentPlayer // 发送当前玩家信息}));}
}// 在 placePiece 函数中调用 sendMove
// ... (现有 placePiece 函数代码)
// if (!checkWin(...)) {
//     sendMove(row, col); // 成功落子且未赢,则发送给服务器
//     currentPlayer = currentPlayer === 1 ? 2 : 1;
// }
// ...

代码速览: 前端通过new WebSocket()建立连接,ws.onopenws.onmessagews.onclosews.onerror处理连接生命周期和消息。当玩家落子后,通过ws.send()将落子信息发送给服务器。

  • 后端(Node.js + ws库为例)

后端需要一个WebSocket服务器来接收客户端连接、管理游戏房间、同步玩家操作。

// 假设使用 Node.js 和 ws 库
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });const gameRooms = {}; // 存储游戏房间信息 { gameId: { players: [], board: [] } }wss.on('connection', ws => {console.log('一个新客户端连接了!');ws.on('message', message => {const data = JSON.parse(message);switch (data.type) {case 'joinGame':// 简单房间匹配逻辑if (!gameRooms[data.gameId]) {gameRooms[data.gameId] = { players: [], board: Array(15).fill(0).map(() => Array(15).fill(0)) };}if (gameRooms[data.gameId].players.length < 2) {gameRooms[data.gameId].players.push(ws);ws.gameId = data.gameId; // 记录玩家所在房间ws.playerRole = gameRooms[data.gameId].players.length; // 1或2ws.send(JSON.stringify({ type: 'gameStart', playerRole: ws.playerRole }));if (gameRooms[data.gameId].players.length === 2) {// 通知双方游戏开始gameRooms[data.gameId].players.forEach(playerWs => {playerWs.send(JSON.stringify({ type: 'opponentJoined' }));});}} else {ws.send(JSON.stringify({ type: 'error', message: '房间已满' }));}break;case 'placePiece':const { row, col, player } = data;const room = gameRooms[ws.gameId];if (room && room.players.includes(ws) && room.board[row][col] === 0) {room.board[row][col] = player; // 更新服务器端棋盘状态// 广播给房间内所有其他玩家room.players.forEach(playerWs => {if (playerWs !== ws) { // 不发给自己playerWs.send(JSON.stringify({ type: 'placePiece', row, col, player }));}});// 后端也应该做胜负判断,并发送胜利/平局消息}break;// ... 其他消息处理,如聊天、认输等}});ws.on('close', () => {console.log('客户端断开连接');// 简单处理玩家离线:通知房间内其他玩家if (ws.gameId && gameRooms[ws.gameId]) {gameRooms[ws.gameId].players = gameRooms[ws.gameId].players.filter(p => p !== ws);if (gameRooms[ws.gameId].players.length === 0) {delete gameRooms[ws.gameId]; // 房间无人则删除} else {gameRooms[ws.gameId].players.forEach(playerWs => {playerWs.send(JSON.stringify({ type: 'opponentLeft' }));});}}});
});console.log('WebSocket服务器已启动在 ws://localhost:3000');

代码速览: 后端使用ws库创建WebSocket服务器。wss.on('connection')监听新连接。ws.on('message')处理客户端发来的消息,例如joinGame(房间管理)和placePiece(落子同步)。核心是当一个玩家落子时,服务器接收到消息后,会广播给同一房间内的所有其他玩家,实现实时同步。

3 实践

一个可玩的游戏只是开始,良好的代码结构和用户体验同样重要。

3.1 代码结构与模块化

为了让代码更易于维护和扩展,你应该将不同功能的代码拆分到不同的文件,并使用ES6模块化进行导入导出。

  • index.html:页面结构
  • style.css:样式
  • board.js:棋盘绘制、渲染逻辑
  • gameLogic.js:落子、胜负判断、玩家切换逻辑
  • websocket.js:WebSocket连接和消息处理(如果实现联机)
  • main.js:入口文件,协调各个模块

干货:ES6模块化

在HTML中引入main.js时,使用type="module"

<script type="module" src="main.js"></script>

main.js中导入其他模块:

// main.js
import { initBoard } from './board.js';
import { handleBoardClick } from './gameLogic.js';
// ...

board.js中导出函数:

// board.js
export function initBoard() { /* ... */ }

3.2 错误处理与用户提示

  • 当用户点击无效位置(已有棋子)时,给出清晰的提示。
  • 联机模式下,处理网络断开、服务器错误等情况,告知用户。

3.3 性能优化

  • 减少DOM操作:尽可能利用DocumentFragment或直接操作innerHTML,避免频繁的DOM增删改查。
  • 使用Canvas:如果对性能要求极高,或者想实现更复杂的动画效果,将棋盘和棋子绘制迁移到<canvas>上会是更好的选择。Canvas是像素级的绘图,性能远高于DOM元素。

3.4 交互体验提升

  • 落子音效:每次落子播放简单的音效,增加代入感。
  • 胜利动画/提示:游戏结束时,用更醒目的方式(如弹窗、棋子闪烁)提示胜利者。
  • 悔棋功能:这是一个更高级的功能。需要维护一个历史操作栈,每次落子时将当前棋盘状态或操作记录入栈,悔棋时从栈中取出上一步状态恢复。

4 最后

4.1 回顾

  • Web前端基础:HTML构建结构,CSS美化样式,JavaScript实现动态交互。
  • 事件驱动编程:学会监听用户事件并响应。
  • 算法思维:设计并优化了胜负判断的核心算法。
  • 实时通信技术:了解了WebSocket在多人游戏中的强大作用。

4.2 不同游戏难度在实现上有什么不同?

这里我们主要实现的是双人对弈的五子棋。如果想增加游戏难度,例如实现人机对战(AI),那将是另一个巨大的挑战:

  • 简单AI:随机落子,或只检查当前落子位置的简单连珠。
  • 中等AI:评估棋盘上每个空位的得分(例如,构成活二、活三、冲四等),选择得分最高的点落子。这需要实现棋型分析算法。
    • 高级AI(如 Minimax算法、Alpha-Beta剪枝):模拟未来多步棋局,通过树搜索和评估函数来找到最优解。这涉及到复杂的博弈论和搜索算法,实现难度呈指数级增长。

4.3 你的Web开发之旅才刚刚开始!

五子棋只是冰山一角。掌握了这些核心技术和思路,你完全可以尝试开发更多有趣的Web小游戏,例如:

  • 俄罗斯方块
  • 扫雷
  • 贪吃蛇
  • 甚至更复杂的策略游戏!

Web开发的魅力就在于此——从零开始,用代码构建出无限可能。现在,是不是觉得做个小游戏也没那么难了?快动手尝试一下吧!最后欢迎体验: 五子棋在线体验

  • 五子棋规则
http://www.lqws.cn/news/585307.html

相关文章:

  • SpringBoot --项目启动的两种方式
  • js遍历对象的方法
  • 【MySQL】数据库基础
  • .net8导出影像图片按现场及天拆分
  • 51单片机CPU工作原理解析
  • 借助 KubeMQ 简化多 LLM 集成
  • YOLOv12_ultralytics-8.3.145_2025_5_27部分代码阅读笔记-torch_utils.py
  • 后台填坑记——Golang内存泄漏问题排查(一)
  • 设计模式(六)
  • 大模型开源技术解析 4.5 的系列开源技术解析:从模型矩阵到产业赋能的全栈突破
  • 2025年06月30日Github流行趋势
  • 遥控器双频无线模块技术要点概述
  • SegChange-R1:基于大型语言模型增强的遥感变化检测
  • 07-three.js Debug UI
  • Webpack原理剖析与实现
  • QT中QSS样式表的详细介绍
  • 【MySQL基础】MySQL索引全面解析:从原理到实践
  • 汽车轮速测量专用轮速传感器
  • 51c~UWB~合集1
  • SpringBoot项目开发实战销售管理系统——项目框架搭建!
  • 【windows上VScode开发STM32】
  • C#数字格式化全解析:从基础到进阶的实战指南
  • 电铸Socket弹片测试全解析:如何提升5G连接器稳定性?
  • 华为物联网认证:开启万物互联的钥匙
  • uni-app开发app保持登录状态
  • 【C++】简单学——模板初阶
  • 中证500股指期货一手多少钱呢?风险如何?
  • 易语言-登录UI演示
  • 一个代理对象被调用时,方法调用的链路是怎样的?
  • 【Kafka使用方式以及原理】