麻雀プログラムの製作ヒントの程度のページ

参考url
日経ソフトウェア−地球に優しいプログラム第10回

2008/06/30
とても大事な事を書き忘れていました。
牌データソートのお話です
これをやっておかないと後々の処理に問題が出るかもです




 

まず上がりの判定ですが1頭4組が見つかれば上がりと成ります。
……すいません、弾幕をこちらに展開しないでください;;

用意する変数は
鳴き用の面子変数
そして鳴いて無い用の面子変数です
鳴き用のほうには例として

nakimentu.ti
nakimentu.pon
nakimentu.kan
nakimentu.ankan

「チー」「ポン」「カン」「アンカン」
の辞書配列を作り、それを配列化して1面子目、2面子目と記録していけばいいでしょう

例:nakimentu.ti[i]

グラフィカル的な処理、つまり鳴き家の方向の牌を横倒しにする処理として
さらに下部に辞書配列を生成するのもよしです
それらを含め、変数の最終形態はこうなります。

例:
nakimentu.ti[i].nakitya //鳴いた家
nakimentu.ti[i].nakihai //鳴いた牌
nakimentu.ti[i].nakimentu //鳴き面子格納

辞書配列ってわかりやすくていいですね。自分は全部数字配列で作って後半地獄でした。
同じように、鳴いていない面子用の変数を作ります

mentu.syun[i] //順子格納
mentu.kotu[i] //刻子格納
mentu.atama[i] //頭格納

以上、変数の初期化を行いましたが、普段ゲームで可変する面子用の変数は
また別に作ります

BaMentu[i]

何のひねりもなく、普通の配列です。
最大13要素になり、その要素の中身は

例:
BaMentu[0].gara
BaMentu[0].suzi
BaMentu[0].ID
BaMentu[0].filename

です。
いわゆる1牌、1牌の生データが格納されています。
ここでやっと牌データの変数の説明をします、順序逆っぽいですが気にしない。
牌データはCSVなど表計算の汎用フォーマットを使います。
プログラム的に読めなければ配列変数でも大丈夫です

%["gara" => 柄, "suzi" => 数字, "ID" => ID, "filename" => 牌のファイル名]

柄はマンズが0ソウズが1ピンズが2字牌が3
数字は数字牌の場合そのまま1~9、字牌の場合は1が東2が南…白が5発が6な感じにしました
IDはデバックのときに、重複牌が無いか確認したり、鳴き牌の推移を知るのに便利な要素です
牌のファイル名をつけていたら描写が楽かも

では、肝心の面子判定の話に移りましょう。
作った身としては、実質麻雀ゲームはテンパイ判定プログラムといっても過言ではないとおもいます。
後1牌で上がれて、何待ちか。
その部分さえ作れば後は役の判定アルゴリズムを見つけるだけです。
チューレンのアルゴリズムを見つけるのは結構苦労しました。

おいといて。
麻雀のテンパイの形はおおよそ3つになります

まず頭待ち
そして順子両面待ち
そして刻子シャボ待ちです

そして、各待ち状態で、形になっていない牌の数が決まっています。
頭待ちの場合必ずどこにも属せない牌が2つ
順、刻は3つ、これを浮き牌と言います。

実際に判定する場合、数ある組み合わせの中から、浮き牌数と待ちの条件が
一致する面子候補が見つかれば『テンパイ』判定に入れます。

面子候補とは?

面子候補とは単純に各1牌1牌を検索し、組を作っていった結果がこれだy…結果を集めたものです。

まず、頭候補を見つける方法から。
BaMentuをfor文などで検索し、片っ端から
同じ牌が2組あるか探し出します。

例の流れとしては
BaMentu内

11234五五七八@ABG南
に頭候補である牌、11五五二つが見つかりました

一時変数に頭を除いた面子を格納します。
tempMentu[0] = (234五五七八@ABG南)//実際は頭牌を消去したBaMentu配列をコピー
tempMentu[1] = (11234七八@ABG南)

頭は一時変数にそれぞれ格納します

tempAtama[0] = [BaMentu[0], BaMentu[1]];
tempAtama[1] = [BaMentu[5], BaMentu[6]];

さて、次に見つけた頭を抜いた手牌から面子を作り上げていきます

ココで注意するのはたとえば123123123という手牌があるとします。
これは123の順子が3つあるとも言えるし111222333という刻子が
あるともとれます。
このように両方の型として通用する牌の並びに対処するため

