いくつか改善を盛り込んで19m30sになり、さらにもう一工夫で1分半以上早くなって18m43sにできました。YouTubeにも載せておきました([TAS] NES Might and Magic Book One (J) 18:43 - YouTube)。
基本的なルートは当初から変えていません。最初のバージョンからの変遷23m30s → 20m28s → 19m30s → 18m43sを見ると、5分くらい無駄を削ったことになりますね。
最初のバージョンを作ったときは、3〜4分は削れそうだけど19分切るのは無理かな〜?と思ってましたが、意外と早くなってびっくりです。でも、そろそろ限界を感じるのでここらで打ち止めにします。
前のバージョン(19m30s版)では、持ち物が溢れるので2人パーティーでクリアしましたが、実は1人旅でも問題ありません。Might and Magicは必須アイテムでも容赦なく捨てられるからです。
例えば、ゴロスの目玉を手に入れるとき、フック付きロープはもちろん、その時点で用済みとなった必須アイテム(金のカギ、ルビーの笛)も捨てて構いません。
改善点の一覧はYouTubeの説明と同じになりますが19m30s版では、
次の18m43s版では、
これ以上を目指すなら、劇的にルート改善するか、メインクエストを飛ばすようなバグがないと厳しそうです。
Might and MagicのTASに挑みたいという奇特な方が居るとはあんまり思えないんですけど、将来の参考のために「やったけどダメだったこと」も残しておきます。
失敗その1、コリンブルッフ洞穴ではなく、エルキューンの町でGEMを取るルート。
エルキューンは宝物を「ぬすむ」とGEMが取れます。GEMを得るまでは早いんですが、エルキューンからソーピガルに帰る時間が掛かるので、総合では遅いです。Might and Magicは町同士が絶妙に離れた嫌らしい場所にありますね。良くできてるじゃん……。
失敗その2、コリンブルッフ洞穴 → デューム城のポータルでエリアA1を無視するルート。
エリアA1はテレポート使用不可のMAPで、迷路が非常に長くて時間が掛かります。コリンブルッフ洞穴からデューム城に行けば、大きく時間削減できるのでは?と期待しましたが、エリアA1の巧妙な仕掛けに阻まれて実現不可能でした。
デューム城はざっくりいうと外部と内部に分かれていて、クエストで用事があるのは内部です。コリンブルッフ洞穴のポータルから行くと外部に着きます。外部から内部には「つうか」の魔法があれば行けます。しかしながらLv.1のキャラクターで「つうか」の魔法を使う容易な方法はないです(少なくとも私は知らない)。
エリアA1に一度出て、デューム城内部に入ろうとしてもダメで、エリアA1の迷路開始点 [15, 7] を踏まないと、デューム城内部へ行く入り口(石垣の隙間 [7, 14])が出現しない仕掛けになっていました。コリンブルッフから来ても、エリアA1の迷路を歩かないと、デューム城内部には入れません。そうか、ズルは許さんってことね。良くできてるじゃん……。
引き続きMight and Magic TAS動画に挑んでいます。前回書いた通り、町以外のMAPではエンカウント判定用のカウンタ変化量が予測できません。しかしコントローラ入力後だけは、カウンタの変化量が必ず0もしくは1になることに気づきました。これを利用し、町以外のMAPのエンカウントが50%の確率で予想できるようになりました。
残念ながら今の知識ではエンカウント予想を100%にはできなさそうです。Might and Magicはキー入力チェックと画面更新が全く同期しないので、フレーム単位(=画面更新)しか動くタイミングがないTASのツールだと、うまく挙動が推定できません。技術的にはモヤっとしますが、動画を作る分にはこれでも十分です。
地上エリアA1やA2のように、歩きだと脱出に時間が掛かるMAPで「わざとエンカウント+逃げる」手段を使えるようになったので、格段に早く脱出できるようになりました。
他にも細かい点を更新して3分くらいタイムを更新し、20分28秒になりました。
メッセージスキップも、未だに仕組みは良くわからないものの、
とすると、比較的メッセージスキップが実現しやすいことが判明しました。
もはや興味のある人はいないと思うので、自分向けのメモです。改善点は下記のとおりです。
TASなら戦闘結果も制御してなんぼだろ!?というご指摘はごもっともですけど、どうも良い乱数が引けず、極端に時間が掛かったり、ドロップアイテムのGEMが足りなくて詰んでしまったり、うまく行かないことが多いです。TASですら戦闘が鬼門なの?このゲーム……??
再現性が不安定すぎるので戦闘をルートから外し、GEMはコリンブルッフ洞穴で拾うことにしました。これでだいぶ安定します。動画をアップロードしてから気づきましたが、5人パーティになっていますが、戦闘しないならメンバーは2人でも良かったかな。うーん、まあいいか。
引き続きMight and Magic TAS動画に挑んでいます。クリアタイムを縮めるには「わざとエンカウントして逃げる」「メッセージスキップ」の2点がほしいですが、メモリダンプを見ていても全く発生条件がわかりません。仕方ないのでファミコンのCPU 6502のアセンブラを読んでいます。もはやリバースエンジニアリングです。
命令が非常にシンプルで、レジスタ幅は8bit、レジスタ数はA, X, Yの3つしかありません。読んでいる分には面白いですが、これでソフト書くことを想像すると辛いです。昔の人はこんなん書いてたのかあ……。ま、それはさておいて、プログラムの解析を行った結果、エンカウントのルールが「半分だけ」分かりました。
カウントの条件判定関数(アドレス0xE85Dとそのサブ関数)をLuaで書き直しました。BizHawkのLuaも初めて使ったので書き方がわからず、1回Pythonで書いてから移植するという無駄な行為をしました……。中心となる条件判定関数はこんな感じです。
function rol(v)
local ncarry = (bit.band(v, 0x80) ~= 0)
v = bit.lshift(v, 1)
v = bit.band(v, 0xff)
if (carry) then
v = bit.bor(v, 0x1)
end
carry = ncarry
return v
end
function ror(v)
local ncarry = (bit.band(v, 1) ~= 0)
v = bit.rshift(v, 1)
v = bit.band(v, 0xff)
if (carry) then
v = bit.bor(v, 0x80)
end
carry = ncarry
return v
end
function adc(v1, v2)
v1 = v1 + v2
if (carry) then
v1 = v1 + 1
end
carry = (v1 > 0xff)
return bit.band(v1, 0xff)
end
function sbc(v1, v2)
v1 = v1 - v2
if (not carry) then
v1 = v1 - 1
end
carry = not (v1 < 0)
return bit.band(v1, 0xff)
end
function judge(v0, v1, v2)
local v24e = v0
v523 = v1
v524 = v2
-- Addr: E8A1
local v248 = 0x73
local v249 = 0xc
-- Addr: E8AB
local v24a = v523
local v24b = v524
local v24c = v523
local v24d = v524
-- Addr: E8BF...E8D2
for x = 7, 1, -1 do
v24c = rol(v24c)
v248 = ror(v248)
if (carry) then
carry = false
ra = adc(v24a, v24c)
v24a = ra
end
end
-- Addr: E8D4
carry = true
v24a = sbc(ra, v24b)
-- Addr: E8DD...E8F0
for x = 4, 1, -1 do
v24d = rol(v24d)
v249 = ror(v249)
if (carry) then
carry = false
ra = adc(v24b, v24d)
v24b = ra
end
end
-- Addr: E8F2
carry = true
v24b = sbc(ra, v24a)
v524 = v24b
-- Addr: E8FC
carry = true
v24a = sbc(v24a, v24b)
v523 = v24a
----------------------------------------------
-- Addr: E871
v24f = v24a
ra = 0
-- Addr: E87B ... E88F
for x = 8, 1, -1 do
v24f = rol(v24f)
ra = rol(ra)
carry = true
ra = sbc(ra, v24e)
if (not carry) then
ra = adc(ra, v24e)
end
end
-- Addr: E891
v24e = ra
v24e = v24e + 1
return v24e
end
引数のv0には現在いるマップから決まる一定の値(アドレス0x6191の値)を渡し、引数v1, v2にはカウンタ0x523, 0x524の値を渡します。エンカウント判定の結果が1であれば敵とのエンカウントという意味になります。あとjudge() を呼ぶ前に、グローバル変数のcarry = falseにしておかないと結果が狂います。
内部でキャリークリアしとけ、って思われるかもしれませんがcarry = trueで呼ぶケース(今回は追っかけていませんが)もあるので、勝手にキャリークリアしてはいかんのです……。
話を元に戻すと、カウンタを255フレーム分、変化させながらこの判定関数を呼ぶと、今から何フレーム後にエンカウントするか予測できるわけです。
例えば、上記の画像だと(43, 157, 179)と出ています。これは43フレーム目にエンカウント処理が真と判定されるという意味です。ややこしいことに、Might and Magicのエンカウント判定は移動するボタン(前後左右、Bボタンのどれか)を押した「次のフレーム」に行われるため、42フレーム後に移動すると必ずエンカウントします。同様の理屈で156フレーム後、178フレーム後も移動すると必ずエンカウントします。
エンカウントを理解できたぞ、これならエンカウント楽勝だろ!と意気込んで地上MAPに行ってみたら、町以外(地上、ダンジョンなど)はカウンタの増減ルールが全く違っていて予測は微塵も機能しませんでした。ええ……そんな……。
町だと1フレームに1回しかコントローラの状態を見ないため、カウンタも1しか変化しません。そのため予測が楽でした。ところが町以外のMAPはアイドル時間に全力でコントローラの状態を見るため、70〜80くらいカウントが一気に変わり、さらに嫌なことに毎フレーム変化量が違うので予測ができません。こりゃ無理ですね。
エンカウントと並ぶもう一方の難問「メッセージスキップ」のルールは全く分かりません。基本的にはボタンを押すとメッセージがスキップされるはずなのに、そうじゃない時間がかなりあります。プログラムを解析していると8フレームほど入力を全く見ていない瞬間が何カ所かあって、スキップと関連していそうでしたが、仕組みの解明には至りませんでした。
メッセージスキップを諦めるとかなり時間を食ってしまう(最速スキップで11〜16フレーム、スキップしないと160〜170フレーム)ので、困りましたね。
ファミコン版Might and Magic Book Oneにはたくさんのクエストがありますが、クリアに必要なクエストはさほど多くありません。図示するとこんな感じです。
なお、ファミコン版Might and Magic Book Oneの情報は、マイトアンドマジック1攻略情報 さんが非常に参考になります。地図がめっちゃ見やすいです。
クエストの位置など詳しい情報は攻略情報さんを見ていただくとして、下記ではTASで使った攻略ルートを簡単に説明します。
以上がMight and Magicの世界に起こった事件と、その謎に迫るメインクエストを超スピード解決する手順となります。初めてプレイする方がこの手順でクリアすると、たぶん訳が分からないと思います。
お時間がある方は、攻略サイトを見ながらでも構わないので、金の板や町のヒント、囚人たちなどに出会い話すと、しっかり作られた世界感が味わえると思います。
もう一つのメインクエストの流れです。オーラのクエストは、ストーリーにはあまり関係ないです。ラストダンジョンの歯ごたえを増すためのクエストという感じです。アストラル世界は全てのストーリーの終着点で、ああ、そういうことだったんだ!と思うはずです。
この他にも諸侯のクエスト、全く関係ないクエストなど、たくさんありますので、ぜひ遊んでみてください。全体的に良くできたゲームシステムだと思うんですけど、難易度の高さ(敵が強すぎる……)に、挫けそうになるのがイケてないポイントではあります。
子ども時代のトラウマ難易度ゲームMight and Magic Book Oneに、大人になってから挑みましたが、変わらず撃沈しました。やっぱり難しいです。でもエミュレータなら、Lv.1で最速クリアする動画を作れるんです。いわゆるTAS(Tool Assisted Speed-run)動画です。
TASするにあたっては、定番のBizHawkというエミュレータを使います。ROMをロードしてTool - TAStudioを選ぶと、フレーム単位でキー入力を選べる画面が出現します。
操作に自信があるならRecording modeを使って自分の操作を記録し、あとでファインチューンすると良いでしょう。私は操作にあまり自信がなかったので、1フレームずつチマチマとマウスで入力しました。
とりあえずエンディングまで辿り着いたので、YouTubeにもアップロード([TAS] NES Might and Magic Book One (J) 23:30 - YouTube)しました。YouTubeのアップロード側機能は初めて使いましたが、とても使いやすいですね。
TASとしてのクオリティはあまり良くないです。攻略ルートがかなり適当で、これより早くクリアできる動画は簡単に作れると思います。容易に改善できそうなところは、
5人パーティーだとオーラのクエストが長いし、戦闘で行動できない人が出やすく無駄が多いので避けたいですが、4人パーティーだと「ようせいx 3」が出ないので、妥協しました。
あとMight and Magicの大きな特徴として、敵から逃げるとMAPの特定の位置に必ず戻される仕様があります。わざとエンカウントし逃げれば、歩くよりも早く移動ができる可能性があります。TASではぜひとも活用すべき事項ですが、エンカウントのルールがわからず、エンカウント確率もかなり低いため、総当たりだと全然エンカウントしません。これは厳しい。ギブアップ。
とまあ、色々諦めたしょぼいTAS動画を作っただけで非常に疲れました……。世の中のハイレベルTAS動画の作者さんは凄いですわ〜。
Might and Magicファミコン版は日本語版と英語版があります。このうち英語版のTASは既に存在していて、クリアタイムがたったの8分([TAS] NES Might and Magic by Dammit in 08:07.72 - YouTube)です!超早ですね。
英語版はアイテムを別のアイテムに化けさせるバグがあって、そのバグを突くとゴミから終盤のクエストでもらえるはずのアイテムを錬成でき、ほぼ全てのクエストを無視してラストダンジョンに挑めます。
日本語版ではバグが修正されているほか、クエストの仕様も違うため、少なくとも同じ攻略法ではクリアできません。
Nintendo Switch Onlineに加入すると、ファミコンやスーパーファミコンのソフトでも遊べるので、子供の時に挫折したゲーム(最近だとファミコンウォーズ、スーパーピクロス)をやっています。グラフィクスは近年のゲームと比べるまでもなくショボいですけど、今も名作は名作です。面白いですね。
子どもの時に挫折したゲームはいくつもありますが、ナンバーワンがMight and Magic Book One : the Secret of the Inner Sanctumです。この時代に流行ってたWizardryみたいな3D風の迷路を歩いていく海外製RPGです。難易度が異常に高くて有名で、小学生の私はLv.2すら拝むことなく諦めました。
この手のSwitchには収録されていないソフトも遊びたいなあ?と思って、ニューファミコンを物色していたのですが、結構でかくて邪魔そうだし、中古の割に高いです。今でも人気なのか……侮ってましたね。
ですが、我々にはPCとエミュレータがあるじゃないですか。幸いなことに、ファミコンソフトそのものはそんなに高くないので、ROMダンパーでROMを吸い出して、PCで遊ぶことにしました。
私はレトロダンパー(メーカーのサイト)を使っています。クライアントを起動して、認識ボタンを押し、吸い出すだけでOKなので便利です。
早速、中古のカセットを購入しました。外観は割とズタボロというか、年季入った姿です。ま、動けば良いのさ。
ファミコン版Might and Magic Book Oneのカセット
GAKKENのロゴの通り、なぜか日本語ローカライズは学習研究社(学研)が行っています。今見ると不思議です。教科書作ってる学研が、なぜゲームの移植を……。
レトロダンパーさんで吸い出すときはこんな感じになります。吸い出したROMをエミュレータに放り込んでみたところ、正常に動作しているようです。良きかな良きかな。
まずは普通に遊んでみました。今なら意外とクリアできるのでは?と思ったのも束の間、あっ、無理でした。調子乗ってすみませんでした。在りし日の絶望が蘇りました。
最初から難しすぎます。基本的には1バトルごとに休憩+セーブって感じです。攻撃が当たらないのも辛く、一方的にボコられて死んでしまいます。死んだら復活させるお金がないのでリセットです。
さらにWikipediaを見てびっくりしたのは、次の一文です。
「ファミコン版は(中略)やや簡単に調整された部分が多い」
えっ?これで?嘘だろ……??オリジナル版はどれだけ鬼畜難易度なの?
息抜きにYouTubeで攻略動画を見ていると、ラストダンジョン(イドの迷宮、アストラル世界)の音楽がとてもカッコ良いですね。何とか辿り着きたいですが、最初の町(ソーピガルの町)から脱出できていない身からすると、果てしなく遠いです。
TASに挑んだ記録。
解析したときの情報。その他。
Might and Magicの攻略、解析の参考になるサイトです。
日記を漁って携帯の遍歴を書き出してみました。日記を書く習慣がなかった頃の機種や時期は不明です。
(基本的には)長く使っていた機種は気に入っていた機種です。ガラケー時代はいずれも良い機種で、バッテリーが死ぬまで使ってました。最後のP-03Bだけ1年しか使っていませんが、不満があったわけではなく、知人に携帯を譲るため手放しました。たしか。
スマホ時代は国内メーカーの質は明らかに落ちました。SO-02Cはソツなく良かったんですけど、ストレージが少なすぎで買い替え直前は容量不足で挙動不審でした。SH-01Fは性能良いものの、電池がなくなるのが早く、本体が熱すぎでした。この機種で懲りてAndroidハイエンド機を買わなくなりました。
今になって調べてみたところ、この2機種はマシな部類だったようで、富士通ARROWSのように「カイロ機能搭載」「電話ができない」「メールがこない」など、怨嗟にまみれたレビューが未だに残っている機種もあります。悲惨です。
日本だけ異常にiPhone普及率が高い理由って、国内メーカーが2010年代初頭にやらかしたから……!?と思ってしまいました……。
メモ: 技術系の話はFacebookから転記しておくことにした。
目次: RISC-V
メインCPUからサブCPUを起こすとき基本的には、
RAMの初期値が不定であると仮定すると、サブCPUが下手にポーリングすると、不定値によって条件が成立してしまい、メインCPUからの起動司令がないのに勝手に起動してしまう事態に陥ります。
先程書いた基本的な構造を素直に書くとこんなコードになるでしょう。
/* メインCPUはHARTID=8, サブCPUはHARTID=0...3とする */
#define HARTID_MAIN 8
#define HARTID_SUB_START 0
#define HARTID_SUB_END 3
#define HARTID_MAX 9
struct {
int boot_wait;
int boot_done;
} init_core[HARTID_MAX] = {};
int get_hartid(void)
{
int i;
__asm__ volatile("csrr %0, mhartid" : "=r"(i));
return i;
}
/* メインCPUが実行する */
void boot_main(void)
{
for (int i = HARTID_SUB_START; i < HARTID_SUB_END; i++) {
init_core[i].boot_wait = 1;
}
}
/* サブCPUが実行する */
void boot_sub(void)
{
int hartid = get_hartid();
while (!init_core[hartid].boot_wait) {
/* busy loop */
}
}
残念ながらこのコードは正常に動作しません。共有RAMつまりinit_core[hartid].boot_waitの値が起動直後から != 0だったとき、boot_sub() はboot_main() からの起動司令を待つことなく起動してしまうからです。
共有RAMの不定値に対処する方法を考えます。基本的にはサブCPUが変数を初期化(boot_wait = 0)してから待ちに入れば良いのですが、新たな問題が生じます。メインCPUとサブCPUの実行順序はどちらが先という保証はないため、
以上の順で実行されるとメインCPU側の起動司令が消されてしまい、ハングアップする可能性があります。この問題の回避のため、変数を1つ追加し、サブCPUのブートが終わるまで、メインCPUは繰り返し起動司令を送るように変更します。
先程書いた基本的な構造を素直に書くとこんなコードになるでしょう。
/* メインCPUはHARTID=8, サブCPUはHARTID=0...3とする */
#define HARTID_MAIN 8
#define HARTID_SUB_START 0
#define HARTID_SUB_END 3
#define HARTID_MAX 9
struct {
int boot_wait;
int boot_done;
} init_core[HARTID_MAX] = {};
int get_hartid(void)
{
int i;
__asm__ volatile("csrr %0, mhartid" : "=r"(i));
return i;
}
/* メインCPUが実行する */
void boot_main(void)
{
for (int i = HARTID_SUB_START; i < HARTID_SUB_END; i++) {
init_core[i].boot_done = 0;
while (!init_core[i].boot_done) {
init_core[i].boot_wait = 1;
}
}
}
/* サブCPUが実行する */
void boot_sub(void)
{
int hartid = get_hartid();
init_core[hartid].boot_wait = 0;
init_core[hartid].boot_done = 0;
while (!init_core[hartid].boot_wait) {
/* busy loop */
}
init_core[hartid].boot_done = 1;
}
残念ながらこのコードも正常に動作しません。共有RAMへの値の反映が他のCPUに即座に見えること(アトミック性)を暗に期待しているからです。
今日のマルチコアシステムでは、boot_wait = 0としたときに、他のCPUにも即座に同じ値が見えているとは限りません。主な要因としては、
などがあります。通常の変数への代入、参照が他のCPUに即座に値が見えないことにより、おかしくなるパターンはいくつか考えられそうですが、ありがちなパターンとして、
以上の順で実行されるとメインCPU側が起動司令を送らないまま、サブCPU側も何もできずハングアップする可能性があります。この問題の回避のため、通常の変数への代入、参照ではなく他のCPUにも値が見えるように初期化、代入(アトミックアクセスする)必要があります。
従来C言語でアトミックアクセスを行うためには、実装対象アーキテクチャの知識やアセンブラの記述を必要とするなど、やや困難が伴いました。ですがC11でアトミックアクセス用の定義stdatomic.hが追加されたことで、アトミックアクセスはかなり楽になりました。素敵ですね。
ひとまず速度を全く気にせず、全てのアクセスをアトミックアクセスに入れ替えると、こんなコードになるでしょう。
/* メインCPUはHARTID=8, サブCPUはHARTID=0...3とする */
#define HARTID_MAIN 8
#define HARTID_SUB_START 0
#define HARTID_SUB_END 3
#define HARTID_MAX 9
struct {
atomic_int boot_wait;
atomic_int boot_done;
} init_core[HARTID_MAX] = {};
int get_hartid(void)
{
int i;
__asm__ volatile("csrr %0, mhartid" : "=r"(i));
return i;
}
/* メインCPUが実行する */
void boot_main(void)
{
for (int i = HARTID_SUB_START; i < HARTID_SUB_END; i++) {
atomic_store(&init_core[i].boot_done, 0);
while (!atomic_load(&init_core[i].boot_done)) {
atomic_store(&init_core[i].boot_wait, 1);
}
}
}
/* サブCPUが実行する */
void boot_sub(void)
{
int hartid = get_hartid();
atomic_store(&init_core[hartid].boot_wait, 0);
atomic_store(&init_core[hartid].boot_done, 0);
while (!atomic_load(&init_core[hartid].boot_wait)) {
/* busy loop */
}
atomic_store(&init_core[hartid].boot_done, 1);
}
C11のアトミックアクセスは何も指定しない場合、一番制限の強い(= 確実に他のCPUに見えるものの、アクセス速度は遅い)memory_order_seq_cstアクセスになります。マルチコアのブートを行うにあたって、常に制限が強いアクセスは必要ありませんが、とりあえずこれで動くはず。
< | 2021 | > | ||||
<< | < | 10 | > | >> | ||
日 | 月 | 火 | 水 | 木 | 金 | 土 |
- | - | - | - | - | 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | - | - | - | - | - | - |
合計:
本日: