1 Star 0 Fork 1

Baymax / koa-ddz-server

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

介绍

基于Node.js、Koa、Typescript、Socket.io实现的ddz游戏服务端

主要功能:游客登录、房间功能、准备功能、发牌功能、抢地主功能(抢地主流程)、出牌功能(牌型校验、牌型比较、出牌流程)、Socket对象管理

客户端(基于Cocos Creator2.x)https://gitee.com/baymax668/cocos-ddz-client

项目启动

npm install
npm run server

目录结构

|-- 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

游客登陆

URLhttp://localhost:3000/api/v1/user/login/guest

Method:Post

参数:None

返回值

名称 类型 描述
id string 用户id
name string 用户名/昵称
balance number 余额

进入房间

URLhttp://localhost:3000/api/v1/room/join

Method:Post

参数

名称 类型 描述
user object 用户对象(包含id、name、balance)

返回值:None

离开房间

URLhttp://localhost:3000/api/v1/room/leave

Method:Post

参数

名称 类型 描述
uid string 用户id
roomId string 房间id

准备

准备阶段使用

URLhttp://localhost:3000/api/v1/game/prepare

Method:Post

参数

名称 类型 描述
uid string 用户id
roomId string 房间id
isPrepare boolean 是否准备

准备就绪

发牌阶段。客户端整理好牌后,需要调用这个api,表示准备就绪

URLhttp://localhost:3000/api/v1/game/ready

Method:Post

参数

名称 类型 描述
uid string 用户id
roomId string 房间id

叫/不叫地主

抢/不抢地主也是这个api

URLhttp://localhost:3000/api/v1/game/call

Method:Post

参数

名称 类型 描述
uid string 用户id
roomId string 房间id
isCall boolean 是否叫地主

出牌

URLhttp://localhost:3000/api/v1/game/play

Method:Post

参数

名称 类型 描述
uid string 用户id
roomId string 房间id
isPlay boolean 是否是出牌(否则为不出牌)
cards Array<Poker> 要出的牌

Socket api

注意:客户端只通过Socket监听数据,不会通过Socket发任何数据

/**
 * 游戏相关的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 状态值
/**
 * 游戏阶段
 */
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

思路

  1. 点数可以设定一个权值,用于排序比较。例如,3的权值为300、4的权值为400...
  2. 花色设置一个较小的权值,如,方块为0、梅花为1、红桃为2,黑桃为3
  3. 点数和花色的权值相加,得到这副牌得权值,即可比较大小,并且实现两级排序的功能。(因为点数的权值为百位的,不同点数基于百位进行比较;点数中的花色位于个位,不同点数的花色根据个位进行排序)

关键代码

// Poker.ts
export class Poker implements IPokerVo {
    // ...
    /**
     * 用于比较大小
     */
    public get value() {
        return this.suit + this.pointWeight * 100
    }
    // ...
}
// PokerUtil.ts
 /**
   * 对一副牌从大到小排序
   * @param cards 
   * @returns 
   */
public static sort(cards: Array<IPokerVo>) {
    return cards.sort((c1: IPokerVo, c2: IPokerVo) => {
        const poker1 = new Poker(c1)
        const poker2 = new Poker(c2)
        return poker1.compare(poker2)
    }).reverse()
}

牌型校验

说明:牌型校验用于判断一个牌组是否是符合游戏的牌型(单张、对子、三张、三带一...),因为不同牌型的大小比较方式不同,需要获取牌型才可以进行两组牌型大小比较。

image-20230527172237117

大致思路

  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的遍历出来,判断是否连续,再将剩余的牌判断是否是同数量单张,或者同数量对子。

核心代码

 // PokerUtil.ts
 /**
   * 检测一副牌的牌型(hand会将cards牌组排序)
   * @param cards 
   */
  public static hand(cards: Array<IPokerVo>): 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个牌的点数相同时,返回这张牌的点数。

核心代码

 // PokerUtil.ts
 /**
   * 判断cards2能否压cards1
   * @param cards1 
   * @param cards2
   */
  public static legal(cards1: Array<IPokerVo>, cards2: Array<IPokerVo>): 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张牌两两随机交换。

核心代码

// PokerManager.ts
/**
 * 扑克牌管理类
 */
export default class PokerManager {
  // ...

  // 获取一副牌
  public getDeck(): Poker[] {
    let cards: Array<Poker> = []
    // 花色
    // 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)
    }
  }
}
// Peferee.ts
/**
 * 裁判类
 * 管理房间的游戏流程和规则
 */
export default class Referee {
  private players: Array<Player | undefined> = []         // 玩家集合

  /**
   * 发牌
   */
  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人抢地主,还是没人抢地主
  • 根据抢地主情况,执行由第一个叫地主的玩家确定地主、直接确定地主、或者重新发牌操作

核心代码

//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进行进行单元测试

目的:对每个模块单独测试,保证模块本身逻辑正确。减少运行时测试功能的繁琐性

运行单元测试

npm test

Http和Socket对象关联

说明:系统限制客户端使用Http发送请求。通过监听Websocket获取服务器主动推送的信息

难题:如何将http和socket对象关联起来

思路:SocketManager类中使用map根据用户的id和socket对象关联。在http请求函数中要处理对应用户的socket时,获取用户id根据SocketManger获取用户对应的socket对象即可。

核心代码

// SocketManger.ts
/**
 * 客户端socket对象管理类
 * 根据用户id保存对应的socket对象,方便http和socket进行关系对应
 * 注意:用户连接时必须在auth中携带用户uid才可以socket连接
 */
export default class SocketManager {
  private clientSockets: Map<uid, Socket>                 // 根据用户id保存用户socket对象
  private connectCallbacks: Set<ConnectCallbackFn>        // 连接回调函数集合

  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)

第二十三章《斗地主游戏》第3节:项目完整代码_扑克json_穆哥学堂的博客-CSDN博客

tinyshu/ddz_game: 斗地主游戏 (github.com)

更多

游戏规则./doc/2-游戏规则.md

功能设计与原型图./doc/3-游戏功能与原型设计.md

游戏英文描述英语介绍斗地主及其规则 - 田间小站 (tjxz.cc)

git commit规范https://zhuanlan.zhihu.com/p/182553920

空文件

简介

基于Node.js+Koa+TypeScript实现的ddz服务端 展开 收起
TypeScript
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
TypeScript
1
https://gitee.com/baymax668/koa-ddz-server.git
git@gitee.com:baymax668/koa-ddz-server.git
baymax668
koa-ddz-server
koa-ddz-server
master

搜索帮助