今回はドラクエ1・2リメイククリア記念として
ドラクエ4のコインバグについてやっていきたいと思います。


解説動画はこちら



ドラクエ4とは

ドラゴンクエストIV 導かれし者たち
1990年2月11日にエニックス(合併前)から発売された
ファミリーコンピュータ用ロールプレイングゲーム
『ドラゴンクエストシリーズ』の第4作目

5つの章に分かれたシナリオ、AIによる戦闘システムや
5人以上の仲間キャラクターと同時に
冒険できる馬車システムが導入された作品

ゲームの2,3,5章にはミニゲームとしてカジノがあり
そこではコインバグが使える部分がありました




ドラクエ4のカジノシステムと「コインバグ」

ゲーム内の2,3,5章でいける大都市エンドール
この都市にはカジノがあり、コインが買える

1枚の価格
2章 : 10ゴールド
3章 : 200ゴールド
5章 : 20ゴールド

第3章ではコイン83,887枚を184ゴールドで
第5章では838,861枚を4ゴールドで購入できる

これはワークエリアの算術オーバーフローに起因する判定処理のバグが原因

購入金額の計算を3バイトで行っているため、カジノでコインを購入する際
計算金額が16,777,215ゴールドを超えるとオーバーフローが発生
(2 ** 24 = 16777216)

本来のコインの金額と16777216との差が
購入金額となっていたようです



ファミコンの当時のハードウェア情報


CPU:Ricoh 2A03(6502の派生)
メモリ構造:8bit CPU・少ないRAM

参考 : PS5のCPU:64ビット、スーパーファミコンのCPU:16ビット

ソフト開発はアセンブリ言語を使用
数値の表現方法(8bit × n バイトで多バイト値を管理)
多バイト演算が必要(24bit/32bitの値を手動で扱う)

スプライト制限・PPUなど
制約だらけの開発環境だったようです



ファミコン的な多バイト計算のしくみ


当時の 8bit CPU は一度に扱えるのは 0..255(1バイト)だけ
大きな数は複数バイトに分割して管理する

例:24ビット値は
LO(下位8ビット)
MID(中位8ビット)
HI(上位8ビット)という3バイトで保持


「足し算」は常に低位バイト→高位バイトの順に行い
各段で キャリー(繰り上がり) を次の段へ渡す:
LO = LO1 + LO2 → もし >255 なら carry=1 で LO &= 0xFF
MID = MID1 + MID2 + carry → 同様に繰り上げ
HI = HI1 + HI2 + carry → 最終的にさらに繰り上がれば(HI が 8bit を越える)

その上位のバイトが必要になるが、実装次第で捨てられる


乗算」は 8bit CPU で直接1回の命令で出来ないため
一般に次のどちらかで実装:

1.部分積の加算(schoolbook):
各バイトを8bit×8bitで掛けて(最大16bitの部分積)
適切に桁(バイト)シフトして加算する

2.繰り返し加算(加算を price 回繰り返す):
result = 0;
for i in range(count):
    result += price のようにして加える
(実際はより効率的なシフト+加算で実装されることが多い)


オーバーフローが起きる理由

ゲーム側が「結果を格納する領域」を
3 バイト(24ビット)だけ確保していると
数学的に 24 ビットを超える値は
自動的に上位ビットが切り捨てられる
(つまり結果は result mod 2^24)
これがバグの本質



838,861 × 20 がなぜ 4 になるか

1.コイン枚数(24bit)をバイトに分解:
コイン枚数 = 838,861
LO = 205 = 0xCD
MID = 204 = 0xCC
HI = 12  = 0x0C
検算: 12 * 65536 + 204 * 256 + 205
= 786432 + 52224 + 205 = 838861


2.各バイトの部分積を求める(コイン価格 = 20)
p0 = LO * 20 = 205 * 20 = 4,100 → hex 0x1004
  →bytes = [0x04, 0x10, 0x00, 0x00]
p1 = MID * 20 = 204 * 20 = 4,080 → hex 0x0FF0 → shift 1 byte
  → bytes = [0x00, 0xF0, 0x0F, 0x00]
p2 = HI * 20 = 12  * 20 = 240   → hex 0x00F0 → shift 2 bytes
  → bytes = [0x00, 0x00, 0xF0, 0x00]

3.部分積を下位バイトから順に加算(変数 acc は 4 バイトで保持):

初期
acc = [0x00, 0x00, 0x00, 0x00]

加算 p0
acc = [0x04, 0x10, 0x00, 0x00]


加算 p1:
acc[0] += 0x00 → 0x04
acc[1] += 0xF0 → 0x10 + 0xF0 = 0x100 -> acc[1]=0x00, carry=1
acc[2] += 0x0F + carry -> 0x00 + 0x0F + 1 = 0x10
acc[3] += 0x00 -> 0x00
→ acc = [0x04, 0x00, 0x10, 0x00]

加算 p2:
acc[0] += 0x00 -> 0x04
acc[1] += 0x00 -> 0x00
acc[2] += 0xF0 -> 0x10 + 0xF0 = 0x100 -> acc[2]=0x00, carry=1
acc[3] += 0x00 + carry -> 0x00 + 1 = 0x01
→ acc = [0x04, 0x00, 0x00, 0x01]


4.acc を 32bit で読むと 0x01000004
( 0x01000004 = 16,777,220)

ゲームはここで下位3バイトのみ(0x000004)を保存
→ 0x000004 = 4 が実際にストアされる

数学的には 838,861 * 20 = 16,777,220 だが
16,777,220 mod 2^24 = 4 となり
格納領域(24bit)に収めると 4 になる


