引き続き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アクセスになります。マルチコアのブートを行うにあたって、常に制限が強いアクセスは必要ありませんが、とりあえずこれで動くはず。
この記事にコメントする
まれにxtermの256色指定エスケープシーケンスに対応していない端末があってvimの表示が変な色になってしまいます。チェック用のスクリプトを作っておきました。単純に背景色を変更するエスケープシーケンスと、空白文字、色を元に戻すエスケープシーケンスを連打するだけです。
#!/bin/sh
ESC_ORG="\e[0m"
print_colors()
{
for i in ${*};
do
printf " %3d\e[%dm " ${i} ${i};
echo -n ${ESC_ORG}
done
echo
}
print_xterm_colors()
{
for i in ${*};
do
printf " %3d\e[48;5;%dm " ${i} ${i};
echo -n ${ESC_ORG}
done
echo
}
echo "System colors (ESC[Nm):"
print_colors `seq 40 47`
echo
echo "xterm 256 colors (ESC[48;5;Nm):"
for i in `seq 0 8 248`;
do
j=`expr ${i} + 7`
print_xterm_colors `seq ${i} ${j}`
done
実行するとこんな感じになります。
対応していない端末だとこうなりますと言いたいところでしたが、対応していない端末が見当たりませんでした。前はあった気がするんだけどなあ……?
この記事にコメントする
| < | 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 | - | - | - | - | - | - |
wiki
Linux JM
Java API
2002年
2003年
2004年
2005年
2006年
2007年
2008年
2009年
2010年
2011年
2012年
2013年
2014年
2015年
2016年
2017年
2018年
2019年
2020年
2021年
2022年
2023年
2024年
2025年
過去日記について
アクセス統計
サーバ一覧
サイトの情報合計:
本日: