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