局域网内双升游戏的设计(三)--算法

发牌算法

        当每个玩家都准备完毕后,那么第一步就是要发牌。需要做到一个完全随机的发牌,就要保证每张牌发到每个玩家手里的概率都是一样的,而且牌的顺序是等概率随机打乱的。程序中采用的是如下的发牌算法(感谢Dr.Light提供):

        假如有两幅牌,编号从1到108,首先随机选出一个,并且将牌发给玩家,然后将这个编号的牌与108号牌交换编号,那么剩下的牌就是从1到107号。于是再从中选出一个,重复以上的过程,这样一来,算法的复杂度就是O(n)。

牌的逻辑

在升级中,牌只有三种形式,一种是拖拉机,一种是单张(对子其实就只是长度为1的拖拉机),另一种就是甩牌时两种牌的混合。在程序中,将牌的类型抽象为三个类,如下图所示:(CCardFactory只是创建牌用的,不是具体的牌类型)

图 1.             牌的类结构图

        里面的几个虚函数主要解决了以下几个问题:

        Ø        牌对象的创建。

        Ø        两手牌比大小。

        Ø        甩牌时判断是否能够甩,即:保证甩出来的各个牌对象在其他玩家中,都是最大的。

        Ø        先出拖拉机(对子)时,对方出牌时必须从最长的拖拉机开始出,通俗点的意思就是有对子必须先出对子。在后面的讨论中,我们将其称为出的牌是否饱满。

        在研究牌的算法前,首先要将牌按照当前的主的类型和数字来排列好。为了计算的方便,将牌散列到一个大小为52(常主是一样大)的哈希表中,这样,从中找出拖拉机就相对方便些。

图 2.             散列后牌的分布

        假如方片为主,那么哈希表中每一部分对应的牌的类型就如上图所示,其中,常主和方块同属于主,(图中黄色标记)。这样一来,2244(3为主)就很容易被识别为拖拉机。方块AA黑桃33(3为主)也是连在一起的,也很容易被识别为拖拉机,同时,主的大小比其他的牌都大,这在杀的判断过程中也是有用的。另外,还需要一个判断的方法,即:一堆牌中是否都是属于同一个类型的牌(哈希表中是否为同一个颜色),如果不同类型,那么就不用判断。

        在打常主(亮大王)的时候,会有一些区别(4个常主一样大),这些小区别这里不赘述。

        接下来就是具体的算法。

牌对象的创建

        在每一轮出牌的过程中,只保存一个牌的对象,就是当前这轮中,最大的一手牌,保存在m_pCurrentCards中。下一个人出的牌,都由m_pCurrentCards来创建,通过虚函数createCards来实现,这样的好处是可以将不同类型的创建规则分散到不同的类中去。

        举例说明:如果当前第一个人出了一个3344的拖拉机,而第二个人出了4个单张,此时由m_pCurrentCards(保存着3344的CPairCards对象)来创建一个新的牌,于是就按照CPairCards的规则来创建一个牌对象,返回一个NULL,那么说明第二个人没有大过第一个人的牌,就可以忽略他。如果第三个人出了个5566拖拉机,此时由m_pCurrentCards的对象(保存着3344的CPairCards对象)来创建一个新的牌,就是保存着5566的CPardCard的对象,大过了第一个人出的牌,于是将此时m_pCurrentCards给替换为保存着5566的CPardCard对象。

        首先按照上一节所说的方法将玩家打的牌散列到哈希表中。然后调用m_pCurrentCards的createCards函数创建新牌。单张牌和拖拉机(对子)的创建比较简单,就不做说明,主要说下甩牌时CBlendCards的创建。在介绍具体创建过程之前,先介绍一下程序中定义的一个动作:strip。

        从字面上看,就是剥离。假设第一个出牌的人出了一个对子和一个单张,那么CBlendCards对象中分别有一个CSingleCard和CPairCards对象。那么,就要依次从后面人出的牌中,剥离出一个单张和一个对子,即:创建一个CSingleCard和CPairCards对象,并且从哈希表中删除对应的计数(在操作中,要考虑是否类型匹配)。下面将用图的形式说明:

图 3.             散列后的黑桃AKK

        如上图所示:假如某人先甩了黑桃的AKK,那么将牌散列到哈希表中后就如上图所示(省略其他部分)。此时由CCardFactory来创建牌,首先用一个CPairCards的对象对其做strip操作,操作后等于将其中的一个对子给剥离出来,并创建一个CPairCards对象:

图 4.             剥离出一个对子后

        同理,再剥离出后,最后生成一个CPairCards对象和一个CSingleCard对象。两者组合成一个CBlendCards对象。


图 5.             最后生成的牌对象

        此时如果第二个玩家出了红桃AKK(假设红桃为主),那就是杀了这一对。此时由于不是第一手牌,那么就会由当前最大的牌来创建新的牌。也就是刚生成的CBlendCards对象。生成的过程也是使用strip操作,先用CPairCards对象对其strip,生成一个新的对子对象:

