Seaside Laboratory

Posts

KFX の enemy.lst に潜む閾値バグ

KFM で乱数を使用している箇所を調べていたら enemy.lst の処理だけ乱数の使い方が他と異なっていたので、正しく実装できているのか確かめるため、KFX の逆アセソースを見たところ以下のような処理になっていた。

// 乱数を生成
int val = abs( rand() ) / 328;

// 閾値チェック
if ( threshold >= val )
{
    // 行確定
}

まず、乱数を生成する rand 関数は、

rand 関数は、0 ~ RAND_MAX (32767) の範囲で、擬似乱数の整数を返します。

という仕様なので呼び出すと 0 ~ 32767 の値が返ってくる。abs は絶対値を返す関数だが rand は負数を返さないので無視して問題ない。最後に 328 での除算が行われ、RAND_MAX だった場合は約 99.89 になり、整数キャストで小数部が切り捨てられ、最終的に val の値は 0 ~ 99 の範囲に収まることになる。

次の条件式では enemy.lst に書かれた閾値と生成した乱数を比較しているので、閾値によって確率は変化し、成立すれば行が選択されるという仕組み。

特に変わったことをしていない単純なコードではあるが、処理にあやしげな部分があることに気づいてしまった…。

あやしい処理

より正確な表現をするために行の選択確率を表すパラメーターのことを「閾値」と表記しているが、キャラ制作者の立場からすれば「確率」と言われた方がしっくりくるだろうし、CARROT 公式ページにあった enemy.lst の説明でも「確率」という言葉が使われている。

1 番目(距離) 相手との距離で、軸の差で計算します。最大 320 ドットなので 320 以内です。
2 番目(確率) これは 3 項目のコマンドを実行する確率です。1 行前との差が値になります。
3 番目(コマンド) コマンドです。1,2,3,4,6,7,8,9,p,P,k,K,n,N が使えます。

確率と考えた場合、0 と書いたら 0% を意味するので、わざわざそんな値を設定しようという発想自体生まれないが、もし 0 を設定したらどうなるのだろうか。閾値チェック式は以下のように展開されることになる。

// 閾値チェック
if ( 0 >= val )

val は 0 ~ 99 の範囲をとる乱数なので 0 が生成された場合、

// 閾値チェック
if ( 0 >= 0 )

という式になる。使われている比較演算子が「以上」なので、含まれるべきでない値が含まれ、条件が成立してしまうことに問題がある。

ここまではコードから読み取った仮説なので実際にどうなるのか検証を行う。

検証 (1)

まず、以下の内容で enemy.lst を作成し、全エンジンで動作テストを行う。

Distance--100--Command--COMMENT
    100   100  nnn7p    ;近距離
    320   100  2p       ;遠距離
遠距離は屈み弱パンチ。
近距離になると後方ジャンプ弱パンチ。

キャラが動作中でも安定してコマンドが実行されるよう「近距離」行には n が入れてある。

何故か XPlus は立ち弱パンチになったがテストに支障はないのでスルー。

問題なく動作したので「近距離」行の閾値を 0 に書き換え再度テストを行う。

Distance--100--Command--COMMENT
    100     0  nnn7p    ;近距離(出ないはず)
    320   100  2p       ;遠距離

「近距離」行を実行するには乱数が 0 になるまでひたすら待つしかないが、「遠距離」行の処理に 2 フレームかかったとしても毎秒 12.5 回程は行の更新が行われるので、タイムオーバーまで待てば十分な試行回数を確保できると踏んだ。

で、結果はどうなったかというと KFX だけコマンドが実行された。

ラウンド開始早々に実行される。

拡張エンジンの中に KFX との互換性を重視しているものがあってもおかしくはないので、正直どうなるのか予想が付かなかったが、再実装する際に適切なコードに置き換わった、ということなのだろう。

検証 (2)

以前書いた「KFX の enemy.lst 図解」でも触れているが、どんな状況であっても何れかの行が選択されるよう、閾値が 100 の行を必ず定義することが推奨されている。この 100 は乱数の上限より大きく、必ず条件を成立させる特別な値として使われているが、KFX の条件式はあやしいので上限についても検証を行う必要がある。

まず、乱数が最大値を生成した場合、

// 閾値チェック
if ( threshold >= 99 )

という式になる。閾値に 100 を入れると、

// 閾値チェック
if ( 100 >= 99 )

必ず成立する条件式にはなるが、比較演算子が「以上」なので、

// 閾値チェック
if ( 99 >= 99 )

99 でも成立してしまう。

この仮説を検証するため、以下の内容で enemy.lst を作成し、全エンジンで動作テストを行う。

Distance--100--Command--COMMENT
    320    99  2p       ;
    320   100  nnn7p    ;

99 を最大値として扱うエンジンであれば 99 の行が見つかった時点で行選択は終了するので、100 の行は決して実行されることはない。

結果は、仮説通り KFX だけ 100 の行が実行されなかった。今までの検証結果を踏まえると乱数が 100 通りあるという点は全エンジンで共通しているが、比較式の違いによって KFX だけ 1 のズレが生じている。

以前作成した図は拡張エンジンのものを指していることになってしまったので、新たにズレがある KFX バージョンを加え、アニメーションにしてみた。

閾値 0 による底上げと 100 の先端が範囲外に出ていることが分かる。

KFX で正確な確率になるよう閾値を調整するという考え方もあるが、そもそも KFX は enemy.lst の処理が不正確であるのと、正確に処理を行うエンジンの方が多いのでお勧めできない。