
今回は牌山と河(捨て牌)の表示について説明します。
牌山
天鳳など牌山をすべて表示する麻雀アプリもありますが、電脳麻将 ではドラ表示牌と残りツモ枚数だけを表示しています*1。このため実装は単純です。
Majiang.UI.Shan
牌山のHTMLの出力は @kobalab/majiang-ui のクラス Majiang.UI.Shan で実装しています。まず、コンストラクタ で表示領域と牌山データを結びつけます。
/* * Majiang.UI.Shan */ "use strict"; module.exports = class Shan { constructor(root, pai, shan) { this._root = root; this._pai = pai; this._shan = shan; } /* ...... */ }
root で指定したDOMノードに牌山を表示するインスタンスを生成します。pai は 電脳麻将UI 〜 牌 - koba::blog で生成した関数、shan は表示対象の牌山を表す Majiang.Shan のインスタンスです。
Majiang.UI.Shan は root が以下の構成であることを期待します。
root .baopai /* ドラ表示牌 */ .fubaopai /* 裏ドラ表示牌 */ .paishu /* 残りツモ数 */
.baopai、.fubaopai はドラ表示牌・裏ドラ表示牌の表示領域、.paishu は残りツモ数の表示領域です。これらの領域がなくてもエラーにはならず、その領域のHTML出力をスキップします。例えば、和了点の表示画面など残りツモ数が不要なときは .paishu を省略すればよい訳です。
redraw() で全体を、update() で残りツモ数のみを再表示します。
redraw() { /* ドラ表示牌を表示する */ let baopai = this._shan.baopai; $('.baopai', this._root).empty(); for (let i = 0; i < 5; i++) { $('.baopai', this._root).append(this._pai(baopai[i] || '_')); } /* 裏ドラ表示牌を表示する */ let fubaopai = this._shan.fubaopai || []; $('.fubaopai', this._root).empty(); for (let i = 0; i < 5; i++) { $('.fubaopai', this._root).append(this._pai(fubaopai[i] || '_')); } /* 残りツモ数を表示する */ return this.update(); } update() { /* 残りツモ数を表示する */ $('.paishu', this._root).text(this._shan.paishu); return this; }
河
河は基本的に牌を並べるだけですが、6枚ごとに区切りを入れ、リーチ宣言牌は横向きにします。
Majiang.UI.He
河のHTMLの出力は @kobalab/majiang-ui のクラス Majiang.UI.He で実装しています。まず、コンストラクタ で表示領域と河のデータを結びつけます。
/* * Majiang.UI.He */ "use strict"; /* ...... */ module.exports = class He { constructor(root, pai, he, type = 0) { this._root = root; this._pai = pai; this._he = he; this._type = type; hide($('.chouma', this._root)); // リーチ棒を非表示にする } /* ...... */ }
root で指定したDOMノードに河を表示するインスタンスを生成します。pai は 電脳麻将UI 〜 牌 - koba::blog で生成した関数、he は表示対象の河を表す Majiang.He のインスタンスです。type で河の表示方法を指定できます。0 は通常の表示、1 はツモ切りを暗転表示、2 ではさらに鳴かれた牌も表示に加えます。
Majiang.UI.He は root が以下の構成であることを期待します。
root .chouma /* リーチ棒 */ .dapai /* 捨て牌 */
.chouma にはリーチしていることを示す表示(千点棒の画像など)を期待し、リーチがない間は非表示にします。.dapai は捨て牌の表示領域です。
redraw() で河全体を再表示します。
redraw(type) { if (type != null) this._type = type; /* 捨て牌をいったん空にする */ $('.dapai', this._root).empty(); let lizhi = false; let i = 0; /* 捨て牌を順に追加していく */ for (let p of this._he._pai) { if (p.match(/\*/)) { // リーチがあった場合 lizhi = true; show($('.chouma', this._root)); // リーチ棒を表示する } /* type が 2 でない場合、鳴かれた牌は表示しない */ if (this._type != 2 && p.match(/[\+\=\-]$/)) continue; let pai = this._pai(p); if (this._type != 0 && p[2] == '_') { // ツモ切りの場合 add_label(pai.addClass('zimo'), label.zimo); } if (p.match(/[\+\=\-]$/)) { // 鳴かれた牌の場合 add_label(pai.addClass('fulou'), label.fulou); } if (lizhi) { // リーチ宣言後最初の捨て牌 pai = $('<span>').addClass('lizhi') .attr('aria-label',label.lizhi) .append(pai); lizhi = false; } $('.dapai', this._root).append(pai); /* 6枚ごとに区切りを入れる */ i++; if (i < 6 * 3 && i % 6 == 0) { $('.dapai', this._root).append($('<div>').addClass('break')); } } return this; }
.dapai で示されたDOMノードに牌を追加していきます。リーチがあった場合はコンストラクタで非表示にしていた .chouma を再表示します。ツモ切りの場合はその牌に .zimo、鳴かれた場合は .fulou のマークを追加します。リーチ宣言後最初の捨て牌は横向きにする必要があるため、.lizhi で囲みます。この牌はリーチ宣言牌とは限らないことに注意してください*2。6枚ごとの区切りは .break でマークした div 要素を入れることで示しています。
dapai() で打牌を表示します。
dapai(p) { let pai = this._pai(p).addClass('dapai'); if (p[2]== '_') pai.addClass('zimo'); if (p.match(/\*/)) pai = $('<span>').addClass('lizhi') .attr('aria-label',label.lizhi) .append(pai); $('.dapai', this._root).append(pai); return this; }
打牌した直後の牌には .dapai のマークがついています。これを目印に「まだ河に並びきっていない」状態の表示をすることが可能です。ツモ切りの場合はその牌に .zimo を追加します。リーチ宣言牌は .lizhi で囲みます。
CSS
CSSも見てみましょう。やはり Stylus の Mixin で実装しています。
he-size($width, $pai-height, $type = block) width: $width >:has(.chouma) line-height: 0 width: pai-width($pai-height) * 6 height: ($pai-height / 4) text-align: $type == line ? left : center .chouma width: @width * 0.6 max-height: @height >.dapai line-height: 0 height: $type == line ? $pai-height : $pai-height * 3 .pai pai-size: $pai-height .pai.dapai transform: translate($pai-height / 12, $pai-height / 24) .pai.zimo opacity: 0.8 .pai.zimo.dapai opacity: 0.6 .pai.fulou opacity: 0.4 .lizhi width: $pai-height display: inline-block text-align: left transform: rotate(270deg) .pai.dapai transform: translate(- $pai-height / 24, $pai-height / 12) .break display: $type == line ? inline-block : block width: pai-width($pai-height) * 0.1
$width で表示領域の幅、$pai-height で捨て牌の高さを指定します。$type は表示形式の指定です。block を指定すると6つ切りで縦に並べる一般的な表示、line だと横一列の表示になります。デフォルト値は block です。


この2つの形式の違いはCSSだけで実現できることに注意してください。6つ切りの間にはさんだ .break のマークのある要素をブロック要素にすれば縦に並べる表示、インライン要素にすれば横一列の表示になる訳です。
.dapai .pai.zimo はツモ切りした牌、.dapai .pai.zimo.dapai はツモ切り直後の牌、.dapai .pai.fulou は鳴かれた牌ですが、これらは opacity の値を変えることで見分けられるようにしています*3。
.dapai .lizhi はリーチ宣言牌を囲む領域でしたが、幅を牌の高さに合わせて正方形の領域にして牌を左寄せにした後、その中心を基準に反時計回りに90°回転(rotate(270deg))させることで横向きにしています。