TOP > プログラミング関係解説&調査 > マップ乱数について

マップ乱数について

ダメージ計算式の簡易解説(乱数計算)では、戦闘における乱数について説明した。
だが、テレポートの仕様簡易解説では、乱数生成方法について説明を省いた。
そのあたりについて説明する。

ここまでに述べた通り、テレポートの飛び先などの計算で使われる乱数は、戦闘中にダメージ量計算などに使われる戦闘用乱数とは計算方法が異なる。
テレポートの飛び先計算で使われる乱数の計算方法は、マップ(フィールド)にいる時に常に計算され続けている乱数の計算方法とほぼ同一である。
世界の合言葉は森部様に、マップでの乱数に関して以下のような説明がある。

マップ乱数は、マップっぽい画面が見えている間、フレーム単位で変化する。
(マップっぽい画面という表現は曖昧ですが・・)
メニュー画面や名前入力画面では変化しない。
デモでおぼろ丸が降りてくる場面などでは変化する。

ということで以下では、マップ上で計算されている乱数のことは「マップ乱数」とする。

以下ではマップ乱数の生成方法について長々と話が続くので、マップ乱数計算方法だけ知りたいという方は、計算方法のまとめまで飛ばしていただきたい。

マップ乱数の生成概要

テレポートの飛び先計算及び、アイテム改造やたい焼き屋の客の確率計算に使われる、マップ乱数生成のサブルーチンのアドレスは、$C0/B900$C0/B924である。
実際には、この間に、$C0/BA16$C0/BA75というサブルーチンが2回挟まっている。
アドレスで記すと、

 $C0/B900$C0/B911
$C0/BA16$C0/BA75(1回目)
$C0/B914$C0/B91D
$C0/BA16$C0/BA75(2回目)
$C0/B920$C0/B924

以上で、乱数がインデックスレジスタX(以下Xと表記する)に出力という手順である。
マップ上では、1フレーム(つまり1/60秒)毎に1回の頻度で上の計算が繰り返されているようである。
(※マップ上だと、最初の「$C0/B900$C0/B911」が、「$C0/B90C$C0/B911」に置き換わっているが、内容に大きな違いはない)
マップやフィールド上では乱数計算以外の諸々(キャラのモーションやモブキャラの移動、エンカウント関係など)も一緒に計算されているので、乱数計算だけで1/60秒かかっているという意味ではない。
乱数計算など諸々込みで1フレームに1回ループし計算している、ということである。

また、その乱数は同時にメモリ$00:0036$7E:000D$00:000Dと同値)にも出力され、次の乱数の生成の計算にも用いられる。
$C0/BA16$C0/BA75の計算には[$00:0030]~[$00:0037]に収納された数値が使われており、何度も乗算を行うなどプログラミング初心者の筆者には謎の計算のように見えたが、色々といじりまわしたりネットで調べてみた結果、$C0/BA16$C0/BA75のサブルーチンは、
「16bit×16bit(16進数4桁×4桁)の乗算を行うサブルーチン」
らしい、と判明した。
つまり、

 $C0/B900$C0/B911
→16bit×16bitの乗算を行うサブルーチン(1回目)
$C0/B914$C0/B91D
→16bit×16bitの乗算を行うサブルーチン(2回目)
$C0/B920$C0/B924

こんな順序でマップ乱数が計算されているようだ。

ひとつ不思議なのは、テレポートの飛び先計算などで、上の乱数生成サブルーチンが使われているものの、「0~255の乱数生成」にも、「0~98の乱数生成」にも、同じサブルーチンを使っていることである。
ということは、サブルーチン実行前に、乱数の最大値を何かしらの変数としてサブルーチンに入れているはずである。
そのあたりについては後に記す。

16bit×16bitの乗算を行うサブルーチン

「16bit×16bitの乗算を行うサブルーチン」こと、$C0/BA16$C0/BA75の部分の説明から行う。
先に述べた通り、このサブルーチンには8種のメモリ[$00:0030][$00:0037]を使っている。
このサブルーチンに入る前に、[$00:0030]に被乗数(16bit)、[$00:0032]に乗数(16bit)をセットする。
16bit、つまり16進数で4桁の値をメモリに入れると、下位2桁が該当アドレスのメモリ、上位2桁が該当アドレス+1のメモリに入るのがポイント。

例えば、$1234×$ABCDの16bit×16bit掛け算をしたい場合、[$00:0030]$1234[$00:0032]$ABCDを入れる。
各アドレスは8ビット、つまり16進数にして2文字しかデータを収められない。
よって、
[$00:0030]$1234を入れる」
という時は、[$00:0030]には下2桁の$34しか入らない。
$12の部分は、[$00:0030]の次の[$00:0031]に入る。
という8bitモードと16bitモードの違いはこれまでも触れてきた通りである。

「16bitモードで、[$00:0030]$1234[$00:0032]$ABCDを入れる」
を、実際に行った結果は以下の通りである。

アドレス数値(8bitモード)
[$00:0030]$34
[$00:0031]$12
[$00:0032]$CD
[$00:0033]$AB

$1234」「$ABCD」が分割されて[$00:0030][$00:0033]に入った状態でサブルーチンを実行する、ということになる。

さて、以下が実際の「16bit×16bitの乗算を行うサブルーチン」である。
また、符号付8bit×8bitの計算を4回実行しているが、どこで何を実行したか区別するため、8bit×8bitに関係するところには①②③④と丸数字を付けておく。

$C0/BA16 PHX                   ;Xをスタックにプッシュ
$C0/BA17 LDA $30    [$00:0030] ;Aに[$00:0030]をロード
$C0/BA19 STA $4202  [$00:4202] ;①A([$00:0030])を被乗数として[$00:4202]にセット
$C0/BA1C LDA $32    [$00:0032] ;Aに[$00:0032]をロード
$C0/BA1E STA $4203  [$00:4203] ;①A([$00:0032])を乗数として[$00:4203]にセット
$C0/BA21 NOP                   ;(乗算の計算待機)
$C0/BA22 NOP                   ;(乗算の計算待機)
$C0/BA23 NOP                   ;(乗算の計算待機)
$C0/BA24 LDA $31    [$00:0031] ;Aに[$00:0031]をロード
$C0/BA26 LDX $4216  [$00:4216] ;①掛け算の結果をXにロード
$C0/BA29 STX $34    [$00:0034] ;①(掛け算の結果)を[$00:0034]に書き込み
$C0/BA2B STA $4202  [$00:4202] ;②③A([$00:0031])を被乗数として[$00:4202]にセット
$C0/BA2E LDA $33    [$00:0033] ;Aに[$00:0033]をロード
$C0/BA30 STA $4203  [$00:4203] ;②A([$00:0033])を乗数として[$00:4203]にセット
$C0/BA33 NOP                   ;(乗算の計算待機)
$C0/BA34 NOP                   ;(乗算の計算待機)
$C0/BA35 NOP                   ;(乗算の計算待機)
$C0/BA36 LDA $32    [$00:0032] ;Aに[$00:0032]をロード
$C0/BA38 LDX $4216  [$00:4216] ;②掛け算の結果をXにロード
$C0/BA3B STX $36    [$00:0036] ;②(掛け算の結果)を[$00:0036]に書き込み
$C0/BA3D STA $4203  [$00:4203] ;③A([$00:0032])を乗数として[$00:4203]にセット
$C0/BA40 NOP                   ;(乗算の計算待機)
$C0/BA41 NOP                   ;(乗算の計算待機)
$C0/BA42 NOP                   ;(乗算の計算待機)
$C0/BA43 REP #$20              ;Mフラグクリア Aレジスタ16bitモード
$C0/BA45 LDA $4216  [$00:4216] ;③掛け算の結果をAにロード
$C0/BA48 CLC                   ;キャリーフラグをクリア
$C0/BA49 ADC $35    [$00:0035] ;③(掛け算の結果) + [$00:0035] (16bitモード)
$C0/BA4B STA $35    [$00:0035] ;A(③ + [$00:0035])を[$00:0035]に書き込み (16bitモード)
$C0/BA4D SEP #$20              ;MフラグON Aレジスタ8bitモード
$C0/BA4F LDA #$00              ;Aに$00をロード
$C0/BA51 ADC $37    [$00:0037] ;A + [$00:0037]
$C0/BA53 STA $37    [$00:0037] ;Aを[$00:0037]に書き込み
$C0/BA55 LDA $30    [$00:0030] ;Aに[$00:0030]をロード
$C0/BA57 STA $4202  [$00:4202] ;④A([$00:0030])を被乗数として[$00:4202]にセット
$C0/BA5A LDA $33    [$00:0033] ;Aに[$00:0033]をロード
$C0/BA5C STA $4203  [$00:4203] ;④A([$00:0033])を乗数として[$00:4203]にセット
$C0/BA5F NOP                   ;(乗算の計算待機)
$C0/BA60 NOP                   ;(乗算の計算待機)
$C0/BA61 NOP                   ;(乗算の計算待機)
$C0/BA62 REP #$20              ;Mフラグクリア Aレジスタ16bitモード
$C0/BA64 LDA $4216  [$00:4216] ;④掛け算の結果をAにロード
$C0/BA67 CLC                   ;キャリーフラグをクリア
$C0/BA68 ADC $35    [$00:0035] ;A④(掛け算の結果) + [$00:0035] (16bitモード)
$C0/BA6A STA $35    [$00:0035] ;A(④ + [$00:0035])を[$00:0035]に書き込み (16bitモード)
$C0/BA6C SEP #$20              ;MフラグON Aレジスタ8bitモード
$C0/BA6E LDA #$00              ;Aに$00をロード
$C0/BA70 ADC $37    [$00:0037] ;A + [$00:0037]
$C0/BA72 STA $37    [$00:0037] ;Aを[$00:0037]に書き込み
$C0/BA74 PLX                   ;Xに値をプル
$C0/BA75 RTS                   ;サブルーチン戻り

