乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

ドラクエ

今回はドラクエ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の流れを埋めるサブストーリー
うるさ過ぎるサマルトリア兄弟
クソ弱いローレシア王子
幼い頃にクリアできなかったので感動しました。
ありがとうスクエアエニックスの人!!





今回は昔懐かしの「復活の呪文」と
BASE64の関係についてです

解説動画はこちら



復活の呪文について
さて、復活の呪文を知らない人のために
復活の呪文についてですが

ドラクエ1-2に実装されていた
データの進行状況を保存する方法のことで

ひらがなの羅列を入力し
正しい呪文であったら
対応するステータスが再現される
というものです

発売当時のファミリーコンピューターには
ゲームの保存機構が付いていなかったので
知っていたら40代以上確定という代物

その後のドラクエ 3や
ファイナルファンタジーなどは
セーブ機能があるので
この復活の呪文を知る世代は
少なくなっているかもしれません


ドラクエ1の復活の呪文の仕様

ドラクエ 1と2では復活の呪文の
使用が違いますが
ドラクエ 1は次の様です

使える文字種(64文字種)
あいうえお かきくけこ
さしすせそ たちつてと
なにぬねの はひふへほ
まみむめも らりるれろ
やゆよわ 
がぎぐげご ざじずぜぞ
だぢづでど ばびぶべぼ

呪文の長さ
20文字固定

実データ長
6(bit)×20=120(bit)=15バイト相当

有名な復活の呪文
くわたきよはらしのずかなかはたはらいしい


この
ひらがな+濁音文字で64文字というのが
BASE64の仕様と似ている訳です

ここでBASE64の仕組みを見てみましょう


BASE64
データを64種類の印字可能な
英数字のみを用いてそれ以外の文字を
扱うことの出来ない通信環境にて
マルチバイト文字や
バイナリデータを扱うためのエンコード方式

電子メールなどに利用されているような仕組みです
最近だと画像なんかも有りますね

BASE64の変換の仕組みが
復活の呪文の変換方法と似ているので
BASE64の仕組みを見てみましょう


BASE64の仕組み

まずはBASE64文字対応表を用意します
アルファベットを6ビットで対応させた
辞書データのようなものです

使える文字種は
英大文字(26種):A - Z
英子文字(26種):a - z
数字(10種):0 - 9
記号(2種): + / 

これはPythonだとstringライブラリで
用意できます

あとはこれを6ビットの2進数文字列にします
import string

upper = string.ascii_uppercase
lower = string.ascii_lowercase
digits = string.digits
base64_str = upper + lower + digits + '+/'

dict64 = {}
for i ,s in enumerate(base64_str):
    b = bin(i)
    str64 = '{0:06}'.format(int(str(b)[2:]))
    dict64[str64] = s
    print(s ,'\t', str64)
A 000000
B 000001
・・・
9 111101
+ 111110
/ 111111

こんな感じで64文字分のデータを用意します

次にBASE64に変換する文字列を用意します
今回は「Otupy」

BASE64に変換するには
このアルファベットを文字列から16進数に直し
16進数から2進数文字列に変換して連結します

text = 'Otupy'

out = []
for t in text:
    hex_str = t.encode('utf-8').hex()
    int_num = int('0x' + hex_str , 0)
    bin_num = bin(int_num)
    out_str = '{0:08}'.format(int(str(bin_num)[2:]))
    out.append(out_str)

print(out)

encode_str = ''.join(out)
print(encode_str)
['01001111', '01110100', '01110101', '01110000', '01111001']

0100111101110100011101010111000001111001

連結するとこんな感じの文字列になります

この連結した文字列を
6桁(ビット)で区切って
さっき作った対応表に当ててみると

values = []
for i  in range(0,len(encode_str),6):
    key = encode_str[i:i+6]
    key = key if len(key)==6 else key + '0' * (6-len(key))
    value = dict64[key] if key in dict64 else '='
    values.append(value)
    print(key , value)
