
今回は牌山と河(捨て牌)の表示について説明します。
天鳳など牌山をすべて表示する麻雀アプリもありますが、電脳麻将 ではドラ表示牌と残りツモ枚数だけを表示しています*1。 このため実装は単純です。
牌山の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 〜 牌 で生成した関数、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枚ごとに区切りを入れ、リーチ宣言牌は横向きにします。
河の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 〜 牌 で生成した関数、he は表示対象の河を表す Majiang.He のインスタンスです。 type で河の表示方法を指定できます。 0 は通常の表示、1 はツモ切りを暗転表示、2 ではさらに鳴かれた牌も表示に加えます。
Majiang.UI.He は root が以下の構成であることを期待します。
root
.choma /* リーチ棒 */
.dapai /* 捨て牌 */
.choma にはリーチしていることを示す表示(千点棒の画像など)を期待し、リーチがない間は非表示にします。 .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ノードに牌を追加していきます。 リーチがあった場合はコンストラクタで非表示にしていた .choma を再表示します。 ツモ切りの場合はその牌に .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も見てみましょう。 やはり Stylus の Mixin で実装しています。
he-size($width, $pai-height, $type = block)
width: $width
>.lizhi
line-height: 0
width: pai-width($pai-height) * 6
height: ($pai-height * 2 / 7)
text-align: $type == line ? left : center
.chouma
width: @width * 0.6
>.dapai
line-height: 0
height: $type == line ? $pai-height : $pai-height * 3
.pai
pai-size: $pai-height
.pai.dapai
transform: translate(@height / 18, @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(- @height / 18, @height / 24)
.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))させることで横向きにしています。
以下で実際の表示を確認できます。