麻雀アプリに「牌操作」はあるのか?

麻雀アプリのストアでの評価を見ると「牌操作がひどい」というように牌操作という言葉を聞くことがあります。ここでいう牌操作とは、

  1. 配牌が何らかの意図で偏らされている*1
  2. 局開始後にツモが意図的に変更される*2

という意味で使われているようです。

では、実際のところどうなのでしょうか?

このような疑惑が出る背景は、30年以上前に麻雀ゲームが出始めたころ*3、まともな手役作りのプログラムが難しかったので、コンピュータ側を配牌時にあらかじめテンパイさせておき、適当なタイミングで和了するというインチキな実装があったという黒歴史にあるようです。しかし、天鳳などの対CPU戦のないアプリではそもそも手役作りのアルゴリズムは不要ですし、今や麻雀AIも開発されている時代です。今もこんなことが継続されているのでしょうか?

正しいプログラムとは何か

まず、正しいプログラムはどうあるべきでしょうか。以下の2点に集約されると思います。

  1. ランダムな牌の並びで牌山生成する
  2. 牌山の決められた位置から配牌し、ツモをとる

つまり、

  1. 意図的な偏りのある牌山を作らない
  2. 牌山の任意の場所から配牌したり、ツモをとったりする不正を行わない

というふうに言い換えられます。イカサマに例えると 1 は積み込み、2 はすり替えに相当しますね。

牌山生成

では具体的にどのようにプログラムすればよいでしょうか。牌山生成は

  1. あらかじめ決めた順序*4に牌を並べる
  2. そこから乱数でランダムに1牌を選び、牌山に移動する
  3. これを136回繰り返す

となります。*5

ですが、この方法だと「牌が混ざりすぎる」と信じる人が多いようです。彼らが根拠として挙げるのは、開発者コラム 第3話 牌の偏り 後編~初期プログラムの失敗 | オンライン麻雀 Maru-Jan 公式サイト です。ここには

特に、配牌が酷すぎて、毎回七種七牌みたいな感じなのです。よく混ざっていると言えば、混ざっているのですが、和了は七対子ぐらいしか望めないような感じで、その出来映えに愕然としました。

と書かれていますが、これは正しくありません。麻雀牌13枚のすべての組合せの平均シャンテン数は 3.58 であることが分かっています*6。3.58シャンテンは「七対子ぐらいしか望めない」配牌ではありません。

なぜこのような風説を信じてしまうのでしょうか。その原因は「ランダムとは均等で偏りがないことである」という誤解にあるのだと思います。実際にはランダムには偏りがあります。サイコロを振ってみればわかるように、同じ目が連続して出ることは普通にあるので、短期ではけっして均等ではありません。けれどこれを繰り返せばいろいろな形に偏ることで結果的にいずれ均等に「収束」します。これがランダムと均等の正しい理解です。ランダムとは簡単に言えば「予測不能」のことです。均等であればある意味予測可能ですよね*7

配牌とツモ

配牌は、

  • 牌山の先頭から13枚ずつ牌を取り分ける

とすればいいですし、ツモは

  • 牌山の先頭から1枚ツモる

とすればよいでしょう。

他のやり方でもよい*8ですが、毎回同じやり方とすることが重要です。

正しくプログラムされていることをどのように保証するのか

麻雀アプリ作成者は「オマエのアプリは牌操作していてけしからん」という言いがかりに悩まされます*9。なのでプログラムに不正がないことを確認する方法を公開している麻雀アプリもあります。天鳳雀魂 がそうです。どのように保証しているのでしょうか。

天鳳

天鳳では「乱数生成のアルゴリズム」*10とその半荘で使った「タネ」を公開しています。コンピュータが生成する乱数は疑似乱数とも呼ばれます。なぜ「疑似」かというとコンピュータ内部ではサイコロを振る訳にもいかないので、ランダムな結果を出すことが知られている計算式にのっとって乱数列を「計算」するからです。この時にタネが同じなら得られる乱数列は同じになります*11。つまり天鳳では乱数生成アルゴリズムとタネを公開することで牌山を検算可能にし、正しく乱数を使っていることを保証しているのです。牌山を計算できれば配牌やツモに不正がないことは確認できますよね。

