麻雀の局進行のプログラム方式 - koba::blog で 電脳麻将 の局進行はGame(サーバ)とPlayer(クライアント)間の通信で実現していることを説明したが、今回はその実装方法について。
まず両クラスの役割分担だが、以下とした。
Majiang.Game
- 牌山(
Majiang.Shan
)、手牌(Majiang.Shoupai
)、河(Majiang.He
)の原本管理と画面表示。 Majiang.Player
への状態通知、牌譜記録。Majiang.Player
からの応答受信と次の処理の決定(流局判定含む)。- 和了時、流局時の持ち点移動と連荘判定、および和了、流局画面表示。
- 半荘終了判定(次局へ進む、飛び終了、西入、サドンデスなど)と終局画面表示。
Majiang.Player
Majiang.Game
からの通知受信。- 適切な応答を返すための通知からの状況構築。
- ルール上打牌、副露、槓、和了、流局が可能か判断。
- 打牌、副露、槓、和了、流局の選択・判断。
Majiang.Game
への応答送信。
上記を見れば分かるように、画面表示はすべてMajiang.Game
で行っており、局進行の上で局面全体で判断すべきものはMajiang.Game
、当該Playerが単独で判断すべきものはMajiang.Player
が行っている。打牌や副露などの妥当性チェックをMajiang.Game
で行っていないが、これはGame、Playerが同一のプログラム上で動作しているから。麻雀サーバを実現する場合はクライアントは別プログラムになるので妥当性チェックが必要になるだろう。
では、具体的な実装方法について。
Majiang.Game
まずコンストラクタで Majiang.Player
のインスタンスを生成する。
Majiang.Game = function() { this._speed = 3; this._stop = false; this._callback; this._chang = { title: (new Date()).toLocaleString(), player: ['私','下家','対面','上家'], qijia: Math.floor(Math.random() * 4), zhuangfeng: 0, jushu: 0, changbang: 0, lizhibang: 0, defen: [ 25000, 25000, 25000, 25000 ], hongpai: { m: 1, p: 1, s: 1 } }; this._player = [ new Majiang.UI(0) ]; // 仮親は常にUI for (var id = 1; id < 4; id++) { this._player[id] = new Majiang.Player(id); // 仮南以降はAI } }
仮東はUI(人間のPlayer)としているが、Majiang.UI
は Majiang.Player
のサブクラスなので、通信のコードは共有している。
Majiang.Player
への通知は2つのメソッド notify_players()
、call_players()
で行う。 notify_players()
は非同期メッセージ、call_players()
は同期メッセージである。
Majiang.Game.prototype.notify_players = function(type, data) { var self = this; for (var l = 0; l < 4; l++) { (function(){ var id = self.player_id(l); var lb = l; setTimeout(function(){ self._player[id].action(type, data[lb]); }, 0); })(); } }
入力パラメータのtype
、data
はすでに 麻雀の局進行のプログラム方式 - koba::blog で説明した通り。ループ変数と別のスコープを作る必要があるので処理がむやみに複雑になっているが、やりたいことは Majiang.Player
のメソッド action()
の呼出しである。
Majiang.Game.prototype.call_players = function(type, data, timeout) { var self = this; this._status = type; if (this._speed != 0 && timeout != null) { if (this._player.filter( function(p){ return p instanceof Majiang.UI }).length) timeout = 0; } else timeout = this._speed * 200; this._reply = []; for (var l = 0; l < 4; l++) { (function(){ var id = self.player_id(l); var lb = l; var delay = (self._player[id] instanceof Majiang.UI) ? 0 : timeout; setTimeout(function(){ self._player[id].action(type, data[lb], function(type, reply){ self.next(id, type || '', reply) }); }, delay); })(); } }
こちらも処理がむやみに複雑だが、重要なのは action()
の呼出しと callback の設定である。call_players()
は同期メッセージなので応答を受け取る必要があるが、callback を呼出せば応答が返るようになっている。応答はメソッド next()
で受け取る。
Majiang.Game.prototype.next = function(id, type, data) { var self = this; if (id != null) this._reply[id] = { type: type, data: data }; if (this._reply.filter(function(x){return x}).length < 4) return; if (this._stop) return; if (this._status == 'zimo') this.reply_zimo(); else if (this._status == 'dapai') this.reply_dapai(); else if (this._status == 'fulou') this.reply_fulou(); else if (this._status == 'gang') this.reply_gang(); else if (this._status == 'gangzimo') this.reply_zimo(); else if (this._status == 'hule') this.reply_hule(); else if (this._status == 'pingju') this.reply_pingju(); else if (this._status == 'jieju') this.reply_jieju(); }
id
は応答メッセージ送信者を識別するid(仮東から順に 0〜3)。type
、data
は 麻雀の局進行のプログラム方式 - koba::blog で説明した通り。応答メッセージは4人のPlayer全員分待つ必要があるので、応答が4つそろったときに起動するようにしている。
この後、応答から適切に次の処理を選ぶのだが、その状態遷移は次回説明する。
Majiang.Player
Majiang.Player
はメソッド action()
を呼ばれることで起動するイベント駆動型のプログラムになっている*1。
Majiang.Player.prototype.action = function(type, data, callback) { if (type == 'kaiju') this.kaiju(data); else if (type == 'qipai') this.qipai(data); else if (type == 'zimo') this.zimo(data, callback); else if (type == 'dapai') this.dapai(data, callback); else if (type == 'fulou') this.fulou(data, callback); else if (type == 'gang') this.gang(data, callback); else if (type == 'kaigang') this.kaigang(data); else if (type == 'gangzimo') this.zimo(data, callback, 'lingshang'); else if (type == 'hule') this.hule(data, callback); else if (type == 'pingju') this.pingju(data, callback); else if (type == 'jieju') this.jieju(data, callback); }
この後、適切に状態を管理して思考ルーチンを呼出すのだが、その構造は次々回にでも。*2