麻雀の手牌を表示するプログラム - koba::blog に続いて牌山と河を表示するプログラムを説明しようかと思いましたが、そのためにはまず牌山と河がJavaScriptでどのように表現されているかを知る必要があります。ということで今回は牌山と河のデータ構造について。
牌山のデータ構造
電脳麻将 で牌山を表現するクラスは Majiang.Shan です。これを見ると以下のように表現されていることが分かります。
{ _pai: [], // 牌山 /* …… */ }
コンストラクタでこれを初期化します。
constructor(hongpai = {m:0,p:0,s:0}) { /* まず、変数 pai に決められた順序で牌を並べる */ let pai = []; for (let s of ['m','p','s','z']) { for (let n = 1; n <= (s == 'z' ? 7 : 9); n++) { for (let i = 0; i < 4; i++) { if (hongpai[s] && n == 5 && i < hongpai[s]) pai.push(s+0); // 赤牌 else pai.push(s+n); } } } /* 変数 pai からランダムに1枚抜き取り、インスタンス変数 _pai に加えるという操作を 136回繰り返す */ this._pai = []; while (pai.length) { this._pai.push(pai.splice(Math.random()*pai.length, 1)[0]); } /* …… */ }
クラス変数 _pai
に136枚の牌がランダムに並べられました*1。JavaScriptの Math.random()
は牌山生成に使うには充分なランダムさです。他の乱数生成系を使ったり、何度も洗牌するのは意味のないことです*2。
ランダムに並べられた136枚の牌を以下のように使います。サイコロは振りません。開門場所を決めるサイコロは不正防止が目的であり、牌山がランダムに作られていれば本来不要だからです*3。また、実際の牌山の配列と王牌の使用方が微妙にずれています*4が、これも気にしても仕方のないことと思います。毎回同じ場所を割り当てることが重要です。
王牌 | 嶺上牌 | _pai[0] ~_pai[3] | 4枚 |
---|---|---|---|
ドラ表示牌 | _pai[4] ~_pai[8] | 5枚 | |
裏ドラ表示牌 | _pai[9] ~_pai[13] | 5枚 | |
ツモ牌 | _pai[14] ~_pai[83] | 70枚 | |
配牌 | 北家 | _pai[84] ~_pai[96] | 13枚 |
西家 | _pai[97] ~_pai[109] | 13枚 | |
南家 | _pai[110] ~_pai[122] | 13枚 | |
東家 | _pai[123] ~_pai[135] | 13枚 |
ツモは _pai[83]
→ _pai[14]
の順に使用します。嶺上牌は _pai[0]
→ _pai[3]
の順です。カンにより本来はツモれるはずであった牌(_pai[14]
とか)がツモれなくなるのは麻雀の競技性と思うので、ツモ牌と嶺上牌は分けるべきと思います。王牌は常に14枚なので、残りツモ数は、
this._pai.length - 14
となります。
河のデータ構造
河を表現するクラスは Majiang.He です*5。
{ _pai: [], // 捨て牌 /* …… */ }
クラス変数 _pai
に捨て牌を順に並べるだけです。リーチ宣言牌は m1* (一萬切りリーチ)のように * を付与します。鳴かれた牌も河に残しますが、m1*= (一萬切りリーチの宣言牌を対面がポン)のように鳴きを示すマークを付与します。鳴きを示すマークは副露牌のときのルールと同様です。下家に一萬をチーされた場合()は、副露メンツは m1-23、河の牌は m1- となります。表示上は鳴かれたリーチ宣言牌を河から取り除き、次の捨て牌を(鳴かれたリーチ宣言牌の代わりに)横向きに置く必要がありますが、それは表示ルーチンの役割分担としています。