图 6.             使用CBlendCards对象生成新对象

        同理,在使用CSingleCard对其strip后,就生成了一个新的CBlendCards对象,其中包含一个CPairCard和一个CSingleCard:


图 7.             根据第二个人出的牌生成的结果

        有了strip操作,那么创建CBlendCards的步骤就是针对它拥有的CSingleCard和CPairCards对象,依次调用strip操作,如果每个都能正常strip,那么就将生产的牌对象组成一个CBlendCards对象,就生成了新出的牌对象。strip操作在后续还会使用到。

        如果是第一手出牌的话,当前没有最大的牌,就由CCardFactory来创建。CCardFactory虽然也是CShengjiCardBase的子类,但是它并不代表具体的牌。只是根据第一个玩家打过来的牌的集合来创建出一个牌的对象。它内部分别有一个CSingleCard、CPairCards和CBlendCards对象,每次新建的时候使用默认的这几个对象来创建。如果三个对象都创建不了,说明出牌错误。一轮出牌结束后,统计完分数,就将当前最大的牌删除,并且使用CCardFactory对象替换m_pCurrentCards中原有的牌。

两手牌比大小

        每个玩家出牌后,就需要判断出来的牌的大小。现在就体现出前面保存最大牌的好处了。每次只要将新出的牌与最大的牌比大小就可以了。而具体的比大小的工作在每种牌各自的重载函数largerThan中做即可。

        拖拉机(对子)和单张的牌比较好比。主要说明一下甩牌CBlendCard的大小比较机制。在升级规则中如果甩牌的话,所以的牌都必须是其他人中最大的,否则不能出。那么,在甩牌后,如果想比出牌人更大的话,就必须要用主来杀。在我们程序中,保证最大这个是由出牌的时候的机制来保证,而比大小的时候,只要依次判断每个牌是否都大即可,如果新出的牌中,某一部分牌不大,就说明新出的牌没有大过m_pCurrentCards。

甩牌时判断是否能够甩

        在第一手出牌时,还需要判断当前的牌是否能出的出去:甩牌时每一部分的牌都需要比别人手中同花色最大的牌还要大。虚函数getIllegalCards实现了这个功能。它能获取一堆牌中,比其他人手中的牌小的那部分牌。

        判断的逻辑主要用到了前面说到的strip操作。当甩牌后,会由CCardFactory创建一个CBlendCards对象,其中有若干个CSingleCard和CPairCards对象。此时,将其他玩家的手牌散列到哈希表中,然后依次调用CBlendCards中各个牌对象的stripCards方法,剥离出对应的牌对象。如果某一次剥离出的牌对象比当前CBlendCards中的对象大,说明甩牌失败,需要强制出小。

出牌是否饱满

        根据前面的问题说明,先出拖拉机(对子)时,对方出牌时必须从最长的拖拉机开始出,通俗点的意思就是有对子必须先出对子。在后面的讨论中,我们将其称为出的牌是否饱满。

        拿一个简单的例子作为说明:

图 8.             出牌饱满性示意图

        假设玩家1先出了一个拖拉机,玩家2手中有776543的牌,按照规则,应该将一对7给出出去,但是玩家2却没有将对7打出,所以应该要能够识别出这种错误状态。

        首先,玩家1打出拖拉机后,会生成一个CPairCards的对象,长度为2,接着用这个CPairCards对象对玩家2的手牌和玩家2打出的牌分别提取其中一个最饱满的牌。何为最饱满的牌?如上图所示,如果出了一个拖拉机后,饱满长度按照以下排列:长度为2的拖拉机>对子>单张。提取方法就是前面所说的strip,但是要strip直到生成牌的个数相等。

        举例说明,按照玩家1打出的牌,生成一个CPairCards牌对象后,这个对象的长度为2,那么从2开始,先用一个长度为2的CPairCards对象strip手牌,结果得到是空,因为玩家我2手中没有拖拉机,然后依次递减,用长度为1的CPairCards对象来strip手牌,得到一个长度为1的CPairCards对象(对7)。但是到现在还不能结束,要等到生成的对象的牌个数等于4张(AAKK)的时候才能结束。于是继续strip长度为1的CPairCards,返回为空,因为剥离出一个对7后只有单张牌。那么就换CSingleCard对象继续做以上的操作,直到strip出两个CSingleCard对象后才算结束。此时,strip出的对象牌数加起来是4(CPairCards和两个CSingleCard对象)。操作结束。

        同理,用手牌生成的长度为2的CPairCards对象对玩家2打出的牌做同样操作,得到4个CSingleCard对象。

        最后,通过饱满度从大到小比较得到的两组对象,比较后发现,从手牌中得到的一组对象中,对7对应的CPairCards对象的饱满度大于从打出的牌中得到的CSingleCard对象。所以出牌错误。

总结(出牌说明)

        下面两个流程图分别给出了第一手牌和非第一手牌时,服务器收到出牌消息后的处理:

图 9.             第一手牌的处理

图 10.         非第一手牌的处理

          参考代码+编译后的程序:http://download.csdn.net/detail/hustxyj/7024091

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值