きほんはまる?

DQRTAについていろいろ書いていきます。こっちでは最低限のアウトプットには書かれない細かい話がメインです。

SFC版DQ2 はぐれメタル1+ホイミスライム4のはぐれメタルのHP決定について

SFCDQ2並走部で話題になっており、独自に調査していたのでそのまとめ。
ch.nicovideo.jp

結論としては、はぐれメタル1+ホイミスライム4出現時のはぐれメタルのHPは、ほとんどの場合で4となるが、極めて低い確率で5となる場合もある」となる。

1. SFCDQ1・2における乱数生成処理

SFCDQ1・2における乱数生成処理は、Kriさんが述べているように計算式としては以下のようになる。
DQ1,2(SFC)乱数生成について

R_{n+1} = R_n * 3 + 0{\rm x}3549 + $7e0067

ただし上記ページでも述べられている通り、0x3549の加算時に発生したcarryは乱数の上位16bitには加算されず、その後の$7e0067の加算時に下位16bitに加算される仕様となっている。$7e0067の加算時に発生したcarryは正しく上位16bitに加算される。

このように数式だけで処理を正確に表現し切れないので、C++による再現ソースコードを提示する。
上記留意点に注意した上で、乱数生成処理を再現したソースコードが以下のようになる。
(add_with_carry関数は引数と返り値を__builtin_add_overflowに揃えるべきかとも思ったが、今回はこちらの方が都合がいいため下記のように定義した。)

#include <cstdint>

uint16_t add_with_carry(uint16_t a, uint16_t b, bool *carry)
{
  uint32_t ext_ret = a + b + static_cast<uint16_t>(*carry);

  uint16_t ret = static_cast<uint16_t>(ext_ret);
  *carry = static_cast<bool>((ext_ret >> 16) & 0x1);

  return ret;
}

uint16_t dq12rand(bool ret_16_t = false)
{
  static uint32_t rand;
  static uint16_t frame;
  // Assume little endian.
  static uint16_t *rand_upper = reinterpret_cast<uint16_t*>(&rand) + 1;
  static uint16_t *rand_lower = reinterpret_cast<uint16_t*>(&rand);

  uint16_t ret;
  bool carry = false;

  rand *= 3;

  *rand_lower = add_with_carry(*rand_lower, 0x3549, &carry);
  *rand_lower = add_with_carry(*rand_lower, frame, &carry);
  *rand_upper = add_with_carry(*rand_upper, 0, &carry);

  ret = *rand_upper;

  if (!ret_16_t) {
    ret &= 0xff;
  }

  return ret;
}

2. エンカウント内容決定処理

エンカウント内容決定処理は、下記サイトで述べられているアルゴリズムで行われる。
DQ2(SFC)系小ネタ集:後でできない事を知らせるゲーム攻略サイト

ロンダルキア6Fにおけるエンカウント内容決定は、以下のようになっている。

  1. SR$0BA550をcallし、2Byteの乱数を得る。
  2. 乱数を0x10で割った剰余が0xfの場合はぐれメタル1+ホイミスライム4が出現する。

再現ソースコードは以下のようになる。

uint16_t r = dq12rand(true) % 0x10;

if (r == 0xf) {
  // はぐれメタル1+ホイミスライム4出現
} else {
  // その他のエンカ
}

つまりRが0xXXXfXXXX(X : Don't care)になる場合、はぐれメタル1+ホイミスライム4が出現することになる。

3. はぐれメタルHP決定処理

ロンダルキア6Fにおいてはぐれメタル1+ホイミスライム4が出現する場合、はぐれメタルのHP決定は2章のエンカウント内容決定処理における乱数生成の直後の乱数生成によって行われる。

  1. SR$0BA550をcallし、1Byteの乱数を得る。
  2. 乱数を0x8で割った剰余が[0x0, 0x3]の場合はHP:=5、 [0x4, 0x7]の場合はHP:=4となる。

再現ソースコードは以下のようになる。

uint16_t hp;
uint16_t r = dq12rand() % 0x8;

if (r < 0x4) {
  hp = 0x5;
} else {
  hp = 0x4;
}

1Byteの乱数を得る場合、$7e00e2の1Byte値が乱数となる。

4. はぐれメタル1+ホイミスライム4出現時の乱数の挙動

2,3章の仕様に沿って、はぐれメタル1+ホイミスライム4出現時の乱数の挙動を追ってみる。

  1. 2章より、はぐれメタル1+ホイミスライム4が出現する際にRの取り得る範囲は[0xXXXf0000, 0xXXXfffff]となる。(以下Rの上位12bitは結果に関係しないので、全てDon't careとする。)
  2. その直後にはぐれメタルのHP決定のための乱数生成が行われる。まず乱数式中の最初の計算となる3の乗算によってRの取り得る範囲は[0xXXXd0000, 0xXXXffffd]となる。
  3. 続いて0x3549の加算が行われるが、この時のcarryはRの上位2Byteに加算されないため、Rの取り得る範囲は[0xXXXd0000, 0xXXXffffd]となる。
  4. 最後に$7e0067の加算が行われる。$7e0067の取り得る範囲は[0x0, 0x4ff]であり、この計算 時のcarryはRの上位2Byteに加算されるため、Rの取り得る範囲は[0xXXX00000, 0xXXX004fc]と[0xXXXd0000, 0xXXXfffff]となる。
  5. このRがはぐれメタルのHP決定時の値となる。この時、$7e00e2の下位4bitは0xd, 0xe, 0xf, 0x0の4通りとなるため、0x8で割った剰余は0x5, 0x6, 0x7, 0x0の4通りとなる。

5. はぐれメタル1+ホイミスライム4出現時のHP5はぐれメタルの出現率

はぐれメタルのHP計算時に$7e00e2の下位4bitが0x0となるのは、乱数生成式の乗算時のcarryが2かつ$7e0067の加算時のcarryが1である場合であるので、極めて稀なケースとなる。
はぐれメタル1+ホイミスライム4出現決定時の乱数値とはぐれメタルのHP決定時の$7e0067の値の生起確率はそれぞれ同様に確からしく、かつ独立であることを前提にすると、はぐれメタルのHPが5となる確率は0.3248%となる。
よって、剰余が0x5, 0x6, 0x7の場合がほとんどであり、この場合にはぐれメタルのHPは4となる。