两人斗地主
一、体系结构图
- 通讯模型
- 大功能模块切换关系
二、逻辑流程图
-
登录
-
主页面
出牌
三、服务器与通信逻辑
- socket原理
- 应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
- 握手协议
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。
四、功能设计与实现
-
牌型数据结构
- 采用精灵图来绘制图片,将牌按照牌面大小存储到数组中去,一共有54张牌,分成5行存储,1-4行每一行都是从A至K,第5行是大王和小王
- 每个元素的第一个数字表示行数,行数用来表示花色,第一行表示红桃,第二行表示方片,第三行表示黑桃,第四行表示梅花。
- 每个元素后面的两个数字表示该牌的面值大小,在整副牌中牌A的大小是12,牌2的大小是13,牌3的大小是1,牌4的大小是2,牌5的大小是3,牌6的大小是4,牌7的大小是5,牌8的大小是6,牌9的大小是7,牌10的大小是8,牌11的大小是9,牌Q的大小是10,牌K的大小是11,另外牌小王的大小是14,牌大王的大小是15。
<pre>
var allpokers=[
112,113,101,102,103,104,105,106,107,108,109,110,111,
212,213,201,202,203,204,205,206,207,208,209,210,211,
312,313,301,302,303,304,305,306,307,308,309,310,311,
412,413,401,402,403,404,405,406,407,408,409,410,411,
515,514
];
</pre>
-
生成牌数组
- 一共两个人,每个人27张牌,随机从给定的数组中获取一张牌,将这张牌的坐标存到该用户牌型的数组中去,然后从总牌中删去取出的牌,给用户生成完27张牌之后,将剩下的27张发给对方,存到对方的牌型数组中去。
<pre>
for(var i = 0; i < 27; i++) {
index=parseInt(Math.random()*allpokers.length);
pokerObj.pokers.push(allpokers[index]);
allpokers.splice(index,1);
}
</pre>
- 一共两个人,每个人27张牌,随机从给定的数组中获取一张牌,将这张牌的坐标存到该用户牌型的数组中去,然后从总牌中删去取出的牌,给用户生成完27张牌之后,将剩下的27张发给对方,存到对方的牌型数组中去。
-
牌型
- 单张:前面提到过,大小顺序从3(最小)到大王(最大);
- 一对:两张大小相同的牌,从3(最小)到2(最大);
- 三张:三张大小相同的牌;
- 顺子:多张连续大小的牌,例如8-9-10-J-Q;
- 连对:连续大小的对子,例如10-10-J-J-Q-Q-K-K;
- 飞机:两对三张大小一样的牌,例如10-10-10-J-J-J;
- 炸弹:四张大小相同的牌,炸弹能盖过除王炸的其他牌型,大的炸弹能盖过小的炸弹;
- 王炸:大王和小王,王炸能盖过所有的牌型。
-
牌型状态机
用户每次出的牌都存到数组中去,然后每次读取数组中的一张牌,来判断牌型的状态
- 压牌
- 将先进来的用户的id设置为0,先进来的用户先出牌,即id为0的用户出牌,id为1表示等待,之后通过每次出完牌之后更改id值来控制出牌顺序。
- 获取用户出的牌以及对方出的牌,通过牌型状态机判断两者的牌型
- 先判断用户选的牌是不是符合出牌规则
- 比较两者牌型以及出牌的长度是否一致,同时比较用户选择的牌的最小值是否大于对方的最小值,如果大于,就可以压牌。
- 判断用户的牌型是不是为FOUR,如果用户的牌型为FOUR,对方的不为FOUR,那么就可以压牌
- 用户的牌型和对方的牌型都为FOUR,比较二者的最小值,如果用户的最小值大于对方的最小值,即可压牌。
- 否则的话就无法出牌
五、系统模块
-
界面绘制
- 前端界面
<div id="box"> <div id="userBox"> <div id="usrImg"> </div> <div id="usrPoker"> </div> </div> <div id="showBox"> <div class="showPoker"></div> </div> <div id="otherShow"> <div class="showPoker"></div> </div> <div id="otherBox"> <div id="otherImg"> <img src="/images/usr.gif"> </div> <div id="otherPoker"> </div> </div> </div> <div id="startBox"> <p id="info">waiting others......</p> <button id="start">开始游戏</button> </div> <div id="alert">您的牌小于上家</div>
-
通过获取每张牌的坐标,进行绘制。
for(var i=0;i<this.num;i++) { z=this.pokers[i]%100; y=(parseInt(this.pokers[i]/100)-1)*120; if(z==12||z==13){ x=(z-12)*90; } else if(z==14){ x=(z-13)*90; } else if(z==15){ x=(z-15)*90; } else{ x=(z+1)*90; } $('#usrPoker').append('<div id="poker'+i+'L"></div>'); $('#usrPoker #poker'+i+'L').css({ left: i*22+'px', backgroundPosition: (0-x)+ 'px ' + (0-y) + 'px' }); }
-
socket通信
js端用户登录之后,在js端触发"login"事件,将用户的id发送到服务器.
<pre>socket.emit("login",uid);</pre>服务器端接收"login"事件,将用户的id存到在线数组中去。
<pre>
socket.on('login', function (usrName) {
if (online.indexOf(usrName) != -1||online.length>=2) {
console.log("existed");
} else {
uid=usrName;
users[usrName] = socket.id;
sockets[socket.id] = socket;
online.push(usrName);
}
});
</pre>js端登录之后进入开始页面,点击开始按钮,触发"start game"事件
<pre>socket.emit("start game",uid,pokerArray); </pre>服务器端获取"start game"事件,两个用户都进入的话,就进行发牌,把牌传回客户端,否则的话用户进入等待状态。
<pre>
socket.on('start game', function (usrId,obj) {
var index;
var pokerObj=obj;
if(online.length==2){
for(var i = 0; i < 27; i++) {
index=parseInt(Math.random()*allpokers.length);
pokerObj.pokers.push(allpokers[index]);
allpokers.splice(index,1);
}
socket.broadcast.emit('draw',pokerObj.pokers);
socket.emit('draw',allpokers);
} else{
//先进来的id设为0
var id=0;
socket.emit('wait',id);
}
});
</pre>-
js端画牌和等待
<pre>//初始化牌,并第一次打牌,id为0的先出牌
socket.on('draw',function (obj) {
$("#startBox").hide();
initOther();
pokerArray.status=PLAY;
pokerArray.pokers=obj;
doFirstPlay();
pokerArray.drawPokers();
});
socket.on('wait',function (id) {
$("#start").hide();
$("#info").show();
pokerArray.id=id;
console.log(id);
pokerArray.status=WAIT;
console.log(pokerArray.id);
});
</pre> js端出牌,出完之后发送到服务器
<pre> socket.emit("play card",uid,pokers);</pre>服务器端将用户出的牌发送给对方的客户端
<pre>
socket.on("play card",function(uid,pokers){
var index=online.indexOf(uid);
if(index!=-1){
socket.broadcast.emit("show card",uid,pokers);
}
});
</pre>对方客户端接受到用户出的牌之后,进行显示,对方收到牌之后,就发送到服务器进行下一次出牌
<pre>
socket.on("show card",function (other,pokers) {
//获取对方出的牌的数组
pokerArray.oppPoker=pokers;
//显示在对方的桌面上
$('#otherShow .showPoker').html("");
for(var i=0;i<pokers.length;i++){
y=(parseInt(pokers[i]/100)-1)120;
z=pokers[i]%100;
if(z==12||z==13){
x=(z-12)90;
} else if(z==14){
x=(z-13)90;
} else if(z==15){
x=(z-15)90;
} else{
x=(z+1)90;
}
$('#otherShow .showPoker').append('<div id="ospoker'+i+'"></div>');
$('#otherShow .showPoker #ospoker'+i).css({
left: i32+'px',
backgroundPosition: (0-x)+ 'px ' +(0-y) + 'px'
});
}
oppPokerLength=oppPokerLength-pokers.length;
initOther(oppPokerLength);
socket.emit('doPlay',pokerArray.id);
});
</pre>服务器判断该谁出牌,然后分别发送给客户端
<pre>
socket.on('doPlay',function (sid) {
var id;
if(sid==0){
id=1;
} else{
id=0;
}
socket.broadcast.emit('startPlay',(1-id));
socket.emit('startPlay',id);
});
</pre>客户端接收到服务器端传来的出牌信息,进行出牌
<pre>
socket.on('startPlay',function(id){
pokerArray.status=PLAY;
pokerArray.id=id;
doFirstPlay();
});
</pre>用户先出完牌,将游戏结束的信息发送到服务器
<pre>socket.emit("Game Over");</pre>服务器将游戏结束的信息发送给双方。
<pre>
socket.on('stop game',function () {
$("#info").text("Game Over");
$("#start").hide();
$("#startBox").show();
});
</pre>客户端接收到服务器端传来的游戏结束。
<pre>
socket.on('stop game',function () {
$("#info").text("Game Over");
$("#start").hide();
$("#startBox").show();
});
</pre>
-
出牌的函数
- 每个用户对象
<pre>
function Poker() {
this.pokers=[]; //用户被分到的牌
this.status=START;//初始化用户的状态为status
this.id=1;//初始化id为1
this.oppPoker=[];//对方每次出的牌
}
Poker.prototype.num = 27;
//初始化画牌
Poker.prototype.drawPokers=function () {
var x,y,z;
this.sort(this.pokers);
for(var i=0;i<this.num;i++) {
z=this.pokers[i]%100;
y=(parseInt(this.pokers[i]/100)-1)120;
if(z==12||z==13){
x=(z-12)90;
} else if(z==14){
x=(z-13)90;
} else if(z==15){
x=(z-15)90;
} else{
x=(z+1)90;
}
$('#usrPoker').append('<div id="poker'+i+'L"></div>');
$('#usrPoker #poker'+i+'L').css({
left: i22+'px',
backgroundPosition: (0-x)+ 'px ' + (0-y) + 'px'
});
}
}
//每次出完牌之后进行重新画牌
Poker.prototype.reDrawPoker=function (poker) {
var x,y;
$('#usrPoker').html("");
this.sort(poker);
for(var i=0;i<poker.length;i++){
y=(parseInt(poker[i]/100)-1)120;
if(poker[i]%100==12||poker[i]%100==13){
x=(poker[i]%100-12)90;
} else if(poker[i]%100==14){
x=(poker[i]%100-13)90;
} else if(poker[i]%100==15){
x=(poker[i]%100-15)90;
}
else{
x=(poker[i]%100+1)90;
}
$('#usrPoker').append('<div id="poker'+i+'L"></div>');
$('#usrPoker #poker'+i+'L').css({
left: i22+'px',
backgroundPosition: (0-x)+ 'px ' + (0-y) + 'px'
});
}
this.pokers = poker;
}
//对牌按照牌的面值大小进行排序
Poker.prototype.sort = function (poker) {
poker.sort(function (a,b) {
a=a%100;
b=b%100;
return a-b;
});
}
</pre> - 函数
- 初始化socket,以及鼠标点击事件
<pre>
function init() {
initSocket();
pokerArray=new Poker();
$('body').on('click',clickMachine);
}
</pre> - 用户状态机
<pre>
function clickMachine(e){
$("#alert").hide();
switch(pokerArray.status){
case START:
doStart();
break;
case PLAY:
//肯定要先选择,才能再出牌
doPlayCard(e.target);
break;
case GAMEOVER:
socket.emit("Game Over");
break;
default:
break;
}
}
</pre> - initOther()初始化对方的牌
<pre>
function initOther() {
$("#otherBox #otherPoker").html("");
for(var i=0;i<oppPokerLength;i++){
$("#otherBox #otherPoker").append('<div id="opoker'+i+'"></div>');
$('#otherBox #otherPoker #opoker'+i).css({
left: i*22+'px'
});
}
}
</pre> - function doFirstPlay() 出牌之后把当前用户的出牌按钮隐藏起来
<pre>
function doFirstPlay() {
if(pokerArray.id==0){
$('#userBox').append(' <div id="pokerBtn"><button id="play">出牌</button><button id="pass">不出</button></div>');
}
}
</pre> - typeMachine(type,n,m)牌型状态机
<pre>
function typeMachine(type,n,m){
switch(type){
case ONE:
if(n==m){
type=TWO;
} else if(n==m-1){
type=TWO_2;
} else if(n==m+1){
type=BIGGEST;
}
else{
type=ERROR;
}
break;
case TWO:
if(n==m){
type=THREE;
} else if(n==m-1){
type=THREE_2;
} else{
type=ERROR;
}
break;
case TWO_2:
if(n==m-1){
type=TWO_2;
} else{
type=ERROR;
}
break;
case THREE:
if(n==m){
type=FOUR;
} else if(n==m-1){
type=FOUR_2;
} else {
type=ERROR;
}
break;
case THREE_2:
if(n==m){
type=TWO;
} else if(n==m-1){
type=THREE_2;
} else{
type=ERROR;
}
break;
case FOUR:
type=ERROR;
break;
case FOUR_2:
if(n=m){
type=FIVE;
} else {
type=ERROR;
}
break;
case FIVE:
if(n==m){
type=SIX;
} else {
type=ERROR;
}
break;
case SIX:
type=ERROR;
break;
default:
break;
}
return type;
}
</pre> - getType(array) 获取用户或者对方的出牌牌型
<pre>
function getType(array) {
var type=ONE;
var fir,next;
for(var i=0;i<array.length-1;i++){
fir=array[i];
next=array[i+1];
type=typeMachine(type,fir,next);
}
return type;
}
</pre> - compare(ownPoker,oppPoker)比较牌型,进行压牌
<pre>
function compare(ownPoker,oppPoker) {
var ownType,oppType;
getValue(ownPoker,oppPoker);
ownType=getType(ownValue);
oppType=getType(oppValue);
console.log(ownType);
if(ownType!=ERROR && oppType!=ERROR){
if(ownType==oppType && ownValue.length==oppValue.length) {
if (ownValue[0] > oppValue[0]) {
return true;
}
} else if(ownType==FOUR && oppType!==FOUR){
return true;
} else if(ownType==FOUR && oppType==FOUR){
if(ownValue[0] > oppValue[0]){
return true;
}
} else if(ownType==BIGGEST){
return true;
} else{
alertMessage="您的牌小于上家!";
return false;
}
} else{
alertMessage="您的牌不符合出牌规则,请重新选择!"
return false;
}
ownValue.splice(0,ownValue.length);
oppValue.splice(0,oppValue.length);
}
</pre> - doSlide(event)选择牌
<pre>
function doSlide(event){
var s=event.id.slice(5,-1);
if($(event).attr('selected')){
$(event).css({bottom: '10px'}).removeAttr('selected');
var index = pokers.indexOf(pokerArray.pokers[parseInt(s)]);
if (index > -1) {
pokers.splice(index, 1);
}
} else {
$(event).css({bottom:'30px'}).attr('selected',true);
pokers.push(pokerArray.pokers[parseInt(s)]);
}
}
</pre> - doPlayCard(event)用户为出牌的状态进行选择牌或者出牌
<pre>
function doPlayCard(event) {
var match;
var value=event.id;
var reg=/L/g;
while(match = reg.exec(value)){
value="NUM";
}
switch(value){
case PLAY:
$('#showBox .showPoker').html("");
//到时候在这边先进行比较,如果大于的话就出牌,否则的话就弹出不出
if(compare(pokers,pokerArray.oppPoker)||pokerArray.oppPoker.length==0){
for(var i=0;i<pokers.length;i++){
y=(parseInt(pokers[i]/100)-1)120;
z=pokers[i]%100;
if(z==12||z==13){
x=(z-12)90;
} else if(z==14){
x=(z-13)90;
} else if(z==15){
x=(z-15)90;
}
else{
x=(z+1)90;
}
$('#showBox .showPoker').append('<div id="spoker'+i+'"></div>');
$('#showBox .showPoker #spoker'+i).css({
left: i32+'px',
backgroundPosition: (0-x)+ 'px ' +(0-y) + 'px'
});
var index = pokerArray.pokers.indexOf(pokers[i]);
if (index > -1) {
pokerArray.pokers.splice(index, 1);
}
}
if(pokerArray.pokers.length==0){
pokerArray.status=GAMEOVER;
pokerArray.reDrawPoker(pokerArray.pokers);
} else{
pokerArray.reDrawPoker(pokerArray.pokers);
socket.emit("play card",uid,pokers);
pokers.splice(0,pokers.length);
$('#userBox #pokerBtn').remove();
}
} else{
console.log(alertMessage);
$("#alert").text(alertMessage);
$("#alert").show();
}
break;
case "NUM":
doSlide(event);
break;
case PASS:
pokerArray.status=PASS;
$('#userBox #pokerBtn').remove();
socket.emit('doPlay',pokerArray.id);
socket.emit('doRetype');
break;
default:
break;
}
}
</pre>
- 初始化socket,以及鼠标点击事件
- 每个用户对象