麻将胡牌算法(遍历+剪枝)

简介

这个算法采用以遍历为基础的方法对胡牌进行搜索,并加入了一定的剪枝策略。没有对算法复杂度和运行时间进行测试,但整体运行速度基本可观,基本剪去了大部分无用的搜索。这个算法是部署在我的个人网站上的,有兴趣的童鞋可以到我的个人网站去体验一下。因为框架原因,所以我使用TypeScript语言编写的,用JavaScript语言的童鞋可以稍加修改后服用。下面介绍算法的实现逻辑和代码。

麻将想要胡牌的话首先当前手中牌数肯定是除三余一的,也就是可能是1,4,7,10,13张牌。然后再加上一张胡牌的话总张数就是除三余二的,也就是可能是2,5,8,11,14张牌。除了十三幺七小对这两种特殊牌型外,胡牌的基本规则就是总牌数能够形成AA+mABC+nAAA这种牌型。也就是必须得有一个AA型牌型(叫做将),剩余的牌就可以每三个分为一组(此后将ABC与AAA型统称为三元组),然后检测是否能拆解成m个AAA和n个ABC牌型就可以判断是否能胡牌。

麻将胡牌算法及代码

1. 方法引入

该算法共引入了两个方法,分别为 deepClonecountItemInArray 。其中 deepClone 可以实现数组的深复制,由于我使用的vant框架带有这个函数便直接使用了,使用其他的深复制方法代替也可以。 countItemInArray 是自己封装的一个方法,用以统计一个元素在数组中出现的次数,后续计算胡牌需要用到,代码如下

import {deepClone} from "vant/es/utils/deep-clone";
import {countItemInArray} from "@/utils";

//from @/utils
export const countItemInArray = (array: Array<any>, item: any): number => {
    let count = 0
    for (let index = 0; index < array.length; index++) {
        if (item == array[index]) count++
    }
    return count
}

2. 类型定义

2.1 牌定义

将筒从一筒到九筒用数组分别定义为[11, 12, 13, 14, 15, 16, 17, 18, 19]
将条从一条到九条用数组分别定义为[21, 22, 23, 24, 25, 26, 27, 28, 29]
将萬从一萬到九萬用数组分别定义为[31, 32, 33, 34, 35, 36, 37, 38, 39]
将东、南、西、北、中、發、白,用数组分别定义为[41, 42, 43, 44, 45, 46, 47]
并将所有的牌按顺序构成数组,方便后面索引牌的特征使用

export const allTiles = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 46, 47]

2.2 牌特征定义

定义牌的特征结构,结构包括是否能和周边的牌联系( isContactNear )、能够联系的牌( contactTiles )、牌的图片路径( imagePath ),方便后续计算使用

export type tileFeature = {
    isContactNear: boolean,
    contactTiles: number[],
    imagePath: string,
}

并将所有的牌按顺序构成牌特征数组,借助 allTiles 的索引来找到对应牌的特征

export const tilesFeature: tileFeature[] = [
    {isContactNear: true, contactTiles: [11, 11 + 1], imagePath: 'o1.png'},
    {isContactNear: true, contactTiles: [12 - 1, 12, 12 + 1], imagePath: 'o2.png'},
    {isContactNear: true, contactTiles: [13 - 1, 13, 13 + 1], imagePath: 'o3.png'},
    {isContactNear: true, contactTiles: [14 - 1, 14, 14 + 1], imagePath: 'o4.png'},
    {isContactNear: true, contactTiles: [15 - 1, 15, 15 + 1], imagePath: 'o5.png'},
    {isContactNear: true, contactTiles: [16 - 1, 16, 16 + 1], imagePath: 'o6.png'},
    {isContactNear: true, contactTiles: [17 - 1, 17, 17 + 1], imagePath: 'o7.png'},
    {isContactNear: true, contactTiles: [18 - 1, 18, 18 + 1], imagePath: 'o8.png'},
    {isContactNear: true, contactTiles: [19 - 1, 19], imagePath: 'o9.png'},
    {isContactNear: true, contactTiles: [21, 21 + 1], imagePath: 'l1.png'},
    {isContactNear: true, contactTiles: [22 - 1, 22, 22 + 1], imagePath: 'l2.png'},
    {isContactNear: true, contactTiles: [23 - 1, 23, 23 + 1], imagePath: 'l3.png'},
    {isContactNear: true, contactTiles: [24 - 1, 24, 24 + 1], imagePath: 'l4.png'},
    {isContactNear: true, contactTiles: [25 - 1, 25, 25 + 1], imagePath: 'l5.png'},
    {isContactNear: true, contactTiles: [26 - 1, 26, 26 + 1], imagePath: 'l6.png'},
    {isContactNear: true, contactTiles: [27 - 1, 27, 27 + 1], imagePath: 'l7.png'},
    {isContactNear: true, contactTiles: [28 - 1, 28, 28 + 1], imagePath: 'l8.png'},
    {isContactNear: true, contactTiles: [29 - 1, 29], imagePath: 'l9.png'},
    {isContactNear: true, contactTiles: [31, 31 + 1], imagePath: 'w1.png'},
    {isContactNear: true, contactTiles: [32 - 1, 32, 32 + 1], imagePath: 'w2.png'},
    {isContactNear: true, contactTiles: [33 - 1, 33, 33 + 1], imagePath: 'w3.png'},
    {isContactNear: true, contactTiles: [34 - 1, 34, 34 + 1], imagePath: 'w4.png'},
    {isContactNear: true, contactTiles: [35 - 1, 35, 35 + 1], imagePath: 'w5.png'},
    {isContactNear: true, contactTiles: [36 - 1, 36, 36 + 1], imagePath: 'w6.png'},
    {isContactNear: true, contactTiles: [37 - 1, 37, 37 + 1], imagePath: 'w7.png'},
    {isContactNear: true, contactTiles: [38 - 1, 38, 38 + 1], imagePath: 'w8.png'},
    {isContactNear: true, contactTiles: [39 - 1, 39], imagePath: 'w9.png'},
    {isContactNear: false, contactTiles: [41], imagePath: 'fe.png'},
    {isContactNear: false, contactTiles: [42], imagePath: 'fs.png'},
    {isContactNear: false, contactTiles: [43], imagePath: 'fw.png'},
    {isContactNear: false, contactTiles: [44], imagePath: 'fn.png'},
    {isContactNear: false, contactTiles: [45], imagePath: 'fz.png'},
    {isContactNear: false, contactTiles: [46], imagePath: 'ff.png'},
    {isContactNear: false, contactTiles: [47], imagePath: 'fb.png'},
]

3. 计算胡牌

将当前手中的牌排序后输入胡牌计算算法( mahjongCalculate 函数输入的牌已经是经过排序的),首先对手牌检测是否为十三幺和七小对这种特殊牌型,若不是特殊牌型则将其输入普通牌型胡牌计算算法得到胡牌。

export const mahjongCalculate = (tiles: number[]): number[] => {
    let checkThirteenYaoResult = checkThirteenYao(deepClone(tiles))
    if (checkThirteenYaoResult.length > 0) {
        return checkThirteenYaoResult
    }
    let checkSevenPairResult = checkSevenPair(deepClone(tiles))
    if (checkSevenPairResult.length) {
        return checkSevenPairResult
    }
    return checkCommon(deepClone(tiles))
}

3.1 检测十三幺牌型

十三幺牌型为固定牌型,胡牌也是固定胡牌,检测非常方便,只要检测输入手牌是否和十三幺牌型一致即可

const checkThirteenYao = (tiles: number[]): number[] => {
    const thirteenYao: number[] = [11, 19, 21, 29, 31, 39, 41, 42, 43, 44, 45, 46, 47]
    if (tiles.toString() == thirteenYao.toString()) {
        return thirteenYao
    } else {
        return []
    }
}

3.2 检测七小对牌型

七小对牌型也相对较为固定,牌型为6AA+B,胡牌为B。这里采用双指针法来遍历手牌,前后指针始终错位1张牌。如果检测到配对则指针全部前进两张牌,若未配对则指针全部前进一个并记录下胡牌。最终对配对数和胡牌数加以校验即可得到结果

const checkSevenPair = (tiles: number[]): number[] => {
    let result: number[] = []
    if (tiles.length < 13) return result
    tiles.push(0)
    let pairCount = 0
    let startPoint = 0
    let endPoint = 1
    let huTiles: number[] = []
    while (endPoint < 14) {
        if (tiles[startPoint] == tiles[endPoint]) {
            pairCount++
            startPoint += 2
            endPoint += 2
        } else {
            huTiles.push(tiles[startPoint])
            startPoint += 1
            endPoint += 1
        }
    }
    if (pairCount == 6 && huTiles.length == 1) {
        result = huTiles
    }
    return result
}

3.3 检测普通牌型胡牌

普通牌型胡牌计算算法需要输入当前手牌,根据当前手牌检测所有可能的胡牌,并逐个判断可能的胡牌是否为真的胡牌,若是真的胡牌则加入胡牌结果并最终返回结果。

const checkCommon = (tiles: number[]): number[] => {
    let possibleHuTiles: number[] = []
    for (let i = 0; i < tiles.length; i++) {
        let currentTile = tiles[i]
        let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
        for (let j = 0; j < currentPossibleHuTiles.length; j++) {
            let currentPossibleHuTile = currentPossibleHuTiles[j]
            if (!possibleHuTiles.includes(currentPossibleHuTile)) {
                possibleHuTiles.push(currentPossibleHuTile)
            }
        }
    }

    let huTiles: number[] = []
    for (let i = 0; i < possibleHuTiles.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.push(possibleHuTiles[i])
        checkTiles.sort((a, b) => (a - b))
        if (checkIsHu(checkTiles)) {
            huTiles.push(possibleHuTiles[i])
        }
    }
    return huTiles
}
3.3.1 检测所有可能的胡牌

所有可能的胡牌一定是每张手牌的可以联系到的牌的总和(以此为条件进行初步的剪枝),每张牌可以联系到的牌可以从 tilesFeature 中当前牌的 contactTiles 获得。构建一个数组用来存放可能的胡牌结果,遍历当前手牌,对于当前牌可以联系到的牌,若不在可能的胡牌数组中则将其添加,以此来避免重复添加

let possibleHuTiles: number[] = []
for (let i = 0; i < tiles.length; i++) {
    let currentTile = tiles[i]
    let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
    for (let j = 0; j < currentPossibleHuTiles.length; j++) {
        let currentPossibleHuTile = currentPossibleHuTiles[j]
        if (!possibleHuTiles.includes(currentPossibleHuTile)) {
            possibleHuTiles.push(currentPossibleHuTile)
        }
    }
}
3.3.2 检测可能的胡牌是否为真的胡牌

遍历所有可能的胡牌,将可能的胡牌添加到手牌中,添加后需要对手牌数组重新排序,随后送入胡牌检测器检测能否构成胡牌。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再添加可能的胡牌并送入胡牌计算器,以免修改原数组导致结果出错。

let huTiles: number[] = []
for (let i = 0; i < possibleHuTiles.length; i++) {
    let checkTiles = deepClone(tiles)
    checkTiles.push(possibleHuTiles[i])
    checkTiles.sort((a, b) => (a - b))
    if (checkIsHu(checkTiles)) {
        huTiles.push(possibleHuTiles[i])
    }
}
检测牌型能否为胡牌(核心)

经过添加可能的胡牌后,当前手牌张数应满足除三余二,即可能是2,5,8,11,14张牌。此时若需满足胡牌条件,则必定需要有且仅有一对将牌。所以我们需要找到手牌中可以作为将牌的牌( findPossiblePairs ),并遍历可能的将牌,将手牌中的将牌剔除,随后送去检测是否能进行三元组拆解,若能完成拆解则该牌型可以胡牌,否则不能胡牌。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再剔除将牌,以免修改原数组导致结果出错。

const checkIsHu = (tiles: number[]): boolean => {
    let possiblePairs: number[] = findPossiblePairs(tiles)
    for (let i = 0; i < possiblePairs.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.splice(checkTiles.indexOf(possiblePairs[i]), 2)
        if (checkTriples(checkTiles)) return true
    }
    return false
}
(1)查找可能的将牌

此处采用类似于检测七小对的算法,也即双指针法。若遇到配对则在查重之后加入可能的将牌数组。

const findPossiblePairs = (tiles: number[]): number[] => {
    let possiblePairs: number[] = []
    let startPoint = 0
    let endPoint = 1
    while (endPoint < tiles.length) {
        if (tiles[startPoint] == tiles[endPoint]) {
            if (!possiblePairs.includes(tiles[startPoint])) possiblePairs.push(tiles[startPoint])
            startPoint += 2
            endPoint += 2
        } else {
            startPoint += 1
            endPoint += 1
        }
    }
    return possiblePairs
}
(2)检测是否能每三张牌进行拆解(重点)

此处采用递归来迭代拆解三元组,消除一个三元组后进入更深一层的消除,递归终止条件为手牌为空。递归内容如下

  1. 检测递归终止条件
  2. 创建存放可能的三元组的数组,同时创建一个字符串类型的数组用以判断重复
  3. 从输入的手牌创建一个无重复牌的数组,以此来避免重复的牌反复送入检测三元组,实现剪枝的目的
  4. 循环将无重复牌的手牌数组的每个牌及原输入的手牌数组送入 findPossibleTriplesOfTile 进行检测,对每个检测到的结果判断不与存放可能的三元组数组重复后,将其添加到数组中,检测重复可以根据字符串型的数组和三元组转换成字符串进行判断。通过检测重复再次实现剪枝的目的。
  5. 遍历可能的三元组数组,剔除手牌中的该三元组,剔除后再次送入 checkTriples 函数进行递归。此处在遍历过程中需要先对手牌使用深复制( deepClone )复制一份,随后再剔除三元组,以免修改原数组导致结果出错。
const checkTriples = (tilesWithoutPair: number[]): boolean => {
    if (tilesWithoutPair.length == 0) return true
    let allPossibleTriples: Array<number[]> = []
    let allPossibleTriplesString: string[] = []
    let unRepeatTiles = Array.from(new Set(tilesWithoutPair))
    for (let i = 0; i < unRepeatTiles.length; i++) {
        let possibleTriplesOfTile: Array<number[]> = findPossibleTriplesOfTile(tilesWithoutPair, unRepeatTiles[i])
        for (let j = 0; j < possibleTriplesOfTile.length; j++) {
            let possibleTriple = possibleTriplesOfTile[j]
            if (!allPossibleTriplesString.includes(possibleTriple.toString())) {
                allPossibleTriples.push(possibleTriple)
                allPossibleTriplesString.push(possibleTriple.toString())
            }
        }
    }
    for (let i = 0; i < allPossibleTriples.length; i++) {
        let possibleTriple = allPossibleTriples[i]
        let checkTilesWithoutPair = deepClone(tilesWithoutPair)
        for (let k = 0; k < 3; k++) {
            checkTilesWithoutPair.splice(checkTilesWithoutPair.indexOf(possibleTriple[k]), 1)
        }
        if (checkTriples(checkTilesWithoutPair)) return true
    }
    return false
}
(3)查找当前牌在手牌中可能的拆解牌型(重点)

该算法需要输入待消除三元组的手牌和一张手牌中的牌来检测传入牌在手牌中可能构成的三元组,构建数组来存放传入牌可能的三元组。
若传入的牌能够和附近的牌联系(即筒、条、萬),则开始匹配ABC型的三元组。其中若牌面数字为1或9,则只有一种情况,即123或789。若牌面数字为2或8,则各有两种情况,即123和234或678和789。其他情况下则均有三种情况,即[n-2, n-1, n], [n-1, n, n+1], [n, n+1, n+2]
随后匹配AAA型的三元组,使用 countItemInArray 方法来统计传入牌在手牌中出现的次数,如果至少有三张,则加入可能的三元组数组。

