# koa-ddz-server
**Repository Path**: baymax668/koa-ddz-server
## Basic Information
- **Project Name**: koa-ddz-server
- **Description**: 基于Node.js+Koa+TypeScript实现的ddz服务端
- **Primary Language**: TypeScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2023-05-26
- **Last Updated**: 2023-08-28
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 介绍
基于Node.js、Koa、Typescript、Socket.io实现的ddz游戏**服务端**。
**主要功能**:游客登录、房间功能、准备功能、发牌功能、抢地主功能(抢地主流程)、出牌功能(牌型校验、牌型比较、出牌流程)、Socket对象管理
**客户端(基于Cocos Creator2.x)**:[https://gitee.com/baymax668/cocos-ddz-client](https://gitee.com/baymax668/cocos-ddz-client)
**项目启动**:
```bash
npm install
npm run server
```
**目录结构**:
```bash
|-- src
| |-- app.ts ## 【程序入口】
| |-- constant ## 【枚举和常量】
| | |-- GameStage.ts # 游戏状态/阶段
| | |-- PokerHand.ts # 牌型
| | |-- PokerPoints.ts # 牌点数
| | |-- PokerSuits.ts # 牌花色
| | |-- SocketGameEvent.ts # socket游戏事件枚举
| | |-- SocketRoomEvent.ts # socket房间事件枚举
| |-- controllers ## 【控制器】存放路由对应的程序逻辑
| | |-- ExampleController.ts
| | |-- GameController.ts # 游戏相关控制器
| | |-- RoomController.ts # 房间相关控制器
| | |-- UserController.ts # 用户相关控制器(用户登录)
| |-- data ## 【数据模型】
| | |-- Player.ts # 玩家
| | |-- Poker.ts # 卡牌
| | |-- Referee.ts # 裁判(每个房间都有一个裁判管理游戏流程)
| | |-- ResData.ts # http响应数据对象
| | |-- Room.ts # 房间
| | |-- SocketData.ts # socket通信数据对象
| | |-- User.ts # 用户
| |-- game ## 【游戏相关】
| | |-- PokerManager.ts # 扑克牌管理,生成一组扑克牌
| | |-- RoomManager.ts # 房间管理
| |-- middlewares ## 【中间件】
| | |-- ErrorHandle.ts # 异常处理
| | |-- NotFound.ts # 404处理
| | |-- Result.ts # 封装通用响应函数(succ、err、result)
| |-- routes ## 【http url路由】
| | |-- ExampleRoute.ts
| | |-- GameRoute.ts
| | |-- RoomRoute.ts
| | |-- UserRoute.ts
| | |-- index.ts # 自动导入所有路由
| |-- utils ## 【工具类】
| | |-- ArrayUtil.ts # 数组工具类(数据交互函数)
| | |-- PokerUtil.ts # 扑克牌工具类(排序、牌型校验、牌组比较等)
| | |-- RandomUtil.ts # 随机函数封装
| |-- websocket ## 【socket管理】
| |-- GameNotifier.ts # 游戏相关通知
| |-- SocketManager.ts # 用户管理所有socket对象,将user.id和socket对象关联
|-- test ## 单元测试
| |-- PokerUtil.test.ts # PokerUtil.ts的测试(测试牌组校验、排序、以及牌组大小比较)
| |-- TestCode.ts
|-- package-lock.json
|-- package.json
```
# 项目架构
**说明**:整个系统使用主要使用B/S架构,后端执行流程为Koa的`洋葱模型`。
执行流程主要为:中间件(middleware)->路由(route)->控制器(controller)->具体代码逻辑
## 基本架构
**基本架构图**:
## 详细架构
**详细架构图**:
# 网络通信模型
- 客户端通过Http发送数据(目的:为了复用后端中间件,并且方便实现请求响应获取响应结果)
- 客户端监听Socket,获得服务端主动推送的数据
> 其实Socket.io具备请求响应功能
**通信基本模型图**:
# API
## Http api
### 游客登陆
**URL**:http://localhost:3000/api/v1/user/login/guest
**Method**:Post
**参数**:None
**返回值**:
| 名称 | 类型 | 描述 |
| :-----: | :----: | :---------: |
| id | string | 用户id |
| name | string | 用户名/昵称 |
| balance | number | 余额 |
### 进入房间
**URL**:http://localhost:3000/api/v1/room/join
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :--: | :----: | :-------------------------------: |
| user | object | 用户对象(包含id、name、balance) |
**返回值**:None
### 离开房间
**URL**:http://localhost:3000/api/v1/room/leave
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :----: | :----: | :----: |
| uid | string | 用户id |
| roomId | string | 房间id |
### 准备
> 准备阶段使用
**URL**:http://localhost:3000/api/v1/game/prepare
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :-------: | :-----: | :------: |
| uid | string | 用户id |
| roomId | string | 房间id |
| isPrepare | boolean | 是否准备 |
### 准备就绪
> 发牌阶段。客户端整理好牌后,需要调用这个api,表示准备就绪
**URL**:http://localhost:3000/api/v1/game/ready
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :----: | :----: | :----: |
| uid | string | 用户id |
| roomId | string | 房间id |
### 叫/不叫地主
> 抢/不抢地主也是这个api
**URL**:http://localhost:3000/api/v1/game/call
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :----: | :-----: | :--------: |
| uid | string | 用户id |
| roomId | string | 房间id |
| isCall | boolean | 是否叫地主 |
### 出牌
**URL**:http://localhost:3000/api/v1/game/play
**Method**:Post
**参数**:
| 名称 | 类型 | 描述 |
| :----: | :------------: | :------------------------: |
| uid | string | 用户id |
| roomId | string | 房间id |
| isPlay | boolean | 是否是出牌(否则为不出牌) |
| cards | Array\ | 要出的牌 |
## Socket api
**注意**:客户端只通过Socket监听数据,不会通过Socket发任何数据
```typescript
/**
* 游戏相关的Socket事件
*/
export enum SocketGameEvent {
CHANGE_STAGE = 'ctx-game-stage', // 修改游戏阶段
PREPARE = 'ctx-game-prepare', // 有人准备/取消准备
DEAL = 'ctx-game-deal', // 系统发牌
SET_CALL_PLAYER = 'ctx-game-set-call-player', // 系统通知轮到谁叫地主
CALL = 'ctx-game-call', // 有人叫地主/不叫地主
SET_LANDLORD = 'ctx-game-set-landlord', // 系统设置地主
SET_PLAY = 'ctx-game-set-play', // 系统通知轮到谁出牌
PLAY_CARDS = 'ctx-game-play-cards', // 有人出牌/不出牌
RESULT = 'ctx-game-result', // 游戏结果
}
```
### 游戏阶段改变
> 系统通知进入某个阶段
**Event**:ctx-game-stage
**返回值**:
| 名称 | 类型 | 描述 |
| :---: | :----: | :----: |
| stage | number | 状态值 |
```typescript
/**
* 游戏阶段
*/
export enum GameStage {
PREPARATION = 0, // 准备阶段
REFEREE_DEEL = 1, // 发牌阶段
CALL_LANDLORD = 2, // 叫地主阶段
GAMING = 3, // 游戏中(玩家轮流出牌)
SETTLEMENT = 4, // 结算中
}
```
### 有人进入房间
**Event**:ctx-room-join
**返回值**:
| 名称 | 类型 | 描述 |
| :----: | :----: | :----------: |
| player | object | 用户数据对象 |
| index | number | 座位 |
### 有人离开房间
**Event**:ctx-room-leave
**返回值**:
| 名称 | 类型 | 描述 |
| :---: | :----: | :----: |
| uid | string | 用户id |
| index | number | 座位 |
### 有人准备
**Event**:ctx-game-prepare
**返回值**:
| 名称 | 类型 | 描述 |
| :-------: | :-----: | :-----------: |
| uid | string | 用户id |
| isPrepare | boolean | 准备/取消准备 |
### 系统发牌
**Event**:ctx-game-deal
**返回值**:
| 名称 | 类型 | 描述 |
| :---: | :----: | :------------------: |
| uid | string | 用户id |
| cards | Array | 系统发给这个用户的牌 |
### 轮到谁叫地主
> 系统通知轮到哪个玩家叫地主
**Event**:ctx-game-set-call
**返回值**:
| 名称 | 类型 | 描述 |
| :--: | :----: | :----: |
| uid | string | 用户id |
### 有人抢/不抢地主
> 系统通知谁点击了抢/不抢地主
**Event**:ctx-game-call
**返回值**:
| 名称 | 类型 | 描述 |
| :----: | :-----: | :--------: |
| uid | string | 用户id |
| isCall | boolean | 是否抢地主 |
### 系统设置地主
**Event**:ctx-game-set-landlord
**返回值**:
| 名称 | 类型 | 描述 |
| :---: | :----: | :----: |
| uid | string | 用户id |
| cards | Array | 地主牌 |
### 轮到谁出牌
> 系统通知轮到哪个玩家出牌
**Event**:ctx-game-set-play
**返回值**:
| 名称 | 类型 | 描述 |
| :--: | :----: | :----: |
| uid | string | 用户id |
### 有人出牌
**Event**:ctx-game-play-cards
**返回值**:
| 名称 | 类型 | 描述 |
| :----: | :-----: | :------: |
| uid | string | 用户id |
| isPlay | boolean | 是否出牌 |
| cards | boolean | 出的牌 |
### 游戏结束
**Event**:ctx-game-result
**返回值**:
| 名称 | 类型 | 描述 |
| :--: | :--: | :--: |
| - | - | - |
# 成果、难点、思路
## 牌组排序
**说明**:牌组排序是一个两级排序,先排序**点数**,相同的点数再根据**花色**排序。

**思路**:
1. **点数**可以设定一个权值,用于排序比较。例如,3的权值为300、4的权值为400...
2. **花色**设置一个较小的权值,如,方块为0、梅花为1、红桃为2,黑桃为3
3. 点数和花色的权值**相加**,得到这副牌得权值,即可比较大小,并且实现两级排序的功能。(因为点数的权值为百位的,不同点数基于百位进行比较;点数中的花色位于个位,不同点数的花色根据个位进行排序)
**关键代码**:
```typescript
// Poker.ts
export class Poker implements IPokerVo {
// ...
/**
* 用于比较大小
*/
public get value() {
return this.suit + this.pointWeight * 100
}
// ...
}
```
```typescript
// PokerUtil.ts
/**
* 对一副牌从大到小排序
* @param cards
* @returns
*/
public static sort(cards: Array) {
return cards.sort((c1: IPokerVo, c2: IPokerVo) => {
const poker1 = new Poker(c1)
const poker2 = new Poker(c2)
return poker1.compare(poker2)
}).reverse()
}
```
## 牌型校验
**说明**:牌型校验用于判断一个牌组是否是符合游戏的牌型(单张、对子、三张、三带一...),因为不同牌型的大小比较方式不同,需要获取牌型才可以进行两组牌型大小比较。

**大致思路**:
1. 根据一个牌组的数量去划分他有可能的牌型(如,长度为**1**,那么就为**单张**;为**2**,有可能是**对子、王炸**;为**3**,有可能是**3张**...)
2. 划分出牌型后,根据牌型的特性进行校验
3. 有些牌型的校验方式是一样,可以使用同一个校验函数(如,单张、对子、三张、炸弹,都是校验点数是否全都相等)
**牌型对应的牌数**:
- 单牌:=1张
- 对子:=2张
- 火箭:=2张
- 三张:=3张
- 三带一:=4张
- 炸弹:=4张
- 三带二:=5张
- 单顺子:>=5 && <=12 张
- 四带二:=6、8 张
- 双顺子:>=6 && len%2=0;枚举数量6、8、10、12、14、16
- 三顺子(飞机):>=6 && len%3 == 0;枚举数量6、9、12、15
- 飞机带翅膀:>=8 && len%2=0;枚举数量8、10、12、14、16
**牌组校验思路**:
- **对子、三张、炸弹**:校验方式一致。遍历相等比较即可
- **三带一、三带二、四带二**:校验思路相似。牌点数都放到Map的key中,value累加计数,那么map中只有两个key才能符合;并且value符合三带一、三带二、四带二
- **单顺子**:排序后使用步长为1循环,判断两数之间是否是连续关系
- **双顺子**:排序后使用步长为2的循环,判断两数是否相等,并且**两两**之间是否是连续关系
- **三顺子(飞机)**:排序后使用步长为3的循环,判断三数是否相等,并且**三三**之间是否是连续关系
- **飞机带翅膀**:牌型后,点数作为key存放到Map中,value设为该点数的数组;先将value.length为3的遍历出来,判断是否连续,再将剩余的牌判断是否是同数量单张,或者同数量对子。
**核心代码**:
```typescript
// PokerUtil.ts
/**
* 检测一副牌的牌型(hand会将cards牌组排序)
* @param cards
*/
public static hand(cards: Array): PokerHand {
// 每张牌是否唯一(安全校验)
if (!this.isOnly(cards)) {
return PokerHand.NONE
}
// 排序
cards = this.sort(cards)
const count = cards.length
// 按牌数划分检测逻辑
if (count === 1) {
// 单张
return PokerHand.T_A
} else if (count === 2) {
if (this.isSame(cards)) {
// 对子
return PokerHand.T_AA
} else if (this.isRocket(cards)) {
// 王炸(火箭)
return PokerHand.T_ROCKET
}
} else if (count === 3) {
if (this.isSame(cards)) {
// 三张
return PokerHand.T_AAA
}
} else if (count === 4) {
if (this.isSame(cards)) {
// 炸弹
return PokerHand.T_AAAA
}
else if (this.isAAAB(cards)) {
// 三带一
return PokerHand.T_AAA_B
}
} else if (count >= 5) {
if (this.isABCDE(cards)) {
// 顺子
return PokerHand.T_ABCDE
} else if (this.isAAABB(cards)) {
// 三带二,三带两对 AAA BC / AAA BBCC
return PokerHand.T_AAA_BB
} else if (this.isAABBCC(cards)) {
// 双顺子 AABBCC
return PokerHand.T_AABBCC
} else if (this.isAAABBB(cards)) {
// 三顺子(飞机) AAABBB
return PokerHand.T_AAABBB
}else if (this.isAAABBBCD(cards)) {
// 飞机带翅膀 AAABBB CD / AAABBB CCDD
return PokerHand.T_AAABBB_CD
}
}
return PokerHand.NONE
}
```
## 牌型比较
**说明**:不同的牌型,比较大小的方式不一样,并且还要考虑两个牌型是否能进行比较。
**火箭和炸弹的情况**:火箭(王炸)>炸弹>其他牌型。火箭和炸弹可以压其他牌型
**思路**:
1. 牌组1是火箭(王炸),那么不需要进行比较,直接返回false
2. 判断牌组1和牌组2的牌型,牌型相等,并且牌数量相等,才可以进行同类牌型比较,否则就要判断牌组2是否是火箭、炸弹
3. 同类比较:
- 单张、对子、三张、炸弹、顺子、双顺子、三顺子都是比较最大那张的点数
- 三带一、三带二、三带两对,都是判断三张中最大的那张点数
4. 非同类比较
- 判断牌组2是否是火箭或者是炸弹,是才可以进行比较
**三带一取出3张相同的点数思路**:牌组首先有序,使用长度为3的滑动窗口遍历数组,当3个牌的点数相同时,返回这张牌的点数。
**核心代码**:
```typescript
// PokerUtil.ts
/**
* 判断cards2能否压cards1
* @param cards1
* @param cards2
*/
public static legal(cards1: Array, cards2: Array): boolean {
// 获取牌组牌型,并且对牌组进行排序
const hand1 = this.hand(cards1)
const hand2 = this.hand(cards2)
if (hand1 == PokerHand.T_ROCKET || hand2 == PokerHand.NONE) {
// cards1是王炸,直接false
// cards2没有牌型,直接false
return false
}
// && hand2 != PokerHand.NONE
if (hand1 == PokerHand.NONE) {
// cards1没有牌型,cards2有牌型(上一个if已确定) 直接true
return true
}
if (hand1 === hand2 && cards1.length === cards2.length) {
// 牌型相等、并且牌数相同
switch (hand1) {
// 单张
case PokerHand.T_A:
// 对子
case PokerHand.T_AA:
// 三张
case PokerHand.T_AAA:
// 炸弹
case PokerHand.T_AAAA:
// 顺子
case PokerHand.T_ABCDE:
// 双顺子
case PokerHand.T_AABBCC:
// 三顺子
case PokerHand.T_AAABBB:
// 比较最后一张点数权重的大小
if (this.compareLastByPointWeight(cards1, cards2) < 0) {
return true
}
break;
// 飞机带翅膀
case PokerHand.T_AAABBB_CD:
const cards1By3 = this.getThreeSomeArr(cards1)
const cards2By3 = this.getThreeSomeArr(cards2)
// 比较最后一张点数权重的大小
if (this.compareLastByPointWeight(cards1By3, cards2By3) < 0) {
return true
}
break;
// 三带一
case PokerHand.T_AAA_B:
// 三带二、三带两对
case PokerHand.T_AAA_BB:
const card1 = this.getThreeSome(cards1)
const card2 = this.getThreeSome(cards2)
if (!card1 || !card2) {
throw new Error('no three are the same')
}
if (this.compareLastByPointWeight([card1], [card2]) < 0) {
return true
}
break;
}
} else if (hand1 !== hand2) {
// 牌型不相等;牌数可相等,也可不相等
// cards2是王炸、炸弹,可以直接压
if (hand2 === PokerHand.T_ROCKET || hand2 === PokerHand.T_AAAA) {
return true
}
}
// 牌型相等,但是牌数不相等,直接false
return false
}
```
## 发牌管理
**说明**:需要生成一副牌(54张),洗牌后预留3张地主牌,并且获取3组牌(每组17张)用于分发给3名玩家
**思路**:封装PokerManger类,实现生成一副牌,并且实现洗牌功能。在房间的Referee类中,分发这副牌
**洗牌思路**:获取两个随机值(0-53),对54张牌两两随机交换。
**核心代码**:
```typescript
// PokerManager.ts
/**
* 扑克牌管理类
*/
export default class PokerManager {
// ...
// 获取一副牌
public getDeck(): Poker[] {
let cards: Array = []
// 花色
// SPADE = 3, // 黑桃♠
// HEARD = 2, // 红桃♥
// CLUB = 1, // 梅花♣
// DIAMOND = 0, // 方块♦
const suits = [PokerSuits.DIAMOND, PokerSuits.CLUB, PokerSuits.HEARD, PokerSuits.SPADE]
// 牌值
const points = [
PokerPoints.P_A, PokerPoints.P_2, PokerPoints.P_3, PokerPoints.P_4, PokerPoints.P_5,
PokerPoints.P_6, PokerPoints.P_7, PokerPoints.P_8, PokerPoints.P_9, PokerPoints.P_10,
PokerPoints.P_J, PokerPoints.P_Q, PokerPoints.P_K
]
suits.forEach(suit => {
points.forEach(point => {
cards.push(new Poker({ suit, point }))
})
})
// 大小王
cards.push(new Poker({
suit: PokerSuits.NONE,
point: PokerPoints.P_RJ
}))
cards.push(new Poker({
suit: PokerSuits.NONE,
point: PokerPoints.P_BJ
}))
// 洗牌
this.shuffle(cards)
return cards
}
/**
* 洗牌
* @param cards
*/
public shuffle(cards: Poker[]) {
// 两两随机交换
for (let i = 0; i < cards.length; i++) {
const i1 = RandomUtil.getRangeInt(0, cards.length)
const i2 = RandomUtil.getRangeInt(0, cards.length)
// 交换
ArrayUtil.swap(cards, i1, i2)
}
}
}
```
```typescript
// Peferee.ts
/**
* 裁判类
* 管理房间的游戏流程和规则
*/
export default class Referee {
private players: Array = [] // 玩家集合
/**
* 发牌
*/
public deal() {
// 生成一副扑克牌
const cards = PokerManager.instance.getDeck()
// 取出3张地主牌
this._landlordCards = []
for (let i = 0; i < 3; i++) {
const card = cards.pop()
if (card != undefined) {
this._landlordCards.push(card)
}
}
// 发牌
for (let i = 0; i < 3; i++) {
const player = this.players[i]
if (player === undefined) {
throw new Error(`[referee] player is undefined`)
}
player.initCards(cards.splice(0, 17)) // 17张牌
// console.log('[referee] player.cards:', player.cards)
}
// 设为发牌阶段
this.stage = GameStage.REFEREE_DEAL
}
}
```
## 抢地主流程
**说明**:系统随机选定一个玩家开始叫地主,以顺时针轮一圈执行抢地主,地主归属权会为最后一个执行叫地主的玩家。
**抢地主情况**:
- 抢地主轮完一圈,有2个及以上玩家抢地主,最后需要第一名叫地主的玩家来决定地主权。若第一名叫地主的玩家继续抢,那么由他作地主,反之由最后一个叫地主的玩家作地主。
- 抢地主轮完一圈,只有一名玩家抢地主,那么直接由他作地主
- 抢地主轮完一圈,
**思路**:(灵感来自iterator的next函数)
- 设置一个`nextCallLandPlayer()`函数,他会返回下一个进入【叫地主环节】的玩家。每次从玩家数组中,根据一个基准下标往右取一个玩家对象。
- 需要记录第一个执行【叫地主环节】的玩家下标,用于判断是否轮完一圈
- 系统通知房间的所有人,轮到谁进入【叫地主环节】
- 对应的玩家需要执行【叫地主/不叫地主】操作。执行完毕后,系统会记录这个名玩家是否叫地主,之后系统获取下一个进入【叫地主环节】的玩家,并且通知房间内的所有人
- 轮完一圈后,系统会判断抢地主的情况,是2个及以上都抢了地主,还是只有1人抢地主,还是没人抢地主
- 根据抢地主情况,执行由第一个叫地主的玩家确定地主、直接确定地主、或者重新发牌操作
**核心代码**:
```typescript
//Referee.ts
/**
* 获取下一个叫地主的玩家
* 说明:
* - 地主归属权为最后一个执行【叫地主】的玩家;
* - 所有人都不叫地主,那么第一个开始执行【叫地主/不叫地主】操作的玩家不需要再执行操作,直接返回null,系统要重新发牌
* - 仅有1人叫地主,那么ta不需要再执行一次【叫地主/不叫地主】操作,直接归属ta为地主,并且函数返回null
* - 2人及以上(包含所有人)叫地主,那么第一次【叫地主】的玩家,需要再执行一次【叫地主/不叫地主】操作
* @returns 玩家对象 | null;玩家对象表示下一个执行【叫地主/不叫地主的玩家】;null表示没有下一个玩家了,叫地主环节结束。
* 可以通过get landlordPlayer() 获取地主玩家对象
*/
public nextCallLandloardPlayer(): Player | null {
// 结束叫地主阶段
if (this.isEndCall) {
return null
}
// 往右遍历为顺时针
this.currentRoundPlayerIndex++
if (this.currentRoundPlayerIndex >= this.players.length) {
this.currentRoundPlayerIndex = 0
}
// 表示轮完了一圈
if (this.beginCallPlayerIndex == this.currentRoundPlayerIndex) {
this.isEndCall = true
// 判断是否还需要由第一个【叫地主】的玩家确定地主归属权
if (this.isAllPlayerNotCallLand()) {
// 没人叫地主,直接返回null
return null
} else if (this.isOnePlayerCallLand()) {
console.log('this.isOnePlayerCallLand():', true)
// 只有一人叫地主,并且返回null
return null
} else {
// 2个或以上玩家叫地主,将由第一个执行【叫地主】的玩家决定地主归属权
this.currentRoundPlayerIndex = this._firstCallLandPlayerIndex
}
}
// 当前回合玩家对象
const player = this.players[this.currentRoundPlayerIndex]
if (player === undefined) {
throw new Error(`[referee] player is undefined`)
}
return player
}
```
## 房间管理
**说明**:用户加入房间时,要进入一个每满的房间
**思路**:
- 设置2个Map,key存放房间id,value存放房间对象。一个表示以满房间,一个表示未满房间
- 为了让用户能快速找到房间,再设置一个map,根据用户id存放用户所在的房间
- 加入房间时,如果空房间map为空,那么创建一个新的房间
- 如果加入房间后房间已满,那么房间加入已满房间的map中
## 单元测试
使用mocha对PorkUtil.ts进行进行单元测试
**目的**:对每个模块单独测试,保证模块本身逻辑正确。减少运行时测试功能的繁琐性
**运行单元测试**:
```bash
npm test
```
## Http和Socket对象关联
**说明**:系统限制客户端使用Http发送请求。通过监听Websocket获取服务器主动推送的信息
**难题**:如何将http和socket对象关联起来
**思路**:SocketManager类中使用map根据用户的id和socket对象关联。在http请求函数中要处理对应用户的socket时,获取用户id根据SocketManger获取用户对应的socket对象即可。
**核心代码**:
```typescript
// SocketManger.ts
/**
* 客户端socket对象管理类
* 根据用户id保存对应的socket对象,方便http和socket进行关系对应
* 注意:用户连接时必须在auth中携带用户uid才可以socket连接
*/
export default class SocketManager {
private clientSockets: Map // 根据用户id保存用户socket对象
private connectCallbacks: Set // 连接回调函数集合
private _io: Server = null!
/**
* 客户端连接回调
* @param socket
*/
private onConnect(socket: Socket): void {
// 获取用户id
const { uid } = socket.handshake.auth
// 无用户id,则不需进行下面的步骤(无效登录连接)
// token校验...
if (uid == null) {
socket.disconnect()
return
}
console.log('[socket] client connected, its uid is:', uid, socket.id)
// 保存socket
this.clientSockets.set(uid, socket)
// 通知监听事件
this.connectCallbacks.forEach(fn => { fn(uid, socket) })
// 监听离开
socket.on('disconnect', this.onDisconnect(uid, socket))
}
}
```
# 参考
[Solitaire: 纸牌游戏-学习Cocos项目 (gitee.com)](https://gitee.com/pimple_village_secretary/Solitaire)
[第二十三章《斗地主游戏》第3节:项目完整代码_扑克json_穆哥学堂的博客-CSDN博客](https://blog.csdn.net/shalimu/article/details/128182742)
[tinyshu/ddz_game: 斗地主游戏 (github.com)](https://github.com/tinyshu/ddz_game)
# 更多
**游戏规则**:[./doc/2-游戏规则.md](./doc/2-游戏规则.md)
**功能设计与原型图**:[./doc/3-游戏功能与原型设计.md](./doc/3-游戏功能与原型设计.md)
**游戏英文描述**:[英语介绍斗地主及其规则 - 田间小站 (tjxz.cc)](https://www.tjxz.cc/6069)
**git commit规范**:https://zhuanlan.zhihu.com/p/182553920