麻雀ルールのカスタマイズ(3) ~ 赤牌とドラ

電脳麻将 ver.2.0 で開発中の新機能「パラメータによるルールのカスタマイズ」。麻雀ルールのカスタマイズ(2) ~ 流局処理と連荘判断 - koba::blog からだいぶ間が空いたが今回の話題は 赤牌ドラ。これらに関わる パラメータ は以下の通り。

  • 赤牌
  • 裏ドラあり
  • カンドラあり
  • カン裏あり
  • カンドラ後乗せ

パラメータの意味

赤牌
萬子、筒子、索子それぞれの赤牌の数。デフォルト値は { m: 1, p: 1, s: 1 }
裏ドラあり
デフォルト値は true。
カンドラあり
デフォルト値は true。
カン裏あり
デフォルト値は true。裏ドラあり、あるいはカンドラありが false の場合もカン裏は発生しない。
カンドラ後乗せ
true の場合、大明槓、加槓でのカンドラ発生のタイミングをリンシャン牌取得の次の動作(打牌/カン宣言)と同時にする*1。デフォルト値は true。

赤牌とドラの決定

今までルールの決定は局進行を司るクラス Majiang.Game が行なっていたが、赤牌とドラに関しては牌山を表現するクラス Majiang.Shan で行なう。

赤牌の枚数

constructor() でルールで指定された赤牌の数に応じて牌山を生成している。

    constructor(rule) {

        this._rule = rule;
        let hongpai = rule['赤牌'];

        /* 全ての牌を順に並べる */
        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 (n == 5 && i < hongpai[s]) pai.push(s+0);    // 赤牌
                    else                          pai.push(s+n);    // 赤牌以外
                }
            }
        }

        /* …… */
    }

ドラ追加判断

表ドラはインスタンス変数 _baopai、裏ドラは _fubaopai に追加する。参照はそれぞれメソッド get baopai()get fubaopai() で行う。裏ドラは表ドラ発生のタイミングで同時に追加するが、牌山を「閉める」(ツモれない状態にする)までは参照できない。

    get baopai() { return this._baopai.filter(x=>x) }
                                    // カンドラなしの場合はカンの度に _baopai 
                                    // に '' を加えるので、それを取り除く
    get fubaopai() {
        return ! this._closed ? null    // 牌山を「閉める」まで裏ドラは見せない
             : this._fubaopai ? this._fubaopai.concat()     // 裏ドラあり
             :                  null;                       // 裏ドラなし
    } 
表ドラ、裏ドラ

constructor() で表ドラ、裏ドラを設定するが、裏ドラあり が false の場合は null とし、追加ができないようにする。

    constructor(rule) {
        /* …… */
        this._baopai     = [this._pai[4]];
        this._fubaopai   = rule['裏ドラあり'] ? [this._pai[9]] : null;
        /* …… */
    }
カンドラ、カン裏

gangzimo() でインスタンス変数 _weikaigang を true にすると kaigang() が呼び出せるようになる。

    gangzimo() {
       /* …… */
        this._weikaigang = this._rule['カンドラあり'];
       /* …… */
    }

kaigang() が呼ばれると、カンドラ、カン裏を追加する。_weikaigang が false のときは kaigang() を呼び出せない。

    kaigang() {
       /* …… */
        this._baopai.push(this._pai[4]);        // カンドラを追加
        if (this._fubaopai && this._rule['カン裏あり'])
            this._fubaopai.push(this._pai[9]);  // カン裏を追加
        this._weikaigang = false;
       /* …… */
    }

カンドラ後乗せのタイミング

後乗せのカンドラは次の打牌と同時に開かれる。これは打牌者はカンドラが何であるか知らないが、その打牌で和了した場合にカンドラが有効になることを意味する。これと同様に考えるならば、カンヅモ後に続けてカンをし、それに槍槓が発生した場合、カンをしたものはカンドラを知らないが、和了時にはカンドラが有効になるのが正しいと思う。電脳麻将ではこの考えにしたがい、カンが連続したときはそのカンが暗槓、加槓にかかわらず一律直前のカンドラを有効にしている*2

天鳳では連続したカンが暗槓ならば直前のカンと暗槓のカンドラが2つ続けて開かれるが、連続したカンが加槓のときは次の打牌までカンドラは開かれない。つまり連続したカンが加槓だと槍槓が発生してもカンドラは有効にならない。これはルール上の矛盾だと思うので、電脳麻将は天鳳ルールを採用しない。

カンドラ発生のタイミングはクラス Majiang.Game が制御する。gangzimo()カンドラ後乗せ の値にしたがい、カンドラを即時に発生させるか保留するかを決定する。

    gangzimo() {
       /* …… */
        if (! this._rule['カンドラ後乗せ'] ||
            this._gang.match(/^[mpsz]\d{4}$/)) this.kaigang();
       /* …… */
    }

保留した場合は、直後の dapai() もしくは gang() でカンドラが開かれる。

    dapai(dapai) {
       /* …… */
        if (this._gang) this.kaigang();
       /* …… */
    }
    gang(gang) {
       /* …… */
        if (this._gang) this.kaigang();
       /* …… */
    }

*1:天鳳ではカン宣言の段階ではカンドラはまだ増えないのでルールが微妙に異なる

*2:雀魂もおそらく同じ