上のサブルーチンで16bit×16bitの乗算をどうやっているのか、理屈を説明するため、わかりやすく10進法の話に例えることにしよう。
4桁の掛け算、たとえば、

1234×5678

を計算するとする。
ただし、「掛け算のみ、2桁×2桁の計算か、×10nしかできない」という条件付きである。
この掛け算は、それぞれの数字を分割すると、

(1200+34)×(5600+78)

である。つまり、

1200×5600 + 1200×78 + 5600×34 + 34×78
=12×56×104 + (12×78 + 56×34)×102 + 34×78

と、4回の2桁×2桁の計算及び、×104×102に分解できる。

この分解&計算を16進数で行ったのが、上のサブルーチンだと考えれば良い。
(前にも記したが、スーパーファミコンのプログラミング言語65C816には、乗除算の機能がなく、コプロセッサという別機能に数値を渡して乗除算を行う。しかも計算の桁数によって様々な工夫をしなければならない。今回は、2桁×2桁に分解して計算するという工夫で4桁×4桁を実現しているのである)
104102の部分を、164162に変えれば、16進数の計算と同じである。
数字の分解自体は、既に[$00:0030][$00:0033]で4分割されているから、サブルーチン前に終えている。
それらが「4回の2桁×2桁の計算(=符号付8bit×8bitの計算)」で処理され、答えもまた4分割されて[$00:0034][$00:0037]に入る、という手順である。
16bit×16bitの答えの最大値は$FFFF×$FFFF = $FFFE0001、つまり16進数だと桁数の最大値は8である。
サブルーチン計算後、実際に入る数値は、8桁を4分割して、

アドレス数値
[$00:0034]計算結果の上から7・8桁目
[$00:0035]計算結果の上から5・6桁目
[$00:0036]計算結果の上から3・4桁目
[$00:0037]計算結果の上から1・2桁目

である(各8bit)。

例:
最初に記した、$1234×$ABCDの16bit×16bit乗算の答えは「$0C374FA4」である。
(WindowsパソコンならWindows付属の電卓をプログラマー電卓モードに切り替え、16進数のHEXモードに切り替えて入力すれば答えが計算される。また、検索すれば16進数の計算ができるウェブサイトも多数ある)
上サブルーチン実行前に、以下のように数値をセットすると、

アドレス数値
[$00:0030]$34
[$00:0031]$12
[$00:0032]$CD
[$00:0033]$AB

サブルーチン実行後には、[$00:0034]~[$00:0037]に以下のように答えが出力される。

アドレス数値
[$00:0034]$A4
[$00:0035]$4F
[$00:0036]$37
[$00:0037]$0C

乱数生成計算

以上、16bit×16bit乗算サブルーチンの詳細が判明したところで、全体を見返してみる。

 $C0/B900$C0/B911