010011 T
110111 3
010001 R
110101 1
011100 c
000111 H
100100 k


こんな感じで文字列を変換できました
なお、変換後の文字列の長さは4の倍数分の長さにして
足りないところは=で埋めるようです

v_str = ''.join(values)
encode64_str = v_str if len(v_str)%4==0 else v_str + '=' * ((8-len(v_str))%4)
print(encode64_str)

T3R1cHk=

これがBASE64に変換した際の文字列です

長々と仕組みを見てきましたが
PythonにはBase64ライブラリがあるので
この変換自体は1行で出来ちゃいます

import base64

# T3R1cHk=
text = 'Otupy'
st = base64.b64encode(text.encode())
print(st)
b'T3R1cHk='


道中の、64文字の変換表を用いて
データを変換させる仕組みの部分が
復活の呪文に近い所ですね

と言うわけで
復活の呪文ぽい実装を見てみましょう


復活の呪文っぽい実装


64文字のひらがなを用意して
それに対応するように変換します

import re

charas = '''
あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほ
まみむめもらりるれろやゆよわ
がぎぐげござじずぜぞばびぶべぼぱぴぷぺぽ
'''.replace('\n','')

# 復活の呪文にする
def jyumon_encode(bin_num):
    b = bin_num + '00000'
    b = b[0:-(len(b)%6)]
    encoded = ''.join(map(lambda c: charas[int(c, 2)], re.split('(.{6})', b)[1::2]))
    return encoded

# 復活の呪文からデータにする
def jyumon_decode(encode_text):
    decode = ''.join(map(lambda c: format(charas.find(c), '06b'), encode_text))
    return decode

20文字分のひらがなを入れてみましょう

encode_text = 'くわたきよはらしのずかなかはたはらいしい'

decode_text = jyumon_decode(encode_text)
print(decode_text)

re_encode_text = jyumon_encode(decode_text)
print(re_encode_text)
000111101011001111000110101010011001100011001011011000110011000101010100000101011001001111011001100011000001001011000001 くわたきよはらしのずかなかはたはらいしい


呪文からデータへ
データから呪文を生成します

ゲームではおそらく
このようなデータを使って
ゲームのステータスを再現していたのでは
なかろうかと思います



まとめ

現在ではBASE64への変換などは
ライブラリで一発で出来ちゃいますが
1986年(昭和61年)当時は
BASE64ライブラリとか無いんじゃないかと

と言うわけで文字の変換表などを
実装していただろうと推測します

各桁の数値をデータに対応させて
たんだろうなーと思うと
1文字間違えて地獄を見た日々を思い出しますね


ドラクエ1の容量が512kbit(64KB)らしいので
画像とプログラム含めでこのサイズに
納めるのは凄まじいテクニックが
必要だったんだろうと思います

今では画像だけで1MBとか
余裕で越してしまうので
当時の技術力の高さが
窺い知れますよねー

というわけで
今回は昔懐かしの
「復活の呪文」とBASE64
についてでした


それでは

前回作成したドラクエ1風のシミュレーターを改造して
ドラクエ2風のチーム戦が出来るようにしました。

解説動画はこちら



さて前回のプログラムを改造して行きましょう。

前回は1vs1だったので
複数vs複数に対応するように改造します。

前回の奴をそのまま使用するのと改造するもの
新規のものといろいろ出てきますが

まずは前回の流用。
キャラクタークラスはそのまま流用できますが
チーム戦になると生存フラグが必要になってきますので
フラグを追加します、1が生存0が死亡。

ダメージ計算や素早さ計算はそのまま流用します。
import numpy as np

# キャラクタークラス
class character():

    def __init__(self,name,hp,attack,defence,agility):
        self.name = name
        self.hp = hp
        self.attack = attack
        self.defence = defence
        self.agility = agility
        self.survive = 1

