麻雀の局進行のプログラム実装

麻雀の局進行のプログラム方式 - 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.UIMajiang.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);
        })();
    }
}

入力パラメータのtypedata はすでに 麻雀の局進行のプログラム方式 - 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)。typedata麻雀の局進行のプログラム方式 - 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

*1:Majiang.Game も next() でイベントを待つイベント駆動型といえる

*2:年内に終わらなかったな