麻雀ルールのカスタマイズ(5) ~ 打牌制約

電脳麻将 ver.2.0 で開発中の新機能「パラメータによるルールのカスタマイズ」。今回は最後の話題。麻雀ルールのカスタマイズ(4) ~ 和了役と点計算 - koba::blog までで説明していない パラメータ は以下の3つ。

  • 喰い替え許可レベル
  • ツモ番なしリーチあり
  • リーチ後暗槓許可レベル

どれも打牌の制約に関わるルール。

パラメータの意味

喰い替え許可レベル
喰い替えなし: 0、スジ喰い替えあり: 1、現物喰い替えもあり: 2。デフォルト値は 0。
ツモ番なしリーチあり
デフォルト値は false。
リーチ後暗槓許可レベル
暗槓はできない: 0、牌姿の変わる暗槓はできない: 1、待ちの変わる暗槓はできない: 2。どのケースでも送り槓はできない。デフォルト値は 2。

ツモ番なしリーチ

Majiang.Game のクラスメソッド allow_lizhi()ツモ番なしリーチあり が false の場合、残り牌数4未満でのリーチを禁止している。

    static allow_lizhi(rule, shoupai, p, paishu, defen) {

        /* …… */

        if (! rule['ツモ番なしリーチあり'] && paishu < 4) return false;

        /* …… */
    }

喰い替え

喰い替えの禁止の実装はやや複雑である。以下の手牌から s1 をチーすると残った手牌のどの牌を切っても喰い替えとなってしまう。

s1s1s2s3s4s4s4 p2-p1p3 z7z7-z7

つまりこのような手牌の場合、チー自体を禁止する必要がある訳だ。この処理は Majiang.Shoupaiget_chi_mianzi() で行なっている。

    get_chi_mianzi(p, check = true) {

        /* …… */

        let bingpai = this._bingpai[s];
        if (3 <= n && bingpai[n-2] > 0 && bingpai[n-1] > 0) {
            if (! check     // 喰い替えありか喰い替えとならない手牌がある場合
                || (3 < n ? bingpai[n-3] : 0) + bingpai[n]
                        < 14 - (this._fulou.length + 1) * 3)
            {
                /* チーできるメンツを追加する */
            }
        }
        /* …… */
    }

そして Majiang.Game のクラスメソッド get_chi_mianzi() がルールを指定する。

    static get_chi_mianzi(rule, shoupai, p, paishu) {

        let mianzi = shoupai.get_chi_mianzi(p, rule['喰い替え許可レベル'] == 0);
                                // 喰い替えなしの場合は Majiang.Shoupai に任せる
        if (! mianzi) return mianzi;
        if (rule['喰い替え許可レベル'] == 1        // 現物喰い替えなしの場合は
            && shoupai._fulou.length == 3       // すでに3副露していて残り2枚が
                                                // 現物となる場合だけ鳴けない
            && shoupai._bingpai[p[0]][p[1]] == 2) mianzi = [];
        return paishu == 0 ? [] : mianzi;
    }

打牌時のチェックもスジ喰い替えについては処理が複雑なので Majiang.Shoupaiget_dapai() で実装している。

    get_dapai(check = true) {

        /* …… */

        /* 喰い替えとなる牌を deny に設定する */
        let deny = {};
        if (check && this._zimo.length > 2) {   // 喰い替えなしで副露後の場合
            let m = this._zimo;
            let s = m[0];
            let n = + m.match(/\d(?=[\+\=\-])/) || 5;
            deny[s+n] = true;                       // 現物喰いかえ
            if (! m.match(/^[mpsz](\d)\1\1/)) {     // スジ喰いかえ
                if (n < 7 && m.match(/^[mps]\d\-\d\d$/)) deny[s+(n+3)] = true;
                if (3 < n && m.match(/^[mps]\d\d\d\-$/)) deny[s+(n-3)] = true;
            }
        }

        let dapai = [];     // dapai に打牌できる牌を追加する
        if (! this._lizhi) {    // リーチ後はツモ切りのみ
            for (let s of ['m','p','s','z']) {
                let bingpai = this._bingpai[s];
                for (let n = 1; n < bingpai.length; n++) {
                    if (deny[s+n])        continue; // 喰い替えとなる牌はスキップ
                    /* dapai に牌を加える */
                }
            }
        }
        /* …… */
        return dapai;
    }

