電脳麻将UI 〜 対局者(基盤)

対局者のUIは Majiang.UI.Player*1 で実装しますが、その構造はやや複雑です。今回はまず構造から説明します*2

電脳麻将 で対局者を実現するのは以下の2つのクラスです。

Majiang.AI
パッケージ @kobalab/majiang-ai で実装する 麻雀AI です。
Majiang.UI.Player
パッケージ @kobalab/majiang-ui で実装する打牌選択などの UI です。今回説明したい対象はこちらです。

2つのクラスには共通する部分が多いため、共通の抽象クラス Majiang.Player のサブクラスとして実装しています。電脳麻将では対局エンジンである Majiang.Game と Majiang.Player が 通信プロトコル にしたがいメッセージを送受信することで局が進行します。麻雀AIとの対戦 はブラウザ内で互いに非同期でメソッド呼び出しするスタンドアロン型の実装、ネット対戦 はWebSocket で通信するクライアント・サーバ型での実装となっています。

プログラム階層

Majiang.Player およびそのサブクラスである Majiang.AI、Majiang.UI.Player は以下に示す5階層からなるプログラムとして実装しています。

第1層 通知受信層
Majiang.Game からの 通知メッセージ を受信し、第2層のメソッドを呼び出します。この層に属するメソッドは唯一の入り口である action() のみです。2つのクラスで共通の実装となるため、親クラスの Majiang.Player で実装しています。呼び出す第2層のメソッドは、通知メッセージの種別と1対1に対応します。
第2層 状態管理層
通知メッセージに応じて 卓情報 を更新し、第3層のメソッドを呼び出します。この層も2つのクラスで共通の実装となるため、親クラスの Majiang.Player で実装しています。 呼び出す第3層のメソッドは、第2層のメソッドと1対1に対応します。
第3層 応答送信層
行動を決定し、Majiang.Game へ 応答メッセージ を送信します。この層は2つのクラスで実装が異なります。Majiang.AI の場合は、第4層の思考ルーチンを呼び出して、得られた結果を応答とします。Majiang.UI.Player の場合は、第5層のメソッドを呼び出して選択可能な行動の一覧を提示し、それに対するUIの選択を応答とします。
第4層 思考ルーチン層
Majiang.AI にのみ存在する層です。第5層のメソッドを呼び出して得た可能な行動の中から行動を選択します。いわゆる 麻雀AI と呼ばれる部分です。
第5層 行動提示層
第2層で更新した卓情報を利用して、ルール] にしたがった行動の一覧を返します。2つのクラスで共通の実装となるため、親クラスの Majiang.Player で実装しています。

各層ごとの役割分担をまとまると以下の表となります。

Majiang.Player Majiang.AI Majiang.UI.Player
第1層 通知受信層
第2層 状態管理層
第3層 応答送信層
第4層 思考ルーチン層
第5層 行動提示層

Majiang.AI の各層のメソッドとその呼び出し関係は以下の通りです。

Majiang.UI.Player の場合は以下となります。


プログラム実装

まず、基底クラスの Majiang.Player の実装から見ていきます。

コンストラクタ

    constructor() {
        this._model = new Majiang.Board();
    }

卓情報の更新のために Majiang.Board のインスタンスを生成します。

第1層 通知受信層

この層に属するメソッドは Majiang.Player の action() のみです。

    action(msg, callback) {

        this._callback = callback;      // コールバック関数を保存する

        /* 通知メッセージの種別ごとの第2層のメソッドを呼び出す */
        if      (msg.kaiju)    this.kaiju  (msg.kaiju);             // 開局
        else if (msg.qipai)    this.qipai  (msg.qipai);             // 配牌
        else if (msg.zimo)     this.zimo   (msg.zimo);              // 自摸
        else if (msg.dapai)    this.dapai  (msg.dapai);             // 打牌
        else if (msg.fulou)    this.fulou  (msg.fulou);             // 副露
        else if (msg.gang)     this.gang   (msg.gang);              // 槓子
        else if (msg.gangzimo) this.zimo   (msg.gangzimo, true);    // 槓自摸
        else if (msg.kaigang)  this.kaigang(msg.kaigang);           // 開槓
        else if (msg.hule)     this.hule   (msg.hule);              // 和了
        else if (msg.pingju)   this.pingju (msg.pingju);            // 流局
        else if (msg.jieju)    this.jieju  (msg.jieju);             // 終局
    }

msg に対応するメソッドを呼び出します。callback は応答メッセージ送信に使用するコールバック関数です。応答が不要な場合は callback は null となります。

第2層 状態管理層

卓情報を更新し、第3層のメソッドを呼び出します。

例えば配牌時に呼び出される qipai() は以下の実装となっています。

    qipai(qipai) {

        /* 卓情報を更新する */
        this._model.qipai(qipai);

        /* 処理に必要なインスタンス変数を更新する */
        this._menfeng   = this._model.menfeng(this._id);    // 自風を設定する 
        this._diyizimo  = true;                             // 第1ツモ巡にする
        this._n_gang    = 0;                                // カン数を 0 にする
        this._neng_rong = true;                             // ロン和了可能にする

        /* (必要なら)盤面を表示する */
        if (this._view) this._view.redraw();

        /* (応答が必要なら)第3層のメソッドを呼び出す */
        if (this._callback) this.action_qipai(qipai);
    }

スタンドアロン型 のAIとの対戦と、クライアント・サーバ型 のネット対戦では「誰が盤面を表示するか」が異なります。スタンドアロン型では対局エンジンが自身が生成した卓情報で盤面を表示します。クライアント・サーバ型の場合は、対局者が復元した卓情報(相手の手牌など不明な部分も含む対局者目線の情報)で盤面を表示します。

スタンドアロン型の場合、インスタンス変数 _view は null です。クライアント・サーバ型の場合は、_view には 電脳麻将UI 〜 盤面 - koba::blog で説明した Majiang.UI.Board のインスタンスを設定します。

第3層 応答送信層

第3層では Majiang.Game との通信プロトコルにしたがい、第1層で指定されたコールバック関数を使用して応答メッセージを送信します。Majiang.UI.Player の実装では、第5層のメソッドを利用して利用者に可能な行動*3を提示し、利用者の選択を応答します。

第3層の具体的な実装は 次回紹介 します。

第4層 思考ルーチン層

Majiang.UI.Player には第4層はありません。

第5層 行動提示層

第5層は Majiang.Player で実装しています。Majiang.Game が提供する ユーティリティ関数 を呼び出して可能な行動一覧を提示します。Majiang.UI.Player は、第3層からこの層の呼び出してボタンなどを表示させています。

例えば上家の打牌をチーできるかは get_chi_mianzi() で判定します。

    get_chi_mianzi(shoupai, p) {
        return Majiang.Game.get_chi_mianzi(this._rule, shoupai, p,
                                           this.shan.paishu);
    }

以上、対局者を表すクラスの役割分担について説明しました。次回は、スタンドアロン型の実装を説明します。

*1:ソースプログラムはリファクタリング前の実装ですので、一部ブログ記事と一致しな箇所があることをご了承ください

*2:電脳麻将本」から抜粋しながら説明します

*3:どの牌を打牌する、鳴くか鳴かないかなど