# ダメージ計算
def damage_calc(c1,c2):
    base = c1.attack //2 - c2.defence//4
    rand = base//16+1
    tmp = np.random.randint(0,rand*2+1)-rand
    damage = base + tmp
    return damage

# 素早さ計算
def agility_calc(c):
    base = c.agility
    tmp = base//10
    result = np.random.randint(base-tmp,base+tmp+1)
    return result

チーム戦を行うために、キャラを複数格納できる
チームクラスを作ります。
# チームクラス
class Team():

    def __init__(self,name,teams):
        self.name = name
        self.team = teams

    def append(self,chara):
        self.team.append(chara)
        
    def delete(self,index):
        del self.team[index]


前回は2キャラの対戦なのでチームでの複数対戦だと
行動の順番を決める必要があります。

行動順番は素早さを元に計算し
数値が大きい順に行動するようにします。

その際生存フラグが1のものだけにします。

# 先頭順序の計算
def turn_calc(t1,t2):
    result = {}
    for i,c in enumerate(t1.team):
        ag = agility_calc(c)
        if c.survive==1:
            result['{0}:{1}'.format(t1.name,i)] = ag
    for i,c in enumerate(t2.team):
        ag = agility_calc(c)
        if c.survive==1:
            result['{0}:{1}'.format(t2.name,i)] = ag
    return [k for k,v in sorted(result.items(),reverse=True,key=lambda x:x[1])]


戦闘順番が決まったらその順番通りに攻撃開始です。
相手チームの中で狙う相手を決めるために
相手チームの生存者を探してインデックス値で返します。

ターン中の攻撃は攻撃者からターゲットへの
一方的なやりとりだけにします。

お互いのステータスからダメージ計算し
相手のHPから引き、HPが0になったら倒れます。

# 狙う相手を計算
def target(teams):
    index = [i for i ,c in enumerate(teams.team) if c.survive==1]
    return np.random.choice(index)

# ターン中の攻撃
def attack(c1,c2):
    damage = damage_calc(c1,c2)
    c2.hp = c2.hp - damage if c2.hp - damage>=0 else 0
    print('{0}の攻撃 : {1}に{2}のダメージ'.format(c1.name,c2.name,damage))
    print('{0}のHP : {1}'.format(c2.name,c2.hp))
    if c2.hp<=0:
        print('{0}は力尽きた!!!'.format(c2.name))
        return (c2,0)
    return (c2,9)

チームの全員の生存フラグが0になったら試合終了です。
全員の生存フラグをチェックします。

ターンが終わったら全員のステータスを出すように
ログ出力します。

# 生存チェック
def check_survived(teams):
    res = 0
    for c in teams.team:
        if c.survive==1:
            res+=1
    if res > 0:
        return True
    else:
        return False

# ログ出力
def print_log(t1,t2):
    print()
    result = ''
    for c in t1.team + t2.team:
        result += '{0} , HP : {1}\t'.format(c.name , c.hp )
    print(result)
    print()

ここから戦闘中のアルゴリズムです。
順番を決めて
攻撃対象を決めて
ダメージ計算して
HP0なら生存フラグを0にして
全員生存フラグ0なら勝負ありです。

# 戦闘
def battle(t1,t2):
    print('戦闘開始')
    print_log(t1,t2)
    while True:
        # ターン内の処理
        for m in turn_calc(t1,t2):
            tm , i , flg= m.split(':')[0] , int(m.split(':')[1]),9
            # 攻撃チームの設定
            attack_team = t1 if t1.name==tm else t2
            # ターゲットチームとターゲットの指定
            target_team = t2 if t1.name==tm else t1
            t_num = target(target_team) 
            # 戦闘結果
            target_team.team[t_num],flg = attack(attack_team.team[i],target_team.team[t_num])
            if flg==0:
                target_team.team[t_num].survive=0
            if not check_survived(t1):
                print('\n{0}チームの勝ち'.format(t2.name))
                break
            elif  not check_survived(t2):
                print('\n{0}チームの勝ち'.format(t1.name))
                break
        
        # ターン後
        if not check_survived(t1) or not check_survived(t2): 
            break
        print_log(t1,t2)
    print('戦闘終了')