→16bit×16bitの乗算を行うサブルーチン(1回目)
$C0/B914$C0/B91D
→16bit×16bitの乗算を行うサブルーチン(2回目)
$C0/B920$C0/B924

16bit×16bit乗算サブルーチン以外の部分の実際の計算は、以下の通り。
なおここでは、サブルーチン前にX$100 = 10進数256が入力されたものとして進める。

$C0/B900 PHA                    ; X:0100 Aをスタックにプッシュ
$C0/B901 PHB                    ; X:0100 DBレジスタをスタックにプッシュ
$C0/B902 LDA #$00               ; X:0100 Aに$00をロード
$C0/B904 PHA                    ; X:0100 Aをスタックにプッシュ
$C0/B905 PLB                    ; X:0100 DBレジスタに値をプル($00)
$C0/B906 PHX                    ; X:0100 Xをスタックにプッシュ
$C0/B907 LDX #$3D09             ; X:0100 $3D09をXにロード
$C0/B90A STX $30    [$00:0030]  ; X:3D09 Xを[$00:0030]に書き込み
$C0/B90C LDX $0C    [$00:000C]  ; X:3D09 [$00:000C]をXにロード
$C0/B90E INX                    ; X:xxxx Xをインクリメント(+1)
$C0/B90F STX $32    [$00:0032]  ; X:xxxx Xを[$00:0032]に書き込み
$C0/B911 JSR $BA16  [$C0:BA16]  ; X:xxxx 16bit×16bit乗算サブルーチンにジャンプ
;
;16bit×16bit乗算サブルーチン1回目(計算省略)
;
$C0/B914 LDX $34    [$00:0034]  ; X:xxxx Xに[$00:0034]をロード
$C0/B916 STX $0C    [$00:000C]  ; X:xxxx X(=[$00:0034])を[$00:000C]に書き込み
$C0/B918 STX $32    [$00:0032]  ; X:xxxx X(=[$00:0034])を[$00:0032]に書き込み
$C0/B91A PLX                    ; X:xxxx Xに値をプル
$C0/B91B STX $30    [$00:0030]  ; X:0100 Xを[$00:0030]に書き込み
$C0/B91D JSR $BA16  [$C0:BA16]  ; X:0100 16bit×16bit乗算サブルーチンにジャンプ
;
;16bit×16bit乗算サブルーチン2回目(計算省略)
;
$C0/B920 PLB                    ; X:0100 DBレジスタに値をプル
$C0/B921 LDX $36    [$00:0036]  ; X:0100 Xに[$00:0036]をロード
$C0/B923 PLA                    ; X:00xx Aに値をプル
$C0/B924 RTS                    ; X:00xx サブルーチン戻り

最後、$C0/B921の「LDX $36」でXに出力された値(8bit)が乱数として扱われることになる。
では、各部分を見ていく。

$C0/B900 PHA                    ; X:0100 Aをスタックにプッシュ
$C0/B901 PHB                    ; X:0100 DBレジスタをスタックにプッシュ
$C0/B902 LDA #$00               ; X:0100 Aに$00をロード
$C0/B904 PHA                    ; X:0100 Aをスタックにプッシュ
$C0/B905 PLB                    ; X:0100 DBレジスタに値をプル($00)
$C0/B906 PHX                    ; X:0100 Xをスタックにプッシュ
$C0/B907 LDX #$3D09             ; X:0100 $3D09をXにロード
$C0/B90A STX $30    [$00:0030]  ; X:3D09 Xを[$00:0030]に書き込み
$C0/B90C LDX $0C    [$00:000C]  ; X:3D09 [$00:000C]をXにロード
$C0/B90E INX                    ; X:xxxx Xをインクリメント(+1)
$C0/B90F STX $32    [$00:0032]  ; X:xxxx Xを[$00:0032]に書き込み
$C0/B911 JSR $BA16  [$C0:BA16]  ; X:xxxx 16bit×16bit乗算サブルーチンにジャンプ

最初の16bit×16bit乗算サブルーチン前なので、これから掛け算に使う値を[$00:0030][$00:0033]に入れている。
$C0/B907$C0/B90Aでは、$3D09[$00:0030]に書き込んでいる(16bit)。
よってここで、

アドレス数値
[$00:0030]$3D09
[$00:0031]$3D

が確定である。

