見出し画像

機械学習を用いたイルルカSPモンスターの分類(2)――Webスクレイピング、XMLによるデータ作成

概要

イルルカSPには903種類ものモンスターが存在し、各々が異なる性質を持っている。これらから何匹かを選択し、対戦に用いるモンスターを決める訳であるが、この膨大な数からこれを行うのは極めて困難である。そこで、どのモンスターとモンスターが類似しているか分類することによって、その労力を軽減させるということを考える。それに適した手法として機械学習、特に教師無しのものが挙げられる。本連載は機械学習を用いてイルルカSPに登場するモンスター間の類似性を計算し、対戦に臨むユーザーの助けとする。読者層を鑑み、正確性よりも機械学習に馴染みのない人への分かり易さを随所で優先した。特に本稿ではモンスターのデータの収集と統合を扱う。

モンスターのデータを収集する場所と方法の考察

モンスターのデータが如何なるものかは、前の記事にも示した通り、下表のようなものである。

マジェス・ドレアムの持つデータ

これはマジェス・ドレアムという1種類のモンスターのデータである。これと同じようなものを全モンスターについて用意する必要がある。当然、その作業はゲームを実際に起動して図鑑や育成によって確かめ、コンピューターにマウスとキーボードで一つ一つ丁寧に入力していく訳にはいかない。自分の気になったモンスター1種類について調べる時でさえ、そのようなことは普段しないであろう。況してやそれで終わりではなく、903種類調べなければならないのである。相応の工夫が必要となる。以下、プレイヤーがモンスターのデータを調べるのに使うツール、ウェブサイト等から、如何にして全モンスターのデータを抽出するかを議論する。

ステータス上限と特性、装備可能武器

これは小梢Excelファイル(イルルカSPステータス計算シート)のステータス表という名前のシートの3行目以降をコピーし、新規作成した適当なテキストファイルに貼り付ける。するとデータがタブ区切りで入ったファイルとなるので、タブ文字をカンマに置換することでCSVファイルに出来る。CSVファイルとはデータをカンマと改行で区切って格納したもので、データ分析においてよく使われるファイル形式である。但し、名前検索候補の欄の数字は後の便利の為3からではなく1から始まるよう、ROW()-2の値にしてからCSVに保存した(当たり前だがこの変更を小梢Execlファイル上で保存してはならない。)

小梢Excelファイルのステータス表というシートの内容(一部)
小梢Execlファイルのステータス表から作成したCSVファイルの一部

耐性

ステータス上限と特性は1つのファイルからコピペするだけで手に入ったが、耐性のデータはそのように簡単には手に入らない。ファイルに纏められていない為、インターネット上から情報を取得する必要がある。

先ずはどのウェブサイトを調査するか検討する。イルルカのモンスターの耐性を含んだ情報を載せているウェブサイトとして、以下のようなものが存在する。例としてスライムのページをそれぞれ示す。

データを扱う際のイルルカ固有の注意点を考えながら、各サイトのうちどれが適しているか考察していく。

ゲームエイトは中身が無いことでゲーマーからは悪名高い企業wiki(自由にユーザーが編集出来る訳でもないからwikiでもない)の一つである。とはいえ、これでもまだマシな方ではある。アルテマや神ゲー攻略は全モンスターの耐性データを載せてすらいないのだから(アルテマはモンスターの耐性が全く載っておらず、神ゲー攻略はダイヤモンドスライムのような高ランクモンスターのみ耐性を掲載している)。低ランクモンスターが活躍することも多いイルルカにおいて、全てのモンスターのデータをきちんと取得出来ることは重要である。

とはいえ、所詮は企業wikiなのでゲームエイトも限界がある。モンスターの耐性を、きちんと特性による補正無しで表記しているモンスターと補正込みで表記しているモンスターがいて、かつどちらかの表記で統一されていないのである。例えば、リバイアさまは超ギガボディによる状態異常耐性4段階アップが計算される前の素の特性で書かれているが、マジェス・ドレアムは超ギガボディ、行動速いの影響が入った状態で書かれている(それでいて全ガード+の効果は入っていない)。

データの形式が統一されておらず、特性込みなのかそうでないのか人力で判断しなければならない場合、データをコンピューターに自動的に集めさせても意味が無い。集めた全データを人間が手作業で修正しなければならないのだから、それなら人間が最初からデータを全て手作業で入力するのと同じであり、労力の削減になっていない。

では、耐性データはフォーマットが統一されてさえいれば、集めれば役に立つのであろうか。実はそうではない。特性による耐性変化には境界における切り捨て処理が発生するので、特性による補正適用済みのデータから特性による補正を受ける前のデータを完全に復元出来る訳ではなく、従って後者の形式に則ったデータを収集しなければならない。例えば具体例としてメルトアを考えてみる。メルトアはギガボディと行動遅いの特性を持っており、それ故にマインド、混乱、麻痺、眠りといった状態異常耐性が3+2=5段階上がっている。状態異常耐性は(内部的には完全無効、或いは過剰耐性と呼ばれる状態があるものの)表記上は無効までしか上がらない。従って、5段階上昇した結果無効になったという情報のみからは、元々の耐性が取り得る範囲は弱点、普通、軽減、半減、激減、無効の5段階のうち任意の値を取り得、反射ではないということしか分からない。本来の耐性を推測するには、例えば異常耐性が2段階上がる行動遅いを5段階下がる超行動速いに入れ替え、耐性がどうなるかゲーム中で確認する他ない。こうすると異常耐性は$${3-5=-2}$$、即ち2段階下がることになるが、その時の異常耐性が半減であることにより、メルトアの素の異常耐性が無効であることが漸く判明する。モンスターの特性の入れ替えが可能でそれにより耐性が変化するゲームにおいては素の耐性を知ることが必要であるが、素の耐性は特性による補正適用済みの耐性のデータのみからは算出出来ないということである。ゲームを実際に起動して改めて調査し直さなければならない程度の情報を集めても意味が無いのである。それ故に一部とはいえ、特性による変化が加わったデータになっているゲームエイトからはデータを取る意味が無い。

特性による補正の無いデータも出すことが出来るのは3DS時代から使われていたgame-capというサイトであるが、これもこれでイルルカSPのデータを集める上では欠陥を抱えている。3DS版での情報であるから、スマートフォン移植であるイルルカSPで追加されたモンスター100種類についてのデータは無いのである。その為、例えば下記に示したnote記事で上げられているpdfファイル(以降メビウスpdfと呼称する)からSP追加モンスターのデータを取得して統合するといった工夫が必要になる。

しかしながら、pdfの解析は一般にウェブサイトの解析より手間であり、またpdfからデータを取得した後の統合作業においても手間が想像される。というのも、図鑑におけるモンスターの並び順において、SPで追加されたモンスターは最後に纏めて並ぶのではなく、3DS版から存在したモンスターとモンスターの間に適宜挟まっている、即ち、3DS版から存在するモンスターの図鑑における番号が変化しているからである。ステータスと特性のデータに関してはSPでの並び順でモンスターが並んでおり、SPでの図鑑番号で扱うのが楽なデータとなっている。モンスターの図鑑番号ではなく名前によって統合するという手もあるが、pdf解析という手間の次に更に手間というのも、他により良い方法があるなら先ずそちらを探したくはなる。

さて、そうなるとSPでの追加モンスターまで含めて全部カバーされていて、かつ特性の影響を受けていない素の耐性を掲載しているウェブサイトが求められているということになる。ゲームエイトは後者、game-capは前者の点で問題を抱えていた。では残された凡庸はどうであろうか。凡庸はSP追加モンスターまでカバーされていて、耐性は特性による補正適用済みである。これだけ見ると後者の条件を満たしていないのであるが、モンスターのデータを載せているページではなく、そのモンスターの特性を入れ替え、どのようなスキルを持たせた場合に耐性がどうなるか計算するビルドシミュレーターのページを見ることで、モンスターの素の特性を見ることが出来るのである。即ち、特にスキルは持たせず、耐性に影響する特性を他の特性に入れ替えた場合の耐性を見れば良い。よって問題は、各モンスターのビルドシミュレーターのページへ如何に飛び、その後如何に特性を入れ替えるかである。

しかしこれは、凡庸のURLの仕組みを見ることで解決出来る。ウェブページの中にはURLを変化させずに状態遷移し、画面に表示する情報を変えているものもあるが、凡庸の場合、モンスター、特性、スキル、武器の鍛冶、全ての情報が数字という形でURLに含まれている。

凡庸ビルドシミュレーターのURL構造

よって、耐性に影響しない特性の組を適当に見繕ってから、モンスター番号の部分に3から903の番号を入力することで、その番号に対応するモンスターの素の耐性を見ることが出来る(但し図鑑番号1、2のモントナーは特殊なので後回しにする)。これにより、探すべきウェブページの範囲は定まった。次に問題となるのは、ウェブページの情報からどうやって必要としている耐性のデータだけを自動的に取得するかである。

Webスクレイピングによる耐性データ取得

インターネット上にある情報を人間が手作業で集めるのではなく、コンピューターに自動で集めさせる行為やそれに関係する技術を、大まかにWebスクレイピングと呼ぶ。本稿で行おうとしているウェブサイトからモンスターの耐性データを取得するという行為もこれに当たる。

Webスクレイピングの基礎知識

Webスクレイピングについて記述のある技術書は多々存在するが、書店で手に入り易い本の多くは、簡単なプログラミングによって様々な仕事の効率化を図る本の中の一章に収まっていることが多い。その一例としては次のような本が挙げられる(尚、この本は有名で評判も良いようであるが、2023/1/24に第2版が出るようなのでそれまで待った方が良さそうである)。

その為、機械学習で要求されるような膨大なデータの取得先としてインターネットは有望であるにも関わらず、Webスクレイピングの基礎知識の習得先は案外見付け難い。またWebスクレイピングを専門に扱った本であっても、OSがLinuxでない場合、VirtualBox/VagrantやDockerで仮想環境の導入を前提として解説を行う書籍も多く、Windowsユーザーにとって始め易いものとは言い難いことも多い。その中で、『PythonによるWebスクレイピング』は仮想環境の導入を前提とはせず、かつ内容も充実している為、初めの一冊として優れていると思われる。ここでまとめる基礎知識も基本的にこの本に依存している。

