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

2023-05-16

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

  • 简介
  • 麻将胡牌算法及代码
    • 1. 方法引入
    • 2. 类型定义
      • 2.1 牌定义
      • 2.2 牌特征定义
    • 3. 计算胡牌
      • 3.1 检测十三幺牌型
      • 3.2 检测七小对牌型
      • 3.3 检测普通牌型胡牌
        • 3.3.1 检测所有可能的胡牌
        • 3.3.2 检测可能的胡牌是否为真的胡牌
          • 检测牌型能否为胡牌(核心)
            • (1)查找可能的将牌
            • (2)检测是否能每三张牌进行拆解(重点)
            • (3)查找当前牌在手牌中可能的拆解牌型(重点)
  • 完整代码
  • 效果展示
  • 总结

简介

这个算法采用以遍历为基础的方法对胡牌进行搜索,并加入了一定的剪枝策略。没有对算法复杂度和运行时间进行测试,但整体运行速度基本可观,基本剪去了大部分无用的搜索。这个算法是部署在我的个人网站上的,有兴趣的童鞋可以到我的个人网站去体验一下。因为框架原因,所以我使用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开发并在搭建自己的网站,就想到写这么一个胡牌计算器,方便亲友使用。本来想网上找个现成的胡牌算法的,但有的是没有东南西北中发白这些的,也有的注解不太清楚看起来挺头皮发麻的。最终还是决定自己写一个得了,毕竟胡牌规则并不复杂还算好写。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

麻将胡牌算法(遍历+剪枝) 的相关文章

随机推荐

  • Kaiming He论文阅读笔记二——Plain Vision Transformer Backbones for Object Detection

    Kaiming在2022年发表了一篇Exploring Plain Vision Transformer Backbones for Object Detection 文章的主要目的是追求一种包含较少诱导偏差的主干 原因在于 xff1a 允
  • 通过Python将mp4视频文件转为动画gif

    通过Python将mp4视频文件转换为动画gif有几种方法 xff0c 如imageio moviepy Pillow 这里使用imageio 43 opencv进行转换 通过conda创建虚拟环境Python Test xff0c 将终端
  • 关系型数据库种类

    常见主流数据库分类 xff1a DB2 Oracle Informix Sybase SQL Server PostgreSQL mySQL Access数据库 FoxPro数据库 Teradata 1 IBM 的DB2 DB2是IBM著名
  • 非替换元素和替换元素

    元素是文档结构的基础 xff0c 在css里面 xff0c 每个元素生成了包含内容的框 xff08 box xff09 大家都叫 盒子 但是不同的元素显示方式是不同的 xff0c 有占据一整行的 xff0c 有水平一个挨着一个的 比如 xf
  • Kaiming He论文阅读笔记三——Simple Siamese Representation Learning

    Kaiming He大神在2021年发表的Exploring Simple Siamese Representation Learning xff0c 截至目前已经有963的引用 xff0c 今天我们就一起来阅读一下这篇自监督学习论文 Si
  • 从零开始使用Realsense D435i运行VINS-Mono

    从零开始使用Realsense D435i运行VINS Mono 从零开始使用Realsense D435i运行VINS Mono xff08 1 xff09 安装测试librealsense SDK 2 0 xff08 2 xff09 安
  • VINS-Mono关键知识点总结——边缘化marginalization理论和代码详解

    VINS Mono关键知识点总结 边缘化marginalization理论和代码详解 VINS Mono关键知识点总结 边缘化marginalization理论和代码详解1 边缘化理论1 1 为什么要进行边缘化操作 xff1f 1 2 怎样
  • linux安装clang和clang-format

    EPEL网站提供了clang的RPM安装包 xff0c 所以要想在cnetOs安装clang xff0c 首先需要安装EPEL包 xff1a sudo yum install epel release 接下来安装 clang sudo yu
  • docker学习记录(2)——在 Ubuntu 16.04 上升级 Docker CE

    以root用户为例 apt get update apt get remove docker docker engine docker ce docker io y 确保卸载干净 wget qO https get docker com s
  • vins-mobile

    最近项目需求 xff0c 需要在新版ios设备上面配置vins mobile xff0c 但是vins mobile采用oc代码 xff0c 需要将其迁移到swift vins对时间戳要求比较严格 xff0c 原版修改了opencv源码 x
  • ROS入门之话题消息的定义与使用

    1 定义msg文件 xff1a 在catkin ws src learning topic文件下新建msg文件夹并在文件夹下新建Person msg文件 msg文件中代码如下 xff1a string name uint8 sex uint
  • git为什么会有冲突

    看了百度很多回答 xff0c 觉得和实操有点出入 xff0c 记录一下个人理解 结论 xff1a 冲突的产生就是各分支修改的文件版本不一致 xff08 远程冲突同理 xff09 例 xff1a 分支 m 和分支 d 都有一个相同文件 61
  • 视觉SLAM十四讲:运动方程

    SLAM xff1a 同时定位和建图 xff08 Simultaneous Localization and Mapping xff09 希望机器人从未知环境的未知地点出发 xff0c 在运动过程中通过重复观测到的地图特征 xff08 比如
  • NeRF简介及nerf-pytorch的使用

    NeRF全称为Neural Radiance Field 神经辐射场 是2020年发表的论文 xff0c 论文名字为 NeRF Representing Scenes as Neural Radiance Fields for View S
  • SLAM如何定位与建图

    SLAM xff1a 同时定位和建图 xff08 Simultaneous Localization and Mapping xff09 机器人从未知环境中的未知地点出发 xff0c 在运动过程中通过重复观测到的地图特征 xff08 比如
  • OpenMV——串口通信+发送中心位置

    串口通信 OpenMV本质还是一个单片机 xff0c 可以通过调用pyb中的UART使用串口通信 xff0c 注意发送的数据类型为字符串 xff0c 可以通过json dumps 进行字符串转换 span class token keywo
  • liunx下rpm包mysql安装脚本

    目录 文章目录 前言 一 mysqlshell安装脚本 二 xff0c mysql 配置文件 前言 liunx下mysql安装脚本shell脚本 采用的版本时 mysql 5 7 28 xff0c rpm安装方式 shell安装脚本 xff
  • setTimeout与setInterval的坑以及优缺点

    转自 xff1a setTimeout与setInterval的坑以及优缺点 找寻的千寻 博客园 setInterval和setTimeout的缺陷和优势分析 F ZERO F的博客 CSDN博客 settimeout缺点 说到setTim
  • 登录功能app端的建立与实现

    选择使用Android文件的一些主要包装命名搭建 1 Layout存放布局界面的地方 xff0c values是存放图片和颜色 字体等 2 manifests体现层 61 61 代码 3 执行界面打开 lt application lt 登
  • 麻将胡牌算法(遍历+剪枝)

    麻将胡牌算法 xff08 遍历 43 剪枝 xff09 简介麻将胡牌算法及代码1 方法引入2 类型定义2 1 牌定义2 2 牌特征定义 3 计算胡牌3 1 检测十三幺牌型3 2 检测七小对牌型3 3 检测普通牌型胡牌3 3 1 检测所有可能