1、基于 C#的斗地主游戏设计与实现福州第三中学 组长:刘纪源 指导老师:宋立林组员:刘心灵、卓超【摘要】本文详细论述了如何使用 C#编程语言以 WPF 用户界面框架,运用面向对象的编程思想实现三个玩家在三个不同的终端进行斗地主游戏。对于斗地主游戏,小组首先对其基本构造进行分析,抽象得到进行斗地主游戏所需的基本元素,然后分析其规则,得到游戏的基本模型。根据分析结果,将游戏所需的基本元素在程序上表达出来,封装入类库(存储所有基本元素在程序中的表达的文件) 。接着根据分析出的算法,实现游戏的基本运行,完成第一阶段。第二阶段参考了 Karli Cards(在C#入门经典中的一个的简单纸牌游戏)的界面设计
2、出游戏界面。第三阶段着重实现不同客户端间的通讯,首先分析了游戏进行时需要的信息,建立起了通讯模型,然后实现了各基本元素在各个终端间的传输,最后实现模型,调试后得到成果。 【关键词】斗地主 算法设计 C# WPF(Windows Presentation Foundation) 【正文】一、 课题的选择在选择课题时,小组成员讨论提出多个方案,如数独游戏、计算器、斗地主等。在综合考虑后,小组最终决定以斗地主作为研究方向,理由如下:1. 斗地主作为一种结构相对鲜明的游戏,在抽象完成后的许多组件都可以再利用,有利于之后的再创造;2. 斗地主作为常见的纸牌游戏,组员的熟悉程度较高,可行性较强;3. 图形
3、界面的实现有实际例子可以参考,程序的界面实现不会成为之后工作的阻碍。二、 小组分工在第一阶段,组员刘心灵负责收集斗地主相关规则以及整理部分游戏逻辑,组长刘纪源负责构造游戏模型并以控制台的形式实现基本程序;在第二阶段,组员刘心灵、卓超负责搜集相关界面素材,组长刘纪源负责构造界面模型并实现基于图形界面的程序;在第三阶段,组员卓超负责构造通讯模型,组长刘纪源实现程序通讯功能并进行相关调试。 三、 研究过程(一 ) 游戏本身的实现1. 游戏基本元素的分析与实现斗地主游戏如同大多数纸牌游戏,具有多个基本元素。根据常识,进行一次斗地主游戏需要一副完整的 54 张牌,以及三个玩家,因此首先需要表达 54 张
4、扑克牌的基本元素 牌。每张牌由 Card 类实现,包含三个字段:Suit(花色) 、Rank(大小) 、Joker(大小鬼)。每张牌生成后不可改变,根据构造函数的不同为 Suit ,Rank 或 Joker 赋值,每张牌仅有一种属性,例如这张牌为红桃 A 时,Joker 属性为空;这张牌为大鬼时,Suit 和 Rank属性为空。这个类重载了 ” , ”类(一个大小可变的高级集合)继承而来,可以表示多张牌(一张或没有牌也是可以表示的)已拥有绝大多数集合方法,在这里自定义了一个新方法:Shuffle() ,实现了洗牌。实现了 Cards 类,就可以实现一副牌了。Deck 类可以获取一副 54 张的
5、牌。它是一个静态类,意味着他不需要初始化(多用一行代码生成新的一副牌)直接可以调用。Deck 类有三个属性。调用 Full 可以获取一个顺序的有 54 张牌的 Cards 实例(一副牌组) ,调用 Shuffle 可以获取一个打乱顺序的有 54 张牌的 Cards 实例,而DDZOrdered 则可获取一个 按照斗地主规则洗好的 含四个Cards 实例的简单数组。元素123为三组玩家的原始手图 A-Error! Main Document Only. Card 类的构造图 A-Error! Main Document Only. Cards 类的构造图 A-Error! Main Docume
6、nt Only. Deck 类的构造牌,各 17 张;元素0为地主牌,有三张。至此为止我们就已经实现了斗地主游戏中的牌组的基本表示。接下来实现玩家。首先玩家需要有名字以及一副牌,这已经可以实现。然后,每个玩家还具有一个状态,来表示此时此玩家是否正处在可以出牌的活跃状态。接着按照游戏规则,在抢地主之前,玩家尚未获得地主或农民的身份,身份待定。抢地主过后,每个玩家都具有了对应的身份:地主或农民。在某个玩家出尽手中的牌之后,该玩家成为赢家,若其为农民则另一名农民也为赢家,其余为输家。总之,玩家在整场游戏中有五种身份待定、地主、农民、赢家、输家。在程序中,这五种身份分别对应 Null, LandOwn
7、er, Peasant, Winner, Loser。在 Player 类中,手牌由_hand 字段储存。 _hand 字段不可直接读取和修改,通过 AddCard()及 AddCards()方法可加牌入手牌,ClearHand() 方法清空手牌,GetCards()获取当前手牌的一个副本(对其作出的修改不会影响_hand ) 。玩家名及玩家状态由_name,isActive 字段存储,外部可用PlayerName,IsActive 属性读取和更改。玩家身份由 State 属性储存,外界具有读写权限。lastDiscardCards 字段, LastDiscardCards 属性用于存储最后一
8、次出的牌,作用“ 游戏逻辑的分析与实现”中详细说明。Index 属性存储当前玩家的内部索引,作用将在第三阶段说明。QiangDiZhu(),DiscardCard()方法、CardDiscarded, PlayerHasWon, QiangDiZhuResultSet 事件将在“ 游戏逻辑的分析与实现 ”中详细说明。多个玩家的集合由 Players 类实现,由系统的List类继承而来,可以存放多个玩家。在此自定义了一个新方法:GetPlayer() 方法,可通过玩家内部索引在数组中获得玩家。至此总结,我们将牌抽象为 Card 类,多张牌抽象为 Cards 类,一副牌抽象为 Deck 类,玩家抽
9、象为 Player 类,三个玩家抽象为 Players 类,完成了游戏基本元素的分析与实现,下文进入游戏逻辑的分析及实现。图 A-Error! Main Document Only. Player 类的构造图 A-Error! Main Document Only. Players类的构造2. 游戏逻辑的分析及实现斗地主游戏的主要过程如图 A-6。a) 抢地主在研究过程中,我们遇到的第一个难点是抢地主的过程。因为这个过程逻辑较为复杂,所以我们决定先采用穷举的办法得出所有结果再进行分析。在抢地主过程中,我们需要获取每个玩家的选择,结果可以表示为真或假的逻辑值,因此我们首先对抢地主过程进行穷举,穷
10、举结果如图 A-7 所示:依照穷举结果,我们可以知道,我们需要从每个玩家手中获得至少一个输入,在输入取得之后,根据原始地主的选择,我们可以进一步决定获取输入还是定下地主开始游戏。根据分析结果以及穷举结构,我们完成了抢地主的算法设计,主要如图A-8(附表中)所示。图 A-Error! Main Document Only. 斗地主的主要流程图 A-Error! Main Document Only. 斗地主的过程穷举用 Y 表达真,N 表示假, 表示原始地主的选择, 表示另外两个玩家的选择, 表示最终结果,后跟玩家代号, ”RESTART”表示重新洗牌。b) 牌型的识别和比较游戏逻辑的第二个难点
11、在于实现斗地主牌型的识别和比较。在三人斗地主规则中,玩家被允许出的牌型如下:1. 单牌;2. 对子,两张相同大小的牌;3. 三张牌,可带;4. 炸弹,单独出可带;5. 王炸,不可带;6. 飞机,连续两组以上三张牌,有连续几组就可带几个对子或单牌,主体不带 2;7. 顺子,一连串连续的单牌,五张或以上,不带 2;8. 连对,三组或以上连续对子,不带 2;而根据规则,王炸炸弹其余牌型。其余牌型间,必须为同牌型且格式一致才有可比性,满足时主体中最小的牌决定该牌组的大小。但是我们只实现了多张牌的表示,并没有实现识别多张牌的牌型的功能,并且无法比较同牌型间的大小,因此为了实现斗地主牌型的识别和比较,我们
12、引入一个新的类 ComparableCards 类,结构如图 A-9。表格 1 TypeCs 枚举的含义类型 主体Empty 空的(尚未设置)Single 单牌Double 对子Triple 三牌Boom 炸弹KBoom 王炸Plane 飞机Shunzi 顺子CDouble 连对None 不出ComparableCards 类的主体由一个元素为 Cards 的长度为 6 的简单数组 container字段和类型为 TypeCs 的 type 字段组成。前者作用见表格 2,后者表示该实例的 牌型,详细释义见表格 1。Clear()方法可以清空当前实例并将类型改为 Empty; GetCards(
13、)获取当前实例Container 中的所有牌,Set()和 IsValid()实现了将识别 Cards 牌型以及 牌型间比较的功能,下文将着重解释这两个方法。图 A-9 ComparableCards 类的构造0 存储带的单牌1 存储单牌,顺子2 存储对子,王炸,连对3 存储三张牌,飞机4 存储炸弹5 存储带的对子表格 Error! Main Document Only. container 内部结构Set()方法定义:public bool Set(Cards ocards, bool isFirst)Set()方法需要输入一组牌 ocard 以及是否为本轮第一人的 isFirst,返回一个
14、逻辑值表示输入的牌合法或非法。下面开始介绍其算法。为了对牌组进行分析,我们首先将其排序,然后需要将牌组中不同长度的连续同大小的牌分离,将其分为单张、双张、三张、四张四个类别,然后再根据类别中的牌判别是否符合规则。但是在分类前,几种特殊情况可以单独考虑。1. (图中对应)不出牌,即牌组内没有牌,那么可以通过 isFirst 判断此玩家输入是否合法;2. (图中对应)牌组长度仅为 1,那么可直接判定其合法;3. (图中对应)牌组长度为 2 且两张牌大小一致,那么也可直接判定其合法;4. 牌组中存在大小鬼,那么合法的存在有大小鬼的情况仅有两种:王炸(图中对应)或单牌,若不符合这两者的可直接判为非法输
15、入;分类之后,有几种情况牌可能会混淆:1. (图中对应)将四张一样大小的牌作为飞机的一部分;2. (图中对应)在飞机主体大于 2 时将一个对子视作两张单牌带进飞机。Set()的完整算法如附表中图 A-10 以及图 A-11:IsValid()方法定义: public bool IsValid(ComparableCards B)IsValid()方法需要输入上家出的一个 ComparableCards 实例 B,返回一个逻辑值表示当前的牌相对于上家合法或非法。这个方法按照斗地主规则检查自己的牌型是不是比上家的大或者相同,牌型大则直接返回合法,牌型一样则检查牌型格式是不是一样,牌型格式一样之后比
16、较两者主体最小牌,比上家大就返回合法,牌型比上家小或者牌型或牌型格式不一致都会返回非法。通过这两个方法,我们就实现了斗地主牌型的识别和比较。c) 主游戏逻辑图 A-12 主游戏逻辑流程图图 A-12 为主游戏逻辑,相对较简单,在此不再详细阐述。d) 完整游戏逻辑的实现上文已经说明了大部分的实现难点,所以现在开始实现整体游戏逻辑。游戏整体逻辑引入一个新类 GameViewModel类处理,结构如图 A-13玩家及地主牌由_players,diZhuCards 字段存储;外部可用Players,DiZhuCards 属性读取和更改。字段i,results ,playerIndexes即为图 A-8
17、 中同名的三个数据。StartGame()方法内部调用 CreatePlayers()和 CreateHands()生成了新玩家并且利用 Deck.DDZOrdered 的返回值执行完洗牌和发牌,得到地主牌。CreatePlayers()内部每生成一个玩家调用一次 InitializePlayer(),将此玩家的事件绑定(目的是使这个事件发生时会调用相应的处理方法):事件 QiangDiZhuResultSet 绑到 Player_OnQiangDiZhuResultSet()方法;事件 CardDiscarded 绑定到 player_OnCardDiscarded()方法;事件 Playe
18、rHasWon 绑定到 player_OnPlayerHasWon()方法。然后将新生成的玩家加入 Players 中。StartGame()最后会随机生成一个数字决定原始地主并且用AssignCurrentPlayer()激活原始地主开始抢地主过程。这里就不得不提到 AssignCurrentPlayer()方法了。这个方法是整个游戏循环的实现者,在这里进行详细介绍:AssignCurrentPlayer()方法定义:private void AssignCurrentPlayer(int index, int mode)AssignCurrentPlayer()方法需要输入需要激活的玩家索
19、引 index 以及模式代码 mode 来执行操作。Mode 有三种:1. Mode=1对应图 A-8(抢地主过程)中 的步骤,激活指定玩家(更改指定玩家 IsAcitve 为 true,其余人为 false) ,允许指定玩家输入自己的选择,并且将内部计数器 i 加一。GameViewModel类字 段_playersdiZhuCardsiplayerindexesresults属 性DiZhuCardsPlayers方 法AssignCurrentPlayerCreateHandsCreatePlayersInitializePlayerplayer_OnCardDiscardedplaye
20、r_OnPlayerHasWonPlayer_OnQiangDiZhuResultSetStartNewGameGameState枚 举NotStartInitializingQiangDiZhuSettingDiZhuPlayingSettingWinnerINotifyPropertyChangedState图 A-13GameViewModel 类的主要结构2. Mode=2更改指定玩家身份为地主并将地主牌加 入其手牌,同时更改其余玩家身份为农民,并且 将内部计数器 i 清零。3. Mode=3对应图 A-12(主游戏逻辑)中过程中的 首先检测此玩家是否为赢家或输家,不是的话,激活指定玩
21、家(更改指定玩家 IsAcitve 为 true,其余人为 false) ,允许指定玩家输入自己的选择。介绍完 AssignCurrentPlayer(),我们将举两个典型例子来介绍上文提到的三个处理方法具体的工作步骤。第一个例子是抢地主时一个玩家从被激活到下一个玩家被激活的过程。此情况下当玩家被激活,他的 IsActive 为 true,State 为PlayerState.Null,这时当他决定叫或不叫地主后,会调用玩家的QiangDiZhu()方法(附有玩家的选择) ,此方法会触发带有玩家选项的玩家的QiangDiZhuResultSet 事件,上文已经说明此事件被绑定在Player_O
22、nQiangDiZhuResultSet()上,所以此方法会被调用。这个方法的算法同图 A-8,总之会根据情况调用 AssignCurrentPlayer()的模式 1 继续抢地主或者模式 2 开始游戏。第二个例子是正式游戏时一个玩家从被激活到下一个玩家被激活的过程。此情况下当玩家被激活,他的 IsActive 为 true,State 为PlayerState.Peasant/LandOwner,他决定出的牌之后首先会检查出的牌的有效性,方法是:获取上家以及上家之前的玩家的 LastDiscardCards,然后分为三情况讨论:1. 两者的类型都是 TypeCs.None/Empty,那么就
23、可知此玩家为本轮第一个人,该玩家出的牌只需通过 ComparableCards 的 Set()(isFirst=true)方法即可;2. 若上家的类型不是 TypeCs.None/Empty,就需要首先通过ComparableCards 的 Set()(isFirst=false)方法,然后通过IsValid()(B= 上家的 LastDiscardCards) ,同时通过两个才有效;3. 若上家的类型是 TypeCs.None/Empty,但上家之前的玩家的类型不是 TypeCs.None/Empty,就需要首先通过 ComparableCards的 Set()(isFirst=false)
24、方法,然后通过 IsValid()(B=上家之前的玩家的 LastDiscardCards) ,同时通过两个才有效。若通过了有效性检查,那么会调用会调用玩家的 DiscardCard()方法(附有玩家出的牌(ComparableCards 类型) ) ,这个方法会记录本次出的牌入此玩家的 LastDiscardCards 中,然后从手牌中删除已出的牌,最后检查手牌是否已空,若空,就会引发 PlayerHasWon 事件,从而调用 player_OnPlayerHasWon()方法,设置玩家为赢家或输家;否则引发事件 CardDiscarded ,从而调用player_OnCardDiscarded()方法,让此方法调用 AssignCurrentPlayer()(mode3)激活下一个玩家。至此,我们实现了游戏逻辑的分析与实现,完成了游戏本身的实现。(二) 游戏图形界面的设计游戏界面主要由主窗口与对话框组成。设计主要参照1。1. 主窗口主窗口是各个不同功能对话框的入口以及进行游戏的地方。主窗口未开始游戏时的设计如图 B-1。主窗口开始游戏后的界面为图 B-2。图 B-Error! Main Document Only. 主界面未开始游戏时的设计图 B-Error! Main Document Only.主界面开始游戏后的界面