#推荐
红中房卡麻将游戏开发完整教程:从零搭建可商用镜像麻将房卡系统

2026-08-08 13,462

这两年房卡模式的麻将特别火,尤其是红中麻将,玩法简单刺激,适合亲友局。最近不少朋友问我镜像房卡麻将怎么做——就是那种一个玩家开房生成房号,其他人输入房号加入的模式,不需要匹配、不需要大厅,干净利落。

今天就把这套系统的核心代码分享出来,从后端到前端的关键逻辑都讲清楚。我尽量少废话,多贴代码,毕竟咱搞开发的看代码比看文字舒服。

一、房卡模式的核心设计思路

镜像麻将的精髓在于“房卡即房间”。玩家A创建房间后获得一个数字房号(比如6位数字),发给微信好友,好友在APP里输入房号直接进入,不需要通过大厅匹配。整个流程背后的技术点有三个:房间生命周期管理房号生成与校验断线重连的状态保持

我用Go语言写服务端,WebSocket做长连接通信,房间状态全部存内存里(用sync.Map保证并发安全),定期把非活跃房间持久化到Redis防止服务器重启丢数据。客户端用LayaAir,方便以后出H5版本。

红中房卡麻将游戏开发完整教程:从零搭建可商用镜像麻将房卡系统

二、服务端核心代码实现

1. 房间管理器:创建房间与房号生成

房间号不能简单自增,容易被遍历。我用了一个6位随机数,生成后查重,循环直到不重复。同时房间对象用读写锁保护,因为在牌局中出牌、摸牌操作频繁,不能一把大锁卡住所有协程。

package room

import (
    "sync"
    "time"
    "math/rand"
)

type Room struct {
    ID            string        // 6位房号
    CreatorUID    string        // 房主ID
    Players       [4]*Player    // 固定4个座位
    Status        int           // 0等待中 1游戏中
    CreateTime    time.Time
    mu            sync.RWMutex
    Game          *MahjongGame  // 当前牌局对象
}

type RoomManager struct {
    rooms sync.Map  // map[string]*Room
}

// 创建新房间
func (rm *RoomManager) CreateRoom(uid string) (*Room, error) {
    roomID := generateRoomID()
    // 保证不重复
    for {
        if _, ok := rm.rooms.Load(roomID); !ok {
            break
        }
        roomID = generateRoomID()
    }
    room := &Room{
        ID:         roomID,
        CreatorUID: uid,
        Status:     0,
        CreateTime: time.Now(),
    }
    room.Players[0] = &Player{UID: uid, SeatID: 0, Ready: true}
    rm.rooms.Store(roomID, room)
    return room, nil
}

// 生成6位房间号
func generateRoomID() string {
    const digits = "0123456789"
    b := make([]byte, 6)
    for i := range b {
        b[i] = digits[rand.Intn(len(digits))]
    }
    return string(b)
}

// 玩家加入房间
func (rm *RoomManager) JoinRoom(roomID, uid string) (*Room, int, error) {
    val, ok := rm.rooms.Load(roomID)
    if !ok {
        return nil, -1, errors.New("房间不存在")
    }
    room := val.(*Room)
    room.mu.Lock()
    defer room.mu.Unlock()
    if room.Status != 0 {
        return nil, -1, errors.New("游戏已开始,无法加入")
    }
    // 找空位
    for i, p := range room.Players {
        if p == nil {
            room.Players[i] = &Player{UID: uid, SeatID: i}
            return room, i, nil
        }
    }
    return nil, -1, errors.New("房间已满")
}

2. 红中麻将胡牌算法——癞子(万能牌)处理

红中麻将的特色是“红中”可以当作任何牌。这导致标准胡牌判定要加癞子处理逻辑。我写了一个通用的判断函数,先把手牌里所有红中剔除出来,剩余牌递归拆分成顺子、刻子、对子,缺的部分用红中补足。