広義のWebスクレイピングはインターネット上から情報を取ってくることだと考えることが出来るが、これは更に分けて考える方が良い。即ち、あるURLが与えられた時にそのWebページ(HTMLやCSS、JavaScript)から必要な情報を得ることと、そもそも広大なインターネットでどのようなURLをどのような順序で調査するかを決めることと、2つの作業にである。前者が狭義のWebスクレイピングであり、後者はWebクローリングと呼ばれる。この2つを並べると一見クローリングが先でスクレイピングが後のように思えるが、学習の順序としてはスクレイピングが先だと思われる。というのも、クローリングはあるWebサイト、ページに存在するハイパーリンクからサイト、ページへ移動することで様々なサイト、ページを調査するという形式を取ることが多いのだが、Webページのどこにハイパーリンクがあるのか情報を取り出すのはスクレイピングの領域だからである。今回の耐性データの取得においては、ハイパーリンクでページからページへ移動するという処理は使用しないので、クローリングの知識はほぼ不要である。

そして広義のWebスクレイピングは、Webページから情報を得る都合上、HTML、HTTP、Webサーバー、インターネットセキュリティのようなインターネット関連技術、集めたデータの保管に用いるデータベース、集めたデータを取り扱う画像処理、データサイエンス等、使う知識は広範囲に広がっていて、一つの体系的知識、技術というより、様々な分野の総合的領域であると言える。

Webスクレイピングの実行

Webスクレイピングに適したプログラミング言語としてはPythonの他にRubyがあり、そちらの方が文字列処理等は強いと言われているが、解説書が多いのでPythonを使うことにする。Pythonのインストール方法は特に難しいことも無いので省略。

データ取得先のURLを与えた後、そのHTMLの構造を分析し、データを取り出し易くする為のライブラリ(プログラミング言語がデフォルトでは提供しない便利な機能をまとめたもので、各人が自由に作成し配布することが出来る)としてBeautifulSoup4をインストールする。これはコマンドプロンプトでpip install beautifulsoup4と打ち込むだけなので簡単。Webページの設計書みたいなものであるHTMLでは文字や画像をどのような配置にするか等の情報を表す為に沢山の括弧等が使われているが、それらは手に入れたいデータの中身とは無関係なので、その中身だけをデータの並び方の階層構造を踏まえつつ上手い形で得られるBeautifulSoup4は便利。WebページのHTMLは普通のインターネットブラウザで確認することが可能である。例えばGoogle Chromeであれば右クリックして「ページのソースの表示」を選択するだけである(というより、本来インターネットではHTMLで情報を送受信しており、ウェブブラウザはそれを人間に見易く加工して表示しているだけであるとも言える)。

WebページのHTMLの一例。

ここから実際にプログラムを書き、動かしていく訳だが、Pythonには2つの実行方法がある。即ち、ファイルにプログラムを書いてから実行する方法(非対話的環境)と、数行(殆どの場合1行)だけ入力してすぐに実行、実行結果を見てまた数行入力するというインタラクティブシェルを使った実行方法(対話的環境)である。大規模なプログラムであれば前者でないと構築不可能だが、特定の関数の挙動を確認する等、小規模な試運転には後者が向いている。そこで、対話的環境を用いてBeautifulSoup4を用いると凡庸のビルドシミュレーターがどのように扱われるのか確認した上で、次に非対話的環境でデータを自動的に入手するプログラムを作るものとする。

Pythonに使用するライブラリをインポートし、実際に使ってみる。打ち込むコードは

from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('https://bonyou.info/dqm2sp/simulator-monster-build/?m=179&a=6036270267076049058099272082&s=&w=')

で凡庸のビルドシミュレーターを開ける筈なのだが、これを実行するとssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1129)というエラーが生じ、開くことが出来ない。他のウェブサイトは同じやり方で開くことが出来る(例えばnote記事等な)ので、凡庸なというWebサイトがSSL証明書において何らかの問題を抱えているということが分かる。取り敢えずエラーを回避する方法を探す訳だが、urlopen()が失敗することがあるということが分かった以上、非対話的環境で本格的なデータ収集を行う際にはエラーチェックの処理(URLからページを開けなかった時にはそれ以降の処理を打ち切り、そのページは記録、必要に応じて後に再挑戦する)が必要だということも判明した。

このエラーの解消法はこう。

import ssl
ssl._create_default_https_context = ssl._create_unverified_context

以上により、凡庸ビルドシミュレーターのWebページを開き、それをBeautifulSoupに読ませることが出来る。こうして出来たオブジェクトがBeautifulSoupオブジェクトbsである。

html = urlopen('https://bonyou.info/dqm2sp/simulator-monster-build/?m=179&a=6036270267076049058099272082&s=&w=')
bs = BeautifulSoup(html.read(), 'html.parser')

こうしてHTMLを読めたので、中からデータを取り出してみる。凡庸ではメラ耐性はmera、ギラ耐性はgiraという風にidが振られているので

taisei = bs.find('span',{'id':'mera'})
print(taisei)

とコードを打てばメラ耐性が手に入る筈である。これによって様々な属性への耐性が取得出来る……という訳ではない!なんと、このままではどの属性に対しても普通、無耐性を表すハイフンしか結果は返って来ないのである。これは明らかにおかしいく、ウェブブラウザを人間の目で見た場合と結果が異なっている。

この謎を解く鍵は凡庸ビルドシミュレーターのHTMLの46~48行目にある。

<script defer src="https://code.jquery.com/jquery-3.5.1.min.js"
    integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
    crossorigin="anonymous"></script>

これはjQueryのインポート文である。こう書いても意味が分からないだろうが、重要なのはJavaScriptが使われているということである。凡庸の場合、HTMLに特定の種族、特性を与えた場合のモンスターの耐性は直接は書き込まれておらず、別の場所にあるデータからJavaScriptを用いて計算をその場で行い、その結果が表示されているのである。よって、HTMLの中にはモンスターの耐性のデータを見付けることは出来ない。(注:ビルドシミュレーターではなくモンスターのデータのページであればHTMLに直接耐性のデータが書かれているのでurllib.requestで取得出来るが、特性による補正を全て受けた状態のデータなので取得する意味が無い)。

そこで、urllib.requestとBeautifulSoupに代わり、SeleniumとChromeDriverを使うこととする。

以上の修正を経て、番号179(アロードッグ)のメラ耐性を得て、それを画面に表示するPythonコードは次のようになる。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome import service as fs
from selenium.webdriver.common.by import By
import time

chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_service = fs.Service(executable_path = 'C:\drivers\chromedriver')
driver = webdriver.Chrome(service = chrome_service, options=chrome_options)

driver.get('https://bonyou.info/dqm2sp/simulator-monster-build/?m=179&a=6036270267076049058099272082&s=&w=')
print(driver.find_element(By.ID, 'mera').text)
driver.close()

次に、メラ以外の特性についても得られるようにし、画面に表示するのではなくテキストファイルに書き込んで他のプログラムから使えるように準備をし、そしてアロードッグ以外のモンスターのデータも取得するようにする。

meraはメラ耐性、giraはギラ耐性というようにHTML中で使われている耐性関係のIDは30種類あるが、これらをPythonのタプルとして作っておく。meraやgiraといったID名のリストは本来なら自動で取得すべきではあるが、この程度の量なら手作業でも問題無いであろう。

最終的に凡庸から全モンスターの素の耐性のデータを取り出し、テキストファイルに保存するPythonプログラムのコードは以下のようになる。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome import service as fs
from selenium.webdriver.common.by import By
import time

chrome_options = Options()
chrome_options.add_argument("--headless")
chrome_service = fs.Service(executable_path = 'C:\drivers\chromedriver')
driver = webdriver.Chrome(service = chrome_service, options=chrome_options)

tolerances = ("mera", "gira", "io", "bagi", "hyado", "jibaria", "dein", "doruma", "betan", "honoo", "hubuki",
    "zaki", "doku", "noroi", "mind", "konran", "mahi", "nemuri", "manusa", "mahotora", "hack",
    "jumon", "zangeki", "taigi", "iki", "odori", "down", "rukani", "bomie", "fool")
    

japanese = {"mera":"メラ", "gira":"ギラ", "io":"イオ", "bagi":"バギ", "hyado":"ヒャド", "jibaria":"ジバリア",
    "dein":"デイン", "doruma":"ドルマ", "betan":"ベタン", "honoo":"炎", "hubuki":"吹雪",
    "zaki":"ザキ", "doku":"どく", "noroi":"呪い",
    "mind":"マインド", "konran":"こんらん", "mahi":"マヒ", "nemuri":"ねむり",
    "manusa":"マヌーサ", "mahotora":"マホトラ", "hack":"ハック",
    "jumon":"呪文封じ", "zangeki":"斬撃封じ", "taigi":"体技封じ", "iki":"息封じ", "odori":"踊り封じ",
    "down":"ダウン", "rukani":"ルカニ", "bomie":"ボミエ", "fool":"フール"}

f = open("耐性データ.txt", "w", encoding = "utf-8")

url = "https://bonyou.info/dqm2sp/simulator-monster-build/?m=" + "001-1" + "&a=6036270267076049058099272082&s=&w="
driver.get(url)
time.sleep(1)
f.write(str(1) + ",モントナー" + "\n")
for tolerance in tolerances:
    f.write(japanese[tolerance] + "," + driver.find_element(By.ID, tolerance).text + "\n")

for i in range(3, 904):
    url = "https://bonyou.info/dqm2sp/simulator-monster-build/?m=" + str(i).zfill(3) + "&a=6036270267076049058099272082&s=&w="
    driver.get(url)
    time.sleep(1)
    f.write(str(i) + "," + driver.find_element(By.ID, "name-monster").text + "\n")
    for tolerance in tolerances:
        f.write(japanese[tolerance] + "," + driver.find_element(By.ID, tolerance).text + "\n")
    

f.close()
driver.close()

