零前端基础硬刚《象棋》- 持续更新

背景

象棋是一款益智类游戏,玩家可以通过操控棋子排兵布阵,最终获得游戏的胜利。在作者小时候,该游戏一直是风靡在村里的大街小巷,本着对童年的回味和对象棋的热爱,本主会尽自己的全力完成此款游戏的设计开发,欢迎点赞和收藏!

功能列表

  • 游客登录
  • 账号密码/绑定邮箱登录
  • QQ授权登录
  • 账号注册,支持绑定邮箱,绑定时需要发送验证码
  • 忘记密码找回
  • 版本历史记录展示
  • 登录后展示个人信息(头像、昵称、积分、胜负和、排名)
  • 加入房间
  • 房间换桌
  • 房间踢人
  • 对局准备
  • 棋盘、棋子展示
  • 棋子移动动画
  • 游戏音效
  • 对局聊天、悔棋、认输、求和
  • 换肤功能
  • 对局观战信息展示
  • 观战全员禁言
  • 观战针对个人禁言
  • 观战针对个人踢出
  • 房间邀请功能
  • 大厅用户可观战已建立的对局
  • 大厅用户观战聊天功能
  • 大厅用户观战交换观战视角(红方与黑方互相切换)
  • 大厅用户观战可保持屏幕常亮
  • 复盘功能
  • 复盘分享功能
  • 棋谱功能
  • 数据断连(游戏断开再登录后,可恢复对局)

技术要点

  • React 前端主要语言,配合umijs编写功能
  • Ant-Mobile UI框架,有很多现成的组件供使用
  • umijs 脚本架,快速构建项目
  • socket.io 消息通信
  • node.js javascript引擎,配合ts编写服务端功能
  • ES6 学习ES6的语法,用于编写代码
  • Redis 缓存工具,用于缓存对战信息
  • MySQL 数据库,用户保存游戏产生的数据
  • typescript

项目结构(重点文件/文件夹)

├── src
│   ├── app.js                     应用配置(全局)
│   ├── config.js                  项目配置(全局)
│   ├── global.less                样式配置(全局)
│   ├── assets                     资源文件
│   │   ├── audio                  音效
│   │   ├── font                   字体
│   │   ├── images                 图片
│   │   └── skins                  皮肤
│   ├── button                     自定义按钮
│   ├── circle                     自定义弹窗
│   ├── components                 自定义加载动画
│   ├── header                     自定义标题
│   ├── layouts                    父页面
│   ├── pages                      子页面
│   │   ├── document.ejs           启动加载页
│   │   ├── board                  棋盘界面
│   │   ├── user                   登录/注册/忘记密码
│   │   ├── platform               游戏平台界面
│   │   ├── review                 复盘界面
│   │   ├── rooms                  房间界面
│   │   ├── sahre                  分享页面
│   │   ├── version                版本界面
│   │   └── watch                  观战界面
│   ├── service                    Socket封装
│   │   ├── event.js               socket事件管理(API)
│   │   └── socket.js              socket连接管理
│   └── utils                      工具包
│       ├── board-canvas-utils.js  棋盘绘制工具
│       ├── board-utils.js         棋盘工具
│       ├── cache-key-utils.js     缓存Key管理
│       ├── check-win.js           检测胜利(绝杀/困毙)
│       ├── const-utils.js         常量管理
│       ├── cryptor-utils.js       数据加解密
│       ├── head-canvas-utils.js   头像绘制工具
│       ├── images-res.js          图片资源管理
│       ├── keep-fighting.js       长将检测
│       ├── log-utils.js           日志管理
│       ├── map-res.js             地图静态数据
│       ├── rule-check.js          规则检测
│       ├── rules                  具体规则实现
│       │   ├── basic-rule.js      基本规则检测(车、马、象等)
│       │   └── boss-rule.js       Boss规则检测(碰面、被将军)
│       ├── skin-utils.js          皮肤管理
│       ├── sounds-res.js          音效管理
│       └── storage-utils.js       缓存管理
├── package.json

