基于状态机模型的斗地主游戏(NodeJs&SocketIO)

NodeJs 实现斗地主游戏

1. 系统结构

系统考虑使用Nodejs和SocketIo实现服务器端逻辑,前端使用HTML5。

2. 逻辑流程

1 . 主要逻辑包括用户进入游戏、等待对家进入游戏、游戏过程、结束统计这4个过程。

2 . 游戏过程的逻辑具体如下

3 . 服务器-客户端通讯逻辑如下

3. 客户端界面设计

1 . 登录界面

2 . 发牌界面

<img src="http://upload-images.jianshu.io/upload_images/3120669-eb47262f595a9382.gif?imageMogr2/auto-orient/strip" width="500">

4. 数据结构

4.1 牌型

为了便于计算,使用一维数组定义每张扑克的index,根据图中顺序,按从左到右以及从上到下递增(即左上角的红桃A为0,右上角的红桃K为12,方块A为13,以此类推)

<img src="http://upload-images.jianshu.io/upload_images/3120669-d4a8e8c067b0f6ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" width="500">

4.2 出牌规则

  • 牌的大小顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3。
  • 牌形分为:单张、 一对、 三张、姐妹对(两张三张都可以连接,且连接数量无限)、顺子(数量无限制)、炸弹(不能4带1):
  • 除了炸弹以外,普通牌形不允许对压,相同牌形只有比它大的才能出。
  • 炸弹任何牌形都能出,炸弹的大小为:天王炸,2,A,K,Q,J,10,9,8,7,6,5,4,3。

4.3 比较大小

根据牌型用整数定义扑克的数值大小

  • 从3到K对应的value为2到12
  • A对应13
  • 2对应14
  • 大小王对应16与15

5. 系统模块设计

5.1 出牌对象

var MODAL;
$(init);
function init() {
    new modal();
    //绑定页面上的出牌按钮,根据当前不同的状态运行不同的函数
    $("body").on("click","#sendCards",statusMachine);
}
function statusMachine() {}
var modal = function () {
    var ptrThis;
    var modalBox = {
        //出牌对象的数据
        default:{
            //cards存储服务器发送过来的扑克数组
            cards:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,
            38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53],
            //当前游戏的状态,有DISCARD(发牌),WATING(等待),GAMEOVER(游戏结束)三个状态
            status:"",
            //myIndex为玩家所处于的座位的座位号
            myIndex:0,
            //leftIndex为位于进行游戏的玩家左边的玩家的座位号
            leftIndex:0,
            rightIndex:0,
            //turn与座位号对应,turn表示由对应的座位号玩家进行操作(例如发牌,放弃)
            turn:0,
            //若有两位玩家放弃出牌,则第三位玩家必须出牌,用于标志新的出牌回合的开始
            disCardTrue:false,
            //记录前一位玩家所处的牌,用于实现压牌逻辑
            formercardsType:{}
        },
        //$goal为待插入扑克的jquery对象,cardArray为扑克数组,isDelay为true则延迟插入(隔0.3s插入一张牌)
        placeCards:function ($goal,cardArray,isDelay) {},
        //sort函数所用到的比较函数,a,b都为扑克的index,将扑克按照value从大到小降序排列,value相同则按照花色排序
        comp:function (a,b) {},
        //变换当前扑克牌的状态,未选取->选取,选取->未选取
        toggleCard:function ($this) {},
        //将服务器发送的无序数组按照一定规则进行排序
        cardsSort:function (cards) {},
        //将已被选中并发送的扑克牌从手牌中删除
        removeCards:function () {},
        //判断从服务器发送扑克牌数组是由谁发出的,调用placeCards函数插入扑克
        //turn设置为下一位玩家,根据turn设置status
        //如果扑克牌已被出完,则根据最后一位出牌人来判断当前玩家是胜利还是失败
        justifyWhich:function (obj) {},
        //收到来自服务器转发的某一位玩家发送的投降信息
        someOneTouXiang:function (seats) {},
        //清空玩家发送的扑克
        clearCards:function () {},
        //绘制左右两位玩家的界面,objLeft为左边的玩家的信息,objRight同上
        drawothers:function (objLeft,objRight) {},
        //绘制玩家的界面,包含手牌,obj为相关信息
        drawuser:function (obj) {},
        //向目标jquery对象插入图片,$this为目标jquery对象,obj为相关信息(例如图片路径)
        insertImg:function ($this,obj) {},
        //移除目标jquery对象的图片,$this为目标jquery对象
        removeImg:function ($this) {},
        //开始游戏,seats为服务器发送过来的座位对应着的用户的信息,turn指定座位下标为turn的用户先出牌(turn由服务器的随机数产生)
        //存储服务器发送过来的扑克牌数组,调用cardsSort,drawothers,drawuser,placeCards,initPlay
        startGame:function (seats,turn) {},
        //出牌前的逻辑判断,判断牌是否能压过上家或者是否符合逻辑
        preSend:function () {},
        //在status为WATING时点击出牌调用的函数
        notYourTurn:function () {},
        //压牌逻辑的实现,temp存储着牌型,牌的值和牌的数量
        compWhichLarger:function (temp) {},
        //绑定座位点击坐下事件
        init:function () {},
        //游戏结束正常调用end函数,isWin为true则该玩家胜利
        end:function (isWin) {},
        //重开一局,array为来自服务器的扑克牌数组,turn为先出牌的人
        reStart:function (array,turn) {},
        //切换准备按钮的状态,准备->取消,取消->准备
        readyGame:function () {},
        //游戏结束
        gameover:function (isWin) {},
        //放弃出牌
        giveUp:function () {},
        //放弃出牌得到服务器回应
        giveUpReply:function (giupCount) {},
        //绑定一系列点击事件
        initPlay:function () {}
    }
    MODAL = modalBox;
    return modalBox.init();
}