const findPossibleTriplesOfTile = (tilesWithoutPair: number[], tile: number): Array<number[]> => {
    let possibleTriples: Array<number[]> = []
    if (tilesFeature[allTiles.indexOf(tile)].isContactNear) {
        if (tile % 10 == 1) {
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 2) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 8) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else if (tile % 10 == 9) {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        }
    }
    if (countItemInArray(tilesWithoutPair, tile) >= 3) possibleTriples.push([tile, tile, tile])
    return possibleTriples
}

完整代码

其中 deepClone 方法可以用其他深复制方法替换, countItemInArray 方法在方法引入出有详细定义。

import {deepClone} from "vant/es/utils/deep-clone";
import {countItemInArray} from "@/utils";

export const allTiles = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29,
    31, 32, 33, 34, 35, 36, 37, 38, 39, 41, 42, 43, 44, 45, 46, 47]
export type tileFeature = {
    isContactNear: boolean,
    contactTiles: number[],
    imagePath: string,
}
export const tilesFeature: tileFeature[] = [
    {isContactNear: true, contactTiles: [11, 11 + 1], imagePath: 'o1.png'},
    {isContactNear: true, contactTiles: [12 - 1, 12, 12 + 1], imagePath: 'o2.png'},
    {isContactNear: true, contactTiles: [13 - 1, 13, 13 + 1], imagePath: 'o3.png'},
    {isContactNear: true, contactTiles: [14 - 1, 14, 14 + 1], imagePath: 'o4.png'},
    {isContactNear: true, contactTiles: [15 - 1, 15, 15 + 1], imagePath: 'o5.png'},
    {isContactNear: true, contactTiles: [16 - 1, 16, 16 + 1], imagePath: 'o6.png'},
    {isContactNear: true, contactTiles: [17 - 1, 17, 17 + 1], imagePath: 'o7.png'},
    {isContactNear: true, contactTiles: [18 - 1, 18, 18 + 1], imagePath: 'o8.png'},
    {isContactNear: true, contactTiles: [19 - 1, 19], imagePath: 'o9.png'},
    {isContactNear: true, contactTiles: [21, 21 + 1], imagePath: 'l1.png'},
    {isContactNear: true, contactTiles: [22 - 1, 22, 22 + 1], imagePath: 'l2.png'},
    {isContactNear: true, contactTiles: [23 - 1, 23, 23 + 1], imagePath: 'l3.png'},
    {isContactNear: true, contactTiles: [24 - 1, 24, 24 + 1], imagePath: 'l4.png'},
    {isContactNear: true, contactTiles: [25 - 1, 25, 25 + 1], imagePath: 'l5.png'},
    {isContactNear: true, contactTiles: [26 - 1, 26, 26 + 1], imagePath: 'l6.png'},
    {isContactNear: true, contactTiles: [27 - 1, 27, 27 + 1], imagePath: 'l7.png'},
    {isContactNear: true, contactTiles: [28 - 1, 28, 28 + 1], imagePath: 'l8.png'},
    {isContactNear: true, contactTiles: [29 - 1, 29], imagePath: 'l9.png'},
    {isContactNear: true, contactTiles: [31, 31 + 1], imagePath: 'w1.png'},
    {isContactNear: true, contactTiles: [32 - 1, 32, 32 + 1], imagePath: 'w2.png'},
    {isContactNear: true, contactTiles: [33 - 1, 33, 33 + 1], imagePath: 'w3.png'},
    {isContactNear: true, contactTiles: [34 - 1, 34, 34 + 1], imagePath: 'w4.png'},
    {isContactNear: true, contactTiles: [35 - 1, 35, 35 + 1], imagePath: 'w5.png'},
    {isContactNear: true, contactTiles: [36 - 1, 36, 36 + 1], imagePath: 'w6.png'},
    {isContactNear: true, contactTiles: [37 - 1, 37, 37 + 1], imagePath: 'w7.png'},
    {isContactNear: true, contactTiles: [38 - 1, 38, 38 + 1], imagePath: 'w8.png'},
    {isContactNear: true, contactTiles: [39 - 1, 39], imagePath: 'w9.png'},
    {isContactNear: false, contactTiles: [41], imagePath: 'fe.png'},
    {isContactNear: false, contactTiles: [42], imagePath: 'fs.png'},
    {isContactNear: false, contactTiles: [43], imagePath: 'fw.png'},
    {isContactNear: false, contactTiles: [44], imagePath: 'fn.png'},
    {isContactNear: false, contactTiles: [45], imagePath: 'fz.png'},
    {isContactNear: false, contactTiles: [46], imagePath: 'ff.png'},
    {isContactNear: false, contactTiles: [47], imagePath: 'fb.png'},
]
export const getMahjongImageUrl = (tile: number) => {
    if (allTiles.includes(tile)) {
        return new URL(`/src/assets/service/mahjong/${tilesFeature[allTiles.indexOf(tile)].imagePath}`, import.meta.url).href
    } else {
        return new URL('/src/assets/service/mahjong/fk.png', import.meta.url).href
    }
}
export const mahjongCalculate = (tiles: number[]): number[] => {
    let checkThirteenYaoResult = checkThirteenYao(deepClone(tiles))
    if (checkThirteenYaoResult.length > 0) {
        return checkThirteenYaoResult
    }
    let checkSevenPairResult = checkSevenPair(deepClone(tiles))
    if (checkSevenPairResult.length) {
        return checkSevenPairResult
    }
    return checkCommon(deepClone(tiles))
}

