日常生活中,我有一个非常浪费时间的爱好,那就是麻将。虽然没有经过系统的学习和训练,水平非常一般,但我还是乐此不疲。作为一个程序员,在打麻将之余,也总会不自觉地去思考麻将的和牌算法应该怎么去实现。今天就来谈一下我对这个问题的一些粗略想法。

麻将介绍

麻将,想必大家都不陌生,它是由饼子、万子、索子、字牌、三元牌组成。其中,饼子、万子、索子分别有数字1-9的9种牌,每个数字4张,共计108张。字牌由东、南、西、北组成,三元牌由白、发、中组成,每个也是4张,共计28张。在有些地方的玩法中,只包括饼子、万子、索子,会去掉字牌和三元牌。我们此处的介绍中,以现在比较流行的日麻规则为基础。日麻规则,包括了上述全部的136张牌。

借由国内知名日麻平台“雀魂”的一张图,来看一下所有的麻将牌种类。需要说明的是,除了上面说到的普通的牌之外,下图中还有额外的一张红5索、红5万、红5饼。这是日麻中的赤dora(宝牌),一张赤dora在和牌的时候就可以将手牌的价值提高一番。

1

麻将的和牌,是需要做成4面子+1雀头的牌型。雀头,由2张完全相同的牌组成,比如2张1饼、2张9万。面子,是顺子和刻子的统称。顺子,即3张连续的牌组合,比如1饼2饼3饼或是3万4万5万。刻子,是3张完全相同的牌组合,比如3张1饼或是3张东风。当然了,刻子也可以是4张相同的牌组合,比如4张5索,叫做杠子,可以认为是刻子的加强版。顺子只能由饼子、万子、索子组成,不能由字牌和三元牌组成,因为我们认为不同的字牌和三元牌之间没有连续性。

除了以上的规则之外,通常来说麻将还有两种额外的和牌规则。

一种是七对子,即最后手里的牌型可以组合成7个不同的对子。

还有一种叫做国士无双(国内通常叫做十三幺),即手里聚齐了1万9万1饼9饼1索9索东南西北白发中,再加上另外的以上提到的任意一张牌。

和牌算法

基于以上的规则,我们来构思判断是否可以和牌的算法。

为了下面行文的方面,我们将饼子称为p,万子称为m,索子成为s,则5饼则可以表示为5p,3万可以表示为3m,1索可以表示为1s,以此类推。字牌稍微有些麻烦,我们称之为z,按照东南西北白发中的顺序分别编号为1-7,即1z表示东风,7z表示中。其中,上文中提到的赤dora则分别别是为0p,0m和0s。

为了更好的判断顺子、刻子、雀头,我们可以将基于以上表示规则的麻将牌转化为数字表示。具体的规则如下:

  1. 万子表示为1-9.
  2. 饼子表示为11-19
  3. 索子表示为21-29
  4. 东西南北白发中按照顺序分别表示为31、33、35、37、39、41、43.
  5. 赤dora按照万子、饼子、索子,分别表示为5、15和25.

按照以上的规则来设计,保证了同种麻将牌之间数字是连续的,而不同种麻将牌之间则存在着数字的间隔,可以简化判断三张麻将牌是否为顺子的逻辑。

对于一手麻将牌,在进行是否和牌判断之前,先按照上述规则转换为数字表示,并进行排序。以下的说明,都是基于这个排过序的数字数组。

在上面的基本介绍中,我们说明了3种和牌牌型。七对子和国士无双判断逻辑十分简单,我们此处重点来说明普通和牌型,即4面子+1雀头型。

需要说明的是,本文种的算法默认不存在吃、碰、杠行为,手牌保持13 + 1张的状态。但本文描述的算法稍加改动即可适配,因为吃、碰等副露行为会使得手牌减少,算法调整为(4 - x)面子+1雀头型即可,x为副露次数。