功能细节

1. 通信

socket.io有两个常用方法,发送emit,监听on,当服务器启动后,首先会通过on监听已预设的API,当socket与服务端成功连接后,客户端可以通过emit向相应的API发送消息,此时服务端就能收到该消息并做一系列操作,拿聊天来说,客户端调用sendChatApi发送消息到服务端后,服务端将消息进行敏感词处理,处理完成后将消息通过emit发送到指定的房间,而客户端则会通过on监听此房间的消息,从而实现消息转发。若有疑问,可查看socket.io相关文档

2. 棋盘棋子绘制

游戏素材都来源于网络,棋盘和棋子也就是一张贴图,在一个html中绘制一张贴图,一般用img标签,如何将一张img绘制到不同的位置,可以通过对其进行position: absolute,再根据棋盘的间距和棋子的坐标,设置css的transform: translate(${x}rem,${y}rem)

棋盘绘制示例:

gameMap.map(chess => {
  // gridSpan: 网格大小
  const x = gridSpan * chess.y - (gridSpan / 2);
  const y = gridSpan * chess.x - (gridSpan / 2);
  return (
      <img
        key={chess.id}
        alt={chess.id}
        // 获取棋盘的图片
        src={resMap.get(chess.id)}
        style={{
          // 浮动,并利用translate对棋子进行偏移(left/top是同样的效果)
          position: 'absolute',
          width: `${gridSpan}rem`,
          height: `${gridSpan}rem`,
          // 移动时的动画
          transition: 'transform 0.5s, opacity 0.5s',
          transform: `translate(${x}rem,${y}rem)`,
          zIndex: 3,
        }}
      />
  );
})
3. 棋子规则

车走直、马走日、象走田,要实现这些棋子的规则,首先要将棋盘抽象理解成一个二维数组,x, y 相交的点为棋子所在的点,假设(左上角)车0,0的位置,那么当要走到的位置时,实际上就是坐标0,0 -> 0,1,明白这个道理后,如果你要对车进行规则判定,就非常的简单,车只能直着走,那么必然x -> x1,它两的值是相等的,如果x不相等,则再判断一下y -> y1是不是相等,如果两者都不相等,那必然不可能是横或竖着走了,另外还要考虑其它的一些情况,比如说推算到0,1时这个位置有我方的,那后面的位置,车也是走不了的,如果遇到的是对方的棋,这个位置就能走(对方的棋子可以吃)
在这里插入图片描述

  • 的走法判断逻辑(可以考虑一下马、象这种有蹩脚的是如何判定的)
const handleMoveC = (gameMap, from, to) => {
  const board = listToArray(gameMap);
  // 必须有一个点是在同一条线上
  if (from.x === to.x) {
    const tempChessList = [];
    let start = Math.min(from.y, to.y);
    let end = Math.max(from.y, to.y);
    // 判断其中间有没有棋子
    while (start <= end) {
      // 排除掉自身位置且中间不能有其它棋子
      if (from.y !== start && board[from.x][start]) {
        tempChessList.push({ x: from.x, y: start });
      }
      ++start;
    }
    // 循环结束后进行判断,空棋盘直接返回(要走的位置中间没有其它棋子)
    if (tempChessList.length === 0) {
      return true;
    } else if (tempChessList.length === 1) {
      // 如果有一个棋子,则要处理一下是否是要到达的位置
      const chess = tempChessList.pop();
      if (chess.x === to.x && chess.y === to.y) {
        return true;
      }
    }
    return false;
  } else if (from.y === to.y) {
    const tempChessList = [];
    let start = Math.min(from.x, to.x);
    let end = Math.max(from.x, to.x);
    while (start <= end) {
      if (from.x !== start && board[start][from.y]) {
        tempChessList.push({ x: start, y: from.y });
      }
      ++start;
    }
    // 循环结束后进行判断,空棋盘直接返回(要走的位置中间没有其它棋子)
    if (tempChessList.length === 0) {
      return true;
    } else if (tempChessList.length === 1) {
      // 如果有一个棋子,则要处理一下是否是要到达的位置
      const chess = tempChessList.pop();
      if (chess.x === to.x && chess.y === to.y) {
        return true;
      }
    }
  }
  return false;
};
4. 如何判断游戏结束
  • 当boss没有位置可走时(只有boss自己)
  • 当boss没有位置可走时(有其它棋子,但被堵死,如:象被卡住象眼了)
  • 被将军无路可走(包括没有其它棋子能抵挡boss被击杀)
  • 被将军且boss走棋就会与对方boss碰面
  • 双方都没有可进攻棋子判和
  • 对局中有一方直接认输
  • 对局中有一方发起求和且被求和方也同意和棋
