源码地址:https://github.com/tianlanlandelan/poker/tree/master/src/utils,喜欢请点星,谢谢大家。
- 牌面说明
- 54张牌,2~10、A、J、Q、K各4张(红桃、方块、黑桃、梅花),小王、大王各1张
- 从小到大排序依次为:3、4、5、6、7、8、9、10、J、Q、K、A、2、小王、大王
2.牌型说明
斗地主游戏牌型说明 |
||||
牌型 |
最大张数 |
最小张数 |
牌型规则说明 |
牌型大小说明 |
王炸 |
2 |
2 |
大小王 |
王炸最大 |
炸弹 |
4 |
4 |
4张相同牌 |
炸弹能压除王炸外的任意牌,炸弹间比较时,牌面大的大 |
单张 |
1 |
1 |
一张任意牌 |
只能压相同牌型,牌面大的大 |
对子 |
2 |
2 |
两张相同牌 |
只能压相同牌型,牌面大的大 |
三张 |
3 |
3 |
三张相同牌 |
只能压相同牌型,牌面大的大 |
三带一 |
4 |
4 |
三张相同牌+一张任意牌 |
只能压相同牌型,牌面大的大 |
三带一对 |
5 |
5 |
三张相同牌+两张任意牌 |
只能压相同牌型,牌面大的大 |
顺子 |
5 |
12 |
至少连续5个单张(最小为3,最大为A) |
只能压相同牌型,牌面大的大 |
连对 |
6 |
20 |
至少连续3个对子(最小为3,最大为A) |
只能压相同牌型,牌面大的大 |
飞机 |
6 |
18 |
至少连续2个三张(最小为3,最大为A) |
只能压相同牌型,牌面大的大 |
飞机带单张 |
8 |
20 |
至少连续2个三张(最小为3,最大为A)+与三张相同数量的单张 |
只能压相同牌型,牌面大的大 |
飞机带对子 |
10 |
20 |
至少连续2个三张(最小为3,最大为A)+与三张相同数量的对子 |
只能压相同牌型,牌面大的大 |
四带二 |
6 |
6 |
4张相同牌+2个单张 |
只能压相同牌型,牌面大的大 |
四带两对 |
8 |
8 |
4张相同牌+1个对子 |
只能压相同牌型,牌面大的大 |
3.生成牌逻辑
对局时首先要有一副牌,一副洗好的牌,为了实现这个目标,我们需要定义一个牌的对象,并想办法生成一副牌出来:
1.Poker.ts:定义poker对象
class Poker {
public constructor(id:number,orderValue:number) {
this.id = id;
this.orderValue = orderValue;
}
/**
* 牌面ID
*/
private id:number;
/**
* 牌大小排序值 0-14 数值越小,表示牌越大
*/
private orderValue:number;
这段代码定义一个对象,包含两个属性:id(牌面ID:这张牌是哪张牌)、orderValue(牌大小排序值:这张牌有多大)。
看到这里你可能会问:你这不能体现可视化啊,这张牌是红桃3还是梅花A呢?这就涉及到这张牌的图片属性(image)了,这张牌的image可以和id一样放在Poker对象里作为一个属性存在。
在这里我们使用另一种方式:将牌的图片文件以id命名(如:1.png、2.png),这样在使用时就可以直接使用id来指定它的image了。
2.PukerUtils.ts:定义牌面id,牌面id作为一张牌的唯一标示,可以和牌面的图片绑定
private static pukerIds:Array<number> = [
1,2,3,4,//A
5,6,7,8,//2
9,10,11,12,//3
13,14,15,16,//4
17,18,19,20,//5
21,22,23,24,//6
25,26,27,28,//7
29,30,31,32,//8
33,34,35,36,//9
37,38,39,40,//10
41,42,43,44,//J
45,46,47,48,//Q
49,50,51,52,//K
53,54];//King
这段代码定义一副牌的id,我们以A、2、3、4、5、6、7、8、9、10、J、Q、K小王、大王的顺序依次定义了54张牌的id。
3.PukerUtils.ts:定义牌的排序值
private static pukerSortValues:Array<number> = [
12,12,12,12,//A
13,13,13,13,//2
1,1,1,1,//3
2,2,2,2,//4
3,3,3,3,//5
4,4,4,4,//6
5,5,5,5,//7
6,6,6,6,//8
7,7,7,7,//9
8,8,8,8,//10
9,9,9,9,//J
10,10,10,10,//Q
11,11,11,11,//K
14,15//King
];
这段代码和上段代码相似,定义一副牌的大小排序值,定义的顺序要和pukerIds的顺序相同,1~15的数字分别对应牌3、4、5、6、7、8、9、10、J、Q、K、A、2、小王、大王的大小值。
4.PukerUtils.ts:随机生成一副扑克
public static getRandomPokers():Array<Poker>{
//定义Poker数组,这是最终要生成的一副扑克牌
let pokers:Array<Poker> =new Array<Poker>();
//选中的牌的下标
let index:number;
//将pukerIds重新复制一份作为选牌期间要处理的数组
let newArray:Array<number> = this.pukerIds.slice();
//选中的牌的id
let pukerIndex:number;
//选中的牌的id组成的数组
let array:Array<number> = [];
//遍历扑克牌Id数组
for(let i = 0 ; i < this.pukerIds.length ; i ++){
//随机生成一个小于要处理的数组长度的整数,作为选中的牌的下标
index = Math.floor(Math.random() * newArray.length);
//取数选中的id
pukerIndex = newArray[index];
//将选中的id放入数组
array.push(pukerIndex);
//将选剩下的牌重新组成一个数组
newArray = ArrayUtils.slice(newArray,0,index).concat(ArrayUtils.slice(newArray,index + 1,newArray.length));
}
//遍历选中的扑克id
for(let j = 0 ; j < array.length ; j ++){
//从pukerIds和pukerSortValues中取出对应的属性值组成一幅扑克牌
let poker:Poker = new Poker(this.pukerIds[array[j] - 1],this.pukerSortValues[array[j] -1])
pokers.push(poker);
}
console.log("已随机生成一副牌",pokers.toString());
return pokers;
}
前几段代码定义好了Poker对象以及一副牌的id和大小,通过这段代码我们实现了随机生成一副扑克牌的功能,每一行都有说明,通过这步操作,我们能得到一幅完整的扑克牌,接下来我们只需要将这些牌发给每个玩家就可以了。
4.牌型判断逻辑
对局中,当对方或我们出牌时,首先得知道选的几张牌是否是一手牌,不能乱出,这就涉及到了牌型的判断,在这里我们着重介绍下牌型的一些判断逻辑:
1. PukerType.ts:定义牌型对象
class PukerType {
/**
* 牌型
*/
private type:string;
/**
* 牌型大小排序值
*/
private sort:number;
… …
}
该对象包含两个属性:type(牌型:这是手什么牌)、sort(牌型大小:这手牌有多大)
2. PukerTypeUtils.ts:定义牌型
public static typeBoom:string = "typeBoom";
public static typeKingBoom:string = "typeKingBoom";
public static typeSingle:string = "typeSingle";
public static typePair:string = "typePair";
public static typeThree:string = "typeThree";
public static typeThreeSingle:string = "typeThreeSingle";
public static typeThreePair:string = "typeThreePair";
public static typeStraight:string = "typeStraight";
public static typeStraightPairs:string = "typeStraightPairs";
public static typePlane:string = "typePlane";
public static typePlane2Single:string = "typePlane2Single";
public static typePlane2Pairs:string = "typePlane2Pairs";
public static typeFour2Single:string = "typeFour2Single";
public static typeFour2Pairs:string = "typeFour2Pairs";
在这里统一定义了14中牌型的名称
3. PukerTypeUtils.ts:判断牌型
每种牌型有不同的判断逻辑,在这里列举其中一个牌型做一说明,其他所有牌型判断均在PukerTypeUtils.ts文件里。
所有牌型的判断思路都是:先将一手牌按牌大小进行排序,然后依照不同牌型的判断规则判断该手牌是否符合规则:如果符合规则,返回-1;如果符合规则,则返回该手牌的大小。
/**
* 判断一手牌是否是顺子
* 如果不是,返回-1; 如果是,返回该牌型的大小排序值
*/
public static isStraight(array:Array<Poker>):number{
if(array.length < 5 || array.length >12) return -1;//少于5张或多余12张,牌形不正确
if(array[0].getOrderValue() > PukerUtils.AValue) return -1;//如果最大的牌大于A,牌形不正确
for(let i = 0 ; i < array.length - 1; i ++){
if(array[i].getOrderValue() != array[i+1].getOrderValue() + 1) return -1;//后一张牌不比前一张牌递次小1,牌形不正确
}
return array[0].getOrderValue();
}
这段代码实现的是对一手牌是不是顺子进行判断:
首先:检查张数,顺子要求至少5张牌,最大呢,天顺:3到A,数一数,是12张,所以当这手牌少于5张或多于12张时就不对
其次:检查这手牌最大的牌,顺子的每张牌中最大的是A,所以,当这手牌最大的牌比A还要大时,就不用往下看了,直接Pass掉
再次:检查这手牌是不是连续的,如果是连续的,恭喜你,符合顺子的判断规则,返回这手牌中最大的那一张作为该手牌的大小值
5.牌型比较逻辑
牌有了,也能知道一手牌是什么牌了,我们列举几种情况:
- 你出一个顺子,我也出一个顺子,怎么判断我的顺子能不能压住你的顺子呢
- 你出一个顺子,我出一个连对,行不行
- 你出一个顺子,我出一个炸弹,行不行
- 你出一个5张的顺子,我出一个7张的顺子,行不行
经过以上思考,在牌型比较的过程中,我们把所有牌型分为两类:能压其他牌型的(炸弹和王炸)、不能压其他牌型的(其他所有牌型),这样我们的思路就有了:第一种牌型中,我们具体情况具体判断,在第二种牌型中,我们首先要判断这两手牌是不是同一种牌型。
牌型比较的逻辑都在PukerCompareUtils.ts文件中,具体代码如下:
/**
* 比较两手牌的大小
* a b ,当a大于b时返回true
* 王炸通吃
* 炸弹仅次于王炸
* 其他牌必须牌型相等才能比较
*/
public static comparePukers(a:Array<Poker>,b:Array<Poker>):boolean{
let aSort:Array<Poker> = PukerUtils.sortDescPokers(a);
let bSort:Array<Poker> = PukerUtils.sortDescPokers(b);
if(aSort.length < 1 || bSort.length < 1) return false;//空
if(PukerTypeUtils.isKingBoom(bSort) != -1) return false;//b是王炸
if(PukerTypeUtils.isKingBoom(aSort) != -1) return true;//a是王炸
if(PukerTypeUtils.isBoom(bSort) != -1){//b是炸弹
if(PukerTypeUtils.isBoom(aSort) != -1) return this.compareOne(aSort[0],bSort[0]);//a 也是炸弹
return false;//a 不是炸弹
}
if(PukerTypeUtils.isBoom(aSort) != -1) return true;//a 是炸弹
if(a.length != b.length) return false;//已经排除了炸弹的可能,长度不相等,不能比较
let aType:PukerType = PukerTypeUtils.getType(aSort);
let bType:PukerType = PukerTypeUtils.getType(bSort);
if(aType == null || bType == null || aType.getType() != bType.getType()) return false;//牌型3不相等
return aType.getSort() > bType.getSort();
}
/**
* 比较单牌的大小
*/
private static compareOne(a:Poker,b:Poker):boolean{
if(a.getOrderValue() > b.getOrderValue()){
return true;
}else{
return false;
}
}
6.自动出牌核心机制-选牌
单机游戏的系统出牌和联机游戏的托管功能都要用到系统自动出牌的机制,它的核心就是从你手里的一堆牌中挑出合适的牌型压住上家出的牌,在这里我们简单介绍一下:
思路是:
- 先判断上家的牌型,要是上家是王炸,那想都别想了,无解
- 如果不是王炸,那就还有机会,先判断出的啥牌型,然后从自己的牌中扒拉扒拉看有没有能压住这些牌的牌
- 如果没有,就不出;如果有,就弄他吖的
/** * TODO * 从一手牌中判断有没有比指定牌大的牌 * 这个方法是单机游戏的核心 * 需要持续优化 * aHandPuker 一手牌 * b 指定的牌形 */ public static seekRight(aHandPuker:Array<Poker>,pukerType:Array<Poker>):Array<Poker>{ let myPuker:Array<Poker> = PukerUtils.sortDescPokers(aHandPuker); let puker:Array<Poker> = PukerUtils.sortDescPokers(pukerType); console.log("seekRight",myPuker,puker); let bType = PukerTypeUtils.getType(pukerType); let mask:number = 0; let rightPuker:Array<Poker> = null; if(bType.getType() === PukerTypeUtils.typeKingBoom) rightPuker = null; else if(bType.getType() === PukerTypeUtils.typeSingle) rightPuker = this.seekSingle(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typePair) rightPuker = this.seekPairs(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeThree) rightPuker = this.seekThree(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeThreeSingle) rightPuker = this.seekThreeSingle(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeThreePair) rightPuker = this.seekThreePair(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeStraight) rightPuker = this.seekStraight(myPuker,bType.getSort(),puker.length); else if(bType.getType() === PukerTypeUtils.typeStraightPairs) rightPuker = this.seekStraightPairs(myPuker,bType.getSort(),puker.length); else if(bType.getType() === PukerTypeUtils.typePlane) rightPuker = this.seekPlane(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typePlane2Single) rightPuker = this.seekPlane2Single(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typePlane2Pairs) rightPuker = this.seekPlane2Pairs(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeFour2Single) rightPuker = this.seekFour2Single(myPuker,bType.getSort()); else if(bType.getType() === PukerTypeUtils.typeFour2Pairs) rightPuker = this.seekFour2Pairs(myPuker,bType.getSort()); //TODO 当玩家没有同类型的可出的牌时,在适当的时机判断是否有炸弹、王炸可以出 //选好可出的牌型后,从玩家的牌中按照牌形挑选牌面 if(rightPuker != null && rightPuker.length > 0){ return rightPuker }else{ return []; } }
具体的实现逻辑都在PukerSeekUtils.ts文件中,这个逻辑需要持续优化,比如什么时候该出什么时候不该出等,要做得智能一点,还需要很长的路要走。
源码地址:https://github.com/tianlanlandelan/poker/tree/master/src/utils,喜欢请点星,谢谢大家。