const checkThirteenYao = (tiles: number[]): number[] => {
    const thirteenYao: number[] = [11, 19, 21, 29, 31, 39, 41, 42, 43, 44, 45, 46, 47]
    if (tiles.toString() == thirteenYao.toString()) {
        return thirteenYao
    } else {
        return []
    }
}
const checkSevenPair = (tiles: number[]): number[] => {
    let result: number[] = []
    if (tiles.length < 13) return result
    tiles.push(0)
    let pairCount = 0
    let startPoint = 0
    let endPoint = 1
    let huTiles: number[] = []
    while (endPoint < 14) {
        if (tiles[startPoint] == tiles[endPoint]) {
            pairCount++
            startPoint += 2
            endPoint += 2
        } else {
            huTiles.push(tiles[startPoint])
            startPoint += 1
            endPoint += 1
        }
    }
    if (pairCount == 6 && huTiles.length == 1) {
        result = huTiles
    }
    return result
}
const checkCommon = (tiles: number[]): number[] => {
    let possibleHuTiles: number[] = []
    for (let i = 0; i < tiles.length; i++) {
        let currentTile = tiles[i]
        let currentPossibleHuTiles = tilesFeature[allTiles.indexOf(currentTile)].contactTiles
        for (let j = 0; j < currentPossibleHuTiles.length; j++) {
            let currentPossibleHuTile = currentPossibleHuTiles[j]
            if (!possibleHuTiles.includes(currentPossibleHuTile)) {
                possibleHuTiles.push(currentPossibleHuTile)
            }
        }
    }

    let huTiles: number[] = []
    for (let i = 0; i < possibleHuTiles.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.push(possibleHuTiles[i])
        checkTiles.sort((a, b) => (a - b))
        if (checkIsHu(checkTiles)) {
            huTiles.push(possibleHuTiles[i])
        }
    }
    return huTiles
}
const checkIsHu = (tiles: number[]): boolean => {
    let possiblePairs: number[] = findPossiblePairs(tiles)
    for (let i = 0; i < possiblePairs.length; i++) {
        let checkTiles = deepClone(tiles)
        checkTiles.splice(checkTiles.indexOf(possiblePairs[i]), 2)
        if (checkTriples(checkTiles)) return true
    }
    return false
}
const findPossiblePairs = (tiles: number[]): number[] => {
    let possiblePairs: number[] = []
    let startPoint = 0
    let endPoint = 1
    while (endPoint < tiles.length) {
        if (tiles[startPoint] == tiles[endPoint]) {
            if (!possiblePairs.includes(tiles[startPoint])) possiblePairs.push(tiles[startPoint])
            startPoint += 2
            endPoint += 2
        } else {
            startPoint += 1
            endPoint += 1
        }
    }
    return possiblePairs
}
const checkTriples = (tilesWithoutPair: number[]): boolean => {
    if (tilesWithoutPair.length == 0) return true
    let allPossibleTriples: Array<number[]> = []
    let allPossibleTriplesString: string[] = []
    let unRepeatTiles = Array.from(new Set(tilesWithoutPair))
    for (let i = 0; i < unRepeatTiles.length; i++) {
        let possibleTriplesOfTile: Array<number[]> = findPossibleTriplesOfTile(tilesWithoutPair, unRepeatTiles[i])
        for (let j = 0; j < possibleTriplesOfTile.length; j++) {
            let possibleTriple = possibleTriplesOfTile[j]
            if (!allPossibleTriplesString.includes(possibleTriple.toString())) {
                allPossibleTriples.push(possibleTriple)
                allPossibleTriplesString.push(possibleTriple.toString())
            }
        }
    }
    for (let i = 0; i < allPossibleTriples.length; i++) {
        let possibleTriple = allPossibleTriples[i]
        let checkTilesWithoutPair = deepClone(tilesWithoutPair)
        for (let k = 0; k < 3; k++) {
            checkTilesWithoutPair.splice(checkTilesWithoutPair.indexOf(possibleTriple[k]), 1)
        }
        if (checkTriples(checkTilesWithoutPair)) return true
    }
    return false
}
const findPossibleTriplesOfTile = (tilesWithoutPair: number[], tile: number): Array<number[]> => {
    let possibleTriples: Array<number[]> = []
    if (tilesFeature[allTiles.indexOf(tile)].isContactNear) {
        if (tile % 10 == 1) {
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 2) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        } else if (tile % 10 == 8) {
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else if (tile % 10 == 9) {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
        } else {
            if (tilesWithoutPair.includes(tile - 2) && tilesWithoutPair.includes(tile - 1)) {
                possibleTriples.push([tile - 2, tile - 1, tile])
            }
            if (tilesWithoutPair.includes(tile - 1) && tilesWithoutPair.includes(tile + 1)) {
                possibleTriples.push([tile - 1, tile, tile + 1])
            }
            if (tilesWithoutPair.includes(tile + 1) && tilesWithoutPair.includes(tile + 2)) {
                possibleTriples.push([tile, tile + 1, tile + 2])
            }
        }
    }
    if (countItemInArray(tilesWithoutPair, tile) >= 3) possibleTriples.push([tile, tile, tile])
    return possibleTriples
}

效果展示

十三幺七小对九莲宝灯
十三幺七小对九莲宝灯
普通牌型1普通牌型2普通牌型3
普通牌型1普通牌型2普通牌型3

总结

这个算法核心思想就是借助递归来搜索胡牌,借助一些剪枝的操作来优化,从而快速地计算出胡牌。

做这个算法的起因是过年回家的时候和亲友小聚一下打打麻将,但奈何有初学的亲友容易看不明白胡牌。如果有人听牌了的话还方便帮忙看一下,但如果最早听牌的话就尴尬了,可能人都等困了他还是看不出来到底胡个啥。鉴于这种尴尬场景,再加上最近正好在学Web开发并在搭建自己的网站,就想到写这么一个胡牌计算器,方便亲友使用。本来想网上找个现成的胡牌算法的,但有的是没有东南西北中发白这些的,也有的注解不太清楚看起来挺头皮发麻的。最终还是决定自己写一个得了,毕竟胡牌规则并不复杂还算好写。

有童鞋想体验的话在我的个人网站上有我做的服务,非会员登录就可以使用啦,谢谢支持。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值