5. 匹配机制

当用户进入房间后,后台有一张用户游离表会记录该用户在整个游戏中的状态,包括但不限于当前页面当前房间号房间状态,当用户首次进入房间,该房间无其他人时,房间状态等待中,这个动作,服务端只需要将该用户游离数据表房间号变更为当前分配的房间号并更改房间状态等待中,当另一位玩家也进入房间时,此时服务器会从游离数据表中根据房间状态进行数据筛选,目前匹配规则是房间状态等待中的会优先分配,当房间内人数达到2个时,房间状态会变更为多人等待,此时该房间不会参与匹配。

  • 目前已设计的房间状态
// 空房间
export const ROOM_STATUS_EMPTY = 'EMPTY';
// 有人在房间中等待(仅1个人)
export const ROOM_STATUS_WAIT = 'WAIT';
// 多个人在房间中等待
export const ROOM_STATUS_MULTIPLE_WAIT = 'MULTIPLE_WAIT';
// 匹配成功(双方已准备)
export const ROOM_STATUS_MATCH_SUCCESS = 'MATCH_SUCCESS';
// 对战中
export const ROOM_STATUS_BATTLE = 'BATTLE';
// 对战有一方超时了
export const ROOM_STATUS_TIMEOUT = 'TIMEOUT';
// 对战结束
export const ROOM_STATUS_BATTLE_OVER = 'BATTLE_OVER';
6. 棋子移动

如果你对如何使用emiton已经有足够的了解,那么棋子移动则会变得异常简单,简单来说,棋子移动至少有两个对象的数据传送给服务端,分别是fromto,它们都包含x, y属性,前者为棋子所在位置,后者为要到达的位置,但值得注意的是,对战时,对方看到的是我方的镜像数据,也就是说,棋子通过服务器发送给对方时,对方需要做棋子坐标翻转,下面是一个真实的棋子移动数据包

{
	"fromChessBox": {
		"color": "boxColorBlack",
		"show": true,
		"x": 4,
		"y": 7
	},
	"battleId": "73782847853944809178100889398553",
	"from": {
		"isBlackColor": true,
		"isAttach": true,
		"prefix": "P",
		"isBoss": false,
		"x": 4,
		"y": 7,
		"id": "BRP"
	},
	"to": {
		"x": 4,
		"y": 4
	},
	"toChessBox": {
		"color": "boxColorBlack",
		"show": true,
		"x": 4,
		"y": 4
	},
	"stepExplain": "炮2平5",
	"userId": "tour_47347731",
	"roomId": 7
}
7.悔棋

如果在对局中想要悔棋,首先,如果是我方落子时,我方是无法悔棋的,其次,当非我方落子时,我方发起悔棋后,对方需要同意,我方才会做悔棋操作,悔棋在技术上的实现方案是,将每一次的快照都保存在本地,当调用悔棋方法时,将倒数第一条数据删除,并还原当前棋盘数据成倒数第二条。值得注意的是,当用户在房间或对局中,用户有可能将页面刷新,所以此处跟数据断连功能也是紧密配合的,当用户做了上述操作时,数据断连会将对局的数据从服务器中查询并发送给客户端

8.观战实现

