基於狀態機模型的鬥地主遊戲(NodeJs&SocketIO)

1. 系統結構

系統考慮使用Nodejs和SocketIo實現服務器端邏輯,前端使用HTML5。
前端

2. 邏輯流程

1 . 主要邏輯包括用戶進入遊戲、等待對家進入遊戲、遊戲過程、結束統計這4個過程。 jquery

2 . 遊戲過程的邏輯具體以下git

3 . 服務器-客戶端通信邏輯以下github

3. 客戶端界面設計

1 . 登陸界面數組

2 . 發牌界面服務器

4. 數據結構

4.1 牌型 數據結構

爲了便於計算,使用一維數組定義每張撲克的index,根據圖中順序,按從左到右以及從上到下遞增(即左上角的紅桃A爲0,右上角的紅桃K爲12,方塊A爲13,以此類推)併發

>socket

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 壓牌邏輯
根據牌型數組判斷牌型的邏輯使用狀態機實現,其狀態遷移圖以下:

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項目