また、$C0/B90F[$00:0032]にXを書き込み(16bit)しているが、その前の計算をみると、Xに入っているのは[$00:000C](16bit)を+1した値だということがわかる。
16bitであるから、[$00:000C]の次のメモリ[$00:000D]に入っている8bitの値が、[$00:000C]の上2桁の扱いになる、という点に注意。
[$00:000C](16bit)は、「[$00:000D](8bit)×$100 + [$00:000C](8bit)」と同じ意味になる。

よって、16bit×16bit乗算サブルーチンにて計算したいのは、

$3D09×([$00:000C](16bit) + 1)

ということがわかる。
また、

アドレス数値
[$00:0032][$00:000C](16bit)」の下位2桁
[$00:0033][$00:000C](16bit)」の上位2桁

である。

なお、最初に書いたが、計算した乱数は最終的に$00:0036$7E:000D$00:000Dと同値)にも出力される。
つまり、「ひとつ前に計算された乱数の値を、次の乱数の計算に使っている」ということがここでわかる。
戦闘用乱数と同じで、何かしらの漸化式でマップ乱数も計算しているということだ。
後々の都合から、この漸化式を、[$00:000C]の「C」から取って、

Cn

としよう。
Cnは16bit4桁の値で、上2桁が[$00:000D](8bit)、下2桁が[$00:000C](8bit)ということになる。
よって

乱数[$00:000D] = Cn / $100

で表すことが可能である。

今回、16bit×16bit乗算に入れるひとつ前のCnのことは、Cn-1として、

$3D09 × (Cn-1 + 1)

と、いうことになる。

また、$C0/B906にて一度、X(=$100)がスタックにプッシュ(収納)されている。

;16bit×16bit乗算サブルーチン1回目(計算省略)
;
$C0/B914 LDX $34    [$00:0034]  ; X:xxxx Xに[$00:0034]をロード
$C0/B916 STX $0C    [$00:000C]  ; X:xxxx X(=[$00:0034])を[$00:000C]に書き込み
$C0/B918 STX $32    [$00:0032]  ; X:xxxx X(=[$00:0034])を[$00:0032]に書き込み
$C0/B91A PLX                    ; X:xxxx Xに値をプル
$C0/B91B STX $30    [$00:0030]  ; X:0100 Xを[$00:0030]に書き込み
$C0/B91D JSR $BA16  [$C0:BA16]  ; X:0100 16bit×16bit乗算サブルーチンにジャンプ

1回目の16bit×16bit乗算サブルーチンを行った後。
ここで、

$3D09×([$00:000C](16bit) + 1)

の結果が、[$00:0034][$00:0037]に分かれて出力されたはずである。
先程記した通り、乗算結果は、

アドレス数値
[$00:0034]計算結果の上から7・8桁目
[$00:0035]計算結果の上から5・6桁目
[$00:0036]計算結果の上から3・4桁目
[$00:0037]計算結果の上から1・2桁目

となっている。
その上で、16bit×16bit乗算サブルーチンの後を見てみる。

$C0/B916では、[$00:0034](16bit)を[$00:000C]に書き込んでいる。
[$00:0034](16bit)は、[$00:0035](8bit)・[$00:0034](8bit)のことになるから、結局、「乗算結果の5~8桁目」、下4桁を意味する。
また16bitなので、

[$00:000C]には「1回目の16bit×16bit乗算結果の5~8桁目(16bit)」
[$00:000D]には「1回目の16bit×16bit乗算結果の5~6桁目」

が入ったということになる。
「乗算結果の5~8桁目(=下4桁)」と書くと後々の計算で面倒なので、「16進数から下4桁だけを取り出す」ことにする。
論理積の記号を「&」として、

[$00:000C]には「1回目の16bit×16bit乗算結果 & $FFFF(16bit)

が入った、と書き換えることができる。

例:
$12345678から、下4桁だけ抜き出したい時
$12345678 & $FFFF = $5678
となる。

そして重要なことは、$3D09×([$00:000C](16bit) + 1)を計算した結果、その乗算結果の5~8桁目を[$00:000C](16bit)に上書きした、という点。
最初にも書いたが、[$00:000C]はひとつ前の[$00:000C]から計算している漸化式であり、

Cn = ($3D09 × (Cn-1 + 1)) & $FFFF

ということがわかったのである。
更に、