当创建对局后,对局表会产生一条对战记录,该条记录的状态为对战中,当其它用户进入观战列表时,列表会检索对战表的对战状态,将对战中的数据展示在列表中,另外还有一个重要的概念,当用户进入房间后,后台会将用户的 socket 加入到分配的房间中,而房间是可以存在多人的,消息通知是直接往这个房间推送的,如果用户观战了某一场对局,那么该用户也会加入这个房间中,并且共享这个房间的数据,此时观战的数据来源有两部分,第一部分则是这场对局已保存在库里的数据,另一部分则是对局玩家操作棋子后,将棋子操作信息发送到房间,观战用户则共享房间的数据,再通过逻辑在棋盘上呈现。

9.复盘和分享

a. 通过前面几点的介绍,您大致能了解到,对战是会对每一步保存快照的,而复盘则是对这些快照的数据进行读取展示。
b.分享使用的数据也是一样的,用户将复盘的数据分享时,后台会为盘对局创建一个唯一的分享码,通过分享链接前缀+分享码组成一个分享链接,用户通过分享的链接进入页面后,使用的也是快照数据。

10.邀请功能

为了提高匹配成功率,用户在房间时可以通过在线列表,通过对玩家进行邀请,邀请请求发送到服务器后,服务器通过被邀请的用户账号查找其对应的socket,并通过emit发送邀请信息,同悔棋一样,受邀也需要对方同意,才能加入该房间,如若房间人数已满,房间状态已进行对战,此些情况都会被视为邀请已过期

更新还在继续,如果您对本游戏感兴趣,欢迎点赞收藏,本博主会陆续揭露代码细节...
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
好的,以下是一个简单的前端象棋小游戏的示例代码,使用 HTML、CSS 和 JavaScript 实现: ```html <!DOCTYPE html> <html> <head> <title>象棋小游戏</title> <style> .square { width: 50px; height: 50px; display: inline-block; border: 1px solid black; text-align: center; font-size: 30px; font-weight: bold; cursor: pointer; } .red { background-color: #FFE4E1; color: red; } .black { background-color: #8B4513; color: white; } </style> </head> <body onload="initBoard()"> <div id="board"> </div> <script> // 定义棋盘大小和棋子布局 const ROWS = 10; const COLS = 9; const START_POSITION = [ ['車', '馬', '象', '士', '帥', '士', '象', '馬', '車'], ['', '', '', '', '', '', '', '', ''], ['', '炮', '', '', '', '', '', '炮', ''], ['兵', '', '兵', '', '兵', '', '兵', '', '兵'], ['', '', '', '', '', '', '', '', ''], ['', '', '', '', '', '', '', '', ''], ['兵', '', '兵', '', '兵', '', '兵', '', '兵'], ['', '炮', '', '', '', '', '', '炮', ''], ['', '', '', '', '', '', '', '', ''], ['車', '馬', '象', '士', '將', '士', '象', '馬', '車'], ]; // 初始化棋盘 function initBoard() { const board = document.getElementById('board'); for (let i = 0; i < ROWS; i++) { for (let j = 0; j < COLS; j++) { const square = document.createElement('div'); square.classList.add('square'); // 根据位置设置背景颜色 if ((i + j) % 2 == 0) { square.style.backgroundColor = '#FFEBCD'; } else { square.style.backgroundColor = '#8B4513'; } // 设置棋子文字和颜色 if (START_POSITION[i][j] !== '') { square.innerText = START_POSITION[i][j]; if (i < 5) { square.classList.add('black'); } else { square.classList.add('red'); } } // 绑定点击事件 square.onclick = function() { if (square.classList.contains('selected')) { // 取消选中状态 square.classList.remove('selected'); // TODO: 取消选中棋子的所有合法移动位置 } else if (square.innerText !== '') { // 选中棋子 square.classList.add('selected'); // TODO: 显示选中棋子的所有合法移动位置 } } board.appendChild(square); } } } </script> </body> </html> ``` 这个代码会生成一个简单的棋盘,包含所有的象棋棋子,并且可以点击棋子来选中或者取消选中。你可以根据自己的喜好来添加移动棋子、判断胜负等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小天Smile

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值