Home

電脳麻将UI 〜 牌山と河

牌山

今回は牌山と河(捨て牌)の表示について説明します。


牌山

天鳳など牌山をすべて表示する麻雀アプリもありますが、電脳麻将 ではドラ表示牌と残りツモ枚数だけを表示しています*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 〜 牌 で生成した関数、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 〜 牌 で生成した関数、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

CSSも見てみましょう。 やはり StylusMixin で実装しています。

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 です。

「block」を指定した河 「line」を指定した河

この2つの形式の違いはCSSだけで実現できることに注意してください。 6つ切りの間にはさんだ .break のマークのある要素をブロック要素にすれば縦に並べる表示、インライン要素にすれば横一列の表示になる訳です。

.dapai .pai.zimo はツモ切りした牌、.dapai .pai.zimo.dapai はツモ切り直後の牌、.dapai .pai.fulou は鳴かれた牌ですが、これらは opacity の値を変えることで見分けられるようにしています*3

.dapai .lizhi はリーチ宣言牌を囲む領域でしたが、幅を牌の高さに合わせて正方形の領域にして牌を左寄せにした後、その中心を基準に反時計回りに90°回転(rotate(270deg))させることで横向きにしています。

デモ

以下で実際の表示を確認できます。

  1. ^ 雀魂でも表示してないし、要らないですよね
  2. ^ type が 2 以外でリーチ宣言牌を鳴かれた場合は次の捨て牌を横向きにします
  3. ^ ツモ切りした牌を鳴かれた場合に区別がつきませんが、これは他の麻雀アプリでも同じだと思います