指定した範囲の乱数を作る際の罠(実戦編)

アルゴリズム
スポンサーリンク

はじめに

みなさん、こんにちは。

これまで2回にわたって、指定した範囲で乱数を作る方法や、その「罠」について紹介してきました。改めて振り返ると、乱数って意外と奥が深いですね。

読み忘れた方のためにリンクを貼っておきます!

実例で乱数の“落とし穴”を体感する

はじめに

ここまでの内容を見て、「ビット数の小さい乱数生成器とか、普段はあまり関係ないのでは?」と感じた方もいるかもしれません。でも、実際のアプリやゲーム開発では、この“ちょっとした違い”が意外な落とし穴になることがあります。

今回は短めの記事ですが、具体例を通して乱数の「ありがちなワナ」にハマってみましょう。

ネットワークゲームのレアアイテム確率を考える

例えば、あなたがネットワークゲームを設計しているとします。モンスターを倒したとき、レアアイテムがドロップする確率を0.01%刻みで細かく設定できるようにしたい。使う言語は C または HSP3 を想定したいと思います。

乱数作成も高速化したいし、x = rand() % 10000 で0~9999の値を作って、if(x < 1000) なら10%で成功!

こんな考えで、実際にコードを書いてみましょう。

実際にHSPでやってみる

HSP には rnd(n) という関数がありますが、これはC言語でいう rand() % n と同じです。

#define multiplier 214013
#define addend 2531011

x = 1

goto *start

#defcfunc rnd2 int s
	x = x * multiplier + addend
	return (((x >> 16) & 0x7FFF) \ s)

*start

mes "HSPの rnd(n) は、余りを使用して乱数を切り出している。"
mes "HSPの rnd -> " + rnd(100) + " は"
mes "上記の rnd2 -> " + rnd2(100) + " と同じ。"

このように、rnd(n) も「余り」を使って乱数を作っています。

実際の分布を調べてみる

では、こういった乱数で実際にどのくらい均等な分布になるのか、10万回試して度数分布表を作ってみましょう。

size = 100000
dim bin, 10

repeat size
	x = rnd(10000)
	bin(int(x / 1000))++	
loop

repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

実行してみると、なんと驚きの結果が出ます。

範囲 回数
[ 0, 1000) 12401
[1000, 2000) 12176
[2000, 3000) 11618
[3000, 4000) 9079
[4000, 5000) 9088
[5000, 6000) 9107
[6000, 7000) 9150
[7000, 8000) 9048
[8000, 9000) 9316
[9000,10000) 9017

グラフにしてみると、その偏りがよりハッキリ分かります。

histogram

より正しい乱数を作るには

さて、こうした問題を避けるにはどうすればいいか?

HSP3でより正しい乱数を作る方法をいくつか紹介します。下記のコードは、ビット数を増やす工夫や、余りが出た場合に再抽選する手法なども盛り込んでいます。

#module

#defcfunc rnd15 int s
	// 15ビットの実数の乱数を作成
	return int(((double(rnd(0x8000))) / 0x8000) * s)

#defcfunc rnd30 int s
	// 30ビットの実数の乱数を作成
	return int(((32768.0 * rnd(0x8000) + rnd(0x8000)) / 0x40000000) * s)

#defcfunc rnd52 int s, local a
	// 52ビットの実数の乱数を作成
	a = double(rnd(0x8000))
	repeat 3
		a *= 32768
		a += rnd(0x8000)
	loop
	return int(a / 1152921504606846976.0 * s)

#defcfunc rnd4 int s, local a, local b
	// 繰り返して範囲外は捨てる
	repeat
		a = rnd(0x8000)
		b = a \ s
		if(a - b + s <= 0x8000) {
			break
		}
	loop
	return b

#global

font "MS ゴシック", 12
pos 5, 5

// 作成する乱数の範囲によって、
// 乱数のビットを大きいものを選ぶか、
// 上記の rnd4() を利用するといいです。

randomize
size = 1000000

mes "通常版"
dim bin, 10
repeat size
	x = rnd(10000)
	bin(int(x / 1000))++	
loop
repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

mes "工夫したもの"
dim bin, 10
repeat size
	x = rnd15(10000)
	bin(int(x / 1000))++	
loop
repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

mes "誤差が少ない"
dim bin, 10
repeat size
	x = rnd30(10000)
	bin(int(x / 1000))++	
loop
repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

pos 200, 5

mes "最も誤差が少ない"
dim bin, 10
repeat size
	x = rnd52(10000)
	bin(int(x / 1000))++	
loop
repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

mes "正しい乱数"
dim bin, 10
repeat size
	x = rnd4(10000)
	bin(int(x / 1000))++	
loop
repeat 10
	mes strf("[%5d ,%5d) ... %5d回", (cnt * 1000), ((cnt + 1) * 1000), bin(cnt))
loop

以下が実行結果になります。

上記の「正しい乱数」のアルゴリズムについては、後編でも説明していますので、興味がある方はそちらも参照してください。

おわりに

今回は、身近なプログラムの例を通じて、乱数の落とし穴について紹介しました。普段あまり意識しない部分ですが、乱数の扱いには十分注意が必要です。

より正確な乱数が必要な場合は、ビット数を増やしたり、余りが出るときは再抽選する方法を選ぶと良いでしょう。もし詳しく知りたい方は、ぜひ前編後編もあわせて読んでみてください!

コメント

タイトルとURLをコピーしました