Majiang.Game のクラスメソッド get_dapai() ではルールの指定と現物喰い替えのチェックを行う。

    static get_dapai(rule, shoupai) {

        /* 喰い替えなしの場合は Majiang.Shoupai に処理を任せる */
        if (rule['喰い替え許可レベル'] == 0) return shoupai.get_dapai(true);

        /* 現物喰い替えなしの場合はここで喰い替えをチェックする */
        if (rule['喰い替え許可レベル'] == 1
            && shoupai._zimo && shoupai._zimo.length > 2)
        {
            let deny = shoupai._zimo[0]
                     + (+shoupai._zimo.match(/\d(?=[\+\=\-])/)||5);
            return shoupai.get_dapai(false)
                                .filter(p => p.replace(/0/,'5') != deny);
        }

        /* 喰い替えありの場合は Majiang.Shoupai でチェックしない */
        return shoupai.get_dapai(false);
    }

リーチ後の暗槓

リーチ後の暗槓はルールの指定するレベルに応じて以下のチェックが必要となる。

  1. 送り槓の禁止(全てのルール)
  2. 待ちの変わる暗槓の禁止(リーチ後暗槓許可レベル = 2)
  3. 牌姿の変わる暗槓の禁止(リーチ後暗槓許可レベル = 1)
  4. 暗槓を禁止(リーチ後暗槓許可レベル = 0)

送り槓の禁止

ツモってきた牌以外で槓することを送り槓という。例えば以下の牌姿から m4 をツモって m1 をカンするようなケース。

m1m1m1m1m2m3p4p5p6s7s8z1z1

リーチ後の送り槓を許可するルールはないので Majiang.Shoupaiget_gang_mianzi() で送り槓の禁止を実装した。

    get_gang_mianzi(p) {

        /* …… */

            let p = this._zimo.replace(/0/,'5');    // p は今ツモった牌

        /* …… */
                        if (this._lizhi && s+n != p) continue;
                                                // ツモった牌以外での槓は禁止
        /* …… */
    }

待ちの変わる暗槓の禁止

天鳳で採用されているルール。例えば以下の牌姿で m1 をカンすると m3 待ちがなくなるためカンできない。

m1m1m1m2p4p5p6s7s8s9z1z1z1 m1

Majiang.Game のクラスメソッド get_gang_mianzi() で禁止を実装した。

    static get_gang_mianzi(rule, shoupai, p, paishu, n_gang) {

        /* …… */

                let new_shoupai;
                new_shoupai = shoupai.clone().dapai(shoupai._zimo);
                let n_tingpai1 = Majiang.Util.tingpai(new_shoupai).length;
                                                    // カンする前の和了牌を取得
                new_shoupai = shoupai.clone().gang(mianzi[0]);
                let n_tingpai2 = Majiang.Util.tingpai(new_shoupai).length;
                                                    // カンした後の和了牌を取得
                if (n_tingpai1 > n_tingpai2) return []; 
                                                    // 待ちが減るカンは禁止
        /* …… */
    }

牌姿の変わる暗槓の禁止

Mリーグで採用されているルール。例えば以下の牌姿で s3 をカンすると s3 をトイツとする和了形がなくなるためカンできない。

m2m3p5p6p7s3s3s3s4s5s6s6s6 s3

以下の牌姿も s1 をカンすると九蓮宝燈がなくなるためカンできないとするのが一般的*1

s1s1s1s3s4s4s5s6s7s8s9s9s9 s1

これも Majiang.Game のクラスメソッド get_gang_mianzi() で禁止を実装した。

    static get_gang_mianzi(rule, shoupai, p, paishu, n_gang) {

        /* …… */

                let new_shoupai, n_hule1 = 0, n_hule2 = 0;

                /* カンする前の全ての和了形を取得 */
                new_shoupai = shoupai.clone().dapai(shoupai._zimo);
                for (let p of Majiang.Util.tingpai(new_shoupai)) {
                    n_hule1 += Majiang.Util.hule_mianzi(new_shoupai, p).length;
                }

                /* カンした後の全ての和了形を取得 */
                new_shoupai = shoupai.clone().gang(mianzi[0]);
                for (let p of Majiang.Util.tingpai(new_shoupai)) {
                    n_hule2 += Majiang.Util.hule_mianzi(new_shoupai, p).length;
                }

                if (n_hule1 > n_hule2) return [];   // 和了形が減るカンは禁止

        /* …… */
    }

暗槓を禁止

最高位戦Classicで採用されているルール。リーチ後は一切暗槓できない。やはり Majiang.Game のクラスメソッド get_gang_mianzi() で禁止を実装した。

    static get_gang_mianzi(rule, shoupai, p, paishu, n_gang) {

        /* …… */

            if (rule['リーチ後暗槓許可レベル'] == 0) return [];

        /* …… */
    }

*1:Mリーグでどうかは不明