さて、シミュレーションしてみましょう。
c1 = character('勇者1',100,80,40,66)
c2 = character('勇者2',100,80,40,66)
c3 = character('勇者3',100,80,40,66)
c4 = character('勇者4',200,100,50,66)

s1 = character('スライム1',180,80,30,66)
s2 = character('スライム2',180,80,30,66)
s3 = character('スライム3',180,80,30,66)
s4 = character('スライム4',380,70,40,56)

team1 = Team('勇者チーム',[c1,c2,c3,c4])
team2 = Team('スライムチーム',[s1,s2,s3])

battle(team1,team2)

結果は

戦闘開始

勇者1 , HP : 100 勇者2 , HP : 100 勇者3 , HP : 100 勇者4 , HP : 200
スライム1 , HP : 180 スライム2 , HP : 180 スライム3 , HP : 180

勇者1の攻撃 : スライム2に36のダメージ
スライム2のHP : 144
勇者2の攻撃 : スライム3に34のダメージ
スライム3のHP : 146
スライム3の攻撃 : 勇者3に29のダメージ
勇者3のHP : 71
スライム1の攻撃 : 勇者3に28のダメージ
勇者3のHP : 43
勇者4の攻撃 : スライム2に41のダメージ
スライム2のHP : 103
勇者3の攻撃 : スライム3に36のダメージ
スライム3のHP : 110
スライム2の攻撃 : 勇者4に30のダメージ
勇者4のHP : 170

勇者1 , HP : 100 勇者2 , HP : 100 勇者3 , HP : 43 勇者4 , HP : 170
スライム1 , HP : 180 スライム2 , HP : 103 スライム3 , HP : 110

スライム1の攻撃 : 勇者4に29のダメージ
勇者4のHP : 141
勇者4の攻撃 : スライム2に43のダメージ
スライム2のHP : 60
勇者1の攻撃 : スライム2に32のダメージ
スライム2のHP : 28
勇者2の攻撃 : スライム2に31のダメージ
スライム2のHP : 0
スライム2は力尽きた!!!
スライム3の攻撃 : 勇者4に27のダメージ
勇者4のHP : 114
勇者3の攻撃 : スライム3に30のダメージ
スライム3のHP : 80
スライム2の攻撃 : 勇者4に26のダメージ
勇者4のHP : 88

勇者1 , HP : 100 勇者2 , HP : 100 勇者3 , HP : 43 勇者4 , HP : 88
スライム1 , HP : 180 スライム2 , HP : 0 スライム3 , HP : 80

勇者3の攻撃 : スライム3に35のダメージ
スライム3のHP : 45
勇者1の攻撃 : スライム3に36のダメージ
スライム3のHP : 9
スライム3の攻撃 : 勇者2に31のダメージ
勇者2のHP : 69
スライム1の攻撃 : 勇者3に28のダメージ
勇者3のHP : 15
勇者2の攻撃 : スライム3に34のダメージ
スライム3のHP : 0
スライム3は力尽きた!!!
勇者4の攻撃 : スライム1に43のダメージ
スライム1のHP : 137

勇者1 , HP : 100 勇者2 , HP : 69 勇者3 , HP : 15 勇者4 , HP : 88
スライム1 , HP : 137 スライム2 , HP : 0 スライム3 , HP : 0

勇者1の攻撃 : スライム1に33のダメージ
スライム1のHP : 104
勇者4の攻撃 : スライム1に41のダメージ
スライム1のHP : 63
勇者2の攻撃 : スライム1に34のダメージ
スライム1のHP : 29
勇者3の攻撃 : スライム1に36のダメージ
スライム1のHP : 0
スライム1は力尽きた!!!