5.2 出牌流程

//出牌按钮绑定状态机,根据当前状态运行对应的函数,只有在处于DISCARD状态才能正常发牌
$("body").on("click","#sendCards",statusMachine);
function statusMachine() {
    switch(MODAL.default.status){
        case "DISCARD":
            //运行至preSend函数
            MODAL.preSend();
            break;
        case "WAITNG":
            MODAL.notYourTurn();
            break;
        case "GAMEOVER":
            MODAL.readyGame();
        default:
            break;
    }
}
var modalBox = {
        preSend:function () {
            var array  = new Array();
            //将被选择(用select来标识)的扑克牌的下标取出,插入数组array中
            $(".cardsLine .card").each(function () {
                if($(this).hasClass("select")){
                    array.push($(this).attr("index"));
                }
            });
            //compCards函数参数为排过序的array,因为用户手牌已经按照一定顺序排过序,所以按照一个方向取出来的牌也是具有一定是有序列的
            var temp = compCards(array);
            //console.log(compCards(array));
            //console.log(temp);
            //disCardTrue为true标识之前已经有两个人放弃出牌,所以不需要考虑压牌,只需要牌型符合一定规则即可出牌
            if(MODAL.default.disCardTrue){
                if(temp.type!="ERR"){
                    socketFun.sendCards(array);
                }else{
                    alert("无法出牌");
                }
            }else{
                //temp为储存array牌型以及大小等数据的对象,compWhichLarger函数则是将temp与上一位玩家发的牌进行比较,如果大于则flag为true
                var flag = ptrThis.compWhichLarger(temp);
                if(flag){
                    //将array发送至服务器,如果服务器将接受成功的消息发回,则调用 justifyWhich函数
                    socketFun.sendCards(array);
                }else{
                    alert("无法出牌");
                }
            }
            //ptrThis.sendCards();
        },


        justifyWhich:function (obj) {//ojb为服务器发送的消息,包含发牌人,发的牌的信息
            if(obj.posterIndex!=MODAL.default.myIndex){
                 //如果是别人出的牌,则储存该牌型
                MODAL.default.formercardsType=compCards(obj.array);
            }
            MODAL.default.disCardTrue = false;
            var $goal;//$goal为待渲染的部位


            switch(obj.posterIndex){
                case MODAL.default.myIndex:
                    ptrThis.removeCards();
                    $goal = $(".showCardLine");
                    break;
                case MODAL.default.leftIndex:
                    $goal = $(".leftPlayer").children(".otherCards");
                    break;
                case MODAL.default.rightIndex:
                    $goal = $(".rightPlayer").children(".otherCards");
                    break;
                default:
                    break;
            }

            ptrThis.placeCards($goal,obj.array,false);
            //进入下一回合,轮次加一
            MODAL.default.turn = (MODAL.default.turn+1)%3;
            console.log("Now turn is"+MODAL.default.turn);
            //设置下一回合该玩家是出牌还是等待
            if(MODAL.default.turn==MODAL.default.myIndex){
                MODAL.default.status = "DISCARD";
            }else{
                MODAL.default.status = "WAITNG"
            }
            //如果某一位玩家出完牌,则游戏结束
            if(obj.sendOut){
                if(obj.posterIndex==MODAL.default.myIndex){
                    ptrThis.end(true);
                }else{
                    ptrThis.end(false);
                }

            }
        }
}