これを実行することで全モンスターの素のデータが手に入るが、そのデータを確認の為見てみると、なんと間違っているということが判明する。例えば図鑑番号903、マジェス・ドレアムのザキやマインド耐性が激減になっていたり、他にも邪神ニズゼルファやメルトアに間違いがあったりする。3DS版から登場していたモンスターでもはぐれメタルキングやメタルスターは間違っている。この時点で、凡庸は3DSから既出のモンスターであっても他の攻略サイトからWebスクレイピングで情報を得るということはやっておらず、全てのモンスターに何らかの同一のアルゴリズムを施して素の耐性データを作ったであろうということが推測される。

得られた耐性データの修正

ここで、対処法は3つ程考えられる。

  1. 凡庸の間違い発生箇所の法則性を見付け、それを満たす場所のみ確認する

  2. 3DS版からのモンスターをgame-capからWebスクレイピング、SP新登場モンスターをメビウスpdfからOCRか何かで手に入れる

  3. 全てのモンスターの耐性データを確認する

1.は手作業故の労力がかかることと法則性の確実性の欠如(見付けた法則が正しいとは限らず、法則外にも間違いがある可能性を否定出来ない)が問題である。とはいえ、2枠以上のモンスターのサイズ、行動遅い、全ガード+、各種メタルボディ等、耐性を高める特性を複数持っているモンスターが怪しそうだという目算は簡単に立つし、他の手法が信用出来ない場合には一考の余地がある。

2.はモンスターのデータが載っているURLにモンスター名のローマ字表記が入り、図鑑番号と比べて取り得る範囲が不明確なので探索がし辛いが、各モンスターのページへのリンクをまとめたページは末尾がmonsterlist(21~28の数字).htmlで綺麗に揃っているので、そこから飛べるリンクを全部取得し、必要なもの以外取り除けば良い。例えば以下のコードはmonsterlist28にあるリンクを全て得て画面に表示するコードであるが、ここからmonst_(モンスター名のローマ字表記).htmlだけを選び出し、その冒頭にhttps://~(略)を、後ろに#tを付ける(特性の影響無しでの耐性を表示するようにする)ことによって、求める耐性データの表のあるページに到達出来る。monst_(モンスター名のローマ字表記).htmlを選び出すには正規表現を使う。モンスター名のローマ字表記は全て英小文字だけで構成されているのでmonst_[a-z]+\.htmlで表現出来る筈である(キラーマシン2等数字を名称に含むモンスターであっても、どうやら数字は名前に使われていないようである)。次のコードはモンスターのページ以外も含んだURL全部取得のコードである。モンスターのページのみ抜き出す処理はここ以外の部分が上手くいくことを確認してから考えるものとする。

from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("https://game-cap.com/dqm2/data/monstlist28.html")
bs = BeautifulSoup(html, 'html.parser')
links = bs.find_all('a')
for link in links:
    if 'href' in link.attrs:
        print(link.attrs['href'])

このコードを実行した結果の一部を見せると次のようになる。

monsterlist28のページから張られているリンクのURLの取得結果(の一部)

先述したようにこの方法で得たものから不要なものを消し、少し加工すれば各モンスターのページのURLを取得することが出来る。またgame-capではJavaScriptで計算を行わずデータが直接HTMLに書かれているのでurllib.requestで得ることが出来る筈であるが、表やその中身に名前やIDが振られていないようなので、どうやって取り出すかを調べなければならない。特定のモンスターのページに到達した後は取り敢えずテーブルを全て抽出し、何番目のテーブルに耐性データがあるのか調べる。以下のコードの実行により、それが(1ではなく0から数え始めて)9番目だと確かめられる(余談であるが、守護者ラズバーンのページを開いているのだが、URLに使われているローマ字は守「ぼ」者ラズバーンとなってしまっており、モンスター名のローマ字化アルゴリズムを開発せず、他のページからURLを取得して良かったと思うのであった)。

html = urlopen("https://game-cap.com/dqm2/data/monst_shubosharazuban.html#t")
bs = BeautifulSoup(html, 'html.parser')
tables = bs.find_all('table')
tables[9]

しかし奇妙なことに、BeautifulSoupで開かれたテーブルの中身は#tが無い時のHTMLの時のテーブルの中身であり、#tの効果が反映されていない。HTMLの内容そのものを得ようとしているのにurllib.requestが機能していないのは謎である。取り敢えずこの問題は後回しにする。多分Seleniumを使えば解決する。

そしてgame-capではカバーされないSPでの追加モンスターに関するデータはpdfファイルに纏まっており、pdfの文字列化に関してはPDFMiner3Kというライブラリがあるが、欲しいデータは表として載っている為、これに対応出来るかは未知数である。取り敢えず『PythonによるWebスクレイピング』の著者のgithub

にあるコードをコピペして

from pdfminer.pdfinterp import PDFResourceManager, process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from io import StringIO
from io import open

def readPDF(pdfFile):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, laparams=laparams)

    process_pdf(rsrcmgr, device, pdfFile)
    device.close()

    content = retstr.getvalue()
    retstr.close()
    return content

pdfFile = open("イルルカSP新規追加 新解禁.pdf")
outputString = readPDF(pdfFile)
print(outputString)
pdfFile.close()

というコードを動かすと、UnicodeDecodeError: 'cp932' codec can't decode byte 0xe0 in position 756: illegal multibyte sequenceというエラーが現れる。game-capのみならず、メビウスpdfのコンピューターによる解読も少し手間が掛かるようである。

3.の選択肢は手間は激増するが確実になった1.である。ここまで作業量が増えると複数人で分割するか、間違いを見付ける度に誰かが修正するwiki形式での運用を視野に入れる必要が出てくる。

2.が究極的にはスマートなのであろうが、取り敢えず力技で1.を実行するものとする。修正したモンスターやその箇所を記録しておかないと混乱するのでそれを列挙していく……つもりであったが、調査してみると事前に予想した法則外からも間違いが見つかり、また単純な記載ミスであろうものも散見された為、結局全てを検査する3.の方針を取らざるを得なかった。スクレイピングして得たデータに対し加えた修正点を列挙すると以下のようになる。

  • かまっち(ベタン:-→半減)

  • あおバチ騎兵(メビウスpdfによれば息封じ:弱点→-だが、凡庸が正しい)

  • おおねずみ(ドルマ:-→半減)

  • エビルポット(ドルマ:-→無効)

  • ビックアイ(ビッグアイ→ビックアイ)

  • キラーウェーブ(どく:-→無効)

  • アルミラージ(呪い:-→半減)

  • ドラゴンマッド(マインド:-→無効)

  • ドラムゴート(メビウスpdfによればフール:半減→軽減だが、凡庸が正しい)

  • メイデンドール(メビウスpdfによれば呪い:-→半減だが、凡庸が正しい)

  • トーテムキラー(どく:-→無効)

  • デビルアーマー(どく:-→無効)

  • ふくぶくろ(ベタン:-→半減)

  • あくまの書(ベタン:-→半減)

  • ウパソルジャー(イオ:-→半減)

  • クラウンヘッド(ねむり:-→半減)

  • ぶちキング(マヌーサ:-→無効)

  • へびこうもり(デイン:無効→-、ドルマ:-→無効)

  • エンプーサ(呪い:-→無効)

  • オクトセントリー(マヌーサ:無効→-、マホトラ:-→無効)

  • ユニコーン(イオ、デイン、呪い、呪文封じ、体技封じ、踊り封じ:半減→無効)

  • セイレーンゴースト(メビウスpdfによれば呪い:-→半減だが、凡庸が正しい)

  • ドラゴビショップ(メビウスpdfによればマヌーサ:-→半減で、それが正しい)

  • コハクそう(ドルマ:-→半減)

  • ティコ(呪文封じ:-→半減)

  • メガザルロック(メビウスpdfではザキ耐性-だが、半減の凡庸が正しい)

  • プロトキラー(ベタン:-→半減)

  • ボーンバット(ドルマ:-→半減)

  • ダークスライム(ダウン:-→半減)

  • スマイルリザード(踊り封じ:-→弱い)

  • かぶとこぞう(バギ:-→無効)

  • シャドーノーブル(どく:-→半減)

  • マッスルガード(メビウスpdfによればジバリア:-→弱いで、それが正しい)

  • デスマドモアゼル(メビウスpdfによれば呪い:-→半減だが、凡庸が正しい)

  • ドラゴンロード(メビウスpdfによればマヌーサ:-→半減で、それが正しい)

  • エンタシスマン(ベタン:-→半減)

  • しんかいりゅう(マヌーサ:-→半減)

  • 魔王の書(ベタン:-→半減)

  • うごくせきぞう(ベタン:-→半減、どく:-→無効)

  • マンドラゴラ(踊り封じ:無効→反射)

  • ロック鳥(マホトラ:-→無効)

  • ウルフドラゴン(メビウスpdfにはイオ、デイン弱点が無いが、凡庸が正しい)

  • ブラッドポリス(メビウスpdfには息封じ:弱いと踊り封じ:無効が抜けている。凡庸が正しい)

  • いなずまビリー(いまずまビリー→いなずまビリー)

  • ベル(どく:-→無効)

  • デスソシスト(ドルマ:-→無効)

  • バルザックビースト(ドルマ:-→半減)

  • じごくのマドンナ(息封じ:-→弱い)

  • オカルトビスク(メビウスpdfによれば呪い:-→半減)

  • カプリゴン(ジバリア、ベタン、呪い、体技封じ、息封じ:-→無効)

  • ミルドラース(マホトラ:-→半減)

  • 長老ピピット(こんらん:-→半減)

  • ウィンデオ(踊り封じ半減がメビウスpdfにはないが、凡庸が正しい)

  • かくれんぼう(踊り封じ、ボミエ:-→無効)

  • 怪力軍曹イボイノス(フール:-→半減)

  • スライムマデュラ(どく、呪い、こんらん、マヒ、ねむり:激減→無効)

  • メルトア(マインド、こんらん、マヒ、ねむり:半減→無効)

  • バベルボブル(デイン、ザキ、どく、ねむり、マホトラ:半減→無効)

  • 魔導鬼ベドラー(ベタン:-→半減)

  • 水竜ギルギッシュ(デイン:弱い→-、ベタン:-→弱い)

  • オセアーノン(ねむり:半減→無効)

  • ドルマゲス(ドルマ:吸収→反射)

  • 暗黒の魔神(どく:半減→無効)

  • 覇海軍王ジャコラ(メビウスpdfだとザキ無効で、それが正しい)

  • 邪竜軍王ガリンガ(メビウスpdfだとメラではなくイオ弱点で、それが正しい)

  • キングヒドラ(メラ:-→半減、イオ:半減→-)

  • オリハルゴン(デイン:無効→-、ドルマ:-→無効)

  • はぐれメタルキング(どく、呪い、こんらん、マヒ、ねむり:激減→無効)

  • 大魔王デスタムーア(ねむり:-→半減)

  • メタルスター(どく、呪い、こんらん、マヒ、ねむり:激減→無効)

  • ファイナルウェポン(メビウスpdfだとマインド半減で、それが正しい)

  • 皇帝ウィンディオ(メビウスpdfだとマヒ半減で、それが正しい)

  • 邪神レオソード(ボミエ:-→弱い)

  • 闘神レオソード(呪文封じ:-→弱い)

  • オムド・ロレス(Webスクレイピングした時にJavaScriptが働き終わるまでの待ち時間が足りず、モンスター名~ベタン耐性あたりのデータに抜け、スクレイピングミスでありデータ自体に問題は無い)

  • ガルビルス(ザキ、どく、マインド、こんらん、マヒ、ねむり:激減→無効)

  • 創造神マデサゴーラ(メビウスpdfだと眠りとボミエの半減がないが、実際は眠り半減だけ存在する)

  • 邪竜神ナドラガ(メビウスpdfによればマインド:激減→無効で、それが正しい)

  • 魔界神マデュラーシャ(メビウスpdfだと炎半減がなく、凡庸が正しい)

  • 邪神ニズゼルファ(ザキ、呪い、マインド:激減→無効)

  • マジェス・ドレアム(ザキ、マインド:激減→無効)