以下为对算法流程的描述:

  1. 判断数组中是否包含对子,即两张一样的牌。如果没有,不可能和牌。有,则记录下来。
  2. 对步骤1中统计出的对子,从当前手牌中标记手牌使用情况,并分别执行以下逻辑:
    2.1 如当前未使用的手牌剩余为0,则返回和牌成功
    2.2 如当前剩余未使用手牌中的第1张手牌无法组成一组顺子或刻子,则执行步骤2.4。
    2.3 当前剩余未使用手牌中的第1张手牌,验证是否包含顺子。如包含顺子,标记手牌使用状态,然后重复执行2.1-2.4。
    2.4 将步骤2.3中标记已使用的顺子重新标记未未使用,判断未剩余手牌中的第1张是否可组成刻子,如不包含,则和牌失败,还原手牌状态,返回到步骤2中执行。如可组成,从手牌中将此刻子标记为已使用,然后重复执行2.1-2.4。
  3. 和牌成功,将在步骤2中返回成功。流程执行到步骤3,返回和牌失败。

算法实现

基于以上的算法流程,我们有如下的算法实现。为了编程上的方便,我们没有使用额外的数据结构来保存当前手牌的使用状态,而是简单执行了手牌的复制,并从中去除了已使用的手牌。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* win or not ?
*/

function canWin(majs, maj) {
if (double7(majs) || all19(majs)) return true;
const doubles = pickDouble(majs);
if (doubles.length === 0) {
return false;
}
for (let inx in doubles) {
const copy = majs.slice(0, majs.length);
copy.splice(doubles[inx], 2);
if (allTriple(copy)) {
return true;
}
}
return false;
}

其中,double7函数判断是否为7对子和牌,all19为判断是否为国士无双和牌。实现分别如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 七对子
* @param majs
* @returns {boolean}
*/
function double7(majs) {
const mapper = {};
for (let inx in majs) {
let val = majs[inx];
if (mapper.hasOwnProperty(val)) {
mapper[val] = mapper[val] + 1;
} else {
mapper[val] = 1;
}
}
for (let key in mapper) {
if (mapper[key] !== 2) return false;
}
return true;
}

/**
* 国士无双
* 打表实现
* @param majs
*/
function all19(majs) {
const str = majs.toString();
const array = [1, 9, 11, 19, 21, 29, 31, 33, 35, 37, 39, 41, 43];
for (let inx = 0; inx < 13; inx++) {
array.splice(inx, 0, array[inx]);
if (array.toString() === str) {
return true;
}
array.splice(inx, 1);
}
return false;
}

pickDouble函数为从手牌中统计出已有的对子。编程方便的考虑,此处记录的是对子起始的手牌序号,方便后面从手牌中删除此对子。

1
2
3
4
5
6
7
8
9
10
11
12
13
function pickDouble(majs) {
const indexes = [];
let inx = 0, length = majs.length;
while (inx < length - 1) {
if (majs[inx] === majs[inx + 1]) {
indexes.push(inx);
inx = inx + 2;
} else {
inx = inx + 1;
}
}
return indexes;
}

allTriple函数为逻辑的核心部分,作用在于判断剩下的手牌是否可完整组合成为面子牌型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function allTriple(hands) {
if (hands.length === 0) {
return true;
}

if (hands.length < 3) {
return false;
}

const val = hands[0];
let copy;
if (hands[1] === val && hands[2] === val) {
copy = hands.slice(0, hands.length);
copy.splice(0, 3);
const ret = allTriple(copy);
if (ret) return true;
}
if (hands.indexOf(val + 1) > 0 && hands.indexOf(val + 2) > 0) {
let inx1 = hands.indexOf(val + 1);
let inx2 = hands.indexOf(val + 2);
copy = hands.slice(0, hands.length);
copy.splice(0, 1);
copy.splice(inx1 - 1, 1);
copy.splice(inx2 - 2, 1);
return allTriple(copy);
}
return false;
}



👨‍💻本站使用 Stellar 主题创建

📃本"页面"访问 次 | 👀总访问 次 | 🥷总访客

⏱️本站已运行 小时