いつの間にかバグるんです
正直、PC初心者みたいなことを言いたくはないのですが、データパックって
「何もしていないはずなのにバグる…」
と思えるくらい脈絡なく、唐突に、意味不明にバグりますね…。いや、分かってますよ。原因は開発者自身にあるって。でも、そう思いたくなるくらい、理不尽に感じるくらい、バグというのは奇襲をかけてきます。
そしていくら調べても…
「おかしい、データパックが動かないぞ…」
「おかしい、ソースコードは間違ってないぞ…」
と無限ループになるまでが一連の流れです。
ああ…プログラム全般に蔓延る負のテンプレートかもしれません。
書いた通りに動く
データパックは「開発者が書いた通りに動く」のであり、「開発者が思った通りに動く」わけではないです。
つまり、「開発者が思った通りに動く」ようにしたければ、
- 開発者が思った通りに「正しく」書く
- システムに書いた通り動いてもらう
の2ステップが必要です。
要は一度ソースコードという媒体に書き出す必要があると…開発者が「正しく」書けなければ思った通りには動かないというわけです。
さらに、書いた本人は思った通り「正しく」書いているつもりなので、バグに直面して間違い探しを始めても、脳が間違いを見抜けないという事態に…そう、思い込みや勘違いが強烈に邪魔をしてくるのです。
データパックがバグったら人の脳もバグってしまうのですね…。
そう考えると、開発者が一番幸せになる究極の開発環境は
開発者が思った通りに動く
を1ステップで実現できる環境かもしれません。
見てもだめなら診てもらう
とはいえ、そんな究極の開発環境は現状存在しないと思われますので、別のアプローチでより良い環境にするしかないでしょう。
基本的にバグに直面したら開発者は見ます。
- ソースコードを見る
- ログを見る
- 走行時に変数を出力して見る
- 関数の呼び出し状況を見る
この見るレパートリーが多いと、バグの発見はより楽になると思います。
でも、これでもバグを発見できない時は多く、しばしば時間だけを消費して精神をすり減らすことにつながります。
それは延々続く同じ確認作業の繰り返し…
ここでふと開発者は思います…
「開発者が何度も動かして、見て、判断するのは面倒だ!」
そしてさらにこう思うのです…
「こんな機械的な作業は機械でもできるのでは?」
こういう発想は「テスト駆動型開発」辺りで既に育まれています。
厳密にはその中の「単体テスト」というやり方と言うべきでしょうか。部品を構築する要所要所で単体テストをサッと走らせ、それが正常に動作しているかどうか機械に診てもらうという発想です。
ソースコードに手を加える度にチェックが入り、正常に動作するかチェックしてくれるようになれば、最終的な手間は減りますし、バグった直後にそれを感知できる可能性が高いので、バグ要因の箇所を絞り込みやすくなるでしょう。
正直、完全なテスト駆動型開発や単体テストをデータパック開発で行うのは難しいと思います。
ただ、その中でも使えそうな要素を開発環境に組み込むことくらいはできるのではないでしょうか?
今回はそんな備忘録です。
テストケース
機械的に診てもらうなら、開発者が手動でチェックするみたいに融通が効いたり、操作内の直観的な動きをさせたりは難しいです。
「これをこうして、これはどうだろう?うん、動いてるね!」
というわけにはいかないということです。
機械にお願いするなら
「この場合はこうなるはず」
「この場合はああなるはず」
「この場合はこうならないはず」
という風なシンプルな因果関係のパターンをいっぱい用意して、
「じゃ、想定通りじゃない動きが見つかったら通知してね」
と丸投げするのが一番です。
これがテストケースです。「単体テスト」は、このテストケースの集合体を一括でチェックする仕組みといっても過言ではありません。一番重要なピースです。
今回はこのテストケースをどのように作って、どのように自動実行させるか…ちょっとした単体テストもどきを考えていきます。
テスト対象の実装
では、スケルトンソルジャーというカスタムモブを考えてみます。
距離に応じて武器を切り替える戦闘特化のスケルトンです。
summon minecraft:skeleton ~ ~ ~ { \
Tags:["xxx.skeleton_soldier"], \
equipment:{head:{id:"iron_helmet",count:1}} \
}
# 接近判定
scoreboard players set #at_close_quarters -- 0
execute at @s if entity @p[distance=..8] run scoreboard players set #at_close_quarters -- 1
# 接近戦に切り替え
execute unless score #at_close_quarters -- matches 0 run item replace entity @s weapon.mainhand with minecraft:wooden_sword
# 遠距離戦に切り替え
execute if score #at_close_quarters -- matches 0 run item replace entity @s weapon.mainhand with minecraft:bow
# 接近状態を返す
return run scoreboard players get #at_close_quarters --
execute as @e[type=skeleton,tag=xxx.skeleton_soldier] \
run function xxx:skeleton_soldier/update
{
"replace": false,
"values": [
"xxx:tick"
]
}
これで /function xxx:skeleton_soldier/summon を実行すればスケルトンソルジャーが召喚され、一番近いプレイヤーとの距離で武器を切り替えるようになります。
接近判定は一番近いプレイヤーとの距離を測っていますが、実際はスケルトンが攻撃対象にしているエンティティとの距離を測るべきです。
execute at @s on target if entity @s[distance=..8] run...
が妥当だと思います。
ただ、テスト内容が複雑になるので今回は簡単にしました。
テストケースの実装
それでは、このスケルトンソルジャーがどのように動いてほしいか考えてみます。
今回の場合はとてもシンプルです。
-
/function xxx:skeleton_soldier/summonでスケルトンソルジャーがちゃんと召喚される - 8m以内で
/function xxx:skeleton_soldier/updateが実行されると近距離武器(剣)に切り替わる - 8mより遠くで
/function xxx:skeleton_soldier/updateが実行されると遠距離武器(弓)に切り替わる
この3つが正しく成立すれば、スケルトンソルジャーは問題なく動作すると言っていいでしょう。
テストケースを書くとしたらこんな感じです。
execute at @s positioned ~9 ~ ~ run function xxx:skeleton_soldier/summon
execute unless entity @e[tag=xxx.skeleton_soldier] run say スケルトンソルジャーが召喚されませんでした
execute as @e[tag=xxx.skeleton_soldier] store result score #result -- run function xxx:skeleton_soldier/update
execute unless score #result -- matches 0 run say 遠距離で近距離戦の構えをしています
execute at @s run teleport @e[tag=xxx.skeleton_soldier] ~8 ~ ~
execute as @e[tag=xxx.skeleton_soldier] store result score #result -- run function xxx:skeleton_soldier/update
execute if score #result -- matches 0 run say 近距離で遠距離戦の構えをしています
kill @e[tag=xxx.skeleton_soldier]
これをスーパーフラットのような余計な要素のない環境で実行すれば一瞬でテストが完了します。
もし、問題があれば各種メッセージが表示され、問題がなければ何も表示されません。
これも、厳密には本当に武器を持ち換えているかどうかNBTで確認した方が確実かと思います。
表向きは切り替えたって主張していても、バグのせいで内部では何もしていない関数は普通にありえるからです。
適切なタイミングで自動実行
最後に、これを適切なタイミングで自動実行してもらうよう設定します。
手動だと、忘れたり、無精したりすることがあり得ます。
後で後悔しても仕方ないので、今設定しましょう!
と言っても #minecraft:load 関数タグにテストケースを登録するだけの作業です。
{
"replace": false,
"values": [
"xxx:test"
]
}
ここに登録しておけばソースコードを書き換え・保存してMinecraft内で /reload したら必ずテストケースが実行されます。
/reload はキリのいい形にソースコードを書き換えたタイミングで実行されるようなコマンドなので、テストをするにはちょうどいいはずです。
意外な落とし穴
ただ、今回はちょっと問題がありまして、上記のように登録してもうまく動きません。
というのも xxx:test はプレイヤーが実行することを前提として作った関数だからです。
execute at @s ...
という記述がありますが、これはコマンドを呼び出したプレイヤー @s を基準の位置として、スケルトンソルジャーを遠距離・近距離に配置するよう記述しています。
今回は特にプレイヤーとの距離で動作が変わるので、基準となるプレイヤーが必須なのです。
#minecraft:load と #minecraft:tick はMinecraft側から呼び出される時に、有効な @s というものが存在しません。
なので、以下のようにワンクッション置いて呼ぶようにしましょう。
execute as @p run function xxx:test
{
"replace": false,
"values": [
"xxx:load"
]
}
これを /reload してテストが動作しているのを確認出来たら成功です(とりあえず、スケルトンの破壊される音が聞こえるはず)。
まとめ
なんとなくイメージがついたでしょう?
テストケースを用意することで、スケルトンソルジャーを改良するたびに、わざわざ手動でプレイヤーが近づいたり離れたりして確認する必要がなくなります。
テストケースを作ることは手間ですが、作っておけば長い期間時短効果とバグ検知効果が得られます。/reload した時に何のメッセージも出なければ安心できるわけです(もちろんバグが絶対ない保障にはなりませんが)。
さらに言うと、関数の因果関係をテストケースとして羅列していくと、その関数の仕様がしっかり固まります。
テストケースを作っている時に「この場合どうしようか?」って迷ったなら、その部分の仕様が固まっていない証拠です。
こういうのをこまめに固めておけば、その関数への理解も深まります。
実際の単体テスト
今回は、単体テストから簡単に取り入れることができそうなテストケースだけを拾って扱いました。
本来の単体テストは一般的に xUnit というフレームワークで提供されていて、もっと便利な機能があります。
例えば Suite 。これはテストケースの束なのですが、共通の前処理・後処理を書くことができます。
あと xUnit の種類にもよりますが、#minecraft:load への登録のような、余計な登録作業が不要になるよう作られていることが多いです。
登録作業が必要ということは、そもそもテストケースを登録し忘れる可能性が生まれることを意味するので、本来は危険です。
あと一応 Minecraft にもテストインスタンスというテスト用の機能はありますが、ちょっとまだ扱いづらい印象です。
テストインスタンスだけで単体テスト並みの効果を得るのはちょっと難しいですね(もっと扱いやすくなるといいなぁ…)。
不十分でも使わない手はない
流石にデータパック開発で完璧な単体テストを持ち込むのは難しいかもしれませんが、テストケースで「機械に診てもらう」くらいならある程度やってくれます。
部分的でも有効活用するにこしたことはありません!
バグを減らすためにぜひとも有効活用していきたいものです。いや、ほんと…バグだけは地獄なんで…。

