«前の日記(2015年11月07日) 最新 次の日記(2015年11月13日)» 編集

ema log


2015年11月10日 [長年日記]

_ [Programming]ごいたシミュレーションのソースコードの解説

簡単に先日のシミュレーションのソースコード解説のメモを。デバッグ用コードのコメントアウトは消しています。

定数宣言

まずは、定数代わりにクラスを宣言しています。デバッグ時に文字列表示したくなったからです。遅くなりましたが、まぁコーヒー飲んでれば終わる次元なので問題なし。

class SHI  ; def to_s; "し"; end; def to_i; 0x11; end; end
class GON  ; def to_s; "香"; end; def to_i; 0x21; end; end
class GIN  ; def to_s; "金"; end; def to_i; 0x31; end; end
class KIN  ; def to_s; "銀"; end; def to_i; 0x32; end; end
class BAKKO; def to_s; "馬"; end; def to_i; 0x33; end; end
class KAKU ; def to_s; "角"; end; def to_i; 0x41; end; end
class HI   ; def to_s; "飛"; end; def to_i; 0x42; end; end
class OU   ; def to_s; "王"; end; def to_i; 0x51; end; end

定数でシミュレーション回数を宣言

TIMES = 10000000

main関数

一応、main 関数を作って、プログラム末尾から呼ぶ形にするのが好きです。

def main
山を作る

山を作ります。牌種に応じた枚数の配列を作り、連結していきます。ごいた牌をケースから取り出してるイメージですね。

	yama = Array.new(10, SHI.new)
	yama.concat Array.new( 4, GON.new)
	yama.concat Array.new( 4, GIN.new)
	yama.concat Array.new( 4, KIN.new)
	yama.concat Array.new( 4, BAKKO.new)
	yama.concat Array.new( 2, KAKU.new)
	yama.concat Array.new( 2, HI.new)
	yama.concat Array.new( 2, OU.new)
カウンタの初期化

各種発生回数のカウンタを0に初期化します。

	goshi = rokushi = nanashi = hasshi = goshigoshi = sanngon = yonngon = 0
	damadama = 0
シミュレーション

シミュレーション回数分のループを回します。

	TIMES.times do
配牌

配牌を配ります。山をシャッフルして、8枚ずつ取り出します。1枚ずつ配らなくて良いの?と思われるかもしれませんが、シャッフルが公正なら連続して8枚とっても確率での差は生まれないので8枚連続して配っています。

		yama.shuffle!
		hand1 = yama[ 0,8]
		hand2 = yama[ 8,8]
		hand3 = yama[16,8]
		hand4 = yama[24,8]
「香」周りのカウント

まずは、「香」についてが分かりやすいので順番を元のソースと入れ替えて先に説明します。

「香」だけは自分の積もる確率が知りたかったので、一人分の手牌だけを調べています。Array#count はブロックを付けて渡すと、その内容が true になる個数を返してくれます。ここでは GON クラスの個数、すなわち、手牌に「香」のある個数を調べているわけです。

	# 香は自分の手札に来る確率を求めたいので、自分の手札だけみる
		gon_count  = hand1.count{|o|o.class == GON}

後は、「香」の枚数毎にカウンタを加算するのみです。

		case gon_count
		when 3 then sanngon += 1
		when 4 then yonngon += 1
		end
「王」のダマダマのカウント

次に、「王」のダマダマを調べるコードです。四人の手牌に含まれる王の枚数をカウントし、それぞれについて「王」が2枚あれば、ダマダマカウンタを1増やしています。枚数の数え方は「香」と同一。山に「王」は2枚しか無いことが保証されているので重複の確認などは省いています。

		# ダマダマは発生する確率が欲しいので全部で調べる
		ou_counts = [
			hand1.count{|o|o.class == OU},
			hand2.count{|o|o.class == OU},
			hand3.count{|o|o.class == OU},
			hand4.count{|o|o.class == OU}
		]
		ou_counts.each do |ou_count|
			damadama += 1 if ou_count == 2
		end
「し」周りのカウント

つぎに「し」についてカウントしています。「5し5し」を調べるためにペアを組ませて、そのそれぞれに対してカウントする必要があります。そのために配列の配列を作っています。本来のルールではペアは対角で組みますが、ペアの組み方さえ変わらなければシミュレーション上の差は生まれないため、ここでは可読性を優先。

		# しは5し5しが有るのでペアで調べないといけない
		shi_counts = [
			[
				hand1.count{|o|o.class == SHI},
				hand2.count{|o|o.class == SHI}
			],
			[
				hand3.count{|o|o.class == SHI},
				hand4.count{|o|o.class == SHI}
			]
		]

Ruby では配列の配列のそれぞれをループして調べる際に、次のような書き方で配列の展開が出来ます。shi_count1 に配列の1番目が、shi_count2 に配列の2番目が代入されます。楽ちんですね。

		shi_counts.each do |shi_count1, shi_count2|

後は、ペアのプレイヤー毎に「し」の枚数を調べるだけです。「5し5し」のチェックだけ例外処理が入っています。shi_count1==5 かつ shi_count2==5 のときのみ、「5し5し」のカウンタを、そうでない単なる「5し」の場合には「5し」のカウンタを加算しています。「6し」「7し」「8し」は重複することがあり得ないため、こちらの例外処理は省いています。

			case shi_count1
			when 5
				if shi_count2 == 5
					goshigoshi += 1
				else
					goshi += 1
				end
			when 6 then rokushi += 1
			when 7 then nanashi += 1
			when 8 then hasshi  += 1
			end

			case shi_count2
			when 5
				goshi += 1 if shi_count1 != 5 # 5し5しのときはカウントしない
			when 6 then rokushi += 1
			when 7 then nanashi += 1
			when 8 then hasshi  += 1
			end
		end
	end
結果出力

さて、以上でシミュレーションが終わったので、後は結果を出力するだけです。それぞれの発生回数と、発生確率を計算しているだけですね。

	puts "だまだま:#{damadama}…#{damadama*100.0/TIMES} %"
	puts "五し:#{goshi  }…#{goshi  *100.0/TIMES} %"
	puts "六し:#{rokushi}…#{rokushi*100.0/TIMES} %"
	puts "七し:#{nanashi}…#{nanashi*100.0/TIMES} %"
	puts "八し:#{hasshi }…#{hasshi *100.0/TIMES} %"
	puts "五し五し:#{goshigoshi}…#{goshigoshi*100.0/TIMES} %"
	puts "三香:#{sanngon}…#{sanngon*100.0/TIMES} %"
	puts "四香:#{yonngon}…#{yonngon*100.0/TIMES} %"
end
main 関数呼び出し

最後に、main 関数を呼ぶコードを書いておいて、main 関数内に処理を書くスタイルが好きです。ただそれだけ。

main

以上がソースコードの概略です。プログラミング分かる人向けの説明しか書けません。