勇者チームチームの勝ち
戦闘終了

見事に勇者チーム勝利wwwwwwww

お互いに殴り合うだけの
単純なシミュレーションです。

ドラクエ2風になったんじゃないかと思います。

そう考えると
ドラクエ3以降は複雑怪奇ですねーー

すぐ作れる気がしないので
この先どうなることやら・・・

今回はここまでです。
それでは。

プログラムの文法は一通り学んだんだけど
どうやってプログラム書けばいいか分からないと言う方
朗報です。

ドラクエのシミュレーターを通じて
プログラムの作り方を学んでいきましょう。

解説動画はこちら







さてまず考えることは
小さなところからコツコツとです。

まずは1行から
慣れてきたら複数行
有る程度できるようになったら数千行の奴へ挑戦

と言うようにステップを踏んで
プログラムに慣れていきましょう。

最初はコピペからで十分です。
有る程度コードが打てるようになったら
次の段階へ進みましょう。

プログラムを考える時のコツですが

プログラムに基本は
1.入力 , 2.計算 , 3.出力 を考える
です。

1.入力させるデータ(構造) が何で
2.何をどう計算させるか(アルゴリズム)がどうなってて 
3.最終的に何を出すか(結果)

と言うことを考えます。

これは1行1行にも当てはまります。
全ての行で入力 , 計算 , 出力が行われているはずです。

一体何をどうしているのかを
把握しながらコードを書くのが
プログラムを書くコツになります。

ドラクエのシミュレーションを考えてみましょう。

想定されるシーンは
ドラクエ1

勇者 vs スライム
1対1の対決です。

ここではキャラクターとしては2体。
ステータスは共通のものを使うとします。

次に何をするかと言うと

お互いに攻撃しあってHPを減らしていき
HPが0になったら戦闘は終了しますので
それまで戦いを続けます。

最終的にはどちらかが勝つことになり
これが結果となります。

さて入力データを考えていきましょう。

2体のキャラクターですが
プログラムの入力データとしては
1つのクラスにまとめることができます。

次のようなクラスでまとめられます。

class character():

    def __init__(self,name,hp,attack,defence,agility):
        self.name = name
        self.hp = hp
        self.attack = attack
        self.defence = defence
        self.agility = agility

これを呼び出してみると
c1 = character('勇者',150,60,40,60)
c2 = character('スライム',100,50,30,56)

for k,v in c1.__dict__.items():
    print(k,v)
    
for k,v in c2.__dict__.items():
    print(k,v)
name 勇者
hp 150
attack 60
defence 40
agility 60
name スライム
hp 100
attack 50
defence 30
agility 56

こんな感じで勇者とスライム君ができました。

次はアルゴリズムを考えていきます。

アルゴリズム
1.ダメージ計算

攻撃力÷2-守備力÷4=ダメージ基礎値
ランダム値(ダメージ基礎値 ÷16+1)
ダメージ = ダメージ基礎値 プラスマイナス ランダム値

2.先頭順序
素早さ乱数の多い方が先に行動する
素早さ乱数 = 素早さ プラスマイナス10%のランダム値
例:400 = 360から440の間のランダム値

3.戦闘シーン
ダメージを算出してお互いのHPを削って
どちらかが0になったら終了


まずはダメージ計算です。
基礎値を算出してプラマイランダム値と言うのが
公式的な算出方法なようです。

シリーズによって多少の違いはありますが
基本的な考え方は一緒なのでこれを踏襲します。
import numpy as np

# ダメージ計算
def damage_calc(c1,c2):
    base = c1.attack //2 - c2.defence//4
    rand = base//16+1
    tmp = np.random.randint(0,rand*2+1)-rand
    damage = base + tmp
    return damage

次に先頭順序の計算です。

