# 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)->具体代码逻辑 ## 基本架构 **基本架构图**: image-20230526221319126 ## 详细架构 **详细架构图**: image-20230527163548670 # 网络通信模型 - 客户端通过Http发送数据(目的:为了复用后端中间件,并且方便实现请求响应获取响应结果) - 客户端监听Socket,获得服务端主动推送的数据 > 其实Socket.io具备请求响应功能 **通信基本模型图**: image-20230526222216827 # 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 **返回值**: | 名称 | 类型 | 描述 | | :--: | :--: | :--: | | - | - | - | # 成果、难点、思路 ## 牌组排序 **说明**:牌组排序是一个两级排序,先排序**点数**,相同的点数再根据**花色**排序。 ![image-20230527164031712](README.assets/image-20230527164031712.png) **思路**: 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() } ``` ## 牌型校验 **说明**:牌型校验用于判断一个牌组是否是符合游戏的牌型(单张、对子、三张、三带一...),因为不同牌型的大小比较方式不同,需要获取牌型才可以进行两组牌型大小比较。 ![image-20230527172237117](README.assets/image-20230527172237117.png) **大致思路**: 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