做棋牌开发的人,多半都碰过这样的问题:规则文档写得挺全,真到了要写代码,谁也不清楚“杠上开花”到底该算几番、门清要不要叠加、封顶线卡在哪儿。

九江麻将这套玩法在本地有不少变体,算法列表的意义就在于把散落在口头约定里的规则,收成一张能直接对照实现的表。这篇不聊怎么赢牌,只讲一张算法列表该列哪些东西、哪些地方最容易出错。

一张算法列表,实际由三张表拼起来

很多人以为“算法列表”就是把番型名字和分值排一遍,实际做下来会发现单靠这张表根本跑不通。真正能用的是三块内容:番型表、算分表、听牌判定表。

番型表负责识别牌型并给出番数,算分表负责把番数换算成实际输赢分,听牌表负责回答“打了这张牌之后听什么”。三块之间互相调用,缺一块就得在代码里写死,后期改动会很痛苦。

顺带提一句,有些团队把听牌逻辑塞进胡牌函数里,靠反复模拟出牌来判断,跑起来是能跑,但一次请求要做几十上百次递归,牌局节奏一快就顶不住。分开写,维护成本低得多。

番型判定:先看特殊牌型,再走通用拆分

九江麻将的胡牌结构整体还是四副面子加一对将,但特殊牌型(七对、十三烂一类的本地叫法)在各地口径差别不小。

建议的判定顺序是:先扫一遍是否满足特殊牌型的整体结构,不满足再走通用递归拆分。这个顺序不能反,反过来会出现七对牌被拆成四副刻顺、番数算错的情况。

通用拆分的核心思路

拿 14 张牌,先确定将牌,把剩余牌按花色分桶,每个桶内从最小的牌开始尝试刻子或顺子,能拆完就是胡牌。

递归里最关键的是回溯:某张牌既可以当刻子起点也可以当顺子起点时,两条路都要走,走不通要退回来。

听牌逻辑基本是同一套递归复用:把 13 张手牌对着 34 种牌各加一张,逐张调胡牌判定,能胡的就是听牌。

听牌张数还要考虑剩余张数——四张都在自己手上和牌河里,那这张牌等于没听。

算分环节,分歧往往比番型还多

番型好确认,算分才是扯皮的重灾区。常见需要明确的口径有这些:

  • 底分和番数是加法还是乘法,1 番是翻倍还是加一分;
  • 封顶线设在几番,超过之后是否继续累加;
  • 自摸、点炮、杠牌的结算方向,谁给谁、给几份。
  • 门清、杠上开花、抢杠这类附加番是否可以叠加,叠加上限是多少。

这几条不定死,写出来的算分函数一定会在实战里被玩家挑刺。比较稳妥的做法是把算分表做成配置项,比如用 JSON 描述每个番型的 key、番值、是否可叠加、是否触发封顶,代码只负责读取和求值。规则调整时改配置就行,不用翻代码。

也有团队选择把本地几个区的差异做成规则包,开局时按房间设置选一套。

这个做法成本不低,如果你的用户基本集中在一个区域,其实没必要铺这么大,先把主流口径做准更实际。

实现时容易踩的几个坑

  1. 赖子牌的处理。带赖子的玩法里,递归的判断条件要额外加“可用赖子补位”,同时限制赖子数量,否则会出现一张赖子被重复使用的错误。
  2. 牌型编码。用 0 到 33 的一维数组表示 34 种牌,比二维结构省事,递归时下标计算也更直观。
  3. 性能。胡牌判定本身不慢,慢的是在听牌枚举里反复做深拷贝,改成原地加减再回滚,速度快很多。
  4. 边界。手牌张数不对、同一张牌超过四张,这类输入要先做校验,否则递归会跑出莫名其妙的结果。

另外提一点保留意见:向听数计算在不少项目里被当成必需品,但对普通对局来说,只要听牌判定准、出牌提示不出错,向听数并不是必须上。它更适合做辅助功能,比如提示“打哪张更接近听牌”,要不要做取决于产品形态,别一上来就硬堆。

列表之外,还需要一份对照样例

规则表写得再细,没有样例牌型配着看,后面接手的人一样会懵。

建议每一条番型都附一组 14 张牌的示例,标注清楚为什么算这个番、能否叠加。这份样例的维护成本很低,省下的沟通时间却不少。

算法列表本质上是一份约定文档,写得清的团队,后面改规则、加玩法、接新房间类型都轻松。写得含糊的,每次玩家反馈算分不对,都得从头翻一遍老代码。