この修正作業において、3DS版から存在するモンスターに関しては以下に示す2つの公式ガイドブックのデータを正しいものとして扱い、SPで新規追加されたモンスター(太字)に関しては凡庸のスクレイピング結果とメビウスpdfを比較し、齟齬があった部分に関して、実際にモンスターを耐性の検証が可能な状態に育成してどちらが正しいか検証した(凡庸にもメビウスにも間違いがあり、単純にどちらかの結果のみを正しいものとして採用することは出来なかった為)。検証結果の写真を載せたものも以下に示す。

以上のようにして耐性データも得られた。

データの統合とXML形式による出力

これでモンスターのデータそれ自体は全て揃ったので、別々になっているファイルの内容を統合する。その際、ただ改行で沢山のデータが並んでいるのは扱い辛い為、XMLという形式でデータを書くこととする(XMLやJSON等のファイル形式はデータ分析に用いられるプログラミング言語にはそれを簡単に読み込める文法がある)。

特性によるステータス補正の除外とXML出力

但し、ただ統合すれば良いというものではない。後々の分析のことを考え耐性データは特性の影響を排したものをわざわざ取得したように、ステータス上限に関しても特性の影響を取り除く必要がある。前回の記事(下のリンクを参照)におけるギガハンド最強伝説の誤謬を見よ。

今得られているデータにおけるステータス上限は図鑑において確認出来る値で、図鑑における値は+値が0の時に有効になっている特性による補正を受けたものである。よって、その状態において各モンスターのどの特性が有効になっており、どの程度の補正がかかっているか調べなければならない。

どの特性がどのステータスにどの割合で補正をかけ、小数点以下の扱いをどうするか公式書籍では『究極対戦ガイドブック』のp17に記載されている。そこには表があり、表の上側から順番に倍率を掛け、乗算が全て終わった最後に一回ではなく、乗算したその都度小数点以下切り上げによって整数化すると述べられている(しかし後の検証で、実際には毎回小数点以下切り上げの処理を行う訳ではないことが判明した)。

『究極対戦ガイドブック』における小数点以下の扱い

ではこれで正しい計算手順が判明したかと言えばそうではない。表中の数値は小数第3位を四捨五入したものとも書かれているからである。既存の計算ツール、例えば小梢ExecelファイルではメタルボディによるHP補正0.33を1/3、常にマホカンタによるHP補正0.88を0.875(=7/8)とし、それ以外の値はそのまま使われており、それが恐らく一般的な解釈だと思われるが、可能性としてはメタルボディのモンスターのAI4回行動によるステータス補正の0.44が例えば0.4375(=7/16)で、0.44で計算すると正しい値にならないという可能性が否定されない。実際、後述するが、メタルボディのモンスターがAI3~4回行動の時に各ステータスに掛かる補正は0.47と書かれているが、これは0.468(≒15/32)がより適切であるということが、メタルゴッデスのステータスを計算することにより判明している。但し、0.468と15/32のどちらが本当に正しいかまでは特定していない(ここの区別が必要になる事例はほぼ存在しないであろうが)。

加えて、正確な補正値が分かったとしても、ステータスの値が整数しか取り得ない都合上、ステータス補正が1未満の場合、その計算は単射ではなく、それ故に補正前の値を一意に定めることが原理的に出来ないという問題がある。例えば、0.5を掛けるという計算を考えてみよう。$${100\times0.5=50}$$、$${99\times0.5=49.5}$$であり、$${100\neq99}$$であるならば$${100\times0.5\neq99\times0.5}$$ともなっている。この場合、計算後の値に2を掛けることで$${50\times2=100}$$、$${49.5\times2=99}$$のように元の値を復元することが出来る。しかしながら、計算後の値が整数に限られるとしよう。イルルカでの計算式に倣って小数点以下切り上げとすると$${99\times0.5=50}$$であり、$${100\neq99}$$にも関わらず$${100\times0.5=99\times0.5}$$が成立している。この場合、計算後の値が50であるという情報だけからは、計算前の値が99であるのか100であるのか確定させることが出来ない。即ち、特性による補正の無いモンスターの真のステータスを確定させるには、ある1つの特性の組とその場合のステータス上限値という、図鑑に載っている情報のみからは不可能なのである。

但し、この問題に関しては、他の特性で育成した場合のステータス上限値を調べ、情報量を増やすことで理論上は解決は可能である。例えば0.5倍の他に1/3倍の補正がかかった条件下でのステータスも知ることが出来たとしよう。この時$${100/3=33.\dot{3}→34}$$、$${99/3=33}$$であるから、特性の影響下でのステータスが34なのか33なのかによって元の値が100なのか99なのか同定することが可能である。但し、イルルカで実際にモンスターを育てると個体値や系図によりステータスが更に変動するので、それらの計算式まで把握した上でないとこの作業を進めることは出来ない。また育成にかかる労力も膨大である。

従って、今回は正確な値の導出は諦め、概算値を得るのみとする。その計算は単純にかけられた補正値で割り、割算の計算が全て終わった後に小数点以下切り捨てによって得た値を元の値として用いることとする。