雀魂

雀魂では牌山の「ハッシュ値」を公開することで牌山の改竄を行なっていないことを保証しています。ハッシュ値とはハッシュ関数によって得られる値で、任意のデータが比較的短い長さのハッシュ値に「要約」されます*12。元のデータが少しでも異なれば結果のハッシュ値は全く異なる値になります。そしてハッシュ値から元のデータを逆算することはできません。雀魂では対戦時に牌山のハッシュ値を見ることができ、牌譜でも牌山とハッシュ値を見ることができます。つまり牌譜で見れる牌山のハッシュ値が正しく、それが対戦時のハッシュ値と同じなら、牌山は改竄されていないことになります。ただし牌山自体に偏りがないか検証する方法はありません*13

電脳麻将ではどうプログラムしているのか

電脳麻将ソースコードを公開していますので、ソースコードを見れば一目瞭然です。

牌山生成

Majiang.Shan クラスのコンストラクタ で牌山を生成しています。

constructor(hongpai = {m:0,p:0,s:0}) {

    /* あらかじめ決めた順序に牌を並べる */
    let pai = [];
    for (let s of ['m','p','s','z']) {
        for (let n = 1; n <= (s == 'z' ? 7 : 9); n++) {
            for (let i = 0; i < 4; i++) {
                if (hongpai[s] && n == 5 && i < hongpai[s]) pai.push(s+0);
                else                                        pai.push(s+n);
            }
        }
    }
    this._pai = [];
    while (pai.length) {
        this._pai.push(pai.splice(Math.random()*pai.length, 1)[0]);
                                // 乱数でランダムに1牌を選び、牌山に移動する
    }

    /* ...... */
}

配牌

Majiang.Game クラスのメソッド qipai() で配牌しています。

qipai(shan) {

    /* ...... */

    let qipai = [ [], [], [], []];
    for (let l = 0; l < 4; l++) {
        /* 13枚連続でツモって配牌とする */
        for (let i = 0; i < 13; i++) {
            qipai[l].push(model.shan.zimo());
        }

        /* ...... */
    }

    /* ...... */
}

ツモ

Majiang.Shan クラスのメソッド zimo() で牌山から1枚ツモります。

zimo() {

    /* ...... */

    return this._pai.pop();     // 牌山から1枚ツモる。
}

結論

牌山生成、配牌、ツモには正しいプログラムの方法があり、天鳳、雀魂ではそれを検証するためのデータが公開されています。具体的に不正な操作がないかは、そのデータから毎回検証する必要がありますが、1つでも不正な例が見つかれば信用を失うため、信じるに足りると考えるべきでしょう。

電脳麻将はソースを公開しているため、牌操作はないと断言できます😁

*1:いい手に偏った配牌が来るなど

*2:リーチ後にあたり牌を「つかまされる」など

*3:脱衣麻雀のアーケードゲームとか、ひそかに賭博目的で使われていたジャンピューターとか

*4:一萬~九萬、一筒~九筒、一索~九索、東~北、白~中 とか

*5:2 を「ランダムに2牌を選び、入れ替える」とするのはよくある間違いで、これだと牌は充分に混ざりません

*6:麻雀の数学 より

*7:一萬がツモ切られたばかりなので、山にしばらくは一萬がないとか

*8:4枚ずつ配ってチョンチョンするとか

*9:これにより開発意欲を失う人もいるようなので、非常に有害な言いがかりです

*10:牌操作マニアには有名なメルセンヌ・ツイスタというヤツです

*11:けれども乱数列の結果からタネを逆算することはできません

*12:雀魂で使用している MD5 の場合は任意のデータを128ビットに要約します

*13:が、牌山を検証する方法を公開している態度から信用に足りると思います