Pythonで再現する「疑似アセンブリ」計算コード


当時のファミコンで行われていたであろう計算を
Pythonで模したコードが以下です。
# --- ユーティリティ(24bit <-> 3バイト変換) ---
def to_bytes24(n: int):
    """整数 -> (lo, mid, hi) の3バイト"""
    mask24 = (1 << 24) - 1
    n &= mask24 # 24ビットに制限(実機での格納に相当)
    lo = n & 0xFF
    mid = (n >> 8) & 0xFF
    hi = (n >> 16) & 0xFF
    return lo, mid, hi

def from_bytes24(b):
    lo, mid, hi = b
    return lo | (mid << 8) | (hi << 16)

# --- 8bit 加算(キャリーを明示) ---
def add_with_carry(a: int, b: int, carry_in: int = 0):
    """8bitの加算 -> (結果8bit, carry_out)"""
    s = a + b + carry_in
    result8 = s & 0xFF
    carry_out = 1 if s > 0xFF else 0
    return result8, carry_out

# --- 24bit のバイト単位加算(LO->MID->HI の順でキャリーを伝播) ---
def add24(a_bytes, b_bytes):
    al, am, ah = a_bytes
    bl, bm, bh = b_bytes
    rl, c = add_with_carry(al, bl, 0) # LO を足す(6502での ADC 命令相当)
    rm, c = add_with_carry(am, bm, c) # MID に carry を足す
    rh, c = add_with_carry(ah, bh, c) # HI に carry を足す
    return (rl, rm, rh)

# --- 部分積を使った 24bit x 8bit の乗算(8bit CPU の実装を模倣) ---
def mul24_by_8_partial(count_bytes, price8):
    """
    count_bytes は (lo, mid, hi) の3バイト。
    price8 は 0..255 の 8bit 単価。
    実行は:
      1) 各バイト * price を計算(部分積) -> 最大16bit
      2) それらを適切にシフト(バイト単位)して 4 バイト長の accumulator に足す
      3) 最終的に下位3バイトだけを保存
    """
    lo, mid, hi = count_bytes

    # 各バイトの部分積(16bitまで)
    p0 = lo  * price8   # 部分積0, shift 0 bytes
    p1 = mid * price8   # 部分積1, shift 1 byte (<<8)
    p2 = hi  * price8   # 部分積2, shift 2 bytes (<<16)

    # 各部分積をバイト配列(4バイト:下位から)に展開して加算
    def to_4bytes_le(x):
        return (x & 0xFF, (x >> 8) & 0xFF, (x >> 16) & 0xFF, (x >> 24) & 0xFF)

    # shift としてバイト単位で位置をずらす(p1 は 1 バイトシフト、p2 は 2 バイト)
    b0 = to_4bytes_le(p0)        # p0 << 0
    b1 = to_4bytes_le(p1 << 8)   # p1 << 8  -> 実際は p1 の下位を一つ上のバイトにずらす
    b2 = to_4bytes_le(p2 << 16)  # p2 << 16

    # 4バイト幅で逐次加算(8bit CPU の ADC の連鎖を模倣)
    acc = (0,0,0,0)
    for part in (b0, b1, b2):
        acc = add_4bytes_le(acc, part)

    # 実機が保存するのは下位3バイトのみ(= acc の 0..2 バイト)
    stored24 = (acc[0], acc[1], acc[2])
    return stored24

# 補助:4バイト加算(下位からキャリーを伝える)
def add_4bytes_le(a4, b4):
    r0, c = add_with_carry(a4[0], b4[0], 0)
    r1, c = add_with_carry(a4[1], b4[1], c)
    r2, c = add_with_carry(a4[2], b4[2], c)
    r3, c = add_with_carry(a4[3], b4[3], c)
    return (r0, r1, r2, r3)


簡単に計算できるようにしたコードだとこうなります
当時のFCDQ4のコイン価格を計算してみると
# コイン価格のシミュレーションコード
def simulate_24bit_mul(count, price):
    # count を 3 バイトに分解
    lo = count & 0xFF
    mid = (count >> 8) & 0xFF
    hi = (count >> 16) & 0xFF

    # 部分積
    p0 = lo * price  # fits<=0xFFFF
    p1 = mid * price
    p2 = hi * price

    # 32bit accumulator as int
    full = p0 + (p1 << 8) + (p2 << 16)

    # store as 24bit(下位24bitを取り出す)
    stored24 = full & ((1 << 24) - 1)
    return full, stored24

print(simulate_24bit_mul(100, 20))
print(simulate_24bit_mul(838_860, 20))
print(simulate_24bit_mul(838_861, 20))
print(simulate_24bit_mul(838_862, 20))
print(simulate_24bit_mul(999_999, 20))
(2000, 2000)
(16777200, 16777200)
(16777220, 4)
(16777240, 24)
(19999980, 3222764)




まとめ

ドラクエ4当時の性能では仕方ないバグだったろうと思います。

プログラミング的には相当大変な計算をしていたので
ここまでの確認が出来たかったんだろうと思います
(デバッガーや統合開発環境なさそうだし)

今は実機の性能が上がりすぎて、こういうのは無くなってきてるので
おもしろいバグとかは少なくなってますかね。

なお1990年当時
バグの噂は聞いていて知っていたので、めちゃくちゃ助かったです。
ありがとうエニックスの人!!


PS
ドラクエ1・2リメイク最高でした
ドラクエ3・1・2の流れを埋めるサブストーリー
うるさ過ぎるサマルトリア兄弟
クソ弱いローレシア王子
幼い頃にクリアできなかったので感動しました。
ありがとうスクエアエニックスの人!!