このような条件で今まで集めてきたモンスターのデータをXMLで出力するC#のコードは次のようになる(コードの一部に、この後に述べる能力値合計一定則を満たさないモンスターを書き出す処理も含まれている)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonsterDataMaker
{
    class Program
    {
        static void Main(string[] args)
        {
            MonsterManager monsterManager = new MonsterManager();
            monsterManager.ReadParameters("パラメータ.txt");
            monsterManager.ReadTorelance("耐性データ.txt");
            monsterManager.CalculateParameterTrue();
            monsterManager.WriteMonsterNot8000();
            monsterManager.WriteXML();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Xml.Linq;

namespace MonsterDataMaker
{
    internal class MonsterManager
    {
        public List<Monster> Monsters { get; private set; }

        public MonsterManager()
        {
            Monsters = new List<Monster> { };
        }

        public void ReadParameters(string fileName)
        {
            using (StreamReader sr = new StreamReader(fileName))
            {
                while (!sr.EndOfStream)
                {
                    string[] data = sr.ReadLine().Trim().Split(',');
                    Monsters.Add(new Monster
                    {
                        Name = data[0],
                        ParametersInLibrary = new Dictionary<ParameterName, int>
                        {
                            [ParameterName.HP] = int.Parse(data[1]),
                            [ParameterName.MP] = int.Parse(data[2]),
                            [ParameterName.Attack] = int.Parse(data[3]),
                            [ParameterName.Defense] = int.Parse(data[4]),
                            [ParameterName.Speed] = int.Parse(data[5]),
                            [ParameterName.Wisdom] = int.Parse(data[6])
                        },
                        IdKozue = int.Parse(data[7]),
                        IdLibrary = (int.Parse(data[7]) >= 8) ? (int.Parse(data[7]) - 5) : 1,
                        Phylesis = ParsePhylesis(data[8]),
                        Abilities = new MonsterAbility 
                        {
                            SizeName = data[9],
                            Ability1 = data[10],
                            Ability2 = data[11],
                            Ability25 = data[12],
                            Ability50 = data[13],
                            Ability100 = data[14],
                            AbilityMega = data[15],
                            AbilityGiga = data[16],
                            AbilitySuperGiga = data[17],
                        },
                        Weapons = ParseWeapons(data[18])
                    }); 
                }
            }
        }

        public void ReadTorelance(string fileName)
        {
            using (StreamReader sr = new StreamReader(fileName))
            {
                while (!sr.EndOfStream)
                {
                    int id = int.Parse(sr.ReadLine().Split(',')[0]);
                    Dictionary<string, Torelance> torelance = new Dictionary<string, Torelance> { };
                    for (int i = 0; i < 30; i++)
                    {
                        string[] str = sr.ReadLine().Trim().Split(',');
                        Torelance t = Torelance.Normal;
                        if (str[1] == "弱い") t = Torelance.Weak;
                        else if (str[1] == "軽減") t = Torelance.LittleDecrease;
                        else if (str[1] == "半減") t = Torelance.HalfDecrease;
                        else if (str[1] == "激減") t = Torelance.HeavyDecrease;
                        else if (str[1] == "無効") t = Torelance.NoDamage;
                        else if (str[1] == "回復") t = Torelance.Heal;
                        else if (str[1] == "反射") t = Torelance.Reflect;
                        torelance.Add(str[0], t);
                    }
                    for (int i = 0; i < Monsters.Count(); i++)
                    {
                        if (Monsters[i].IdLibrary == id)
                        {
                            Monsters[i].Torelances = torelance;
                        }
                    }
                }
            }
        }

        public void CalculateParameterTrue()
        {
            for (int i = 0; i < Monsters.Count(); i++)
            {
                Monsters[i].CalculateParameterOriginal();
            }
        }

        public void WriteXML()
        {
            XDocument xDocument = new XDocument();
            XElement rootElement = new XElement("イルルカSPモンスターズ");
            xDocument.Add(rootElement);

            foreach (var monster in Monsters)
            {
                XElement monsterElem = new XElement("モンスター",
                    new XElement("名前", monster.Name),
                    new XElement("位階", monster.IdLibrary),
                    new XElement("系統", JapaneseTranslation(monster.Phylesis)),
                    new XElement("HP", monster.ParametersOriginal[ParameterName.HP]),
                    new XElement("MP", monster.ParametersOriginal[ParameterName.MP]),
                    new XElement("攻撃力", monster.ParametersOriginal[ParameterName.Attack]),
                    new XElement("守備力", monster.ParametersOriginal[ParameterName.Defense]),
                    new XElement("素早さ", monster.ParametersOriginal[ParameterName.Speed]),
                    new XElement("賢さ", monster.ParametersOriginal[ParameterName.Wisdom]),
                    new XElement("サイズ特性", monster.Abilities.SizeName),
                    new XElement("新生前特性1", monster.Abilities.Ability1),
                    new XElement("新生前特性2", monster.Abilities.Ability2),
                    new XElement("特性25", monster.Abilities.Ability25),
                    new XElement("特性50", monster.Abilities.Ability50),
                    new XElement("特性100", monster.Abilities.Ability100),
                    new XElement("メガ特性", monster.Abilities.AbilityMega),
                    new XElement("ギガ特性", monster.Abilities.AbilityGiga),
                    new XElement("超ギガ特性", monster.Abilities.AbilitySuperGiga));

                foreach (var torelance in monster.Torelances)
                {
                    string str = "-";
                    switch (torelance.Value)
                    {
                        case Torelance.Weak:
                            str = "弱点";
                            break;
                        case Torelance.Normal:
                            str = "普通";
                            break;
                        case Torelance.LittleDecrease:
                            str = "軽減";
                            break;
                        case Torelance.HalfDecrease:
                            str = "半減";
                            break;
                        case Torelance.HeavyDecrease:
                            str = "激減";
                            break;
                        case Torelance.NoDamage:
                            str = "無効";
                            break;
                        case Torelance.Heal:
                            str = "回復";
                            break;
                        case Torelance.Reflect:
                            str = "反射";
                            break;
                        default:
                            str = "-";
                            break;
                    }
                    monsterElem.Add(new XElement(torelance.Key, str));
                }

                monsterElem.Add(new XElement("剣", JapaneseTranslation(monster.Weapons.Sword)));
                monsterElem.Add(new XElement("ヤリ", JapaneseTranslation(monster.Weapons.Spear)));
                monsterElem.Add(new XElement("オノ", JapaneseTranslation(monster.Weapons.Axe)));
                monsterElem.Add(new XElement("ハンマー", JapaneseTranslation(monster.Weapons.Hammer)));
                monsterElem.Add(new XElement("ムチ", JapaneseTranslation(monster.Weapons.Whip)));
                monsterElem.Add(new XElement("ツメ", JapaneseTranslation(monster.Weapons.Claw)));
                monsterElem.Add(new XElement("杖", JapaneseTranslation(monster.Weapons.Staff)));

                rootElement.Add(monsterElem);
            }
            xDocument.Save("モンスターデータ.xml");
        }

        public void WriteMonsterNot8000()
        {
            using (StreamWriter sw = new StreamWriter("Not8000Monsters.txt"))
            {
                foreach (var monster in Monsters)
                {
                    int sum = 2 * (monster.ParametersOriginal[ParameterName.HP] + monster.ParametersOriginal[ParameterName.Attack]
                        + monster.ParametersOriginal[ParameterName.Speed]) + monster.ParametersOriginal[ParameterName.MP]
                        + monster.ParametersOriginal[ParameterName.Defense] + monster.ParametersOriginal[ParameterName.Wisdom];
                    if (sum != 8000)
                    {
                        sw.WriteLine($"{monster.Name},{monster.ParametersOriginal[ParameterName.HP]}," +
                        $"{monster.ParametersOriginal[ParameterName.MP]},{monster.ParametersOriginal[ParameterName.Attack]}," +
                        $"{monster.ParametersOriginal[ParameterName.Defense]},{monster.ParametersOriginal[ParameterName.Speed]}," +
                        $"{monster.ParametersOriginal[ParameterName.Wisdom]},{sum}");
                    }
                }
            }
            
        }

        Phylesis ParsePhylesis(string text)
        {
            if (text == "スライム") return Phylesis.Slime;
            else if (text == "ドラゴン") return Phylesis.Dragon;
            else if (text == "自然") return Phylesis.Nature;
            else if (text == "魔獣") return Phylesis.Beast;
            else if (text == "物質") return Phylesis.Material;
            else if (text == "悪魔") return Phylesis.Devil;
            else if (text == "ゾンビ") return Phylesis.Zombie;
            else if (text == "???") return Phylesis.Wonder;
            else return Phylesis.Error;
        }

        string JapaneseTranslation(Phylesis phylesis)
        {
            switch (phylesis)
            {
                case Phylesis.Error:
                    return "エラー";
                    break;
                case Phylesis.Slime:
                    return "スライム";
                    break;
                case Phylesis.Dragon:
                    return "ドラゴン";
                    break;
                case Phylesis.Nature:
                    return "自然";
                    break;
                case Phylesis.Beast:
                    return "魔獣";
                    break;
                case Phylesis.Material:
                    return "物質";
                    break;
                case Phylesis.Devil:
                    return "悪魔";
                    break;
                case Phylesis.Zombie:
                    return "ゾンビ";
                    break;
                case Phylesis.Wonder:
                    return "???";
                    break;
                default:
                    return "エラー";
                    break;
            }
        }

        MonsterWeapon ParseWeapons(string text)
        {
            string[] weapons = text.Split('、');
            bool sword = false, spear = false, axe = false, hammer = false, whip = false, claw = false, staff = false;
            foreach (var w in weapons)
            {
                if (w == "剣") sword = true;
                else if (w == "ヤリ") spear = true;
                else if (w == "オノ") axe = true;
                else if (w == "ハンマー") hammer = true;
                else if (w == "ムチ") whip = true;
                else if (w == "ツメ") claw = true;
                else if (w == "杖") staff = true;
            }
            return new MonsterWeapon
            {
                Sword = sword,
                Spear = spear,
                Axe = axe,
                Hammer = hammer,
                Whip = whip,
                Claw = claw,
                Staff = staff
            };
        }

        string JapaneseTranslation(bool b)
        {
            if (b) return "〇";
            else return "×";
        }

    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonsterDataMaker
{
    public enum ParameterName
    {
        HP,
        MP,
        Attack,
        Defense,
        Speed,
        Wisdom,
    }

    internal enum Phylesis
    {
        Error,
        Slime,
        Dragon,
        Nature,
        Beast,
        Material,
        Devil,
        Zombie,
        Wonder
    }

    public enum Torelance
    {
        Weak = -1,
        Normal = 0,
        LittleDecrease = 1,
        HalfDecrease = 2,
        HeavyDecrease = 3,
        NoDamage = 4,
        Heal = 5,
        Reflect = 6,
    }

    internal class Monster
    {
        public string Name { get; set; }
        public Dictionary<ParameterName, int> ParametersInLibrary { get; set; }
        public int IdKozue { get; set; }
        public int IdLibrary { get; set; }
        public Phylesis Phylesis { get; set; }
        public MonsterAbility Abilities { get; set; }
        public MonsterWeapon Weapons { get; set; }
        public Dictionary<ParameterName, int> ParametersOriginal { get; set; }
        public Dictionary<string, Torelance> Torelances { get; set; }

        public Monster()
        {

        }

        public void CalculateParameterOriginal()
        {
            var parameterChangeRates = Abilities.ParameterChangeRates();
            ParametersOriginal = new Dictionary<ParameterName, int>
            {
                [ParameterName.HP] = 0,
                [ParameterName.MP] = 0,
                [ParameterName.Attack] = 0,
                [ParameterName.Defense] = 0,
                [ParameterName.Speed] = 0,
                [ParameterName.Wisdom] = 0,
            };
            foreach (var parameter in ParametersInLibrary)
            {
                ParameterName parameterName = parameter.Key;
                double approximation = ParametersInLibrary[parameterName];
                List<double> rates = parameterChangeRates[parameterName];
                foreach (double rate in rates)
                {
                    approximation /= rate;
                }
                int lowerOriginal = (int) Math.Truncate(approximation);

                ParametersOriginal[parameterName] = lowerOriginal;
            }
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonsterDataMaker
{
    public enum Size
    {
        Error,
        Small,
        Standard,
        Mega,
        Giga,
        SuperGiga,
    }

    public enum Metal
    {
        NoMetal = 0,
        LightMetal = 1,
        Metal = 2,
        HardMetal = 3,
        SuperHardMetal = 4,
    }

    public enum MoveTimes
    {
        One,
        OneOrTwo,
        OneOrTwoOrThree,
        Two,
        TwoOrThree,
        Three,
        ThreeOrFour,
        Four,
    }

    internal class MonsterAbility
    {
        public string SizeName { get; set; }
        public string Ability1 { get; set; }
        public string Ability2 { get; set; }
        public string Ability25 { get; set; }
        public string Ability50 { get; set; }
        public string Ability100 { get; set; }
        public string AbilityMega { get; set; }
        public string AbilityGiga { get; set; }
        public string AbilitySuperGiga { get; set; }
        public Size Size { get; set; }
        public Metal Metal { get; set; }
        public MoveTimes MoveTimes { get; set; }
        public bool IsMagicReflect { get; set; }
        public bool IsAttackReflect { get; set; }

        public MonsterAbility()
        {
            Size = Size.Error;
            Metal = Metal.NoMetal;
            MoveTimes = MoveTimes.One;
            IsMagicReflect = false;
            IsAttackReflect = false;
        }

        public Dictionary<ParameterName, List<double>> ParameterChangeRates()
        {
            if (SizeName == "スモールボディ") Size = Size.Small;
            else if (SizeName == "スタンダードボディ") Size = Size.Standard;
            else if (SizeName == "メガボディ") Size = Size.Mega;
            else if (SizeName == "ギガボディ") Size = Size.Giga;
            else if (SizeName == "超ギガボディ") Size = Size.SuperGiga;

            List<string> activeAbilities = new List<string> { Ability1, Ability2 };
            if (Size == Size.Mega)
            {
                activeAbilities.Add(AbilityMega);
            }
            else if (Size == Size.Giga)
            {
                activeAbilities.Add(AbilityMega);
                activeAbilities.Add(AbilityGiga);
            }
            else if (Size == Size.SuperGiga)
            {
                activeAbilities.Add(AbilityMega);
                activeAbilities.Add(AbilityGiga);
                activeAbilities.Add(AbilitySuperGiga);
            }

            if (activeAbilities.Contains("ライトメタルボディ")) Metal = Metal.LightMetal;
            else if (activeAbilities.Contains("メタルボディ")) Metal = Metal.Metal;
            else if (activeAbilities.Contains("ハードメタルボディ")) Metal = Metal.HardMetal;
            else if (activeAbilities.Contains("超ハードメタルボディ")) Metal = Metal.SuperHardMetal;

            if (activeAbilities.Contains("AI1~2回行動")) MoveTimes = MoveTimes.OneOrTwo;
            else if (activeAbilities.Contains("AI1~3回行動")) MoveTimes = MoveTimes.OneOrTwoOrThree;
            else if (activeAbilities.Contains("AI2回行動")) MoveTimes = MoveTimes.Two;
            else if (activeAbilities.Contains("AI2~3回行動")) MoveTimes = MoveTimes.TwoOrThree;
            else if (activeAbilities.Contains("AI3回行動")) MoveTimes = MoveTimes.Three;
            else if (activeAbilities.Contains("AI3~4回行動")) MoveTimes = MoveTimes.ThreeOrFour;
            else if (activeAbilities.Contains("AI4回行動")) MoveTimes = MoveTimes.Four;

            if (activeAbilities.Contains("つねにマホカンタ")) IsMagicReflect = true;
            if (activeAbilities.Contains("つねにアタックカンタ")) IsAttackReflect = true;

            var rates = new Dictionary<ParameterName, List<double>>
            {
                [ParameterName.HP] = new List<double> { 1 },
                [ParameterName.MP] = new List<double> { 1 },
                [ParameterName.Attack] = new List<double> { 1 },
                [ParameterName.Defense] = new List<double> { 1 },
                [ParameterName.Speed] = new List<double> { 1 },
                [ParameterName.Wisdom] = new List<double> { 1 },
            };

            if (Size == Size.Small)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.7);
                    rates[ParameterName.MP].Add(0.8);
                    rates[ParameterName.Attack].Add(0.8);
                    rates[ParameterName.Defense].Add(0.8);
                    rates[ParameterName.Speed].Add(0.8);
                    rates[ParameterName.Wisdom].Add(0.8);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.7);
                    rates[ParameterName.MP].Add(0.8);
                    rates[ParameterName.Attack].Add(0.8);
                    rates[ParameterName.Wisdom].Add(0.8);
                }
            }
            else if (Size == Size.Mega)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(1.5);
                    rates[ParameterName.MP].Add(1.5);
                    rates[ParameterName.Attack].Add(1.05);
                    rates[ParameterName.Defense].Add(1.05);
                    rates[ParameterName.Speed].Add(1.05);
                    rates[ParameterName.Wisdom].Add(1.05);
                }
                else
                {
                    rates[ParameterName.HP].Add(1.5);
                    rates[ParameterName.MP].Add(1.5);
                    rates[ParameterName.Attack].Add(1.05);
                    rates[ParameterName.Wisdom].Add(1.05);
                }
            }
            else if (Size == Size.Giga)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(2);
                    rates[ParameterName.MP].Add(2);
                    rates[ParameterName.Attack].Add(1.1);
                    rates[ParameterName.Defense].Add(1.1);
                    rates[ParameterName.Speed].Add(1.1);
                    rates[ParameterName.Wisdom].Add(1.1);
                }
                else
                {
                    rates[ParameterName.HP].Add(2);
                    rates[ParameterName.MP].Add(2);
                    rates[ParameterName.Attack].Add(1.1);
                    rates[ParameterName.Wisdom].Add(1.1);
                }
            }
            else if (Size == Size.SuperGiga)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(2.5);
                    rates[ParameterName.MP].Add(2.5);
                    rates[ParameterName.Attack].Add(1.15);
                    rates[ParameterName.Defense].Add(1.15);
                    rates[ParameterName.Speed].Add(1.15);
                    rates[ParameterName.Wisdom].Add(1.15);
                }
                else
                {
                    rates[ParameterName.HP].Add(2.5);
                    rates[ParameterName.MP].Add(2.5);
                    rates[ParameterName.Attack].Add(1.15);
                    rates[ParameterName.Wisdom].Add(1.15);
                }
            }

            if (MoveTimes == MoveTimes.OneOrTwo)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.9);
                    rates[ParameterName.MP].Add(0.9);
                    rates[ParameterName.Attack].Add(0.9);
                    rates[ParameterName.Defense].Add(0.9);
                    rates[ParameterName.Speed].Add(0.9);
                    rates[ParameterName.Wisdom].Add(0.9);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.86);
                    rates[ParameterName.MP].Add(0.86);
                    rates[ParameterName.Attack].Add(0.86);
                    rates[ParameterName.Wisdom].Add(0.86);
                }
            }
            else if (MoveTimes == MoveTimes.OneOrTwoOrThree || MoveTimes == MoveTimes.Two)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.8);
                    rates[ParameterName.MP].Add(0.8);
                    rates[ParameterName.Attack].Add(0.8);
                    rates[ParameterName.Defense].Add(0.8);
                    rates[ParameterName.Speed].Add(0.8);
                    rates[ParameterName.Wisdom].Add(0.8);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.72);
                    rates[ParameterName.MP].Add(0.72);
                    rates[ParameterName.Attack].Add(0.72);
                    rates[ParameterName.Wisdom].Add(0.72);
                }
            }
            else if (MoveTimes == MoveTimes.TwoOrThree)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.7);
                    rates[ParameterName.MP].Add(0.7);
                    rates[ParameterName.Attack].Add(0.7);
                    rates[ParameterName.Defense].Add(0.7);
                    rates[ParameterName.Speed].Add(0.7);
                    rates[ParameterName.Wisdom].Add(0.7);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.58);
                    rates[ParameterName.MP].Add(0.58);
                    rates[ParameterName.Attack].Add(0.58);
                    rates[ParameterName.Wisdom].Add(0.58);
                }
            }
            else if (MoveTimes == MoveTimes.Three)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.65);
                    rates[ParameterName.MP].Add(0.65);
                    rates[ParameterName.Attack].Add(0.65);
                    rates[ParameterName.Defense].Add(0.65);
                    rates[ParameterName.Speed].Add(0.65);
                    rates[ParameterName.Wisdom].Add(0.65);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.51);
                    rates[ParameterName.MP].Add(0.51);
                    rates[ParameterName.Attack].Add(0.51);
                    rates[ParameterName.Wisdom].Add(0.51);
                }
            }
            else if (MoveTimes == MoveTimes.ThreeOrFour)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.62);
                    rates[ParameterName.MP].Add(0.62);
                    rates[ParameterName.Attack].Add(0.62);
                    rates[ParameterName.Defense].Add(0.62);
                    rates[ParameterName.Speed].Add(0.62);
                    rates[ParameterName.Wisdom].Add(0.62);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.47);
                    rates[ParameterName.MP].Add(0.47);
                    rates[ParameterName.Attack].Add(0.47);
                    rates[ParameterName.Wisdom].Add(0.47);
                }
            }
            else if (MoveTimes == MoveTimes.Four)
            {
                if (Metal <= Metal.LightMetal)
                {
                    rates[ParameterName.HP].Add(0.6);
                    rates[ParameterName.MP].Add(0.6);
                    rates[ParameterName.Attack].Add(0.6);
                    rates[ParameterName.Defense].Add(0.6);
                    rates[ParameterName.Speed].Add(0.6);
                    rates[ParameterName.Wisdom].Add(0.6);
                }
                else
                {
                    rates[ParameterName.HP].Add(0.44);
                    rates[ParameterName.MP].Add(0.44);
                    rates[ParameterName.Attack].Add(0.44);
                    rates[ParameterName.Wisdom].Add(0.44);
                }
            }

            if (Metal == Metal.LightMetal) rates[ParameterName.HP].Add(0.5);
            else if (Metal == Metal.Metal) rates[ParameterName.HP].Add(1.0 / 3.0);
            else if (Metal == Metal.HardMetal) rates[ParameterName.HP].Add(0.25);
            else if (Metal == Metal.SuperHardMetal) rates[ParameterName.HP].Add(0.2);

            if (IsMagicReflect) rates[ParameterName.HP].Add(0.875);
            if (IsAttackReflect) rates[ParameterName.HP].Add(0.75);

            return rates;
        }
        
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MonsterDataMaker
{
    internal class MonsterWeapon
    {
        public bool Sword { get; set; }
        public bool Spear { get; set; }
        public bool Axe { get; set; }
        public bool Hammer { get; set; }
        public bool Whip { get; set; }
        public bool Claw { get; set; }
        public bool Staff { get; set; }

        public MonsterWeapon()
        {
            Sword = false;
            Spear = false;
            Axe = false;
            Hammer = false;
            Whip = false;
            Claw = false;
            Staff = false;
        }
    }
}