// 手牌表示为牌值数组,红中定义为35
const HongZhong = 35

// 判断能否胡牌,返回true,同时返回胡牌牌型拆解
func CanWin(handTiles []int, laizi int) (bool, [][]int) {
    // 去掉癞子
    normalTiles := []int{}
    laiziCount := 0
    for _, t := range handTiles {
        if t == laizi {
            laiziCount++
        } else {
            normalTiles = append(normalTiles, t)
        }
    }
    // 对普通牌排序
    sort.Ints(normalTiles)
    // 尝试所有可能的将牌(对子)
    return tryWin(normalTiles, laiziCount)
}

func tryWin(tiles []int, laiziCount int) (bool, [][]int) {
    if len(tiles) == 0 {
        return laiziCount == 0, nil // 必须没有剩余癞子
    }
    // 尝试取一个对子做将牌
    for i := 0; i < len(tiles)-1; i++ {
        if tiles[i] == tiles[i+1] {
            newTiles := make([]int, len(tiles))
            copy(newTiles, tiles)
            // 移除这对对子
            newTiles = append(newTiles[:i], newTiles[i+2:]...)
            if ok, _ := checkMianZi(newTiles, laiziCount); ok {
                return true, nil
            }
        }
    }
    // 如果癞子足够,也可以拿癞子凑将牌
    if laiziCount >= 2 {
        if ok, _ := checkMianZi(tiles, laiziCount-2); ok {
            return true, nil
        }
    }
    return false, nil
}

// 检查剩余牌和癞子能否全部组成面子(顺子或刻子)
func checkMianZi(tiles []int, laizi int) (bool, [][]int) {
    if len(tiles) == 0 {
        return laizi == 0, nil
    }
    // 尝试用第一个牌组成刻子
    count := 1
    for count < len(tiles) && tiles[count] == tiles[0] {
        count++
    }
    if count >= 3 {
        // 有刻子,移除这三个
        newTiles := make([]int, len(tiles))
        copy(newTiles, tiles)
        newTiles = append(newTiles[:0], newTiles[3:]...)
        if ok, _ := checkMianZi(newTiles, laizi); ok {
            return true, nil
        }
    } else if count == 2 && laizi >= 1 {
        // 两张加一个癞子组成刻子
        newTiles := make([]int, len(tiles))
        copy(newTiles, tiles)
        newTiles = append(newTiles[:0], newTiles[2:]...)
        if ok, _ := checkMianZi(newTiles, laizi-1); ok {
            return true, nil
        }
    } else if count == 1 && laizi >= 2 {
        // 一张加两个癞子组成刻子
        newTiles := tiles[1:]
        if ok, _ := checkMianZi(newTiles, laizi-2); ok {
            return true, nil
        }
    }
    // 尝试组成顺子(仅数字牌且不能跨花色,这里简化判断)
    if tiles[0] < 30 { // 万字、条子、筒子
        // 找 tiles[0]+1 和 tiles[0]+2
        has2 := false
        has3 := false
        idx2, idx3 := -1, -1
        for i := 1; i < len(tiles); i++ {
            if tiles[i] == tiles[0]+1 && !has2 {
                has2 = true
                idx2 = i
            }
            if tiles[i] == tiles[0]+2 && !has3 {
                has3 = true
                idx3 = i
            }
        }
        if has2 && has3 {
            newTiles := []int{}
            for i, t := range tiles {
                if i == 0 || i == idx2 || i == idx3 {
                    continue
                }
                newTiles = append(newTiles, t)
            }
            if ok, _ := checkMianZi(newTiles, laizi); ok {
                return true, nil
            }
        }
        // 缺一张用癞子补的顺子情况,限于篇幅省略…
    }
    return false, nil
}

这段代码虽然简化了花色边界和补癞子的所有组合,但已经把核心思路展示清楚了:先抽将,再递归拆面子。实际项目里还要完善补癞子的多种情况,建议大家自己补齐测试。

