这两天由于拿到了一个麻将图包,运用了一些编译原理课上学的知识,动手实现了一个简单的麻将图片生成器.

项目地址在这里,同时也在自己的服务器上部署了一个demo.

这里简单聊聊这个小工具.

名字

Mahjong,是麻将这个游戏的一种英文表示方法,由于雀魂叫majsoul,这里就只用到j为止,加上图片image的im,就成了mahjim, 这个命名可以说是很没有新意了.

思路

基础

首先要解决的就是如何用字符串表示麻将的问题.天凤有比较完整的记牌方式:

  • m=萬子, p=筒子, s=索子, z=字牌, 0=赤
  • 一般形=4面子1雀頭 / 標準形=一般形+七対形+国士形
  • ツモはその時点で使用していない牌をランダムに選択します
  • 有効牌をクリックすると打牌後にその牌をツモ牌として表示します
  • (n*3+2)枚で開始:(n*3+2)枚目をツモ牌として表示
  • (n*3+1)枚で開始:ツモはページのロード時に毎回変化
  • 和了役の判定はありません
  • 暗槓はできません

但是这套规则只是天风用来计算牌效的小程序使用的,并不能用来表示由于吃碰杠出现的牌横置情况.

而且需要注意的是,天凤使用的这一套标记法,面对字牌并不那么方便,1~7z分别代表的是东南西北白发中,由于日麻和国内的三元牌顺序并不同,所以需要一定的适应.

以这个为基础,我们可以考虑在牌前面加上前缀来表示牌的情况,实际上对于输出图片这个需求而言,任何一张麻将一共就4种状态,正常竖置,横置,横置加杠,翻面.其中翻面我们可以考虑直接输出一张背面的图片,所以可以看作是一种特殊的牌.如果采用_来表示牌的横置,^表示牌加杠,那么就形成了我采取的这种方法来表示一张正常的牌:

pre+num+class

其中pre表示一个前缀,可以是_或者^,也可以没有,用来表示牌的摆放方式.num表示牌的点数,class用来表示牌的种类,可以是p,s,m,z.

但是每个牌都这么写就太烦了,我们需要一些简写的方式.

很容易想到的就是class可以合并,比如123s应该和1s2s3s是等价的,这样也不会有什么歧义.

字牌

可以看出,这个记牌方式对于字牌来说非常不友好,而且按照上面的想法进行了缩写之后,国标的春夏秋冬梅兰竹菊加进来以后,字牌的总数会超过10种.那么这样的缩写就会有问题了.

简单的解决方式就是直接用汉字来输入字牌,那么这样的话,类似中中中,白白白这种经常出现的组合又显得很不方便.所以我们也可以简单的想到一个这样的缩写:3中=中中中,仔细想想就会发现这个缩写和之前的数字牌表示方式是不冲突的.

风格

由于图包中的麻将区分国标和日麻,我们还需要一个额外的设置,来区分cn和jp,简单起见,我们就直接在整个字符串的开头放上一个cn|或者jp|来区分就可以了.

文法

我们做了这样的设计之后,基本整理一下就可以得出文法了:

input   ->  style + "|" + majs | majs // 旧 
input   ->  majs
style   ->  "cn" | "jp"               国标/日麻 // 旧
majs    ->  group + majs | empty      描述麻将牌的字符串
group   ->  ps + class | p + Z        按照牌的种类将牌分组, 比如 "123s456w3中" 有3个group, "123s" "456w" "3中"
F       ->  "东" | "南" | "西" | "北" | "白" | "发" | "中" | "+" | ... | empty 所有字牌, 其中 + 代表牌背
ps      ->  p + ps | empty            牌的列表
p       ->  pre + num
pre     ->  "_" | "^" | empty
num     ->  "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | empty
class   ->  "p" | "s" | "w" | "z"

实现

基本上得出了这样一个文法以后就可以直接去实现它了,我这里由于之前抄龙书附录A写过一个像是编译器前端的东西,所以这里用了类似的结构来实现.

实操的时候发现一些设计对于如此简单的字符串解析其实没有必要.我虽然做了一些调整,但是并没有把冗余的结构删干净,如果真有人阅读我的源码的话,见笑了.

实现之前我对这个项目有一些未来的的设想,即有空了再做的东西,比方说希望能用繁体中文输入字牌啊,之类的.所以实现上采取了一种听起来很蠢的方法.根据上面这个文法.我们实际上只要把简写全部展开,就可以让这个字符串描述一张一张的图片了.比方说cn|_123s 实际上可以看成,cn_1s,cn2s,cn3s这三张图片拼合的结果.所以我实际上只要把所有的简写都展开就可以得到,只指向某一张特定的图的字符串了.

得到了这个串以后,我们保持图片文件名和它一致.就基本可以解决问题了.比如说我横着的国标一条的文件名就叫cn_1s.png,那么实际上对于写程序来说会方便的多.

图片拼接

在实现过程中,本着能不自己造轮子就不自己造轮子的态度.我选择在拼接图片这一步上使用第三方库.

我首先找到了这么一个,看起来很美好,但是作者写这个的时候,是机械的把图片往格子里面放,相当于一个表格,行的大小和列的大小都是固定的.由于横置的麻将牌和竖置的宽度不同,所以会引起一些问题.是不可用的.我简单看了看源码,并没有发现能满足我需求的设置.所以就放弃了它.

然后我又找到了这个,看起来功能强多了,甚至还可以生成动画,结果遇到了另一个问题.

如果用这个东西拼合高度不同的图片,那么它会统一向上对齐,而这是不可调的. 这也许在摆牌河的时候很有用,但是一般来说表示吃碰的时候都是向下的.

而且这个软件的运作方式也很迷惑,有兴趣的可以去看一下他的使用示例.

总之最终我没有找到合适的工具,只能自己实现了,这一步耽误了很多时间. 以后看来这种简单的需求还是自己造轮子比较好(

透明

发现了一个新知识点:jpg是不支持透明图片的,想要就得用png.

效果

56s|789s|3中|05m2|_123s2|4s

56s|789s|3 中|05m2|_123s2|4s

一气通贯役牌中赤宝牌!

特殊处理

由于我们的语法并不是一个完全LL1的文法,比如说我们面对这样的两个输入:

123s 3白

我们会发现我们读数字的时候并不知道这个数字到底是字牌的出现次数还是在描述点数

所以我这里有个特殊的处理:

如果我们只处理123s这种情况的话,读123的时候可以建立三张牌,它们的class暂时是空的.

当s读入的时候,我们向之前读入的123三张牌添加class.

如果模仿这个过程的话:读取数字,建立牌,然后我们读入一个f,把之前读入的num当作次数,从之前的牌中读出信息.

然后将牌的点数置成1,然后class置成读入的f,然后将这张牌复制num次.

这种设计,意味着我们读入这样的字符串的时候仍然需要先读入pre + num,那么我们读一个num但是lookat并不是num的时候需要读出一个1.

这意味着如果我们只输入一个s会得到1s.

也带来了另一个问题:如果输入_^2s这样的字符串,会得到_1s^2s,这太不符合直觉了.

所以要禁止一次输入两个pre.

这样的实现说实话是有点乱的.看来还是得支持括号.