このようにして以下のようなXMLファイルが得られる。

XMLファイルの一部

能力値合計一定則を用いた検算と修正

さて、このようにして得たXMLファイルであるが、先述したようにパラメータに関しては正確な値ではないので、計算がどの程度合っているのか検算をしたい。そこで、黒髭が発見し以下の記事で述べた、モンスターのパラメータの合計値は全て同じであるという法則を確かめてみる(この法則自体は以前からDQMJ3(P)では知られていたが、イルルカでも成り立っていることはイルルカSPリリース前は知られていなかった)。

もし殆どのモンスターがこの法則に従わないなら法則の方が間違っているということになるし、逆に殆どのモンスターが従うとするなら、従わない一部のモンスターに関してはデータに修正が必要だろうという推測が成り立つ。能力値合計一定の法則の計算式は以下のようになっている。

$$
2\times(HP+Attack+Speed)+MP+Defense+Wisdom=8000
$$

但し、モントナーのみ右辺が8200となる。また、黒髭記事ではこの両辺を2で割ったものが用いられていたが、整数の演算ということを加味して両辺を2倍した。

この式の左辺の計算を行い、その値が8000にならなかったモンスターを列挙すると、(モントナー含めて)その数は203種類であった。多くのモンスターが法則に従っていることから、法則は正しく、法則に従うものとしてデータに修正を加える行為に妥当性が見られそうである。そしてその殆どが8000とのズレは1桁程度、即ち単射性の欠如による逆算の非一意性に由来するものであり、ここまでは特に問題無さそうである。ライブラリのデータだけからは本来のパラメータが確定しないモンスターであっても、この情報によって確定するものもいるであろうし、そのような場合に特に有効である。