『順子優先検索』と『刻子優先検索』と言う手続きを行います
といっても単純で

まずは頭抜きの手牌を一時変数にコピーします。

FORループを使い、1番目の牌から次に繋がる数牌を検索し、
見つかれば、その数からまた一つ大きい数字牌を検索します
3つ見つかればその牌を一時的に
tmpSyuntu[x] = [1牌目データ,2牌目データ,3牌目データ]
と言う風に格納し、そして検索している一時変数に入った手牌から該当する牌を消去します。

次に、上記のFOR文が終了したら、今度はまたFOR文を使い
1番目の牌から柄と数字が合う牌を検索します。
上記と同様に3つ見つかれば
tmpKotu[x] = [1牌目データ,2牌目データ,3牌目データ]
と言う風に格納します。

forが終了したら

mentukouho[x].syuntu = tmpSyuntu
mentukouho[x].kotu = tmpKotu

そしてココが肝心ですが、検索で残った牌も記録します

mentukouho[x].uki = 一時変数手牌

最後に今回の検索で使われた頭候補を記録します(上の検索手配から抜かれた2牌の事)

mentukouho[x].atama = tempAtama[0]

上記が順子優先検索です。
次は刻子優先検索に移ります。

再度、手牌を一時変数にコピーします
あとは刻子検索を先に行い
次に順子検索を行います。
それだけで完了です。

最終的に見つけた面子候補は以下のように細分化されます

「頭1組目候補」−「順子優先検索での面子」
        −「刻子検索優先での面子」

「頭2組目候補」−「順子優先検索での面子」
        −「刻子検索優先での面子」

以下頭が見つかった分…

そして最後には、頭待ち検索を行います。
今度は頭候補を検索せず、手牌をそのまま検索にかけます。
手牌を一時変数に格納し『順子優先検索』と『刻子優先検索』を

『順子優先検索』→『刻子優先検索』
『刻子優先検索』→『順子優先検索』

と、上と同じように順番を変えて行います

検索後の記録方式は

mentukouho[x].syuntu = tmpSyuntu
mentukouho[x].kotu = tmpKotu

そしてココが肝心ですが、検索で残った牌も記録します

mentukouho[x].uki = 一時変数手牌

ここまでは頭有り検索と同じですが、次の配列には頭無し検索の面子だという
何かわかりやすいスイッチを入れてください
型を合わせるために空の配列を入れるのもよいでしょう
後々配列カウントさせて頭の有無を判定させることも出来ます。

例:
mentukouho[x].atama = []

検索が終わると、見つけられた面子候補は

「頭候補」 −「順子優先検索での面子」
       −「刻子検索優先での面子」
「頭無候補」−「順子優先検索での面子」
       −「刻子検索優先での面子」

の2種完成します。

それでは、この処理の後は無駄な衝撃…いえ、無駄な面子を削除します。
と言うのも麻雀ゲームの処理の主はテンパイしていないと無意味だからです

テンパイしてるかも知れない大まかなフルイのかけ方は、先ほど面子候補変数に入れた
あまり牌の数で判定します。

頭有り検索の場合
mentukouho[x].uki.countが3以上あれば
頭無し検索の場合
mentukouho[x].uki.countが2以上あれば

ノーテンです。
実際に牌を使ってテンパイとそれ以外の型を作ると解ると思います。

上記条件以外の面子をfor文で検索します
注意として、配列を削除しながらの検索なので一番上位の配列から
つまり、マイナス進行のFOR文を書きます

頭有りの面子か、そうでないかでif文は変わりますが
その辺はmentukouho[x].atamaの中身を調べて判定を行うことが出来ます。

それではテンパイ時、リーチ時の待ち牌検索です!!!!

待ちはおおまかに3種類あります
単騎待ち、他面待ち、シャボ待ち・・・

このうちもっとも単純で簡単なのは単騎待ち。
次に簡単なのがシャボ待ち
そしてもっとも難しいのが多面待ちです

自分は最初、単騎待ちとシャボ待ちを作りましたが
結局後に作る多面待ちの処理一つで片付けることが出来ます。

では、なぜ多面待ちがややこしいのか
それはメンツとして確定している順子や柄違い牌の多面待ちの構成が絡んでくるからです。

