麻雀ルールのカスタマイズ(2) ~ 流局処理と連荘判断

電脳麻将 ver.2.0 で開発中の新機能「パラメータによるルールのカスタマイズ」。麻雀ルールのカスタマイズ(1) ~ 終局判断とポイント計算 - koba::blog に続き今回の話題は 流局処理連荘判断。これらに関わるパラメータは以下の通り。

  • 途中流局あり
  • 流し満貫あり
  • ノーテン宣言あり
  • ノーテン罰あり
  • 最大同時和了数
  • 連荘方式

パラメータの意味

途中流局あり
true の場合、九種九牌、四風連打、四家立直、四開槓を途中流局とする。デフォルト値は true
流し満貫あり
true の場合、流局時に流し満貫の判定と精算を行う。デフォルト値は true
ノーテン宣言あり
true の場合、流局時の対局者からの応答次第でテンパイしていてもノーテンとして扱う。デフォルト値は false
ノーテン罰あり
true の場合、流局時にノーテン罰符の精算を行う。デフォルト値は true
最大同時和了数
1 の場合、和了応答が複数あったときに「頭ハネ」で和了者を決定する。2 の場合、ダブロンを認め、3者が和了応答した場合は三家和の途中流局とする。3 の場合、トリロンを認める。
連荘方式
連荘なし: 0、和了連荘: 1、テンパイ連荘: 2、ノーテン連荘: 3。デフォルト値は 2。

流局判断とノーテン罰符精算

Majiang.Game の打牌応答を処理するメソッド reply_dapai() で流局判断を行い、メソッド pingju() を呼び出して流局処理を行う。

    reply_dapai() {

        let model = this._model;

        /* 応答が和了の場合の処理 */

        /* 打牌がリーチだった場合の場合の処理 */

        /* 四風連打判断 */

        /* 四開槓判断 */

        if (! model.shan.paishu) {          // 牌山が尽きた場合は流局
            let shoupai = ['','','',''];
            for (let l = 0; l < 4; l++) {
                let reply = this.get_reply(l);  // 応答を取得する
                if (reply.daopai) shoupai[l] = reply.daopai;
                                            // 応答が「倒牌」なら手牌を公開する
            }
            return this.delay(()=>this.pingju('', shoupai), 0);
                                            // 流局処理を呼び出す
        }

        /* 応答が副露の場合の処理 */

        this.delay(()=>this.zimo(), 0);     // ツモ処理に遷移する
    }
    pingju(name, shoupai = ['','','','']) {

        let model = this._model;

        let fenpei  = [0,0,0,0];

        if (! name) {       // name が指定されていない場合は通常流局

            /* 対局者4名についてテンパイしているか確認しテンパイ者数を把握する */
            let n_tingpai = 0;          // テンパイ者数を 0 で初期化
            for (let l = 0; l < 4; l++) {
                if (this._rule['ノーテン宣言あり'] && ! shoupai[l]
                    && ! model.shoupai[l].lizhi) continue;
                                        // ノーテン宣言あり かつ 倒牌していない
                                        // かつ リーチしていない 場合は
                                        // ノーテンとして扱う
                if (Majiang.Util.xiangting(model.shoupai[l]) == 0
                    && Majiang.Util.tingpai(model.shoupai[l]).length > 0)
                {           // テンパイの場合
                    n_tingpai++;        // テンパイ者数を 1 増やす
                    shoupai[l] = model.shoupai[l].toString();
                                        // 手牌を公開する
                    /* …… */
                }
                else {      // ノーテンの場合
                    shoupai[l] = '';    // 手牌を公開しない
                }
            }

            /* 流し満貫の処理 */

            /* ノーテン罰符の精算を行う */
            if (! name) {   // 流し満貫がない場合
                name = '荒牌平局';      // 流局理由を「荒牌平局」とする
                if (this._rule['ノーテン罰あり']
                    && 0 < n_tingpai && n_tingpai < 4)
                {           // ノーテン罰符の精算が必要な場合
                    for (let l = 0; l < 4; l++) {
                        fenpei[l] = shoupai[l] ?  3000 / n_tingpai
                                               : -3000 / (4 - n_tingpai);
                                        // テンパイ者:  3000 / テンパイ者数
                                        // ノーテン者: -3000 / ノーテン者数
                    }
                }
            }

            /* …… */
        }
        else {              // name が指定されている場合は途中流局

            /* 途中流局の処理 */
        }

        /* …… */
    }

流し満貫判断と精算

Majiang.Game の流局を処理するメソッド pingju() 内で流し満貫の判断と精算を行う。

    pingju(name, shoupai = ['','','','']) {

        let model = this._model;

        let fenpei  = [0,0,0,0];

        if (! name) {       // name が指定されていない場合は通常流局

            /* 対局者4名についてテンパイしているか確認 */

            if (this._rule['流し満貫あり']) {   // 流し満貫ありの場合

                for (let l = 0; l < 4; l++) {

                    /* 流し満貫を達成しているか確認する */
                    let all_yaojiu = true;  // all_yaojiu を流し満貫達成で初期化
                    for (let p of model.he[l]._pai) {
                        if (p.match(/[\+\=\-]$/)) { all_yaojiu = false; break }
                                                    // 鳴かれているときは不達成
                        if (p.match(/^z/))          continue;   // 字牌なら継続
                        if (p.match(/^[mps][19]/))  continue;   // 一九牌も継続
                        all_yaojiu = false; break;  // それ以外は不達成
                    }

                    /* 流し満貫の精算をする */
                    if (all_yaojiu) {   // 流し満貫達成の場合
                        name = '流し満貫';      // 流局理由を「流し満貫」とする
                        for (let i = 0; i < 4; i++) {
                            fenpei[i] += l == 0 && i == l ? 12000 // 親が達成
                                       : l == 0           ? -4000 // 親が被達成
                                       : l != 0 && i == l ?  8000 // 子が達成
                                       : l != 0 && i == 0 ? -4000 // 子が親に被達成
                                       :                    -2000;// 子が子に被達成
                        }
                    }
                }
            }

            /* ノーテン罰符の精算 */
        }
        else {              // name が指定されている場合は途中流局

            /* 途中流局の処理 */
        }

        /* …… */
    }

途中流局判断

九種九牌

Majiang.Game のツモ応答を処理するメソッド reply_zimo() で判断を行い、メソッド pingju() を呼び出して途中流局処理を行う。

    reply_zimo() {

        let model = this._model;

        let reply = this.get_reply(model.lunban); // 現在の手番の応答を取得する
        if (reply.daopai) {                 // 応答が「倒排」の場合
            if (this.allow_pingju()) {      // 九種九牌で流局可能な場合
                let shoupai = ['','','',''];
                shoupai[model.lunban] = model.shoupai[model.lunban].toString();
                                            // 手牌を公開する 
                return this.delay(()=>this.pingju('九種九牌', shoupai), 0);
                                            // 流局処理を呼び出す
            }
        }

        /* …… */
    }

四風連打

Majiang.Game の打牌応答を処理するメソッド reply_dapai() で判断を行い、メソッド pingju() を呼び出して途中流局処理を行う。

    reply_dapai() {

        /* …… */

        if (this._diyizimo && model.lunban == 3) {  // 一巡目の北家の打牌の場合
            this._diyizimo = false;         // 一巡目を終わらせる
            if (this._fengpai) {            // 四風連打が継続中の場合
                return this.delay(()=>this.pingju('四風連打'), 0);
                                            // 流局処理を呼び出す
            }
        }

        /* …… */
    }

四家立直

同じく Majiang.Game の打牌応答を処理するメソッド reply_dapai() で判断を行い、メソッド pingju() を呼び出して途中流局処理を行う。

    reply_dapai() {

        /* …… */

        if (this._dapai.substr(-1) == '*') {    // 打牌がリーチだった場合

            /* リーチ成立の処理を行う */

            if (this._lizhi.filter(x=>x).length == 4    // リーチ者が4名 かつ
                && this._rule['途中流局あり'])          // 途中流局あり の場合
            {
                let shoupai = model.shoupai.map(s=>s.toString());
                                            // 手牌を公開する 
                return this.delay(()=>this.pingju('四家立直', shoupai));
                                            // 流局処理を呼び出す
            }
        }

        /* …… */
    }