乱数[$00:000D] = Cn / $100

とも記した。
このため、

乱数(8bit) = (($3D09 × (Cn-1 + 1)) & $FFFF) / $100
※Cn-1は16bit

ということになる。

……と、勝手に話が終わりそうな雰囲気だが、実際にはサブルーチンはまだ続いている。
だが、後の処理を見るとわかるとおり、実はこれで合っている。
とりあえず後の処理について続ける。

$C0/B91Aでは、16bit×16bit乗算サブルーチンにスタックへプッシュしていた元々の値$100をプル(戻す)している。
それを[$00:0030]に書き込み(16bit)しているので、

[$00:0030]には$0100
[$00:0031]には$01

が入ったということになる。
※ここでは、サブルーチン開始前のXの値 = $100の場合で計算をしているが、これ以外の値が入ることもある。その話は後で。

ここで、2回目の16bit×16bit乗算サブルーチンで何を計算するかが判明した。
上の数値から、

$0100×「1回目の16bit×16bit乗算結果の5~8桁目(下4桁)」

である。
ビット計算に慣れている方なら、これなら計算するまでもないとすぐに気づくだろう。
16進数で$100を掛け算するということは、下に00をふたつ付けるということである。
また、1~2桁目も必ず00になる。
(10進数で例えると、1234×100 = 00123400であるのと同じ理屈)

何はともあれ、続きを見てみるとしよう。

;16bit×16bit乗算サブルーチン2回目(計算省略)
;
$C0/B920 PLB                    ; X:0100 DBレジスタに値をプル
$C0/B921 LDX $36    [$00:0036]  ; X:0100 Xに[$00:0036]をロード
$C0/B923 PLA                    ; X:00xx Aに値をプル
$C0/B924 RTS                    ; X:00xx サブルーチン戻り

最後にやっていることは実質、$C0/B921の「X[$00:0036]をロード」だけである。
8bitであるから、

[$00:0036] 乗算結果の3・4桁目
[$00:0037] 乗算結果の1・2桁目

が呼び出されてXに入る。
先に述べたが、2回目の16bit×16bit乗算サブルーチンでやったことは、結局、

アドレス桁の位置数値
[$00:0034]2回目の乗算結果の上から7・8桁目$00
[$00:0035]2回目の乗算結果の上から5・6桁目1回目の16bit×16bit乗算結果の7・8桁目がそのまま入る
[$00:0036]2回目の乗算結果の上から3・4桁目1回目の16bit×16bit乗算結果の5・6桁目がそのまま入る
[$00:0037]2回目の乗算結果の上から1・2桁目$00

と、いうことになる。
つまりXに入る値は必ず、「$00??」であり、「16進数1~2桁の数」になることが決定する。
つまり$00~$FF、0から255の乱数を生成したことがわかる。
また、「??」は、「16bit×16bit乗算サブルーチン1回目の乗算結果の5・6桁目」である。

ここで少し巻き戻って、$C0/B916の処理を見直してみる。そこでは新たに、

[$00:000C]には「1回目の16bit×16bit乗算結果の5~8桁目(16bit)」
[$00:000D]には「1回目の16bit×16bit乗算結果の5~6桁目」

を入れたのだから、結局この時点で[$00:000D]に乱数(8bit)が入っていたということになる。
先に記したとおり、

0から255の乱数を生成する計算式:
乱数(8bit) = (($3D09 × (Cn-1 + 1)) & $FFFF) / $100
Cn = ($3D09 × (Cn-1 + 1)) & $FFFF
Cnは16bit

これが結論となるのである。
(合っているかどうかはまったく自信がない)
マップ上では、ひとつ前の乱数が[$00:000C][$00:000D](各8bit)として入っていて、16bitモードで[$00:000C]を呼び出すと、[$00:000C]は「[$00:000D]*$100 + [$00:000C]」の形で読み込まれ、上の計算の結果、上2桁を新乱数としており(新[$00:000D])、下2桁(新[$00:000C])と合わせて次の乱数の計算用に記録される。
という手順なのだろう。

例:Cn-1 = $1234だった時、つまりひとつ前の乱数が$12の時、

Cn = ($3D09×($1234 + 1)) & $FFFF = $44DD
乱数(8bit)は $44DD / $100 = $44

と、いうことになる……と思われる。

生成する乱数の幅