これを考えるには素早さ乱数がわからないとダメなので
先にこれを計算しておきます。
# 素早さ計算
def agility_calc(c):
    base = c.agility
    tmp = base//10
    result = np.random.randint(base-tmp,base+tmp+1)
    return result

# 先頭順序の計算
def turn_calc(c1,c2):
    a1 = agility_calc(c1)
    a2 = agility_calc(c2)
    if a1>=a2:
        return True
    else:
        return False

最後に先頭シーンを考えます。
お互いのHPを削って行って
HPが0になったら先頭終了です。

ターン中の計算と
先頭シーンの制御の部分で分けてみます。
# ターン中の計算
def turn(c1,c2):
    damage = damage_calc(c1,c2)
    c2.hp = c2.hp - damage if c2.hp - damage>=0 else 0
    print('{0}の攻撃 : {1}に{2}のダメージ'.format(c1.name,c2.name,damage))
    print('{0}のHP : {1}'.format(c2.name,c2.hp))
    if c2.hp<=0:
        print('{0}は力尽きた'.format(c2.name))
        return (c1,c2,0)
    damage = damage_calc(c2,c1)
    c1.hp = c1.hp - damage if c1.hp - damage>=0 else 0
    print('{0}の攻撃 : {1}に{2}のダメージ'.format(c2.name,c1.name,damage))
    print('{0}のHP : {1}'.format(c1.name,c1.hp))
    if c1.hp<=0:
        print('{0}は力尽きた'.format(c1.name))
        return (c1,c2,1)
    return (c1,c2,9)

# 戦闘
def battle(c1,c2):
    while True:
        # 勇者が先のターン
        if turn_calc(c1,c2):        
            c1,c2,flg = turn(c1,c2)
            if flg==0:
                print('終了 : {0}の勝ち'.format(c1.name))
                break
            elif flg==1:
                print('終了 : {0}の勝ち'.format(c2.name))
                break
        # モンスターが先のターン
        else:
            c2,c1,flg = turn(c2,c1)
            if flg==0:
                print('終了 : {0}の勝ち'.format(c2.name))
                break
            elif flg==1:
                print('終了 : {0}の勝ち'.format(c1.name))
                break


これで出来ました。

さてシミュレーションしてみましょう。
c1 = character('勇者',100,80,40,66)
c2 = character('スライム',180,80,30,66)

# 戦闘開始
battle(c1,c2)
勇者の攻撃 : スライムに33のダメージ
スライムのHP : 147
スライムの攻撃 : 勇者に31のダメージ
勇者のHP : 69
勇者の攻撃 : スライムに35のダメージ
スライムのHP : 112
スライムの攻撃 : 勇者に29のダメージ
勇者のHP : 40
スライムの攻撃 : 勇者に31のダメージ
勇者のHP : 9
勇者の攻撃 : スライムに34のダメージ
スライムのHP : 78
勇者の攻撃 : スライムに33のダメージ
スライムのHP : 45
スライムの攻撃 : 勇者に28のダメージ
勇者のHP : 0
勇者は力尽きた
終了 : スライムの勝ち

スライムが強すぎて勝てる気がしませんね。

アプリケーションは小さなプログラムの集合体です。
小さなプログラムは数行のコードだったりします。

何が入力で、何を計算させれば良いのかを
少しづつ考えていけば、次第に大きなものを
作れるようになってくと思います。

ドラクエのシミュレーションは
プログラムの初心者がプログラムの作り方を学ぶ
題材としてはうってつけだと思います。

まずは最低限の機能を実装していき
次第に大きくしていきます。

昔作った
ドラクエモンスターライトの
シミュレーターは複数 vs 複数のバトルなので
心折れそうでしたがアンドロイドアプリにすることができました。
(今は公開していませんが)

目標を決めるとそれに向かって努力するので
小さめの目標から初めて
達成を繰り返していくと言う過程を経ることで
大きな目標達成に繋げることができるのではないかと思います。

今回はここまでです。
それでは。





このページのトップヘ