斗地主牌型识别算法详解:从牌值编码到服务端校验的完整实现

发布时间:2026/10/18 20:52:04
斗地主牌型识别算法详解:从牌值编码到服务端校验的完整实现
最近刷斗地主直播弹幕时经常能看到类似“主播的蚂蚁和 ttf byd pm 了想必你的新 12 肯定打的很好吧”这样的黑话。乍一看像乱码仔细琢磨其实就是玩家圈子内部的缩写和梗蚂蚁可能是“码”或者“手牌”的谐音ttf、byd 大概率是某个主播或战队的缩写pm 可能是“碰面/私聊/版本号”之类的圈内用语新 12 则往往指新赛季、新版本或者某套新牌组。对只看热闹的观众来说这些话很难懂但如果你是一名棋牌类游戏后端开发真正值得研究的并不是黑话本身而是黑话背后的牌型编码、出牌校验和版本规则。不同版本规则不同新牌型怎么识别、旧客户端怎么兼容、服务端怎么保证不出错这些才是实打实的技术问题。所以本文不讨论具体主播圈子的梗而是从技术视角拆解一个斗地主类游戏服务端最常写的模块牌型编码与牌型识别算法。内容包括牌值设计、牌型枚举、顺子/连对/飞机/带牌判定、牌型大小比较以及一个可以直接运行的 Python 校验器。即使你的项目语言是 Java、Go只要理解了这套思路迁移成本也很低。1. 背景与核心概念1.1 这些黑话在说什么先回到开头那句黑话。它本质上是在聊“某主播这次手牌和状态都到位了新版本应该能打出不错的战绩”。斗地主圈子里类似的词还有很多比如“蚂蚁”可能是“码”的谐音也就是手牌、筹码量级ttf、byd、pm 这类字母组合通常是主播 ID、战队名或某个版本协议的缩写新 12 则可能指新赛季的 12 级房间、新客户端版本或者某个新的牌组配置。这类黑话对后端开发最大的启发是游戏版本一变语音包、UI、房间配置、牌型规则都可能跟着变。“新 12”做好了前端和后端协议没对齐照样会出现出牌被拒、牌型显示不对的线上事故。真正可靠的做法是把牌型识别这层逻辑放在服务端统一处理前端只做展示和发送指令。1.2 为什么后端需要牌型识别很多人会问客户端不是已经能识别牌型了吗为什么后端还要再写一套原因有几点。第一客户端可以被篡改。玩家如果改了本地代码直接发一对“合法”的 JSON 数据比如声称自己出了一手顺子服务端如果不校验就会出现“手里没这张牌却能打出来”的严重漏洞。第二规则统一需要权威。同一局游戏可能同时存在 Android、iOS、PC 多个客户端每个客户端的实现细节并不完全一致。只有服务端做最终判定才能保证所有人的规则一致避免“我以为能出、系统却不让出”的争议。第三防作弊和审计离不开规则引擎。日志里记录了玩家每一次出牌服务端需要重新计算这一步是否合法才能判断对局是否存在异常。所以在正式棋牌项目中出牌校验不是“锦上添花”而是底线功能。1.3 斗地主牌型体系速览写算法之前先把牌型体系梳理清楚。经典斗地主中常见的可出牌型包括以下几类牌型说明示例单张单独一张牌8对子两张相同牌值88三张三张相同牌值888三带一三张 一张单牌888 5三带二三张 一对888 55顺子至少 5 张连续单牌2 和王不能入顺3 4 5 6 7连对至少 3 个连续对子2 和王不能入连对33 44 55飞机至少 2 个连续三张可带单/带对333 444 或 333 444 55四带二四张相同 两张单牌8888 5 7四带两对四张相同 两个对子8888 55 66炸弹四张相同牌值8888火箭小王 大王小王 大王不同平台对“四带二能不能带对子”“飞机带牌能否拆对”有细节差异写代码时必须以项目策划案为准。本文示例采用比较常见的主流规则。2. 环境准备与数据设计2.1 运行环境本文示例使用 Python 3 实现不需要安装任何第三方依赖直接用标准库collections.Counter做张数统计即可。python3 --version如果你本机只有 Python 2.7建议先安装 Python 3.8 以上版本。示例代码里用到的语法都比较基础主要是字典、列表、枚举和 Counter兼容性很好。项目语言如果不用 Python也没关系。核心逻辑可以平移到 Java、Go、C#只需要把“牌值比较”和“集合统计”换成对应语言的写法。2.2 牌值编码规则斗地主最麻烦的一点是牌面大小不连续3 到 10 是数字J、Q、K、A、2 是字母后面还有小王、大王。如果直接用字符串比较很容易出错。所以正规做法是把所有牌面映射成连续整数。本文采用的映射如下牌面编码值334455667788991010J11Q12K13A14215小王16大王17为什么 2 编码成 15而不是放在 A 后面因为斗地主中 2 是除王之外最大的单牌但它不能进入顺子。设计时把顺子判定条件写成“最大牌值不能超过 14”也就是 A 是顺子的终点这样代码会非常清晰。编码存在一个独立的字典中后续所有函数都只操作整数数组。CARD_VALUE_MAP { 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, J: 11, Q: 12, K: 13, A: 14, 2: 15, SMALL: 16, JOKER: 17 }2.3 牌型枚举设计牌型识别函数最终要返回两个信息牌型类型、主牌大小。主牌大小用于同类型之间的比较比如顺子比尾牌对子比单牌大小飞机比最大一组三张牌的大小。用枚举定义牌型可读性更好也方便后面写接口时做序列化传输。示例中我把牌型定义如下from enum import Enum class CardType(Enum): SINGLE 1 # 单张 PAIR 2 # 对子 TRIPLE 3 # 三张 TRIPLE_ONE 4 # 三带一 TRIPLE_PAIR 5 # 三带二 STRAIGHT 6 # 顺子 STRAIGHT_PAIR 7 # 连对 PLANE 8 # 飞机不带 PLANE_ONE 9 # 飞机带单 PLANE_PAIR 10 # 飞机带对 FOUR_TWO 11 # 四带二 FOUR_TWO_PAIR 12 # 四带两对 BOMB 13 # 炸弹 ROCKET 14 # 火箭枚举只是语义标签真正困难的是判断逻辑。下一节开始拆解。3. 核心算法拆解3.1 统计牌型从手牌到张数映射牌型识别的基础是统计每一张牌在本次出牌中出现的次数。例如[5, 5, 5, 8]统计结果是5:3, 8:1一眼就能看出是三带一。Python 的Counter天然适合这个场景。下面这个函数接受整数牌值列表返回每个牌值出现的次数并额外返回排序后的计数列表和牌值列表方便多种判断复用。from collections import Counter def analyze_cards(cards): counter Counter(cards) counts sorted(counter.values(), reverseTrue) values sorted(counter.keys()) return counter, counts, valuescounts是降序排列的出现次数例如单张 [3]counts [1]对子 [8, 8]counts [2]三带一 [8, 8, 8, 5]counts [3, 1]三带二 [8, 8, 8, 5, 5]counts [3, 2]炸弹 [9, 9, 9, 9]counts [4]很多牌型可以先看counts的形状再进一步校验连续性。3.2 简单牌型判定单张、对子、三张、炸弹、火箭都是最简单的牌型按张数和去重数量直接判断即可。这里有一个细节四张相同牌时counts [4, 1, 1]和counts [4, 2]都可能出现在四带二中所以必须先单独判断“四张相同 炸弹”分支不能混在一起。def judge_simple_type(n, counter, counts): if n 1: return CardType.SINGLE if n 2: if sorted(counter.keys()) [16, 17]: return CardType.ROCKET if len(counter) 1: return CardType.PAIR return None if n 3 and len(counter) 1: return CardType.TRIPLE if n 4 and len(counter) 1: return CardType.BOMB if n 4 and counts [3, 1]: return CardType.TRIPLE_ONE if n 5 and counts [3, 2]: return CardType.TRIPLE_PAIR if n 6 and counts[0] 4 and len(counter) 3: return CardType.FOUR_TWO if n 8 and counts [4, 2, 2]: return CardType.FOUR_TWO_PAIR return None注意三带一的场景里如果剩余那张单牌和某张被带牌数值相同例如[8, 8, 8, 8]张数会是 4这时不是三带一而是炸弹。所以必须先判炸弹再判三带一。3.3 顺子与连对判定顺子的判定条件有三个至少 5 张。所有牌出现一次不能重复。最大牌值不能超过 14也就是 2 和王不能进入顺子。排序后相邻牌值差必须为 1。连对的条件类似只是把“单张连续”换成“对子连续”且至少 3 对。def is_straight(cards): if len(cards) 5: return False values sorted(cards) if values[-1] 14: return False if len(set(values)) ! len(values): return False for i in range(1, len(values)): if values[i] - values[i - 1] ! 1: return False return True def is_straight_pair(cards): if len(cards) 6 or len(cards) % 2 ! 0: return False counter, counts, values analyze_cards(cards) if len(values) 3: return False if any(cnt ! 2 for cnt in counter.values()): return False if values[-1] 14: return False for i in range(1, len(values)): if values[i] - values[i - 1] ! 1: return False return True这里有几个新手容易踩的坑。第一个A 能不能入顺主流规则是 A 可以当顺子的头比如 10 J Q K A 是合法顺子但 A 后面不能接 2因为 2 的编码是 15所以values[-1] 14这一行就把 2 排除了。第二个顺子长度问题。默认至少 5 张但有些玩法支持“迷你顺子”或者“长顺子”具体最低张数应该做成配置项不要写死在代码里。第三个连对不能拆成单牌。连对 33 44 55 的判断中必须保证每个牌值都恰好出现两次不能有某个牌值出现一次或三次。3.4 飞机与带牌判定飞机是斗地主里最复杂的牌型。它由至少两组连续三张组成例如 333 444、777 888 999。飞机又可以分为不带、带单、带对三种。完整实现飞机判定时我采用“先找连续三张段再扣掉三张后检查剩余牌”的思路。这样可以复用不会把三带一和飞机搞混。def find_plane(cards): n len(cards) if n 6: return None counter, counts, values analyze_cards(cards) usable_triples [v for v in values if v 14 and counter[v] 3] if len(usable_triples) 2: return None runs [] i 0 while i len(usable_triples): j i while j 1 len(usable_triples) and usable_triples[j 1] usable_triples[j] 1: j 1 if j - i 1 2: runs.append(usable_triples[i:j 1]) i j 1 if not runs: return None for run in runs: k len(run) rem counter.copy() ok True for v in run: if rem[v] 3: ok False break rem[v] - 3 if not ok: continue rem_cards [] for v, cnt in rem.items(): rem_cards.extend([v] * cnt) left len(rem_cards) if left 0: return CardType.PLANE, run[-1] if left k: return CardType.PLANE_ONE, run[-1] if left 2 * k: rc Counter(rem_cards) if all(cnt 2 for cnt in rc.values()): return CardType.PLANE_PAIR, run[-1] return None这段代码的核心思想是先把所有可能是三张的牌值取出来比如[333, 444, 555, 889]中只有 3、4、5 是三张组然后枚举每个连续段假设它是飞机主体再从总牌里扣掉主体的牌看剩余部分是否能构成带牌。需要说明的是这里采用的是比较严格的主流规则飞机不带连续三张张数为 3 的倍数且不能包含 2 和王。飞机带单飞机主体 与飞机组数相同的单牌剩余单牌可以相同也可以不同。飞机带对飞机主体 与飞机组数相同的对子剩余牌必须两两相同。不同平台对“剩余单牌能否相同”“飞机能不能带一对”有不同玩法如果项目需要兼容多种规则建议把规则选项做成配置项。3.5 牌型比较逻辑牌型识别完成后比较逻辑就简单了。核心规则如下火箭最大可以压任何牌。炸弹大于火箭以外的所有牌型。如果两边都是炸弹比较四张牌的大小。相同牌型之间比较主牌大小主牌就是该牌型中用于比较的关键牌值。不同牌型不能互相压制除非有一方是炸弹或火箭。def can_beat(new_cards, old_cards): result_new judge_type(new_cards) result_old judge_type(old_cards) if result_new is None or result_old is None: return False new_type, new_main result_new old_type, old_main result_old if new_type CardType.ROCKET: return True if old_type CardType.ROCKET: return False if new_type CardType.BOMB: if old_type CardType.BOMB: return new_main old_main return True if old_type CardType.BOMB: return False if new_type ! old_type: return False return new_main old_main这个函数接受两个牌值列表new_cards是玩家当前想要出的牌old_cards是上一手牌。如果can_beat返回 True说明玩家可以打出这一手。判断主牌时要注意“三带一”和“三带二”的主牌是那三张相同牌的值而不是带的牌。四带二的主牌是四张相同牌的值炸弹同理。4. 完整实战可运行的牌型校验器4.1 创建项目结构下面把上面的代码整合成一个可运行的校验器。项目只有一个文件方便复制测试。dou_dizhu/ └── dou_dizhu_cards.py如果是在正式项目中建议拆成card_value.py、card_type.py、card_judge.py、card_battle.py四个模块每个模块职责单一。这里为了演示方便全部写在同一个文件里。4.2 编写核心代码完整代码如下你可以直接复制保存为dou_dizhu_cards.py运行。 文件路径dou_dizhu/dou_dizhu_cards.py 斗地主牌型识别与出牌校验示例 from collections import Counter from enum import Enum CARD_VALUE_MAP { 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, J: 11, Q: 12, K: 13, A: 14, 2: 15, SMALL: 16, JOKER: 17 } class CardType(Enum): SINGLE 1 PAIR 2 TRIPLE 3 TRIPLE_ONE 4 TRIPLE_PAIR 5 STRAIGHT 6 STRAIGHT_PAIR 7 PLANE 8 PLANE_ONE 9 PLANE_PAIR 10 FOUR_TWO 11 FOUR_TWO_PAIR 12 BOMB 13 ROCKET 14 def parse_cards(card_strs): result [] for s in card_strs: if s in CARD_VALUE_MAP: result.append(CARD_VALUE_MAP[s]) else: raise ValueError(无法识别的牌面: s) return result def analyze_cards(cards): counter Counter(cards) counts sorted(counter.values(), reverseTrue) values sorted(counter.keys()) return counter, counts, values def is_straight(cards): if len(cards) 5: return False values sorted(cards) if values[-1] 14: return False if len(set(values)) ! len(values): return False for i in range(1, len(values)): if values[i] - values[i - 1] ! 1: return False return True def is_straight_pair(cards): if len(cards) 6 or len(cards) % 2 ! 0: return False counter, counts, values analyze_cards(cards) if len(values) 3: return False if any(cnt ! 2 for cnt in counter.values()): return False if values[-1] 14: return False for i in range(1, len(values)): if values[i] - values[i - 1] ! 1: return False return True def find_plane(cards): n len(cards) if n 6: return None counter, counts, values analyze_cards(cards) usable_triples [v for v in values if v 14 and counter[v] 3] if len(usable_triples) 2: return None runs [] i 0 while i len(usable_triples): j i while j 1 len(usable_triples) and usable_triples[j 1] usable_triples[j] 1: j 1 if j - i 1 2: runs.append(usable_triples[i:j 1]) i j 1 if not runs: return None for run in runs: k len(run) rem counter.copy() ok True for v in run: if rem[v] 3: ok False break rem[v] - 3 if not ok: continue rem_cards [] for v, cnt in rem.items(): rem_cards.extend([v] * cnt) left len(rem_cards) if left 0: return CardType.PLANE, run[-1] if left k: return CardType.PLANE_ONE, run[-1] if left 2 * k: rc Counter(rem_cards) if all(cnt 2 for cnt in rc.values()): return CardType.PLANE_PAIR, run[-1] return None def get_main_value(counter, n): for v in sorted(counter.keys()): if counter[v] n: return v return None def judge_type(cards): n len(cards) if n 0: return None counter, counts, values analyze_cards(cards) # 火箭 if n 2 and sorted(cards) [16, 17]: return CardType.ROCKET, 17 # 单张 if n 1: return CardType.SINGLE, cards[0] # 对子 if n 2 and len(counter) 1: return CardType.PAIR, cards[0] # 三张 if n 3 and len(counter) 1: return CardType.TRIPLE, cards[0] # 炸弹必须优先于三带一 if n 4 and len(counter) 1: return CardType.BOMB, cards[0] # 三带一 if n 4 and counts [3, 1]: main get_main_value(counter, 3) return CardType.TRIPLE_ONE, main # 三带二 if n 5 and counts [3, 2]: main get_main_value(counter, 3) return CardType.TRIPLE_PAIR, main # 顺子 if is_straight(cards): return CardType.STRAIGHT, max(cards) # 连对 if is_straight_pair(cards): return CardType.STRAIGHT_PAIR, max(values) # 飞机相关 plane_result find_plane(cards) if plane_result is not None: return plane_result # 四带二一个四张 两张任意单牌 if n 6 and counts[0] 4: main get_main_value(counter, 4) return CardType.FOUR_TWO, main # 四带两对一个四张 两个对子 if n 8 and counts [4, 2, 2]: main get_main_value(counter, 4) return CardType.FOUR_TWO_PAIR, main return None def can_beat(new_cards, old_cards): result_new judge_type(new_cards) result_old judge_type(old_cards) if result_new is None or result_old is None: return False new_type, new_main result_new old_type, old_main result_old if new_type CardType.ROCKET: return True if old_type CardType.ROCKET: return False if new_type CardType.BOMB: if old_type CardType.BOMB: return new_main old_main return True if old_type CardType.BOMB: return False if new_type ! old_type: return False return new_main old_main def can_play(hand, chosen): hand_counter Counter(hand) chosen_counter Counter(chosen) for v, cnt in chosen_counter.items(): if hand_counter.get(v, 0) cnt: return False, 牌不在手牌中 return True, OK def print_type(card_strs): cards parse_cards(card_strs) result judge_type(cards) if result is None: print(card_strs, 无法识别牌型) else: card_type, main result print(card_strs, , card_type, 主牌, main) if __name__ __main__: print_type([3, 4, 5, 6, 7]) print_type([3, 3, 4, 4, 5, 5]) print_type([8, 8, 8, 5]) print_type([J, J, J, Q, Q, Q]) print_type([SMALL, JOKER]) print_type([9, 9, 9, 9]) print( * 40) old parse_cards([8, 8, 8, 5]) new parse_cards([9, 9, 9, 6]) print(能否压过上一手:, can_beat(new, old))4.3 运行与验证运行命令python3 dou_dizhu_cards.py预期输出类似[3, 4, 5, 6, 7] CardType.STRAIGHT 主牌 7 [3, 3, 4, 4, 5, 5] CardType.STRAIGHT_PAIR 主牌 5 [8, 8, 8, 5] CardType.TRIPLE_ONE 主牌 8 [J, J, J, Q, Q, Q] CardType.PLANE 主牌 12 [SMALL, JOKER] CardType.ROCKET 主牌 17 [9, 9, 9, 9] CardType.BOMB 主牌 9 能否压过上一手: True从结果能看到J Q 连着的两组三张被识别成了飞机主牌取最大的一组三张牌值 12。三带一的主牌是 8 而不是 5这样在比较时才会正确三个 9 带一个 6 能压过三个 8 带一个 5。4.4 结果说明上面的代码验证了几个关键场景顺子3 到 7长度 5识别为 STRAIGHT主牌 7。连对33 44 55识别为 STRAIGHT_PAIR主牌 5。三带一888 带 5识别为 TRIPLE_ONE主牌 8。纯飞机JJJ QQQ识别为 PLANE主牌 Q。火箭小王大王识别为 ROCKET炸弹9999识别为 BOMB。比较三张 9 带 6 能压过三张 8 带 5因为主牌 9 8。实际开发时你还会遇到很多边界输入比如 A2345、双王带其他牌、飞机带对与四带两对的歧义。这些都要根据策划规则单独写测试用例。5. 常见问题与排查思路5.1 A 的顺子边界很多新手会把 A 放在顺子末尾代码里出现了[10, J, Q, K, A]这是合法的。但有人会把 A 放在顺子开头比如[A, 2, 3, 4, 5]这是非法的因为 2 不能入顺。排查的方法是统一检查顺子的最大值和最小值。本文代码里用values[-1] 14排除 2这个逻辑已经够用了。如果你的项目允许“A 可以当作最大也可以当作最小”的玩法就需要额外加配置项否则不建议自己改边界。5.2 2 能不能进飞机飞机规则与顺子类似组成飞机的三张牌不能包含 2 和王。也就是说222 333 不符合飞机要求222 也不能作为“三张组”参与飞机。文章代码中的usable_triples [v for v in values if v 14 and counter[v] 3]就是干这件事的。如果你发现某个牌组被误判为飞机优先检查牌值编码是否正确尤其是 2 是否被映射成了 15。5.3 四带二与炸弹优先级四带二包含四张相同牌但它不是炸弹。出牌时必须先判炸弹再判四带二。问题是如果代码先看到了四张相同牌就返回炸弹那么四带二就永远识别不出来。本文代码的顺序是先判火箭再判单张、对子、三张、炸弹最后用n 6 and counts[0] 4识别四带二。这保证了炸弹优先同时不会丢失四带二。如果你把四带二判断放在炸弹之前对局中就会出现把炸弹当四带二出的严重事故。5.4 输入无序导致的统计错误前端传上来的牌组可能是乱序的比如玩家点了 5、8、5、8代表 55 和 88但顺序是乱的。识别算法不应该依赖输入顺序所以所有函数在内部都对牌值做了排序或使用 Counter 统计。如果你在写 Java 版本时用了List.equals直接比较顺序就会出现“同一手牌换个顺序就识别失败”的问题。正确做法是先排序或者转成数量映射再比较。问题现象常见原因解决思路顺子识别失败2 被当成顺子成员检查排序后最大值是否大于 14三带一误判为炸弹四张相同牌炸弹判断放在三带一之前四带二识别不出来只判断了炸弹增加四带二独立分支同一手牌换顺序结果不同直接比较原始顺序先排序或使用统计结构飞机带对识别错误对子数量与飞机组数不一致用剩余牌数量和对子数量双重校验6. 最佳实践与工程建议6.1 服务端权威校验任何棋牌类游戏的出牌逻辑都应该以服务端为准。客户端可以做提示、高亮、预判但最终能不能出必须由服务端重新计算一次。服务端校验还意味着不要相信客户端传来的“牌型”字段。客户端可以只传牌面数组服务端自己解析牌型。这样即使客户端被篡改服务端依然能挡住非法出牌。操作上建议把judge_type和can_beat做成无状态工具模块方便单元测试。6.2 牌型判定的防作弊与公平性牌型识别本身不复杂复杂的是对抗作弊。常见作弊行为包括客户端改包、后台模拟点击、插件自动出牌。服务端要做的是记录每一手牌的原始数据和时间戳。在关键操作前进行牌权校验防止越权出牌。对异常高频操作做风控告警。所有随机发牌逻辑必须在服务端生成随机种子不能依赖客户端。需要强调的是本文示例只是正常的规则引擎开发不涉及任何外挂和非法工具。正规游戏开发中这些校验都是为了保证玩家之间有公平对局。6.3 数据协议与版本兼容“新 12”这类黑话放到工程里其实就是新版本协议。客户端升级之后牌型规则可能变化比如新增一种玩法、调整飞机带牌规则。这要求服务端协议设计里带版本号而不是直接覆盖旧逻辑。建议的做法牌型枚举中新增字段时不要修改已有枚举值。房间配置中增加规则版本字段比如rule_version12。服务端根据房间版本选择不同的牌型解析器。日志中记录每局使用的规则版本方便回溯。这样线上出现“旧客户端打出的牌在新规则下不合法”的时候可以通过版本号快速定位而不是把所有问题都归咎于前端。6.4 日志、监控与线上兜底牌型判定是高频逻辑一局游戏可能调用几十次。线上要关注接口耗时如果牌型解析在 1 毫秒以上基本是代码有重复遍历需要优化。异常输入比例统计无法识别牌型的出牌次数如果比例突然升高可能是有玩家在尝试绕过校验。弃牌率异常机器人自动出牌可能产生大量非法出牌请求这些请求会暴露在风控日志中。每一次出牌都可以打印结构化日志包含房间号、用户ID、手牌剩余数、目标牌组、判定结果。排查问题时这些日志比任何“感觉”都可靠。7. 总结与下一步学习路线本文从一句斗地主直播黑话开始落到棋牌游戏开发最核心的牌型识别模块。你读完应该已经掌握了几个关键点牌值编码设计、牌型枚举、单张/对子/三带/炸弹判断、顺子与连对校验、飞机带牌逻辑、牌型比较以及一个可以运行的最小示例。下一步建议你把代码迁移到自己的主力语言中。如果项目用 Java可以尝试把Counter换成HashMapInteger, Integer枚举换成本地 Java 枚举再写一组 JUnit 测试覆盖边界场景A2345、双王炸、四带二、飞机带单。另一个值得深入的方向是“手牌推荐算法”给出当前手牌和上家牌型自动找出所有能压过的牌组。这本质上是把牌型枚举和组合搜索结合起来比单纯判型更有挑战也很适合作为个人项目练手。最后开发这类功能时要保持一个底线只做正规的休闲游戏业务分析对局数据、保证公平性、完善规则校验远离任何灰产和作弊工具。牌型算法本身不难难的是在一套可靠的服务端体系里把它放对位置。希望本文能给你一个清晰的起点。