5.3 客户端SocketIO消息模型

var socket = io.connect('http://localhost:3000');
var X = window.scriptData;                          //截取服务器发送过来的数据
    //收到服务器发送的不同的消息类型,调用对应的出牌模型中的函数
    socket.on("connect",function () {
        socket.emit("addUser",X._id);                   //添加用户
    })
    socket.on("playerSit",function (obj) {
        MODAL.insertImg($(".seat").eq(obj.index).children(),obj);
    })
    socket.on("leave",function (index) {
        MODAL.removeImg($(".seat").eq(index).children());
    })
    socket.on("seatsInfo",function (obj) {
        console.log("seatsInfo"+obj);
        for(var key in obj){
            console.log(key);
            MODAL.insertImg($(".seat").eq(obj[key].index).children(),obj[key]);
        }
    })
    socket.on("gameStart",function (obj,turn) {//服务器通知玩家游戏开始
        MODAL.startGame(obj,turn);
    })
    socket.on("postCards",function (obj) {//服务器返回出牌人以及出牌信息
        MODAL.justifyWhich(obj);
    })
    socket.on("reStart",function (array,turn) {//服务器返回重新开始游戏的信息
        MODAL.reStart(array,turn);
    })
    socket.on("giveup",function (giupCount) {//服务器返回放弃信息
        MODAL.giveUpReply(giupCount);
    })
    socket.on("renshu",function (seats) {
        MODAL.someOneTouXiang(seats);
    })
var socketFun = {
    //出牌对象通过socketFun调用相关函数与服务器通信
    sit:function ($this) {
        var obj = {
            id:X._id,
            index:$this.parent().index()
        }
        socket.emit("sitSeat",obj);
    },
    sendCards:function (array) {
        var sendOut;
        if(($(".cardsLine .cards").children().length-array.length)==0){
            sendOut = true;
        }else{
            sendOut = false;
        }
        var obj = {
            array:array,
            posterIndex:MODAL.default.myIndex,
            sendOut:sendOut
        }
        socket.emit("postCards",obj);
    },
    readyMsg:function (obj) {//告知服务器该玩家准备
        socket.emit("readyMsg",obj);
    },
    giveUp:function () {//告知服务器放弃出牌
        socket.emit("giveup");
    },
    touxiang:function (index) {//告知服务器该玩家投降
        socket.emit("touxiang",index)
    }

}

5.4 压牌逻辑
根据牌型数组判断牌型的逻辑使用状态机实现,其状态迁移图如下:

