対局者のUIは Majiang.UI.Player*1 で実装しますが、その構造はやや複雑です。 今回はまず構造から説明します*2。
電脳麻将 で対局者を実現するのは以下の2つのクラスです。
2つのクラスには共通する部分が多いため、共通の抽象クラス Majiang.Player のサブクラスとして実装しています。 電脳麻将では対局エンジンである Majiang.Game と Majiang.Player が 通信プロトコル にしたがいメッセージを送受信することで局が進行します。 麻雀AIとの対戦 はブラウザ内で互いに非同期でメソッド呼び出しするスタンドアロン型の実装、ネット対戦 はWebSocket で通信するクライアント・サーバ型での実装となっています。
Majiang.Player およびそのサブクラスである Majiang.AI、Majiang.UI.Player は以下に示す5階層からなるプログラムとして実装しています。
各層ごとの役割分担をまとまると以下の表となります。
| 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 のインスタンスを生成します。
この層に属するメソッドは 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 となります。
卓情報を更新し、第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 ~ 盤面 で説明した Majiang.UI.Board のインスタンスを設定します。
第3層では、第1層で指定されたコールバック関数を使用して、通信プロトコルにしたがい応答メッセージを送信します。 Majiang.UI.Player の実装では利用者がUIで応答を選択します*3。
第3層の具体的な実装は 次回紹介 します。
Majiang.UI.Player には第4層はありません。
第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);
}
以上、対局者を表すクラスの役割分担について説明しました。 次回は、スタンドアロン型の実装を説明します。