斗地主开发的数学思维

843 阅读3分钟

和朋友正在尝试编写一个斗地主的游戏(联机版本),改文章还会继续更新

斗地主由52张卡片组成,分别是梅花1K,方块1K,红心1K,黑桃1K,再加上大小王,我们把 J, Q, K 记为 11, 12, 13, 我们作如下定义

type PockerType = 
    'SPADE' | // 黑桃
    'HEART' | // 红心
    'DUAMOND' | // 方块
    'CLUB' | // 梅花
    'RED_JOKER' | // 大王
    'BLACK_JOKER' // 小王
    
interface Pocker{
    type: PockerType,
    value: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | null
}

牌型

我们把每种出牌规则成为牌型 (type),一般的不同的牌型不能混出(炸弹除外),同种牌型有大小之分 (value),在斗地主中满足这样的规则

  1. 每次出牌必须和上个牌型一致
  2. 如果前面没有牌型,则可以任意出一个牌型
  3. 炸弹可以拦截任意一种牌型,只有更价值(value)的炸弹能拦截炸弹
  4. 王炸可以拦截所有的牌

斗地主中牌型主要有

  1. 单张
  2. 对子
  3. 连对 - 2连对
  4. 连对 - 3连对
  5. 连对 - 4连对
  6. 连对 - 5连对
  7. 连对 - 6连对
  8. 3带1
  9. 3带2
  10. 飞机带一对
  11. 飞机带两张
  12. 顺子
  13. 炸弹
  14. 连炸
  15. 三连炸
  16. 王炸

我们给不同的排至一个权重值(typeValue),进行决策判断时,在不考虑炸弹的情况下,易得出当 oldTypeValue != typeValue 时,无法出牌,当 oldTypeValue -= typeValue 时,我们还需计算新的牌是否比旧的牌大

因此,我们的一个出牌动作里面,应该宝行两个变量

interface PockerValue {
    typeValue: string,
    value: number
}

不同牌型的 value 值计算方式不一定相同,同时,在判牌阶段,相同牌型,value 值更高的大过 value 低的

牌型判断

前面讲了牌型,对于任意序列,我们还应该有一定的机制分析出他是何种牌型,不同的判刑的判断规则并不一定相同,另外,上述的规则里面可能还有遗漏,或者在未来可能会有新的规则补充,因此,我们设计一个流水线性的牌型判断器,流水线上面挂着各种牌型的判断器,牌从流水线的一端流入,依次经过流水线处理,当处理单元成功命中牌型时,则流水线工作停止,结果抛出

需要注意的是,由于部分牌可能会满足多个牌型结果,因此再挂载处理单元的时候,要考虑到优先级的顺序关系,比如,炸弹的判断顺序会大于其他顺序,他应该在流水线的开头

import KingBOOM from './process/KingBOOM'
import SingleProcess from './process/SingleProcess'
const pipeline = [
    KingBOOM, // 王炸判断在前
    SingleProcess
]

export default function getPockerType(list: Pocker[]): PockerValue{
    
    for(let process of pipeline){
        const item = process.judge(list)
        if(item){
            return item
        }
    }
    
    throw new Error('错误的排列组合')
}

牌值判断

在牌型判断器的处理单元内部,处理单元除了要处理是否匹配牌型,还要准确输出该序列的value值,比如,一个处理器单元可能要这么编写

export default new class TypeSingle{
    judge(list: PockerType[]): PockerValue | false {
        if(list.length != 1){
            return false
        }
        
        const pocker = list[0]
        let val = pocker.value
        if(pocker.type == 'RED_JOKER'){
            val = 15
        }else if(pocker.type == 'BLACK_JOKER'){
            val = 14
        }
        
        return {
            typeValue: 'SINGLE',
            value: val
        }
    }
    
    test(oldPockerValue: PockerValue, newPockerValue: PockerValue){
        return oldPockerValue.value < newPockerValue.value;
    }
}

对于顺子,使用AABB方式记录,AA表示起始的扑克卡片,BB表示连张个数

export default new class TypeSingle{
    judge(list: PockerType[]): PockerValue | false {
        if(list.length < 5 || list.some(item => item.value == null)){
            return false
        }
        
        const temp = list.map(item => item.value)
            .sort((a, b) => a - b)
            
        for(let i=1; i<temp.length; i++){
            if(temp[i-1] + 1 != temp[i]){
                return false
            }
        }
        
        return {
            typeValue: 'SUNZI',
            value: temp[0] * 100 + temp.length - 1
        }
    }
    
    test(oldPockerValue: PockerValue, newPockerValue: PockerValue){
        return oldPockerValue.value < newPockerValue.value &&
            oldPockerValue.value % 100 == newPockerValue % 100;
    }
}

对于连对, 依然使用AABB

export default new class TypeSingle{
    judge(list: PockerType[]): PockerValue | false {
        if(list.length % 2 == 1 || list.some(item => item.value == null)){
            return false
        }
        
        const temp = list.map(item => item.value)
            .sort((a, b) => a - b)
            
        for(let i=0; i<temp.length; i+=2){
            if(temp[i] != temp[i+1]){
                return false
            }
        }
        
        return {
            typeValue: 'LIANDUI',
            value: temp[0] * 100 + temp.length - 1
        }
    }
    
    test(oldPockerValue: PockerValue, newPockerValue: PockerValue){
        return oldPockerValue.value < newPockerValue.value &&
            oldPockerValue.value % 100 == newPockerValue % 100;
    }
}

出牌判断

出牌时,将新的排序列分析出牌型后,然后调用对应牌型的test方法,进行大小值的判断,如果返回真,则说明牌有小