008.png
function compCards(array) {
    if(array.length==2&&data[array[0]].value==16&&data[array[1]].value==15){//天王炸
          var         cardsType={
                            count:array.length,
                            type:"KINGBOMB",
                            value:data[array[0]].value
                        };
           return cardsType;
    }
    //ptr指向array的下标
    var ptr;
    //end标志状态机是否结束
    var end = false;
    //data存储着每一张扑克的value,避免多次运算value
    var box = {
        cardsType:{
            count:array.length,
            type:"ONE",
            value:data[array[0]].value
        },
        setType:function (type) {
            this.cardsType.type = type;
        },
        statusOne:function () {
            if(this.cardsType.count==1){
                end = true;
                return ;
            }
            if(data[array[0]].value==data[array[1]].value){          //如果第一个和第二个数字相同
                this.setType("TWO");
                return ;
            }
            if(data[array[0]].value==data[array[1]].value+1){
                this.setType("STRAIGHT");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusTwo:function () {
            if(this.cardsType.count==2){
                end = true;
                return ;
            }
            if(data[array[1]].value==data[array[2]].value){
                this.setType("THREE");
                return ;
            }
            if(data[array[1]].value==data[array[2]].value+1){
                this.setType("TWO-ONE");
            }else{
                this.setType("ERR");
            }

        },
        statusThree:function () {
            if(this.cardsType.count==3){
                end = true;
                return ;
            }
            if(data[array[2]].value==data[array[3]].value){
                this.setType("BOMB");
                return ;
            }
            if(data[array[2]].value==data[array[3]].value+1){
                this.setType("THREE-ONE");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusStraight:function () {
            if(this.cardsType.count< 5){
                this.setType("ERR");
                end = true;
                return ;
            }
            if(ptr< this.cardsType.count-1){
                if(data[array[ptr]].value!=data[array[ptr+1]].value+1){
                    this.setType("ERR");
                    end = true;
                    return ;
                }
            }else{
                end = true;
                return ;
            }
        },
        statusTwoOne:function () {
            if(ptr==this.cardsType.count-1){                //TwoOne处于中间状态,结束则出错
                this.setType("ERR");
                return ;
            }
            if(data[array[ptr]].value==data[array[ptr+1]].value){
                this.setType("TWO-TWO");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusTwoTwo:function () {
            if(ptr==this.cardsType.count-1){
                end = true;
                return ;
            }
            if(data[array[ptr]].value==data[array[ptr]].value+1){
                this.setType("TWO-ONE");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusThreeOne:function () {
            if(ptr==this.cardsType.count-1){
                this.setType("ERR");
                return ;
            }
            if(data[array[ptr]].value==data[array[ptr+1]].value){
                this.setType("THREE-TWO");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusThreeTwo:function () {
            if(ptr==this.cardsType.count-1){
                this.setType("ERR");
                return ;
            }
            if(data[array[ptr]].value==data[array[ptr+1]].value){
                this.setType("THREE-THREE");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusThreeThree:function () {
            if(ptr==this.cardsType.count-1){
                end = true;
                return ;
            }
            if(data[array[ptr]].value==data[array[ptr+1]].value+1){
                this.setType("THREE-ONE");
            }else{
                this.setType("ERR");
            }
            return ;
        },
        statusBomb:function () {
            if(ptr==this.cardsType.count-1){
                end = true;
                return ;
            }
            if(data[array[ptr]].value!=data[array[ptr+1]].value){
                this.setType("ERR");
            }
        },
        ERR:function () {
            end = true;
            return ;
        }
    };
    for(ptr = 0;ptr< box.cardsType.count;++ptr){
        console.log("END:"+end);
        console.log(box.cardsType);
        if(end){

            break;
        }

        switch(box.cardsType.type){
            //ONE表示单张牌,这个ONE状态结束有效
            case "ONE":
                box.statusOne();
                break;
            //TWO表示一对,结束有效
            case "TWO":
                box.statusTwo();
                break;
            //THREE表示三张一样的牌,结束有效
            case "THREE":
                box.statusThree();
                break;
            //STRAIGHT表示顺子,根据array长度判断是否有效
            case "STRAIGHT":
                box.statusStraight();
                break;
            //TWO-ONE表示形如xx(x+1)(x+1)(x+2)的牌型,结束无效,返回类型ERR
            case "TWO-ONE":
                box.statusTwoOne();
                break;
            case "TWO-TWO":
            //TWO-TWO表示形如xx(x+1)(x+1)(x+2)(x+2)的牌型,结束有效
                box.statusTwoTwo();
                break;
            //THREE-ONE表示形如xxx(x+1)(x+1)(x+1)(x+2)的牌型,结束无效,返回类型ERR
            case "THREE-ONE":
                box.statusThreeOne();
                break;
            //THREE-TWO表示形如xxx(x+1)(x+1)(x+1)(x+2)(x+2)的牌型,结束无效,返回类型ERR
            case "THREE-TWO":
                box.statusThreeTwo();
                break;
            //THREE-THREE表示形如xxx(x+1)(x+1)(x+1)(x+2)(x+2)(x+2)的牌型,结束有效
            case "THREE-THREE":
                box.statusThreeThree();
                break;
            //BOMB表示炸弹,返回有效
            case "BOMB":
                box.statusBomb();
                break;
            //ERR表示牌型不合逻辑,无效
            case "ERR":
                box.ERR();
                break;
        }
    }
    return box.cardsType;

}

详细代码见GITHUB的pokepoke项目

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 两人斗地主 一、体系结构图 通讯模型 大功能模块切换关系 二、逻辑流程图 登录login.PNG 主页面开始界面....
    wuyumumu阅读 445评论 0 0
  • 斗地主,绝对是一门技术活,高情商,也会给这种技术涡轮增压。 如果你是一名“斗地主”扑克游戏爱好者,如果你想斗地主...
    公子韬韬阅读 9,384评论 11 39
  • 1.体系结构图 2.逻辑流程图 2.1简易流程图 2.2详细流程图 3.服务器-客户端通讯图 4.数据结构 4.1...
    Zoemings阅读 767评论 0 1
  • 知行易难,行胜于言。 有志之人立长志,无志之人常立志~说一句话是多么的简单,而成一件事,却是多么的困难,毕竟,海口...
    转眼泪已倾城阅读 327评论 0 0
  • 我是在丝绸之路上与你刚刚遇见两千年之后我们依然牵着手在时光的马匹上策马奔腾 因为你的降临满天星空拥抱着我的身躯在这...
    浪平阅读 321评论 7 8