2023年7月15日发(作者:)
期末作业题目:实现扑克牌的联网游戏
课 程 名 称: 网络游戏开发
学 院:信息工程与自动化学院
专 业: 计算机科学与技术
一、扑克游戏设计介绍
用C++实现斗地主游戏主要有三部分。第一部分是界面设计;第二部分是游戏内核(包括出牌大小、如何过牌、出牌等)设计;第三部分是网络部分。
该游戏由3个人玩,用一副牌,地主为一方,其余两家为另一方,双方对战,先出完牌的一方获胜,出牌规则类似“争上游”。发牌:一副牌,留3张底牌,其它发给3家,底牌加到地主手中。叫牌:叫牌按出牌顺序轮流开始叫牌,每人只能叫一次。叫牌的人为地主,如果都选择不叫,则重新发牌,重新叫地主。出牌:首先将3张底牌交给地主,3张底牌为可见。由地主开始出牌,然后按逆时针顺序依次出牌,轮到用户跟牌时,用户可按右下方“过牌”按钮表示不出,或者按照规则按“出牌”选择符合规则的牌,直至某一方出完牌为止。
牌型说明:1.双王。2.炸弹(四张大小相同的牌)。3.单牌(单张牌)。4.双牌(两张大小相同的牌)。5.三张牌(三张大小相同的牌)。6.三带一手(三张大小相同的牌+1张单牌或一对牌)。7.单顺(5张或更多的连续单牌,不包括2点和双王,不分花色)。8.双顺(3对或更多的连续对牌(不包括2点和双王)。9.三顺(两个或更多的连续“三张牌”,不包括2点和双王)。10.飞机带翅膀(3顺+同数量的一手牌)。11.四带二(4张牌+两手牌)。
牌型比较:双王>炸弹>一般牌型(单牌,对牌,三张牌,三带一手,单顺,双顺,三顺,飞机带翅膀,四带二)。一般牌型:只有牌型且张数相同的牌才可按牌点数比较大小。其中三带一,三带二,飞机带翅膀,四带二组合牌型,比较其相同张数最多的牌点数大小。
游戏术语:地主(叫的一方为地主,可获得底牌)。
游戏规则:只能出大于上家的牌,没有则不出,直到一方出完,则游戏结束。
二、 基于Socket的网络编程
用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不 同应用程序进程间的网络通信和连接。
生成套接字,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。Socket原意是“插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。
Host A上的程序A将一段信息写入Socket中,Socket的内容被Host A的网络管理软件访问,并将这段信息通过Host A的网络接口卡发送到Host B,Host
B的网络接口卡接收到这段信息后,传送给Host B的网络管理软件,网络管理软件将这段信息保存在Host B的Socket中,然后程序B才能在Socket中阅读这段信息。
要通过互联网进行通信,至少需要一对套接字,一个运行于客户机端,称之为Client Socket,另一个运行于服务器端,称之为Server Socket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
Socket编程实现原理(从连接的建立到连接的关闭,每个socket应用都大致包括以下几个基本步骤):
1 .服务器端socket绑定于特定端口,服务器侦听socket等待连接请求;
2 .客户端向服务器和特定端口提交连接请求;
3 .服务器接受连接,产生一新的socket,绑定到另一端口,由此socket来处理和客户端的交互,服务器继续侦听原socket来接受其他客户端的连接请求;
4 .连接成功后客户端也产生一socket,并通过它来与服务器端通讯(注意客户端socket并不与特定端口绑定)。
5 .接下来,服务器端和客户端就通过读取和写入各自的socket来进行通讯。
三、程序总体分析
把程序分为3部分。第一部分是界面;第二部分是游戏内核,用来计算是否可以出牌,由谁出牌,比较所出牌的大小,游戏是否结束等;第三部分是网络部分。
游戏核心
游戏界面
网络
图3-1 游戏交互图
在网络部分,主机和客户机的区别是参数bool m_Isserver,参数bool
m_ready[3]用来记录3个玩家是否准备就绪。Bool m_ready[3]这个参数中有在服务器上才有存在的意义,因为只有服务器才能开始游戏。服务器的玩家是0号,依次加入的分别为1号和2号。
在游戏核心Manager中,Card PlayCards[3][20]记录的是3个玩家的牌,会不断地更新。CardOutCards[20]记录的是已经出掉的牌,一次最多出20张牌。Int OutPlayer表示走牌的玩家的号数,服务器是0号,后面加入的依次是1号,2号玩家。
DoMsg(int num,退出游戏 过牌/出牌/单击牌/放弃地主/接受地主
准备
用户操作
int action)
点击则改变Click属性 Game_state=-1时选地主
PlayCards[x][y].Click
点牌则改 -1 改变 PlayMain, 过牌,直接改变 Game_State CheckCard(int pl) 不能走牌 CardsInfo(Card ca,int&num,int &min,int &type) 这个函数用来得到牌的信息到底是炸弹, 连牌还是其它牌型 改变OutCards和OutPlayer 什么都不做 出牌 PlayMain 改变谁是地主 OutPlayer OutCards 图3-2 游戏流程图 四、 程序详细设计 4.1 游戏界面设计 1.首先用MFC单文本工程向导建立一个工程。2.窗体初始化。3.位图加载。4.读取当前鼠标的坐标,对位置进行大致的确定。 那么主要来看看54张牌的处理。有一种方法是把牌按照黑桃->红桃- >梅花->方块的顺序摆放,每种花色都可以采用升序或者降序排列;而另一种方法就是把牌摆放成一个矩阵,而牌的排列顺序由数据结构中的纸牌类Card来决定。下面是Card类函数声明: Class Card{ Public; Card; Virtual ~Card(); int Num;//牌面数目 2-10 J=11,Q=12,K=13,A=1,Jok(小)=14,Jok(大)=15,大于15表示此牌不存在; int Pow;//牌的实际大小; int Type;//牌的类型,用0代表黑桃、1代表红桃、2代表梅花、3代表方块; Bool Click;//牌是否被选中,选中时true,否则为false; 从上面的函数声明中看出,一张牌的大小、花色是由Num和Type决定(而大小王只需要Num),同时再按照Num和Type对应的数字进行整理,就可以把54张牌集中起来。因为函数中定义的参数不多,而且也比较简单,所以采用按顺序排列的方式进行,而不是用矩阵的方式来排列。 图片加载后,再来看看如何读取鼠标指针的坐标,对位置进行大致确定。而要完成位置的确定就需要确定鼠标位置的坐标,所以引入(point.x,point.y)来确定鼠标的坐标。而这又需要在CprogramView类中添加一个WM_MOUSEMOVE响应。 void CProgramView::OnMouseMove(UNIT nFlags,CPoint point){ //TODO:Add your message handler code here and/or call default //跟踪光标坐标 int mx=point.x; int my=point.y; CString st; CDC *pDc=GetDc(); (”%d,%d”,point.x,point.y); pDc->TextOut(400,5,st); ReleaseDc(pDc); CView::OnMouseMove(nFlags,point); } 4.2 BUTTON控件实现 利用CButton的初始化函数来创建Button: BOOL Create(LPCTSTR lpszCaption,DWORD dwStyle,const RECT &rect,CWND *pParentWnd,UNIT nId) 添加相关的消息响应函数: 在游戏界面中需要有确定、取消、出牌、过牌四个按钮来完成选地主、过牌等动作。BUTTON按钮的创建只需要在旁边的工具箱中进行简单的拖曳就可以了。所以在程序里用BUTTON控件来制作按钮,然后把按钮和函数进行关联就实现了上述四个不同的动作。下面分析四个按钮具体代码: 确定按钮: void CProgramView::OnOK(){ if(_State!=-2) return;//判断是否为主机 if(!pControl->m_Isseverse){ pControl->SendGetReady();//如果不是主机则发送“准备的消息” } else{ pControl->m_ready[1]=true;//如果是主机则准备完毕 //如果三个玩家都准备完毕后 if(pControl->m_ready[0]&&pControl->m_ready[1]&&pControl->m_ready[2]){ pControl->m_ready[0]=false; pControl->m_ready[1]=false; pControl->m_ready[2]=false; art();//游戏开始 pControl->StartCards();//发牌 } } CDC *pDC=GetDC(); ReleaseDC(pDC); } 取消按钮: void CProgramView::OnCancel(){ Exit(0);//退出程序 } 发牌按钮: void CProgramView::OnSendCard(){ if((15,4)//如果DoMsg为TRUE,则需要发生改变{ PrintAll();//重绘 pControl->SendCards(ds,4);//将玩家出牌信息发给其它玩家 过牌按钮 void CProgramView::OnPass(){ if((16,3))//如果DoMsg为TRUE,则需发生改变{ PrintAll();//重绘 pControl->SendCards(NULL,3);//将玩家出的牌发送给其它玩家,此时发送的是空信息 LBUTTONDOWN事件响应: if(mx>=100&&my>=440&&mx<=455&&my<=575){ CDC *pDC=GetDC(); for(i=0;rds[][i].Num<=15;i++);//判断玩家手中还有多少牌 if(mx<=100+(i-1)*15+71&&my<=576&&mx>=100&&my>=440){ i=SelectNum(i,mx,my);//判断点中的是第几张牌 if((i,1)//判断点中这张牌后是否需要改变 PrintAll(); }ReleaseDC(pDC); } 对SelectNum函数做简单解释: SelectNum(int num,int mx,int my)三个变量分别表示当前玩家手中所剩牌的数量,光标的横坐标,光标纵坐标。 4.3 程序相关绘图 在绘制牌的时候可能遇到如下情况: 1.在还没发牌的时候,牌全部集中在中间。2.画作为底牌的三张牌(当在叫地主的时候,牌只显示背面;当确定地主后,作为底牌的三张牌将显示给所有玩家)。3.画出已被打出的牌。4.画出当前玩家手中未出的牌。5.画出从当前玩家视角看到的其它玩家所剩牌的数量。 1.在还没发牌的时候,牌全部集中在中间: void CProgramView::CardReady(){ CDC *pDC=GetDC(); for(int i=0;i<54;i++) pDC->BitBlt(300+i*2,220,72,97,&Mcard,142,384,SRCCOPY);//截取的是牌的背面 ReleaseDC(pDC); } 2.画剩下的三张底牌: void CProgramView::DrawLeft(){ CDC *pDC=GetDC(); int i; if(_State==-1)//当还在选地主的时候{ for(i=0;i<3;i++) pDC->BitBlt(300+i*40,25,72,97,&Mcard,142,384,SRCCOPY);//画的是牌的背面 } else if(_State>=0&&_State<=5)//当确定地主后{ for(i=0;i<3;i++){ if(ft[i].Num<14) pDC->BitBlt(300+i*40,25,71,96,&Mcard,(ft[i].Num-1)*71,ft[i].Type*96,SRCCOPY); else if(ft[i].Num<=15) pDC->BitBlt(300+i*40,25,71,96,&mCard,(ft[i].Num-14)*71,384,SRCCOPY); } } ReleaseDC(pDC); } 3.画出已出的牌: 因程序比较长,所以只做思想介绍。用K来控制画的类型,当K==0表示为左边出牌,K==1时为当前玩家出牌,K==2时为右边出牌,K==3时全部已出的牌都重画。 4.画出当前玩家手中还没出的牌: 程序需要遍历一遍当前玩家手中还没出的牌,当牌被点起后,需要向上突起,用if(rds[Ac][i].Click来实现上面被选中牌的效果。 5.画出从当前玩家视角看到的其它玩家手中剩的牌: for(i=0;i<20&&cardleft[i].Num<=15;i++){ pDC->BitBlt(55,55+i*10,97,75,&Mcard,214,408,SRCCOPY); } for(i=0;i<20&&cardright[i].Num<=15;i++){ pDC->BitBlt(630,55+i*10,97,75,&Mcard,214,408,SRCCOPY); } 在发牌前把牌全部集中在中间是为了让界面更加清晰,以免的发牌的时候因为玩家面前有牌,又有未发完的牌而产生发牌动作的不连续。在发牌结束后将未发的牌存放在Sendleft中以便于在选地主结束后能把剩下的三张牌直接发给地主。画玩家手中未出的牌要遍历所有手中未出的牌然后排序,是为了保持玩家手中牌排序的美观。而遍历和每隔一段时间更新数据在实际的编程中也经常使用,比如进程守护中的两个程序,要不停的遍历进程运行表来获 得对方程序的运行状态。 4.4 动画制作 程序一共有3个地方需要发牌:1.3个玩家都点击了准备,主机发牌并在主机播放发牌动画;2.客户机收到发牌信息,在客户机播放发牌动画;3.最后一名玩家点击确定后播放发牌动画。发牌效果用Windows本身的定时器(Timer)和常用的游戏循环来实现。 void CProgramView::OnTimer(UNIT nIDEvent){ CDC *pDC=GetDC(); Static int i(1); int j; static int x,y; x=300+54*2,y=220; } 在程序中设置间隔为20ms、ID为1的定时器。制作这个定时器的目的是为了保证发牌动作的完整,连续,不会在发牌的间隔停留不一样的时间。 静态变量i用来记录当前牌的步数,设定为10步,即一张牌需要经过10步发到自己的位置。这样做的目的是为了保证所发的牌不会胡乱排放在玩家面前,而使牌按一定的顺序排列。j是一个循环控制变量。x和y用来记录当前牌起飞的左上角的坐标,根据x和y的值可以计算出每一帧牌左上角的坐标。 在发牌的时候要设定一个timer值。这个值的作用是保证每位玩家的牌不超过17张(在三张底牌揭开前)。timer值每加1便发一次牌,由于一副扑克牌总共有54张,而底牌有3张,所以每位玩家在发底牌前牌的张数不能超过17,所以设定timer<18。在绘制动画的过程中采用了局部重画技术。局部重画就是在背景上截一块和想要覆盖的图像大小一样的背景贴在图上。 在每画一张牌的时候把上次画的牌用背景覆盖掉,就形成了一个牌的飞行动画。当所有牌画完后删除定时器。当发牌动画播放完成后,调用PrintAll()进行重绘,交还程序控制权。那么,发牌动画基本制作完成。 void CProgramView::PrintAll(){ CDC *pDC=GetDC(); pDC->BitBlt(0,0,800,600,&Background,0,0,SRCCOPY); if(_State==-2) CardReady(); DrawLeft(); DrawCardOut(3); DrawMyCard(); DrawOtherCard(); PrintState();//按钮重绘 m_date(true); m_date(true); m_date(true); m_date(true); ReleaseDC(pDC); } 五、算法实现 5.1 游戏开始 假设所有人都已经点击准备按钮,服务器会运行GameStart()来初始化游戏,然后把游戏信息发送到每台客户机。 void Managers::GameStart() int i; if(MainComputer) Power=10; OutCards[0].Num=16; PlayerMaininfo=0; SendCard(); for(i=0;i<3;i++) SortCard(PlayCards[i]); PlayerMain=rand()%3; Game_State=-1; 游戏初始化后进入发牌阶段,那么如何实现所发的牌是随机的?这里用一个SendCard来实现随机发牌。 void Managers::SendCard(){ int i,j,k; bool Cards[55]={false}; //对应54张扑克,其中Cards[54]做初始化用,必须为true //其中0~51为4个1~13,52是小王,53是大王 Cards[54]=true; for(i=0;i<3;i++)//每个人17张牌,每次发一张 for(i=0;i<3;i++) Card &Ca=PlayCards[i][j]; k=54; while(Cards[k]) k=rand()%54; 从0~53中随机得到一个数,如果这数已经用过则产生新的随机数;如果没有就把这个数对应的牌发给PlayCards[i][j]。 用if(k==53||k==52)来判断发的牌是不是王,如果不是则用=k%13+1来判断得到牌的大小,用=k/13来判断得到的牌是什么牌型。 发完牌以后将剩下的三张牌存放在Sendleft中,等选出地主后把这三张牌发给地主。 等牌全部发完后,要整理玩家手中的牌序。那么如何来实现理牌这一操作呢?这里用SortCard(PlayCards[i])这函数来实现。 void Managers::SortCard(Card ca[]) int i,j; Card temp; for(i=0;i<20&&ca[j].Num<=15;i++) for(j=i+1;j<20&&ca[j].Num<=15;j++) if(ca[i].Pow>ca[j].Pow){ Temp=ca[i]; Ca[i]=ca[j]; Ca[j]=temp; 牌发好了,那么如何来选地主呢?在区分是出牌还是接受地主、放弃地主、或者放弃出牌需要依靠Game_State,当GameStart()运行之后Game_State=-1,表示正在选地主;而在游戏中,Game_State一直保持在0~3之间。选择完地主后把原来剩下的3张牌发给地主,然后Game_State变成相应的数字让地主出牌。 5.2 游戏进行 牌发好了如何来比较是否能出牌呢?用函数CardsInfo来判断。 void Managers::CardsInfo(Card ca[],int &num,int &min,int &type){ int i,k,n; int same[10]={0}; int nsame[10]={0}; } same用来记录相同牌的pow;nsame用来记录相同牌的数量;i记录有多少种大小不同的牌;n记录牌的数量。将n分为n>=5、5>n>2、n<=2三种情况。用判断牌是否相连来判断所出的是否为连牌。当牌的数量大于5时,所出的牌可能是连牌、3带n、4带n、三顺或者飞机。三顺的判断类似连牌,但多了个所有的牌必须有且仅有两张。不同的牌超过两种的另一种情况是飞机。在程序中定义了int ty1=0,ty2=0,num3=1记录了单张牌有多少张;ty2记录如果带的是对子则带了多少个对子;num3记录了有多少个三张牌。用CheckCard来计算是否可以出牌。p1选中的牌(Click=1)存放在PreOut中,然后和上次所出的牌OutCards进行比较,看能否出牌。num1、min1、type1用于记录OutCards的信息,mun2、min2、type2用于记录PreOut的信息。 六、游戏配置界面 输入玩家姓名对话框 要进行连网游戏,需要首先进行如图8-2的配置。 客户机配置 游戏主界面 2.游戏结果显示,如果是地主胜利则显示下图的结果: 游戏结果 3.如果中途有玩家退出,则显示如下图界面: 中途玩家退出 七、总结 本设计主要论述了如何实现网络版的斗地主对战程序,并且以一副牌规则为准,论述了网络斗地主的游戏算法的设计。本设计通过启动服务器与客户机,完成了他们之间的连接和数据交换;并且完成了游戏的各种规则以及给予用户正确的操作提示和胜负显示。虽然完成了设计的初始要求,但是在游戏的声效和动画处理以及计算积分方面还有待提高和改善。 通过制作这个项目,我体会最为深刻的一点是系统架构和设计模式的重要性。即使是对于一个并不大的程序,结构的设计和代码的组织都是非常重要的,因为这关系到日后的维护以及扩展。这个游戏之中,有关网络CSocket编程或者博弈树算法的知识都可以直接从无所不包的Internet上获取,甚至可以直接获得一个完整的斗地主算法的源代码级模块。但是对于系统的架构,却完全是自己的事情,几千上万行的代码需要通过合适的方法组织起来,使程序员编写代码更加有条理,更加符合软件工程的标准,这才是最重要的。 在这个游戏的编写中,让自己学会了简单游戏编写的过程,让自己对一个完整程序工程的实现有了大致的了解。现在网上有各式各样的小游戏,让自己有能力慢慢地去开发一个小型游戏,为以后更好的实现游戏的制作提供了一个很好的学习过程。 在程序的编写中也表现出很多不足的地方,本程序也有很多的不足,比如没有记分功能,不能像QQ斗地主一样对每个玩家的出牌时间做限制,界面制作也不够美观,在接下来的学习,编程中要慢慢的努力,去完成更好的程序。
发布者:admin,转转请注明出处:http://www.yc00.com/news/1689408527a243297.html
评论列表(0条)