blog
緑で PASS も出たのに、画面では効いていなかった
SCRAP ARMS NEOの UI を直していて、「直した」はずの修正が画面では効いていなかった、ということが続けて起きた。一度も効いていなかった修正もあれば、レビュー役が PASS を出したあとで穴が見つかった修正もあった。
NEO の UI 改善は、Claude Code の Agent Teams で回している。画面を撮る qa、実装する impl、確認する verifier、審査する critic がいて、全体を見る司令塔は自分のセッションの Claude だ。この記事は、その司令塔が自分で書いた記録と、チームの会話ログを読み返して書いている。
出撃ボタンを薄くした修正
発端は 2026-09-25 の指摘だった。未解放のキャラを見ている間でも出撃ボタンが押せてしまう。直すのは簡単で、押しても出撃せず解放条件をトーストで出し、ボタンを薄くする。その日の 17:51 に、司令塔の Claude が自分で入れた(405d39b)。コミットメッセージの末尾はこうなっている。
claude が直接。tsc 0。
型チェックは通った。ただし画面は撮っていない。薄くする行はこうだ。
put(button(this, L, t('sortie'), ctaW, ctaH, 'danger', () => this.sortie()), pad + Math.round((colW - ctaW) / 2), ctaY)
.setAlpha(characterUnlocked(this.save, this.viewedCharacter) ? 1 : 0.4);
同じ画面を描く draw() には、別の行があった。
const put = (c: Card, x: number, y: number) => { c.setPosition(x, y).setData({ x0: x, y0: y }); cards.push(c); return c; };
if (!animate) { c.setAlpha(1).setY(fy); return; }
put() で置いたものは cards に積まれ、描き終わりにカード全部の alpha を決め直す。演出なしの描画ならその場で 1 に、演出つきなら 0 から 1 へ動かす。出撃ボタンも put() 経由なので、どちらの道でも 0.4 は 1 で上書きされていた。型は合っているので、型チェックは通る。見た目は動かしてみないと分からない。
見つけたのは qa だった。同じ日の 21 時半ごろ、UI 改善 1 周目の実機確認で出撃ボタンを撮って、こう報告している。
実測 alpha=1、スクショ(F4-locked-sortie.png)でも「未解放」ボタンは通常の「出撃」と同じ真っ赤な full brightness
3 時間半ほどの「直ったはず」が、撮った一枚で崩れた。直した修正は 21:38 のコミット(80f3c59)になっていて、そのメッセージにも「405d39b で入れた alpha 0.4 が一度も効いていなかった」と書いてある。
差し戻した先も、確かめていなかった
この直しの途中で、司令塔がもう一つやっている。
impl の 1 回目の直しは、出撃ボタンを put() から外して cards に入れない形だった。alpha は 0.4 のまま残る。それを見て司令塔は差し戻した。
出撃ボタンだけ入場の演出(フェード+スライド)から外すと、ほかのカードと動きがそろわなくなる。
この理由自体は間違っていない。cards から外せば、拠点を開いたときに出撃ボタンだけ演出なしで出てくる。ただ、司令塔は「未解放のキャラ × 入場演出つきの描画」という場面が実際に起きるのかを、コードで確かめていなかった。
2 回目の直しのあとの確認で、qa がこう書いている。
通常操作だと animate=true は初回描画の1回だけで、しかもそのときは常に解放済みキャラ(save.character)なので、「未解放 × animate=true」の組み合わせは実プレイでは起きません。
拠点を開くときは viewedCharacter を保存済み(=解放済み)のキャラに戻してから draw(true) を呼ぶ。キャラを切り替えたときは draw(false) で、演出がない。だから未解放の薄いボタンが入場演出つきで出る場面は存在しない。qa は private の draw(true) を直接呼んで再現し、alpha が 0 から始まって 0.4 に収まるのを測ったうえで PASS を出した。
差し戻しの理由そのものは正しかった。解放済みのキャラでの演出は実際に見える。ただ、確認の項目に「未解放 × 演出」を書く前に、その状態が起きるのかを見ておくべきだった。このときの結論は、記録にこう残っている。
差し戻しや仕様追加の前に「その状態は実際の操作で起きるか」をコードで確かめる。
熟練パネルで、PASS が 2 回続けて穴を通った
翌 09-26 は、NEO の熟練パネル(キャラごとの熟練度を上げ、★で段階を示す画面)の直しだった。1440x900 のパソコン幅で、パネルの高さに画面の 7 割という上限を足す。
14 時台、impl はこの上限を入れ、縮めたことで欄どうしが重なるようになった所に、収まらなければその欄を丸ごと描かないガードを入れた。verifier はこれを見て PASS を出した。見た画像は mastery-h-star1-1440x900.png、★1 の状態だけだ。
数分後、qa の締めの撮影で、★0 の同じ画面から「限界突破」の欄がボタンごと消えていることが分かった。★0 はまだ限界突破していない、いちばんふつうの状態で、そこで主な操作が無くなっていた。司令塔は verifier にこう伝えている。
前回の PASS は ★1 の撮影だけで判定していたので、この穴を通った。
司令塔は、次の判定では 4 サイズ × ★0 / ★1 / ★★ を全部見るよう verifier に頼んだ。impl が横長を 2 列にして直し、レビューが回ってくる。
ここで同じことが起きた。verifier の PASS は、p1-star0-1440x900.png と p1-star0-390x630.png、つまり今度は★0 だけを見た PASS だった。司令塔は少し前に impl の撮った画像を自分で開いていて、1440x900 の★1 で「上限では捧げられません」ボタンがパネルの枠の下端から突き出しているのを見つけていた。トーストが 2 つ同じ場所で重なっていることと、390x630 でトーストがパネルの見出しに重なっていることも同じ画像にあった。verifier は PASS を撤回して 12 枚(4 サイズ × 3 状態)を全部見直し、判定表を書いた。
2周連続で「一部の状態だけ見て通す」を繰り返した。
もう一つ穴があった。同じ日、qa が書いた検査スクリプトは、パネルの外枠を Container(複数の部品をまとめる入れ物)の getBounds() で取っていた。Container の bounds は子を含んで広がる。ボタンがはみ出すと、枠もいっしょに広がるので、「枠の中にある」は常に真になる。qa は ★1 のはみ出しを「OK」と報告し、そのとき目視もしたと書いていたが、司令塔はこの画像を自分で開いて指摘した。qa も同じ原因にほぼ同時に気づいていた。そのあと、子に引きずられない固定サイズの枠(impl が mastery-panel-frame という Zone を足した)を基準にする形に直している。
記録に残した規則
この日、司令塔は次のような規則を記録に書いた。
レビュー担当(verifier / qa)の PASS は、サイズ × 状態の全セルに「開いた画像の名前 + OK/NG」を並べた表があるときだけ受け取る。OK の条件は「ある」ではなく「ある・枠の中・何とも重ならない」。
あわせて、自分でも一番危ないセル(横長 × 中身が一番多い状態)を 1 枚は開く。検査スクリプトの緑は、壊した状態で赤くなるのを見るまで信じない、とも書いている。
つまり PASS には、こういう表がついてくる。
| サイズ | ★0 | ★1 | ★★ |
|---|---|---|---|
| 1440x900 | 画像名・OK/NG | 画像名・OK/NG | 画像名・OK/NG |
| 390x844 | 同上 | 同上 | 同上 |
| 360x740 | 同上 | 同上 | 同上 |
| 390x630 | 同上 | 同上 | 同上 |
verifier が出し直した 12 枚の表は、1440x900 の★1 が NG、360x740 の ★0 が NG(トーストが見出しに重なる)、390x844 と 390x630 の一部を「ボーダーライン」とし、qa の機械検査の結果を待つとしている。「全部 OK でした」より、ずっと情報がある。
もう一つ、検査のほうにも手を入れた。scripts/README.md に、contains の外枠に Container を渡さない、と書いてある。熟練パネルの検査は、見た目を壊す mutate(わざと壊した状態で走らせ、赤になることを確かめる仕掛け)を検査ごとに置いて、コミット(ba5f4c2)にもそう書いた。
検査ごとに、壊すと落ちる mutate を置いた。readable は直す前のビルドで 360x740 / 1440x900 の ★1 が赤になるのを確認。
同じコミットには、別の落ちた例も書いてある。文字を縮める fitText に下限がなく、1440x900 の★1 で説明文が 2.6px の染みになっていたのに、検査は緑だった。
自分の側の読み方
読み返して思うのは、PASS を出した側が範囲を隠していたわけではない、ということだ。1 回目の PASS には mastery-h-star1-1440x900.png を目視した、2 回目には ★0 を最優先で見た、と書いてあった。見た画像の名前は文面に出ている。出ていないのは、見ていない状態のほうだ。抜けに気づくには、受け取る側が「★0 は? ★★ は?」と自分で数え直すしかなかったし、司令塔も 1 回目はそれをしなかった。しかも、見た画像の判断も完全ではなかった。2 回目に「トーストも重なっていません」とされた画像には、トーストの重なりが写っていた。
文章で「確認しました」と受け取ると、見ていない範囲は文面に出てこない。表で受け取れば、空いているセルがそのまま空白として見える。そのうえで司令塔も最低 1 枚、一番危ないセルを自分で開く。全部を開くわけではないが、受け取る側にも見ている目がある状態を作れる。
検査の緑も同じだ。文字列一致の検査はコメントを見逃すでは、対象行をコメントアウトしても緑のままの検査を扱った。熟練パネルの検査も、枠が子といっしょに広がる限り、はみ出しを見ていなかった。緑の数は、何を見ているかを教えてくれない。壊した状態で赤くなるところまで見て、はじめて検査が働いていると言える。
この記事の舞台になった SCRAP ARMS NEO は、ブラウザでそのまま遊べる。