さて、話を最初の方へ戻すが、「0~255の乱数生成」にも、「0~98の乱数生成」にも、同じサブルーチンを使っていると記した。
ここまでは「0~255の乱数生成」の乱数生成の話である。
「0~98の乱数生成」は、どこが違うのだろうか?
先に結論を述べると、乱数生成サブルーチン前のXの値である。
Xに、16進数で「生成したい乱数の最大値+1」を入れてから乱数生成サブルーチンを計算すると、「0~生成したい乱数の最大値」の乱数が算出されるのである。
「0~98の乱数生成」をしたい場合、98は16進数で$62
ということは、X = $63でサブルーチンを開始すると、「$00~$62」の乱数が生成されるはずだ。
Xの値が関係するのは、16bit×16bit乗算サブルーチン2回目である。
2回目の乗算は、

$0063×「1回目の16bit×16bit乗算結果の5~8桁目(下4桁)」

に、変更されることになる。
乗算計算結果の[$00:0036]、つまり乗算結果の3・4桁目(下からだと5・6桁目の位置)が新たな乱数になる、ということは変わらない。
こうすると何が変わるのだろうか。
わかりやすくするため、前にもやってみた、10進数4桁の掛け算の話をもう一度やってみよう。

たとえば、10進数で、

0063×9999

の計算とする。
答えは629937であり、0063×10000(630000)より63少ない。
先程と同じように数を分解すると、

= (0000+63)×(9900+99)
= 0000×9900 + 0000×99 + 9900×63 + 63×99
= 9900×63 + 63×99

となる。
63×99の部分は、計算しても最大4桁。
9900×63の部分は、下2桁は必ず00で、残りの99×63については、100×63=6300を越えることはない。
つまり上2桁(下からだと5・6桁目の位置)が、63を越えることはない。
実際の結果も629937で、上2桁は「62」である。
ここから考えて、

$0063×「1回目の16bit×16bit乗算結果の5~8桁目(下4桁)」

の結果、下からだと5・6桁目の位置の数値は、$00~$62である。

もちろん、$100$0063だけでなく、他の数値でも同じことが言える。
(いちいち計算しなくても、0063×10000の結果からすぐにわかった人もいるかもしれないが、筆者は数字の計算が不得手なのでこうして延々と書き記してやっとわかった次第である)

結果

図に書いてみると以下の通り。

マップ乱数Cnの生成式は、

乱数(8bit) = (X * (($3D09 × (Cn-1 + 1)) & $FFFF) & $00FF0000) / $10000
Cn = ($3D09 × (Cn-1 + 1)) & $FFFF

※すべて16進数
Cnは16bit
Xは「生成したい乱数の最大値-1」(16進数)

こんな感じになるだろうか?
X = $63を入れれば、「0~98の乱数生成」になるだろう。
乱数が0~255の時は、X$100のために計算式がシンプルになって、

乱数(8bit) = (($3D09 × (Cn-1 + 1)) & $FFFF) / $100
Cn = ($3D09 × (Cn-1 + 1)) & $FFFF

※すべて16進数
Cnは16bit

である。

戦闘中の乱数計算方法と比べるとややシンプルである。
戦闘乱数は1~4個前の乱数を記録していて3・4個前の乱数を新しい乱数の計算に使っていたが、マップ乱数はひとつ前の乱数が記録されているだけである。
ただし、形式自体は、戦闘乱数もマップ乱数も漸化式を使っており、擬似乱数列の生成式の一種、線形合同法(線形合同法 - Wikipedia)の考え方を応用したものだろう、と思われる。
また、マップ乱数の方が計算方法がシンプルなのは、マップ上なら1/60秒毎に計算する値だからであろう。戦闘乱数は必要な時にだけ計算され、戦闘中常に計算し続けているのではない。

おまけとして、Cn初期値に$1234をセットした上で、0~255の乱数、0~98の乱数を300個出力するプログラムをJavaScriptで書いてみた。
なんとなくそれっぽい乱数が出力されているようだ。
また、0~255の乱数は「0~127: 152個, 128~255: 148個」という結果になったため、確率1/2・1/2に近いし、0~98の乱数は「0~32: 99個, 33~65: 103個, 66~98: 98個」であるから、確率1/3・1/3・1/3に近い。
該当のJavaScriptはオンラインでプログラムを実行できるpaiza.IOで公開している。