3. 游戏流程控制——摸牌、出牌、吃碰杠胡

服务端维护一个状态机:等待出牌→等待碰杠胡响应→摸牌→等待出牌。所有操作通过WebSocket消息驱动。关键是要处理好并发:同一时刻只能一个人操作,其他人收到的消息都是“等待”。我用一个channel串行化每个牌桌的操作,简单粗暴但很可靠。

type GameTable struct {
    ActionChan chan PlayerAction
    // ...
}

func (gt *GameTable) Run() {
    for {
        select {
        case action := <-gt.ActionChan:
            // 验证当前是否轮到该玩家操作
            // 处理出牌/碰/杠/胡,广播结果
            gt.processAction(action)
        }
    }
}

红中房卡麻将游戏开发完整教程:从零搭建可商用镜像麻将房卡系统

三、客户端关键实现——房号输入与牌桌渲染

客户端用LayaAir,界面拼完之后就是对接协议。我封装了一个NetworkManager来管理WebSocket连接,所有消息用protobuf序列化,体积小解析快。

房卡加入的核心就是两步:①发送JoinRoom协议,带上房号;②接收服务端返回的房间信息(座位、玩家头像昵称、准备状态)。进房后自动更新大厅UI,四人到齐且全部准备后,房主点开始,服务端发牌。

牌桌渲染相对复杂:麻将牌是3D模型还是2D图片视需求而定,我们先用2D序列帧,加载快。每个玩家的手牌区域、出牌区域、碰杠牌区域要独立管理,还要处理牌的选中高亮和飞出动画。代码量比较大,这里只贴出手牌排列的核心思路:

// 手牌排序并更新显示
private refreshHandTiles(): void {
    // 先排序
    this.handTiles.sort((a,b)=>a.value - b.value);
    // 清除旧牌显示
    this.handContainer.removeChildren();
    // 重新生成牌对象
    for(let i=0; i<this.handTiles.length; i++){
        let tile = this.createTileSp(this.handTiles[i]);
        tile.x = i * (TILE_WIDTH + GAP);
        // 给中间牌一个向上的偏移,模拟弧形
        let offsetY = Math.abs(i - this.handTiles.length/2) * 2;
        tile.y = offsetY;
        this.handContainer.addChild(tile);
    }
}

红中房卡麻将游戏开发完整教程:从零搭建可商用镜像麻将房卡系统

四、上线前必测的坑

代码跑通只是第一步,这几个地方上线前务必反复测试:

  1. 房号冲突:高并发下两个玩家同时创建房间,随机数碰撞概率虽低但存在,要加二次查重。

  2. 断线后座位丢失:玩家掉线重连后,服务端一定要根据session恢复其座位和手牌信息,否则他进来变成观战了。

  3. 红中百搭的边界情况:手里四张红中,或者红中凑顺子时花色不能跨越(万和条不能混用),算法里要严格限制。

  4. 操作超时处理:玩家迟迟不出牌,服务端要自动打出默认牌,避免卡死。

整套镜像房卡麻将的开发核心就这些了,代码量其实不小,我这篇文章只能摘最关键的部分,完整的项目代码、SQL建表语句、配置文件我都打包好了。老规矩,觉得有用的话点个赞,有问题直接找我聊。

扫描下方二维码加我微信,发“红中源码”。

客服微信二维码
收藏 打赏

感谢您的支持,我会继续努力的!

打开USDT(trc-20)扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (1)

Ts:本站所有内容均为互联网收集整理和网友上传。仅限于学习研究,请必须在24小时内删除。否则由此引发的法律纠纷及连带责任本站概不承担。

如侵犯到您的合法权益,请联系我们删除侵权资源!

韩仔技术 游戏开发 红中房卡麻将游戏开发完整教程:从零搭建可商用镜像麻将房卡系统 https://www.hanzijs.com/zixue/youxi/8639.html

相关文章

发表评论
暂无评论