记法

我们把牌统一称作 tile, 不使用 card, pai, 或 hai 等叫法。

牌的花色与数值分别称作 suitvalue, 不使用 color, set, 或 number 等叫法。

万、饼、索、风牌、三元牌分别用 M, P, S, F, Y 表示。 FY 可以统称为 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 定义了两个类,T34T37

一般来讲,麻将牌一共有34种。 如果我们把赤5和黑5严格区分,麻将牌就一共有37种——这也正是 T34T37的区别,后者严格区分赤宝牌。

T34是对一个int的简单包装,通过一系列成员函数确保运算有麻将意义。 T37T34继承,多出一个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 判断t1t2是否依次构成边张或两面, 例如 8m 与 9m。 使用竖线符号是因为这条竖线可以看成两张牌之间的缝隙。
  • 重载操作符 ||:
    表达式 t1 || t2 判断t1t2是否依次构成嵌张, 例如 7m 与 9m。 使用双竖线符号是因为这两条竖线可以看成两张牌之间的缺口。
  • 重载操作符 ^:
    表达式 t1 ^ t2 判断t1t2是否依次互为筋牌, 例如 1s 与 4s。 之所以用异或符号,是因为筋牌之间存在互斥关系,与异或类似。

T34对象是不可修改(immutable) 的,除了赋值以外都是const方法。

T37

T37严格区分赤牌与黑牌。

T37T34继承,但两者都不包含虚函数,这是出于以下几点考虑:

  • T37是一个T34,把T37传给T34总是安全且有意义的,所以继承;
  • 我们不需要多态语义。T37传给T34以后,只需要按T34对待;
  • 省略虚函数表以后,T34占用空间就与int无区别,可不计传值成本。

因为T34的析构函数不是虚的,我们不能用T34类型的指针取T37的所有权。 (其实整个Libsaki里也没出现过T34的指针)

T37的构造函数与T34的类似,只是其中一些多出一个区分是否为赤牌的参数。

T34T37的转换是向下转型, 是一个增加信息量的过程(将一张数牌5细化为赤牌或黑牌), 具有相当的危险性。 因此,我们不提供把T34转换成T37的方便写法。 如果想要把T34转换成T37,先想清楚自己是不是搞错了什么, 如果确信自己应该这么做,再使用T37(t34.id34())的写法。

与之相反,从T37T34的转换是安全的,直接传值即可。

有一点需注意,==操作符并没有重写, 因此 0_m == 5_m 返回为true。 如果想在比较时区分赤牌与黑牌,使用looksSame()方法。

T34代表一个理论上的、计算上的概念, 而T37则代表一张看得见、摸得着的麻将牌的实体。 为了强调这点,代码中T34会优先考虑传值,而T37会优先考虑传引用。