乱数の幅を決定する箇所

ついでに、どの時点でXの値がセットされたのか? も説明する。
テレポートの説明の際に、マップ乱数生成サブルーチンへ飛ぶアドレスは、

  • $C0/378F JSR $B900
  • $C0/37AC JSR $B900

の2パターンある、と紹介した。
この2パターンの前後の処理を見てみると、違いがわかる。

$C0/378F JSR $B900からジャンプする時:

$C0/3787 PHB                  ;DBレジスタの値をスタックにプッシュ
$C0/3788 LDA #$4800           ;Aに$4800をロード
$C0/378B PLB                  ;DBレジスタにスタックから値をプル
$C0/378C LDX #$0100           ;Xに$0100をセット
$C0/378F JSR $B900  [$C0:B900];$C0:B900(マップ乱数計算サブルーチン)にジャンプ
;(マップ乱数計算サブルーチン、Xに計算した乱数をセット)
$C0/3792 PLB                  ;DBレジスタにスタックから値をプル
$C0/3793 CPX #$0080           ;Xと$0080(10進数の128)を減算比較(X - $0080)
$C0/3796 BCS $05    [$379D]   ;Cフラグが立っているなら[$C0:379D]へ
$C0/3798 INY                  ;Yをインクリメント
$C0/3799 INY                  ;Yをインクリメント
$C0/379A BRL $E404  [$1BA1]   ;[$C0:1BA1]へジャンプ

$C0/378F JSR $B900
の1つ前に
$C0/378C LDX #$0100
という処理がある。
ここが「X$0100(10進数の256)をセット」にあたり、乱数生成サブルーチンで0~255の乱数を算出するということになる。
乱数生成してからは、
$C0/3793 CPX #$0080
で生成した乱数が$80(10進数128)未満か以上かを判定するので、このあたりのサブルーチンは
「生成した乱数から1/2の確率を判定するサブルーチン」
である。

$C0/37AC JSR $B900からジャンプする時:

$C0/37A4 PHB                  ;DBレジスタの値をスタックにプッシュ
$C0/37A5 LDA #$4800           ;Aに$4800をロード
$C0/37A8 PLB                  ;DBレジスタにスタックから値をプル
$C0/37A9 LDX #$0063           ;Xに$0063をセット
$C0/37AC JSR $B900  [$C0:B900];$C0:B900(マップ乱数計算サブルーチン)にジャンプ
;(マップ乱数計算サブルーチン、Xに計算した乱数をセット)
$C0/37AF PLB                  ;DBレジスタにスタックから値をプル
$C0/37B0 CPX #$0042           ;Xと$0042(10進数の66)を減算比較(X - $0042)
$C0/37B3 BCS $0C    [$37C1]   ;Cフラグが立っているなら[$C0/37C1]へジャンプ
$C0/37B5 INY                  ;Yをインクリメント
$C0/37B6 INY                  ;Yをインクリメント
$C0/37B7 CPX #$0021           ;Xと$0021(10進数の33)を減算比較(X - $0021)
$C0/37BA BCS $05    [$37C1]   ;Cフラグが立っているなら[$C0/37C1]へジャンプ
$C0/37BC INY                  ;Yをインクリメント
$C0/37BD INY                  ;Yをインクリメント
$C0/37BE BRL $E3E0  [$1BA1]   ;[$C0:1BA1]へジャンプ

一方で、
$C0/37AC JSR $B900
の1つ前に
$C0/37A9 LDX #$0063
という処理がある。
これは「X$0063をセット」の意味であり、先に述べた通りに、「0~98の乱数生成」になる。
乱数生成してからは、
$C0/37B0 CPX #$0042
$C0/37B7 CPX #$0021
の2ヶ所の分岐で、「乱数が0~32」「33~65」「66~98」を判定するため、このあたりのサブルーチンは
「生成した乱数から各1/3の確率を判定するサブルーチン」
である。

まとめると、

  • $C0/378F JSR $B900からジャンプする時は、X = 16進数$0100 = 10進数の256なので、0~255($FF)の乱数を生成する。
  • $C0/37AC JSR $B900からジャンプする時は、X = 16進数$63 = 10進数の99なので、0~98($62)の乱数を生成する。

と、いうことになる。



このページをシェアする

上へ