例:
1234567m123p123s 北ツモの北切り147m頭待ち
22234m55p123789s 北ツモの北切り25m5pメンツ待ち

しかしどちらも前回の浮き牌から導き出すことが出来ます。

まず、他面待ちの検索に刻子の情報は必要有りません。
コーツの一部を順子とし、頭として使う場合もありますが
それは順子優先検索で導き出されているので問題有りません。

検索の流れ

  1. まず、メンツ候補の一つを読み込み、その中の浮き牌の柄種類を検索します
  2. 次に浮いた牌の柄と同じ柄の順子を読み出します
  3. そして浮いた牌と順子を纏めます
  4. 一番上のソートアルゴリズムを使い、整列させます
  5. AグループとBグループという牌情報を格納する配列変数を作ります
  6. 纏めた牌データをAグループにコピーし、先頭から1ずつ加算して分割していきます
  7. AグループBグループの中を順子検索し、順子になる牌を消します
  8. AグループBグループ合計し、余った牌の合計が頭有りメンツ候補なら3、頭ナシメンツ候補なら2あればそのあまり牌を記録します
  9. 頭無しメンツの場合、その記録された2牌どちらかが待ち牌となります
  10. 頭有りメンツの場合、余った3牌のうち、2牌の関係がカン待ち、順待ち、対子であれば採用します
  11. カンであれば互いの牌の間の数字牌が待ち牌になります
  12. 順待ちなら小さい数字側からマイナス1した牌か大きい数字側からプラス1した牌が待ち牌になります
  13. 対子であれば同じ牌が待ち牌となります
  14. 以上の条件からテンパイになる捨て牌情報を記録します

急ぎ足で説明しましたがイマイチ解りづらいと思うので、例を使って処理を書いていきます

メンツ ツモった牌

切り待ち


切り待ち

メンツ候補変数内にはいかのように格納されていると思います

頭有りメンツ候補数:0
頭無しメンツ候補数:1(順子優先で作られた1メンツのみ)

mentukouho[x].syuntu[0] =

mentukouho[x].syuntu[1] =

mentukouho[x].syuntu[2] =

mentukouho[x].syuntu[3] =

mentukouho[x].uki

浮き牌の柄を検索し、同じ柄の順子を取り出す
北は字牌なのでそもそも順子は無いが処理の統一性上無視する

合体させます

グループ分け検索開始(頭無し候補の検索、あまり牌が2なら条件達成)

Aグループ
  あまり

Bグループ
あまり

これで1m北待ちと言う待ちが見つけられます

Aグループ
 あまり

Bグループ
あまり

あまり牌が合計5つもあるので不採用

Aグループ
 あまり

Bグループ
 あまりゼロ

これで7m北待ちが見つけられます

Aグループ
 あまり

Bグループ
 あまり

これで4m北待ちが見つけられます

Aグループ
あまり

Bグループ
 あまり

あまりが合計5なので無効

Aグループ
あまり

Bグループ
 あまりゼロ

再び7m北待ちを発見

 

Aグループ
あまり

Bグループ
 あまり

最後も同じ結果で終了。

このように分割し、順子検索をし、その都度あまり牌の数をカウントし
条件内であれば記録しておきます。

今回は簡単な頭待ちのメンツでやりましたが
頭有りメンツでも処理は同じです。
ただ、テンパイにならない牌だけを、捨て牌候補用の変数に入れておきます。
リーチ時にこの変数を読み出し、ノーテンリーチを防いだり
捨て牌候補牌が捨てられた後、待ち牌候補と一致するロン牌が出るとロン出来ると言う処理に
持って行くことが出来ます。

以上で麻雀プログラムの半分は出来ました。
あとは役判定のアルゴリズムを見つけるのと
ローカルルールとの戦いになります。

というのも役のアルゴリズムを決定するのはルールなので
まずはどのルールを採用するのか…という所から始めないといけません。

次回は主な役のアルゴリズムを紹介していこうと思います。
といってもここまで来たらもう言わなくても出来そうですが;;

役の後はアンカンの条件や鳴き牌の処理についての話になると思います。
特にリーチ後のアンカンは待ちを変えてはいけないという制約があります。
残る難しいルーチンはその辺でおしまいな気がします。

本日はこれまで

つづく…?

画像素材元:オンライン対戦麻雀 天鳳 http://tenhou.net (C)C-EGG

inserted by FC2 system