这是一个纸牌的赌博游戏。
这个游戏的规则:每个玩家分别与庄家比点数,但是又不能超过21点。这是最原始,最简单的规则。但是发展到现代。游戏的规则已经变得复杂了很多。比如双倍下注、分牌、五龙等这样例牌的出现。
具体规则可以在维基百科上搜索“21点”。
它们的架构与功能是完全独立的。这意味着我们在构建系统时,应该选择最符合当前需求的架构,并在该架构的框架下实现功能。——《恰如其分的软件架构》
虽然这个21点看似简单,其实不然。当然,如果你只是想实现一个只有最简单21点游戏,而不考虑可扩展性、可维护性,你大可使用一个大大的if-else
实现。这也算是一种架构。
如果你的目标是:当QQ游戏需要添加21点游戏时,只要在你21点游戏底层库上做些配置就可以直接用了(在写本文时,我真不知道QQ游戏到底有没21点游戏)。
或者某家在线赌博网站想运营一个21点游戏,拿来你的21点游戏底层库,在上面进行配置或扩展,就可以直接用了。
说得再细一些。有些赌场对于Blackjack是3倍赔率,而有些是2倍。有些赌场允许分牌后再分,
有些则不允许。有些赌场只允许在首两张牌时进行加注,而有些赌场允许在任何时候进行加注。
而敝人也希望这个底层库尽可能实现所有的场景。
难点在于21点游戏的规则没有标准,每个赌场的规则都可能不同。而我们写出的程序必须应对所有的变化。
解决方案是
而我采用的是第二种解决方案。
分析与设计
首先要知道21点的游戏流程是不会变的,而变的则是例牌的设置、例牌的赔率,而且同一例牌的规则也可能会不同。
所以,游戏流程可以写死,而例牌方面直接使用动态语言Groovy来实现。
设计中有几个重要的概念需要介绍,这样有利于了解我的设计与架构:
玩家(player):首先庄家有可能是人,也有可能是机器。所以,不论机器或人,在赌桌上所有的人、机器都是玩家。
当赌局里的庄家是随机的时候,意味着所有的玩家都可能做庄家。
玩家代理(playerProxy):有些规则是允许一个玩家下多门注的。所以,我们抽象出玩家代理
这个概念。实际掌握手上牌和赌注的是玩家代理。
玩家的所有的操作实际上都是由玩家代理
执行。这样就可以解决一个玩家有多门注,及分牌的问题。
庄家代理(dealerProxy):当一个玩家做了庄家,那么他在赌局中实际是通过庄家代理
来完成操作。
牌型(cardCategory):牌型指的是手上牌所符合的模式。比如首两张牌组成21点,那么这手牌的牌型就是Blackjack。由于赔率很大程度与牌型及玩
家操作决定,而且每个赌场的规则又不一样。所以,这部分使用Groovy实现。
玩家操作(playerAction):玩家在游戏中只有有限的几个操作:要牌(hit),停牌(stand)等。所以,这部分,直接使用java实现。
银行(bank):这是对赌场中钱的管理人的一个抽象。统一管理所有的钱、判断玩家是否还有足够的钱进行加注等。
赌桌(tables):玩家加入赌桌进行赌博。而赌桌则是所有玩家玩牌的接口。
使用Groovy来实现的好处有:
为什么不使用XML来配置呢?
鉴于很多人喜欢使用XML做配置文件,我的想法如下:
当然,如果你希望实现一个使用XML配置的,那也可以。本库的扩展非常好。只需要改WinnerLoserCalculateEngine
这个
输赢计算引擎就可以了。
由于例牌是21点中最大的变化,而且各类繁多。所以,我有必要在这里说明一下,哪些例牌是我已经实现并测试了的。
保险
保险与玩家下的筹码是独立的。是单独结算的。
所以分牌时,保险金不会分
Blackjack:
开牌后就得到了21点(一张面牌为10的牌,一张A)
分牌(split) :
玩家首两张牌的点数相同,则可以选择分牌,并须加注。分出的每门的下注金额须与原注相同。
注:有些赌场可分牌后再分,有些则不能
双倍下注:
发牌后,玩家手上两张牌的点数加起来有11点,就可以进行双倍下注。但是双倍下注后,只能要一张牌。
过五龙:
玩家或庄家手上的牌超过了5张,但还没有爆牌的情况。
同花顺:
玩家手上有三张牌:牌色相同,面值分别是6,7,8的情况。
实际上,你可以发挥你的想像来实现你的例牌。比如,当庄家为Blackjack而玩家为五龙,则为打平等等。
投降(surrender)
首两张牌可以投降,其他情况不可以投降。投降后,玩家只可以拿回原来赌注的一半。
Ace分牌(AceSplit):
这是分牌的特例。当玩家首两张牌都是A牌时,可以选择分牌,但是分牌后只能得到一张牌。
退出
在这里,你看不到什么MVC这样的分层方式。是因为我们这个库目前做的事情就是21点最核心的业务。所以MVC这样的分层方式不适合。
目前,你也看不到关于持久化的内容。因为,一持久化是客户的事情,二我目前还没有时间写一个默认的实现。
这是一个核。实现目标是将21点游戏的业务逻辑集中在这个核内。
技术上:
业务上:
目前还没时间做
目前,关于持久化的工作几乎为0。因为我在游戏开发方面没有经验,有一些问题没有想清楚。
如何实现全自动化测试?
也就是不需要任何的mock。玩家自动玩牌。
如果没有实现自动化测试,那么也就没有测试并发情况也会发生什么。
Groovy脚本配置的带来的安全问题
有一个CasinoTest
测试类。测试类模拟的是一场简单的赌博。从中可以看出如何使用此核心库。
下面是跑测试时的输出:
12:33:54.568 [main] INFO c.z.blackjack.core.CasinoTest - 三名玩家分别存入赌场
12:33:54.573 [main] INFO c.z.blackjack.core.CasinoTest - 甲 存入:400.0
12:33:54.576 [main] INFO c.z.blackjack.core.CasinoTest - 乙 存入:400.0
12:33:54.577 [main] INFO c.z.blackjack.core.CasinoTest - 丙 存入:400.0
12:33:54.580 [main] WARN c.z.blackjack.core.CasinoTest - 三名玩家加入赌桌:1
12:33:54.584 [main] INFO c.z.blackjack.core.CasinoTest - 选举出庄家为:甲
12:33:54.586 [main] INFO c.z.blackjack.core.CasinoTest - 玩家乙 下注:20
12:33:54.586 [main] INFO c.z.blackjack.core.CasinoTest - 玩家丙 下注:20
12:33:56.154 [main] INFO c.z.blackjack.core.CasinoTest - *****开始******
12:33:56.154 [main] INFO c.z.blackjack.core.CasinoTest - 乙 手上的牌,点数:[20] 分别为:[{SPADE-Q}, {SPADE-K}]
12:33:56.154 [main] INFO c.z.blackjack.core.CasinoTest - 丙 手上的牌,点数:[17] 分别为:[{HEART-J}, {HEART-7}]
12:33:56.236 [main] INFO c.z.blackjack.core.CasinoTest - 乙 停牌,点数:20,分别为:[{SPADE-Q}, {SPADE-K}]
12:33:56.319 [main] INFO c.z.blackjack.core.CasinoTest - 丙 停牌,点数:17,分别为:[{HEART-J}, {HEART-7}]
12:33:56.320 [main] INFO c.z.blackjack.core.CasinoTest - 庄家甲 点数为14不足17点
12:33:56.474 [main] INFO c.z.blackjack.core.CasinoTest - 庄家要牌,最后点数:[24] 牌为:[{HEART-6}, {HEART-8}, {DIAMOND-K}]
12:33:56.474 [main] INFO c.z.blackjack.core.CasinoTest - **********所有玩家停牌,开始结算
12:33:56.737 [main] INFO c.z.blackjack.core.CasinoTest - 乙 下注 20.0 赢家为PLAYER 最后余额:420.0
12:33:56.817 [main] INFO c.z.blackjack.core.CasinoTest - 丙 下注 20.0 赢家为PLAYER 最后余额:420.0
12:33:56.817 [main] INFO c.z.blackjack.core.CasinoTest - 庄家甲 最后余额:360.0
12:33:56.818 [main] WARN c.z.blackjack.core.CasinoTest - *******结束
保险规则配置
配置文件为类路径下:Insurance.groovy
例子
//玩家赢时将得到的彩金
winMoney = playerFirstBet * 2
结算相关配置
实质是配置根据玩家的牌型及庄家的牌来计算输赢,并返回赔率
配置文件为类路径下:Settlement.groovy
例子
//如果玩家赢,赢多少
winMoney = 0
switch (playerCardCategory) {
//同花顺
case "royal":
winner = PLAYER
winMoney = playerBetSum * 3
break
case "Blackjack":
switch (playerLatestAction) {
case stand:
if (isDealerBlackjack) {
winner = PUSH
} else if (!isDealerBlackjack) {
winner = PLAYER
winMoney = playerBetSum * 3
}
default:
winner = PLAYER
winMoney = playerBetSum * 3
break
}
break
case "fiveCard":
winner = PLAYER
winMoney = playerBetSum * 3
break
case "bust":
winner = DEALER
break
default:
//庄家爆牌
if (dealerSumPoint > 21) {
if(playerSumPoint <= 21){
winner = PLAYER
winMoney = playerBetSum * 1;
break
}
if (playerSumPoint > 21) {
winner = PUSH
winMoney = 0
break;
}
}else{
if (playerSumPoint > dealerSumPoint) {
winner = PLAYER
winMoney = playerBetSum * 1
} else if (playerSumPoint < dealerSumPoint) {
winner = DEALER
} else {
winner = PUSH
}
}
break
}
牌型配置
实质是根据玩家手上的牌,计算出手上牌所属的牌型
配置文件为类路径下:Special.groovy
例子
switch (_headCard.cardCount) {
case 2:
if (_headCard.sumPoint == 21) { // 首两张牌点数为21点时是Blackjack
name = "Blackjack"
actions = [_report]
} else if (_headCard.sumPoint == 11) { // 首两张下牌点数等于11点时可以双倍下注
name = "doubleDown"
actions = [_hit, _stand, _doubleDown]
} else if (_headCard.isFirstTwoCardPointEquals()) { //首两张牌的面值相同,可进行分牌
//设置分牌后不可以再分。
if (_headCard.isSplit()) {
return
}
name = "split"
actions = [_hit, _stand, _split]
}
break
case 3:
// 同花顺
if (_headCard.containsAllCardFace(6, 7, 8) && _headCard.isCardFaceIdentical()) {
name = "royal"
actions = [_report]
}
break
case 5:
if (_headCard.sumPoint <= 21){
name = "fiveCard"
actions = [_report]
}
break
}
// 爆牌
if (_headCard.sumPoint > 21) {
name = "bust"
actions = [_stop]
}
在配置脚本中可以使用的变量 :
binding.setVariable("PLAYER", Winner.PLAYER);
binding.setVariable("DEALER", Winner.DEALER);
binding.setVariable("PUSH", Winner.PUSH);
binding.setVariable("playerBetAmounts", playerProxy.getBet().getBetAmounts());
binding.setVariable("playerFirstBet", playerProxy.getBet().getBetAmounts().get(0));
binding.setVariable("playerBetSum", playerProxy.getBet().getBetSum());
binding.setVariable("playerLatestAction", playerProxy.getLatestActionName());
binding.setVariable("isPlayerBlackjack", playerProxy.isBlackJack());
binding.setVariable("playerCardCategory", playerProxy.getCardCategoryName());
binding.setVariable("playerSumPoint", playerProxy.getSumPoint());
binding.setVariable("playerCardCount", playerProxy.getHeadCard().getCardCount());
binding.setVariable("dealerCardCategory", dealerProxy.getCardCategoryName());
binding.setVariable("dealerSumPoint", dealerProxy.getSumPoint());
binding.setVariable("dealerCardCount", dealerProxy.getHeadCard().getCardCount());
binding.setVariable("isDealerBlackjack", dealerProxy.isBlackJack());
binding.setVariable(StandAction._name, StandAction._name);
binding.setVariable(HitAction._name, HitAction._name);
binding.setVariable(ReportAction._name, ReportAction._name);
binding.setVariable(DoubleDownAction._name, DoubleDownAction._name);
binding.setVariable(SplitAction._name, SplitAction._name);
binding.setVariable(StopAction._name, StopAction._name);
binding.setVariable(SurrenderAction._name, SurrenderAction._name);
12:35:31.278 [main] INFO c.z.blackjack.core.CasinoTest - 三名玩家分别存入赌场
12:35:31.283 [main] INFO c.z.blackjack.core.CasinoTest - 甲 存入:400.0
12:35:31.285 [main] INFO c.z.blackjack.core.CasinoTest - 乙 存入:400.0
12:35:31.286 [main] INFO c.z.blackjack.core.CasinoTest - 丙 存入:400.0
12:35:31.289 [main] WARN c.z.blackjack.core.CasinoTest - 三名玩家加入赌桌:1
12:35:31.292 [main] INFO c.z.blackjack.core.CasinoTest - 选举出庄家为:丙
12:35:31.293 [main] INFO c.z.blackjack.core.CasinoTest - 玩家甲 下注:20
12:35:31.293 [main] INFO c.z.blackjack.core.CasinoTest - 玩家乙 下注:20
12:35:32.761 [main] INFO c.z.blackjack.core.CasinoTest - 开始
12:35:32.762 [main] INFO c.z.blackjack.core.CasinoTest - 甲 手上的牌,点数:[11] 分别为:[{HEART-2}, {DIAMOND-9}]
12:35:32.762 [main] INFO c.z.blackjack.core.CasinoTest - 乙 手上的牌,点数:[15] 分别为:[{DIAMOND-Q}, {SPADE-5}]
12:35:33.092 [main] INFO c.z.blackjack.core.CasinoTest - 甲 双倍下注,最后点数:[20],牌为:[{HEART-2}, {DIAMOND-9}, {DIAMOND-9}]
12:35:33.233 [main] INFO c.z.blackjack.core.CasinoTest - 乙 停牌,点数:15,分别为:[{DIAMOND-Q}, {SPADE-5}]
12:35:33.233 [main] INFO c.z.blackjack.core.CasinoTest - 庄家丙 点数为11不足17点
12:35:33.322 [main] INFO c.z.blackjack.core.CasinoTest - 庄家要牌,最后点数:[20] 牌为:[{DIAMOND-8}, {SPADE-3}, {DIAMOND-9}]
12:35:33.322 [main] INFO c.z.blackjack.core.CasinoTest - 所有玩家停牌,开始结算
12:35:33.536 [main] INFO c.z.blackjack.core.CasinoTest - 甲 下注 40.0 赢家为PUSH 最后余额:400.0
12:35:33.593 [main] INFO c.z.blackjack.core.CasinoTest - 乙 下注 20.0 赢家为DEALER 最后余额:380.0
12:35:33.594 [main] INFO c.z.blackjack.core.CasinoTest - 庄家丙 最后余额:420.0
12:35:33.594 [main] WARN c.z.blackjack.core.CasinoTest - **结束