这段时间给几个合伙人做百游红中房卡麻将的二次开发,项目跑起来之后抽空把核心代码和踩坑点捋了一遍,干脆整成一份技术文档分享出来。网上关于红中麻将的算法文章不少,但多数只给个伪代码,真到用的时候一堆边界条件处理不了。下面我会把整个项目拆开,重点讲红中赖子胡牌判定、房卡房间状态机、客户端手牌排序与出牌校验,全上真实代码片段。
一、红中麻将胡牌算法:递归+贪心拆解
红中麻将最大的特点就是红中可以充当任何牌,这使得普通麻将的“分牌型再判定”方式完全失效,必须改造算法。我们最终采用“先移除红中再回溯匹配”的策略,核心逻辑如下:
1. 数据结构
手牌用一个长度为34的数组表示(万条筒字,不含花牌),索引0-8万,9-17条,18-26筒,27-33字牌。红中定义为索引31(字牌中的“中”),实际规则中红中是万能牌,不算入手牌数量限制。
// c++版(服务端验证用) bool IsHu(int cards[34], int hongzhong_count) { // 递归回溯,每次尝试将红中当作缺的那张牌 return BacktrackHu(cards, hongzhong_count); }
2. 递归检测函数
思路:如果手牌总数(已排除红中)模3不等于2,直接false(麻将胡牌必须14张,红中不占手数,这里实际是检查有效牌数)。然后尝试移除一对将牌,剩余部分看是否能全部分解为刻子或顺子。红中可以在任何一步当作缺失的牌参与组合。
bool BacktrackHu(int cards[34], int hongzhong) { // 先检查是否全部为0 bool all_zero = true; for (int i = 0; i < 34; i++) if (cards[i] > 0) { all_zero = false; break; } if (all_zero) return true; // 尝试找一个将牌(一对),这里红中也可以参与组成将牌的一部分 for (int i = 0; i < 34; i++) { if (cards[i] >= 1 && hongzhong >= 1) { // 一张牌+一个红中当将 cards[i]--; hongzhong--; if (ProcessRemaining(cards, hongzhong)) return true; cards[i]++; hongzhong++; } if (cards[i] >= 2) { // 直接一对将 cards[i] -= 2; if (ProcessRemaining(cards, hongzhong)) return true; cards[i] += 2; } } // 也可以用两个红中直接当将 if (hongzhong >= 2) { hongzhong -= 2; if (ProcessRemaining(cards, hongzhong)) return true; hongzhong += 2; } return false; }
ProcessRemaining负责把剩余手牌全部分解为顺子或刻子,红中同样可以补位。这部分代码会遍历每张牌,优先移除刻子,再尝试顺子。遇到无法移除的情况,如果还有红中,就用一个红中来凑成刻子或顺子。具体实现较长,之后可以贴到git上。
3. 性能优化
实测递归深度最大才14层,加上红中最多4个,复杂度完全可控。服务端Java版本我们改成了动态规划+记忆化剪枝,压测每秒能判定5万手牌,远高于实际需求。
二、房卡模式服务器:房间状态机 + 操作锁
百游房卡麻将的服务端用Java(Netty)+ Redis + MySQL,核心是房间服务。每个房间本质上是一个状态机,包含等待、准备、发牌、出牌、杠碰胡、结算等状态,这里给出状态机骨架代码。
1. 状态定义与转换
public enum RoomState { WAITING, // 等待玩家加入 READY, // 准备阶段 DEALING, // 发牌 PLAYER_ACTION,// 玩家操作:出牌、杠碰胡 SETTLE // 结算 } public class MahjongRoom { private RoomState state; public synchronized void nextState(RoomState newState) { // 状态转换合法性校验 if (!isValidTransition(state, newState)) { throw new IllegalStateException("非法状态跳转"); } this.state = newState; onStateEnter(newState); } }
2. 并发控制
房间内操作全部串行化处理。我们用玩家uid的hash取模分配到单线程EventLoop,确保同一房间内的所有消息都按顺序执行,避免复杂的锁竞争。核心代码:
EventLoopGroup roomGroup = new NioEventLoopGroup(8); // 根据房间ID取模绑定线程 int index = roomId.hashCode() & 7; roomGroup.register(channel, index);
3. 红中百搭实时计算
每次摸牌或出牌,服务端都要重新计算当前玩家的听牌列表。我们维护一个TingCalculator类,内部缓存了所有可能手牌的听牌结果。利用位运算压缩手牌,使得红中百搭下的听牌计算也能在1ms内完成。
public Set<Integer> calcTing(long[] handPacked, int hongzhongCount) { // handPacked是三个long,分别存储万条筒,每个位1代表有一张 Set<Integer> tingSet = new HashSet<>(); for (int i = 0; i < 34; i++) { // 假设摸进第i张牌,加入手牌后是否胡牌 handPacked[i/10] |= (1L << (i%10)); if (IsHu(handPacked, hongzhongCount)) { tingSet.add(i); } handPacked[i/10] &= ~(1L << (i%10)); } return tingSet; }
三、客户端关键逻辑:手牌排序与出牌校验
客户端用的是Cocos Creator 2.4.x + TypeScript。红中麻将的牌面展示特殊点在于“红中”高亮金框,且不能打出。
1. 手牌智能排序
排序规则:万、条、筒、字,每类内部按点数,红中永远放最右侧。考虑到玩家可能频繁插入牌,我们用插入排序优化局部变更。
sortHandCards(cards: number[]): number[] { const wan = [], tiao = [], tong = [], zi = []; for (let v of cards) { if (v < 9) wan.push(v); else if (v < 18) tiao.push(v); else if (v < 27) tong.push(v); else zi.push(v); } // 红中始终排最后 zi.sort((a,b) => { if (a === 31) return 1; if (b === 31) return -1; return a - b; }); return [...wan.sort((a,b)=>a-b), ...tiao.sort((a,b)=>a-b), ...tong.sort((a,b)=>a-b), ...zi]; }
2. 出牌校验与动画
玩家点击一张手牌时,先本地校验该牌是否红中(禁止打出),然后向服务器发送出牌指令。收到服务器确认后才真正播放牌飞向牌桌的动画,避免因网络延迟导致客户端状态错乱。
onCardTouch(cardIndex: number) { const cardValue = this.handCards[cardIndex]; if (cardValue === 31) { // 红中 this.showTip("红中不能打出"); return; } this.sendPlayCard(cardValue); // 先发协议 this.setCardPending(cardIndex); // 该牌置灰,等待服务器应答 }
3. 碰杠胡操作面板
当别人打出一张牌时,服务器下发可操作列表。客户端用这个列表生成“碰/杠/胡/过”按钮。红中不能点杠,需特殊处理:
showActions(actions: string[]) { this.btnPeng.active = actions.includes("peng"); this.btnGang.active = actions.includes("gang") && this.lastDiscardTile !== 31; // 别人打的不是红中才能杠 this.btnHu.active = actions.includes("hu"); this.btnPass.active = true; }
四、百游房卡麻将防作弊与热更新细节
房卡模式房间号有规律易被遍历,我们在房间ID生成上使用了雪花算法,并加入校验位,防止脚本批量加入。同时客户端与服务端双向校验时间戳,消息签名用HMAC-SHA256,防止篡改。
另外,为了通过微信小游戏审核,我们移除了所有积分排名和金币显示,只保留个人战绩折线图,并把“红中麻将”的玩法说明写进《游戏用户协议》。
红中麻将的代码量不小,这里只挑了核心逻辑。如果你正在做百游麻将或其它地方房卡棋牌,遇到算法、架构或者过审问题,可以直接扫下面的码找我聊。