四開槓

同じく Majiang.Game の打牌応答を処理するメソッド reply_dapai() で判断を行い、メソッド pingju() を呼び出して途中流局処理を行う。

    reply_dapai() {

        /* …… */

        if (this._n_gang.reduce((x, y)=> x + y) == 4) {     // 4つ槓がある場合
            if (Math.max(...this._n_gang) < 4 && this._rule['途中流局あり']) {
                                                    // 1人で4つのカンではない
                                                    // かつ 途中流局あり の場合
                return this.delay(()=>this.pingju('四開槓'), 0);
                                            // 流局処理を呼び出す
            }
        }

        /* …… */
    }

三家和

同じく Majiang.Game の打牌応答を処理するメソッド reply_dapai() で判断を行い、メソッド pingju() を呼び出して途中流局処理を行う。

    reply_dapai() {

        let model = this._model;

        /* 応答が和了の場合の処理 */
        for (let i = 1; i < 4; i++) {   // 打牌者の下家から順に処理を行う
            let l = (model.lunban + i) % 4;
            let reply = this.get_reply(l);  // 応答を取得する
            if (reply.hule && this.allow_hule(l)) {     // 応答が「和了」かつ
                                                        // 和了可能な場合
                if (this._view) this._view.say('rong', l);
                this._hule.push(l);             // 和了者のリストに追加する
            }
            else {
                /* …… */
            }
        }
        if (this._hule.length == 3 && this._rule['最大同時和了数'] == 2) {
                                                    // 和了者が3名 かつ
                                                    // 最大同時和了数が 2 の場合
            let shoupai = ['','','',''];
            for (let l of this._hule) {
                shoupai[l] = model.shoupai[l].toString();
                                            // 手牌を公開する 
            }
            return this.delay(()=>this.pingju('三家和', shoupai));
                                            // 流局処理を呼び出す
        }
        else if (this._hule.length) {
            /* …… */
        }

        /* …… */
    }

連荘判断

Majiang.Game の和了を処理するメソッド hule() および流局を処理するメソッド pingju() で連荘判断を行っている。

    hule() {

        let model = this._model;

        /* …… */

        if (this._rule['連荘方式'] > 0 && menfeng == 0) this._lianzhuang = true;
                                        // 連荘なし以外で親が和了した場合は連荘
        if (this._rule['場数'] == 0) this._lianzhuang = false;
                                        // ただし一局戦の場合は親の和了でも輪連
        /* …… */
    }
    pingju(name, shoupai = ['','','','']) {

        let model = this._model;

        let fenpei  = [0,0,0,0];

        if (! name) {       // name が指定されていない場合は通常流局
            for (let l = 0; l < 4; l++) {
                /* …… */
                if (Majiang.Util.xiangting(model.shoupai[l]) == 0
                    && Majiang.Util.tingpai(model.shoupai[l]).length > 0)
                {           // テンパイの場合
                    /* …… */
                    if (this._rule['連荘方式'] == 2 && l == 0)
                                                    this._lianzhuang = true;
                                        // テンパイ連荘で親がテンパイしている
                                        // 場合は連荘
                }
                else {      // ノーテンの場合
                    /* …… */
                }
            }
            /* …… */
            if (this._rule['連荘方式'] == 3) this._lianzhuang = true;
                                        // ノーテン連荘の場合は流局はすべて連荘
        }
        else {              // name が指定されている場合は途中流局
            this._no_game    = true;
            this._lianzhuang = true;    // 途中流局はすべて連荘
        }

        if (this._rule['場数'] == 0) this._lianzhuang = true;
                                        // 一局戦の流局はすべて連荘
        /* …… */
    }