その他、3DS版から登場しているモンスターであれば、個体値や系図補正が無い時の新生配合時のパラメータが公式ガイドブックに記述されている為、これもパラメータ確定作業に使うことが出来るであろう。新生配合による各パラメータへの補正は1.2倍である。

ところが、時々数十から数百ものズレを生じているモンスターがいる。これは計算の誤差によっては説明出来ない。その一部に関しては、凡庸のサイトを見てみると、記載されているパラメータの数値が上げられているゲーム内のライブラリにおける値と異なっていることが分かった。即ち、凡庸がゲーム内のデータをコンピュータに入力する際に誤り、それが凡庸からデータを取得した小梢Execelにそのまま残っていたという訳である。

しかし、大きなズレを生じているモンスターで、尚且つ凡庸に入力ミスが無いモンスターが多数存在していた。そのようなモンスターはつねにマホカンタ、アタックカンタ持ちばかりであった為、当初はそれらの特性を持つモンスターの計算にバグがあるのかと思っていたが、実際には何とゲーム内のライブラリに表示されている数値がおかしいということが判明した。その一例としてスラ忍レッドのデータを示す。

イルルカSPにおけるライブラリでのスラ忍レッドのステータス
『最強データガイドブック』におけるスラ忍レッドのステータス

この通り、ゲーム内ではスラ忍レッドのHPの最大値は1419であり、凡庸でもこの値が使われているが、3DS版の公式ガイドブックでは対応する箇所のHPは1065である。これだけではどちらが正しいか判別出来ないが、能力値合計一定則の他に、次の2つの事実と発生理由に関する1つの推測が公式ガイドブックの値が正しいことの傍証となる。3DS版のゲーム内でのライブラリでは1065という値が使われていた(下図参照)こと、$${1419\times0.75=1065}$$でアタカン考慮によって2つの異なる数値の関係が自然に理解出来ること、イルルカSPのver1.0.0では配合時、誕生するモンスターの能力値上限においてマホカンタやアタックカンタが考慮されておらず、その点はアップデートで修正されたが、ライブラリに関してはその時のまま修正を忘れてしまったという形で発生原因の自然な推測が可能であること、である。

3DS版イルルカにおけるライブラリでのスラ忍レッドのステータス

以上、①小数の整数化に伴う計算誤差②凡庸の入力ミス③ゲーム内ライブラリの誤りという3つの要因で、XMLファイル内に能力値合計一定則を満たさないモンスターがいるということが分かったが、能力値合計一定則を満たすようにこれらを修正していくと修正点は以下のようになる。①の要因による修正は条件を満たすような解が一意であるとは限らない為、それを含むものに関しては、修正した値ですら厳密に正しいとは限らないことに注意が必要である。また①のみの修正は僅か数ポイントであり、②③の要素を含まない場合の修正の価値は小さい為、①のみか②③を含むかで区別して列挙する。

①のみによる修正(解が一意に定まらないので一部は修正を延期)

  • トマトマーレ(HAから1つ、MWから1つ選び、1ずつ減らす)

  • アラウネ(HもしくはAを1下げる)

  • 黄泉の花(MもしくはDを1下げる)

  • 妖魔軍王ブギー(HかAを1下げるか、MDを1ずつ下げる。Hを下げた場合、全ての数値が5の倍数となり数字のキリが良いので、恐らくH1871→1870が正解だと推測される)

  • 創造神マデサゴーラ(DかWを1下げる。W1161→1160とした場合、全ての数値が10の倍数となりキリが良いので、恐らくWを下げるのが正解だと思われる)

  • 邪竜神ナドラガ(DかWを1下げる。D631→630とした場合、全ての数値が5の倍数となりキリが良いので、恐らくDを下げるのが正解だと思われる)

  • 邪神ニズゼルファ(DかWを1下げる。W671→670とした場合、全ての数値が10の倍数となりキリが良いので、恐らくWを下げるのが正解だと思われる)

  • マジェス・ドレアム(Aを1下げるか、DとWを1ずつ下げる。D(W)551→550とした場合、全ての数値が10の倍数となりキリが良いので、恐らくDWを下げるのが正解だと思われる)

①のみによる修正(解が一意に定まる)

  • ゴースト(M790→789、S805→804)

  • リーファ(A570→569、S765→764)

  • ドラキー(H1304→1303)

  • マンドラ(Wを825→824)

  • リップス(A715→714、W455→454)

  • ファーラット(M860→859、A665→664)

  • ぐんたいアリ(M480→479、D990→989)

  • バブルスライム(H1627→1626、A675→674)

  • ホイミスライム(Wを935→934)

  • プチット族(AとDを725→724)

  • おおにわとり(A728→727)

  • マドハンド(M700→699、S600→599)

  • ドラゴスライム(A765→764、S860→859、W510→509)

  • シーメーダ(D470→469)

  • スライムベス(S680→679)

  • フェアリードラゴン(H1327→1326)

  • おおイグアナ(D655→654)

  • じんめんちょう(S835→834)

  • モーモン(A620→619)

  • ブッチョマン(W655→654)

  • もみじこぞう(M950→949)

  • ひとつめピエロ(D535→534)

  • ストロングアニマル(W619→618)

  • メイデンドール(H1676→1675)

  • のろいのマスク(D861→860、S691→690)

  • スーパーテンツク(D549→550)

  • メドーサボール(S692→691、W1099→1100)

  • ボックススライム(H1654→1653)

  • モーザ(D691→690)

  • エンゼルスライム(A440→439)

  • プチアーノン(H1627→1626、M675→674)

  • カマキリせんし(W538→537)

  • クラウンヘッド(D1037→1036)

  • カンダタこぶん(M455→454、D650→649、W455→454)

  • ガンコどり(H1537→1536、S615→614、W545→544)

  • かぼちゃの騎士(D600→599)

  • スライムブレス(M565→564、S810→809)

  • メタルハンター(A950→949)

  • ケルベロス(M355→354、D595→594)

  • ベホマスライム(H1537→1536、A530→529、S770→769、W955→954)

  • ドラゴビショップ(D782→781)

  • ライバーン(W546→545)

  • まちガエル(D999→1000)

  • コハクそう(D1091→1090)

  • ドラゴンヘビー(A725→724)

  • まどうスライム(D640→639、W950→949)

  • バリクナジャ(S659→658、W1142→1141)

  • ボストロール(A891→890)

  • スマイルリザード(A745→744、W455→454)

  • ヘルホーネット(S1210→1209)

  • ピンクモーモン(H1720→1719)

  • ルーファ(H1447→1446、D695→694)

  • タイラントワーム(W599→600)

  • ウイングタイガー(W599→600)

  • ワンダーエッグ(H1674→1673、M660→659)

  • ダンジョンえび(H1642→1641)

  • スカイドラゴン(D800→799、S855→854)

  • パオーム(D764→763)

  • キラーマシン(H1135→1134、S860→859、W685→684)

  • デスタランチュラ(D673→672、S855→854)

  • ギリメカラ(D719→718)

  • ダークキング(S860→859)

  • 少年レオソード(D504→503、W529→528)

  • スライムファミリー(D657→656、W931→930)

  • ブル(H1720→1719、A540→539、W540→539)

  • 魔王の使い(H1400→1399、A875→874)

  • スライダーキッズ(H1220→1219)

  • キマイラロード(S919→918)

  • 皇帝ガナサダイ(W1037→1036)

  • デッドマスカー(S891→890)

  • セバスチャン(M550→549)

  • プチットガールズ(W635→634)

  • ほうらい大王(A819→818)

  • ピサロナイト(D1030→1029)

  • スライムジェネラル(D710→709、W860→859)

  • ヘルクラウド(D999→1000)

  • 大王イカ(A919→918、W369→368)

  • ゾーマズデビル(A461→460、S611→610)

  • スペディオ(H1584→1583)

  • グラブゾン(H1720→1719、A915→914、S540→539)

  • ギュメイ将軍(W610→609)

  • 勇車スラリンガル(D1049→1050)

  • ぬしさま(S631→630)

  • 邪眼皇帝アウルート(H1410→1409)

  • 魔剣神レパルド(H1220→1219)

  • スライダークロボ(A670→669、D927→926)

  • レティス(S999→1000、W999→1000)

  • ダーククリスタル(W582→581)

  • ローズバトラー(A757→756)

  • バラモスゾンビ(D617→616)

  • ドラゴンマシン(W491→490)

  • さそりアーマー(H1505→1504、S619→618)

  • グリフィンクス(A840→839、S479→478)

  • ヘルゴラゴ(W351→350)

  • 衝撃のしっぽ団(S899→900)

  • ウルベア魔神兵(A949→950、W599→600)

  • 魔王デスタムーア(D599→600、S1099→1100)

  • 破壊神シドー(W1169→1168)

  • スラキャンサー(D949→950)

  • ゴールデンスライム(H541→540)

  • 暗黒皇帝ガナサダイ(A614→613)

  • 暗黒の魔神(D1049→1050)

  • カンダタワイフ(W763→762)

  • カンダタおやぶん(H1440→1439)

  • カカロン(D663→662)

  • ドメディ(S863→862)

  • Vロン(A899→900)

  • スーパーキラーマシン(A763→762)

  • カンダタレディース(A696→695、D579→578、S1014→1013)

  • スライダーキング(S999→1000)

  • カンダタロックス(S999→1000)

  • カンダタセブン(A811→810、D1141→1140)

  • 竜王(D1000→999)

  • スラリン船(D1199→1200)

  • スライバ船(D883→882、W414→413)

  • トロデ(H1794→1793)

  • 魔戦士ホゲイラ(A900→899)

  • 魔戦士ヴェーラ(H1380→1379)

  • 怪蟲アラグネ(A899→900)

  • 天魔クァバルナ(A899→900、S999→1000)

  • 神殿レイダメテス(S431→430)

  • オリハルゴン(S541→540)

  • リバイアさま(W851→850)

  • わたぼう(M685→684、S825→824)

  • 大魔王デスタムーア(S850→849)

  • メタルスター(H881→880)

  • サージタウス(D999→1000)

  • 闇竜シャムダ(W599→600)

  • トーポ(D615→614)

  • 絶望と憎悪の魔宮(D1199→1200)

  • メタルゴッデス(M995→1000、A399→400、W1097→1100)

  • じげんりゅう(A800→799)

  • 真・魔王ザラーム(S896→895)

  • 暗黒神ラプソーン(W1199→1200)

  • 神鳥レティス(S1134→1133)

  • ギスヴァーグ(D728→727)

  • 竜神王(A891→890)

  • エルギオス(H1450→1449、D770→769)

  • 凶魔獣メイザー(W496→495)

  • ネオ・ドーク(S671→670)

  • サイコピサロ(D781→780)

  • 冥獣王ネルゲル(D881→880)

  • グランエスターク(A881→880、W591→590)

  • 魔界神マデュラーシャ(D701→700)

②③を含む修正

  • いたずらもぐら(S780→790)

  • デンデン竜(A840→940、D820→920)

  • スキッパー(A480→780)

  • とっしんこぞう(D845→945)

  • スラ忍イエロー(H1636→1432)

  • スラ忍レッド(H1892→1419)

  • スラ忍ブラウン(H1605→1405)

  • スラ忍グリーン(H1452→1089)

  • スラ忍パープル(H1222→1070)

  • スラ忍ブルー(H1909→1432)

  • スラ忍オレンジ(H1909→1432)

  • スラ忍ピンク(H1236→1082)

  • スラ忍ブラック(H1458→1094)

  • パペットこぞう(M605→604、W1068→1082)

  • こうてつまじん(W651→654)

  • ふくぶくろ(W764→761)

  • バトルレックス(D760→730)

  • ヘルコンドル(D845→745)

  • かりゅうそう(M774→776)

  • コトブキーノ(D552→551)

  • つららスライム(H1645→1440)

  • ウィングデビル(H1362→1621)

  • バズズ(D844→877)

  • ガルハート(S665→664、W662→650)

  • ゲルニック将軍(S833→922)

  • アクバー(A825→824、S352→327、W1045→1044)

  • スラ忍シルバー(H1771→1550)

  • シルバリヌス(H1942→1700)

  • スラ忍ゴールド(H1941→1456)

  • エビルプリースト(H1331→1165)

  • キラーマシン3(H1417→1240)

  • ゆうれい船(H2266→1700、D867→866、W1084→1083)

  • 大地の竜バウギア(H2080→1560)

  • スラ・ブラスター(H2697→1770、D1099→1100、W1199→1200)

  • デスピサロ(H1568→1372)

  • 大魔王ゾーマ(H1443→1263)

  • 邪獣ヒヒュルデ(H2213→1660、A631→630、W961→960)

  • JOKER(H1472→1104)

  • JESTER(H1548→1355)

  • オムド・ロレス(H1991→1742、D1184→1183)

  • ヒヒュドラード(H1542→1350、S861→860)

  • 魔王オムド・レクス(H1502→1315)

  • WORLD(H1302→1140)

完成したXMLファイル

このようにして、既存の全てのウェブサイトより正確なモンスターのデータがXMLファイルとして得られる。

更新履歴

データに更新ミスがあり、それを2023/01/08, 2:00頃に修正。能力値合計計算をしてモントナー、トマトマーレ、アラウネ、黄泉の花以外全員が8000なら修正済のデータ。XMLファイル内容の書き換えが一部正常に保存されていなかった。

2023/01/28、23:35にメタルスライム及びそれと類似した耐性のモンスター(メタルスライム、はぐれメタル、メタルカイザー、メタルキング、クリスタルスライム、スライムマデュラ、ゴールデンスライム、ダイヤモンドスライム、はぐれメタルキング、メタルスター、メタルゴッデス)のどく、呪い、こんらん、マヒ、ねむり耐性を無効から回復に修正。耐性の調査の様子は次のツイート参照。

ステータス計算において発見したこと

サイズ+行動回数のみの場合、整数化は1度だけ

特性によるステータス補正の掛け算を行った時、毎回小数点以下切り上げの処理を行うと、能力値合計一定則を満たさないモンスターが存在し、かつ、そのようなモンスターであっても、サイズ補正を掛けた後は整数化の処理を行わず、行動回数補正まで終わった後に整数化を行えば解が存在する。

その具体例としておおにわとり(メガボディ、AI1~2回行動)が存在する。単純にライブラリの数値$${(H,M,A,D,S,W)=(1568,478,688,755,1042,821)}$$から特性の影響を割り算して消しただけの数値は$${(H,M,A,D,S,W)=(1161,354,728,798,1102,868)}$$である。この時、乗算の度に小数点以下切り上げの処理を行うとすると、各ステータスの取り得る範囲は

HPは1161、MPは354、守備力は798、賢さは798のみ(HPに関して導出の式を提示)。

$$
1160\times1.5=1740\\
1740\times0.9=1566\\
1161\times1.5=1741.5→1742\\
1742\times0.9=1567.8→1568\\
1162\times1.5=1743\\
1743\times0.9=1568.7→1569
$$

攻撃力は727、素早さは1101のみ(攻撃力に関して導出の式を提示)。

$$
726\times1.05=762.3→763\\
763\times0.9=686.7→687\\
727\times1.05=763.35→764\\
764\times0.9=687.6→688\\
728\times1.05=764.4→765\\
765\times0.9→688.5→689
$$

すると、条件を満たす能力値の組は$${(H,M,A,D,S,W)=(1161,354,727,798,1101,868)}$$しか存在しないが、この時その合計値は$${2\times(1161+727+1101)+(354+798+868)=7998\neq8000}$$となり、能力値合計一定則を満たさない。

対して、小数点以下切り上げの処理を行うのは最後に一回だけだとすると、以下のように攻撃力の値として727だけでなく728も許容されるようになる。

$$
727\times1.05\times0.9=687.015→688\\
728\times1.05\times0.9=687.96→688
$$

そして攻撃力として728を選択した場合、$${2\times(1161+728+1101)+(354+798+868)=8000}$$となり、能力値合計がピッタリ8000となる。

この一例だけでは小数点以下切り上げを毎回行うという『究極対戦ガイドブック』の記述と能力値合計一定則、どちらが真実なのか判断に苦しむかもしれないが、小数点以下切り上げの処理はサイズ、行動回数の補正を両方行った後に限るとした場合、(モントナー以外の)全てのモンスターの能力値合計をピッタリ8000に出来ること、わざわざ数ポイントのズレを一部のモンスターにだけ施すゲームデザイン上の意味が分からないことから、私は後者が正しいと判断した。

マホカンタ前には整数化の処理が入る

小数点以下切り上げの処理を最後にのみ行うとした場合、デスピサロのステータスは$${(H,M,A,D,S,W)=(1373,372,896,896,796,604)}$$となるが、この時その合計値は$${2\times(1373+896+796)+(372+896+604)=8002\neq8000}$$となってしまい、能力値合計一定則を満たさない。また、能力値合計一定則という公式からは名言されていない性質を使わずとも、新生配合した際のHPの値が正しくないことが$${1373\times1.5\times0.9\times0.875\times1.2=1946.2275→1947\neq1946}$$という計算により示される。新生配合前、新生配合後、両方のステータスを正しく計算出来る値が存在しないのである。

しかし、サイズと行動回数の補正を行った後、マホカンタの補正前に小数点以下切り上げの処理を加えるとした場合、$${HP=1372}$$となるが、こうすると能力値合計一定則を満たし、新生配合前後、両方のステータスを正しく計算することが出来る。

$$
2\times(1372+896+796)+(372+896+604)=8000\\
1372\times1.5\times0.9=1852.5→1853\\
1853\times0.875=1621.375→1622\\
1853\times0.875\times1.2=1945.65→1946
$$

尚、マホカンタ後と新生配合前の間に小数点以下切り上げの処理を挟んでしまうと$${1622\times1.2=1946.4→1947\neq1946}$$となる為、そのタイミングには整数化処理を入れてはいけない。

デスピサロの他、オムド・ロレスでも整数化処理のタイミングの問題が重要となる。

3~4回行動(メタル)の補正は0.47ではなく0.468

0.47で計算した場合、メタルゴッデスのステータスは$${(H,M,A,D,S,W)=(800,995,398{\rm or}399,1500,1000,1096{\rm or}1097)}$$の4パターンが取り得る範囲であるが、能力値合計一定則を満たさない。0.468にした場合、$${(H,M,A,D,S,W)=(800,1000,400,1500,1000,1100)}$$という、合計が8000となる数値が見つかる。これ以外にも正確な計算の為修正されるべき値はあるかもしれないが、現在のところは発見していない。

『究極対戦ガイドブック』p17

前回の記事と次回の記事へのリンク

著作権者表記

このページで利用している株式会社スクウェア・エニックスを代表とする共同著作者が権利を所有する画像の転載・配布は禁止いたします。
(C) ARMOR PROJECT/BIRD STUDIO/SQUARE ENIX All Rights Reserved.

この記事が気に入ったらサポートをしてみませんか?