麻将牌
记法
我们把牌统一称作 tile, 不使用 card, pai, 或 hai 等叫法。
牌的花色与数值分别称作 suit 和 value, 不使用 color, set, 或 number 等叫法。
万、饼、索、风牌、三元牌分别用 M, P, S, F, Y 表示。 F 和Y 可以统称为 Z。 具体来讲,34张牌表示如下:
1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 9m
1p, 2p, 3p, 4p, 5p, 6p, 7p, 8p, 9p
1s, 2s, 3s, 4s, 5s, 6s, 7s, 8s, 9s
1f, 2f, 3f, 4f
1y, 2y, 3y
1f ~ 4f 分别代表东南西北,1y ~ 3y 分别代表白发中。 F 与 Y 的记法只用于代码内部,显示到UI上时要转换成汉字。
我们用 0m, 0p, 0s 表示赤宝牌。 为表达方便,我们把「不是赤5的数牌5」称作「黑5」。 5m, 5p, 5s 可以指代黑5,也可以泛指所有的数牌5,具体含义由前后文决定。
麻将牌 C++ 类
libsaki/unit/tile.h
定义了两个类,T34
和 T37
。
一般来讲,麻将牌一共有34种。
如果我们把赤5和黑5严格区分,麻将牌就一共有37种——这也正是
T34
和T37
的区别,后者严格区分赤宝牌。
T34
是对一个int
的简单包装,通过一系列成员函数确保运算有麻将意义。
T37
从T34
继承,多出一个mAkadora
字段以区分赤黑。
T34
T34
的唯一成员字段是mId34
,
其取值范围在 0 到 33 之间,对应 1m, 2m, 3m, …, 3y。
我们可以直接通过一个 0 到 33 之间的整数来构建T34
对象,
并通过调用suit()
或val()
方法可获得一只T34
对象的花色与数值。
T34 tile(0); // 整数0代表1m
Suit s = tile.suit(); // 此时s的值为Suit::M
int v = tile.val(); // 此时v的值为1
除了使用 0 ~ 33 的整数,
我们还可以直接通过花色和数值来构建T34
对象:
T34 ta(1); // 0是1m, 所以1就是2m
T34 tb(Suit::M, 2); // 也可以通过花色和数值直接构建2m
assert(ta == tb); // 此时ta和tb相等,都是2m
我们还有一种通过 C++11 的用户自定义字面值实现的写法,
可以很直观地创建T34
对象:
using namespace tiles34; // 解禁神奇写法
T34 ta = 3_p; // 等同于 T34(Suit::P, 3)
T34 tb = 2_y; // 等同于 T34(Suit::Y, 2)
现在我们知道怎么创建一张牌了。下面看如何使用一张牌。
T34
有一个常用方法id34()
,返回值就是代表这张牌的 0 ~ 33 的整数。
T34 t = 3_m;
int i = t.id34(); // 此时i为2
我们之所以用 0 ~ 33 的连续整数代表麻将牌, 是因为这些整数可以直接用作数组下标。 牌只有 34 种,如果使用更复杂的数据结构,指针和缓存的代价未必收得回来。 而使用一个长度为 34 的数组记录一组麻将牌, 不但增删改查都是常数复杂度,而且还支持遍历。
T34
的多数方法都很直白,看代码就能知道是干什么的。
下面重点解释一些不太直观的:
- 重载操作符
%
:
表达式t1 % t2
判断t2
是否为被t1
所指示的宝牌。 我们使用%
符号是因为宝牌指示关系存在周期性,这点与取余运算类似。 - 重载操作符
|
:
表达式t1 | t2
判断t1
与t2
是否依次构成边张或两面, 例如 8m 与 9m。 使用竖线符号是因为这条竖线可以看成两张牌之间的缝隙。 - 重载操作符
||
:
表达式t1 || t2
判断t1
与t2
是否依次构成嵌张, 例如 7m 与 9m。 使用双竖线符号是因为这两条竖线可以看成两张牌之间的缺口。 - 重载操作符
^
:
表达式t1 ^ t2
判断t1
与t2
是否依次互为筋牌, 例如 1s 与 4s。 之所以用异或符号,是因为筋牌之间存在互斥关系,与异或类似。
T34
对象是不可修改(immutable) 的,除了赋值以外都是const
方法。
T37
T37
严格区分赤牌与黑牌。
T37
从T34
继承,但两者都不包含虚函数,这是出于以下几点考虑:
T37
是一个T34
,把T37
传给T34
总是安全且有意义的,所以继承;- 我们不需要多态语义。
T37
传给T34
以后,只需要按T34
对待; - 省略虚函数表以后,
T34
占用空间就与int
无区别,可不计传值成本。
因为T34
的析构函数不是虚的,我们不能用T34
类型的指针取T37
的所有权。
(其实整个Libsaki里也没出现过T34
的指针)
T37
的构造函数与T34
的类似,只是其中一些多出一个区分是否为赤牌的参数。
从T34
到T37
的转换是向下转型,
是一个增加信息量的过程(将一张数牌5细化为赤牌或黑牌),
具有相当的危险性。
因此,我们不提供把T34
转换成T37
的方便写法。
如果想要把T34
转换成T37
,先想清楚自己是不是搞错了什么,
如果确信自己应该这么做,再使用T37(t34.id34())
的写法。
与之相反,从T37
到T34
的转换是安全的,直接传值即可。
有一点需注意,==
操作符并没有重写,
因此 0_m == 5_m
返回为true
。
如果想在比较时区分赤牌与黑牌,使用looksSame()
方法。
T34
代表一个理论上的、计算上的概念,
而T37
则代表一张看得见、摸得着的麻将牌的实体。
为了强调这点,代码中T34
会优先考虑传值,而T37
会优先考虑传引用。