058 交換する魔法：教材QA
検証日：2026-10-05 UTC
対象：01_fire.py / 02_effects.py / 03_transfer.py / 04_round.py / 05_forecast.py
環境：cloud Linux、Python 3.12.14、Pyxel 2.9.9、SDL_AUDIODRIVER=dummy。
配布条件はPython 3.11以上。3.11そのものの実行、Windows/macOS実機GUIは未検証。
今回の実行は許可済みの既存仮想環境。インストールは行っていません。

1. 純粋モデル・実Controller・配布整合性：43テスト PASS
再実行：python -B -m unittest -v test_stages.py
全文：qa/model_tests.txt
・01の火球3傷、HP0下限、終了後不変、固定所有の導入境界
・02の観察用初期配置、回復上限/回復0、防壁2の軽減と期限
・攻撃のない手当・防壁でも対象防壁が消える、自分の防壁は保持
・03以降の同一カード移譲、即時再使用、4枚一意、回復0でも移譲
・非所有札、bool/None/文字列、無効ID、未知actorの拒否
・敵Aの回復不足量判定、火球/火花/防壁/手当の優先順
・自分の効果と移譲のあとに敵が判断し、今渡した札も使える
・敵を倒す場合も移譲済み、敵反撃なし、1枚対3枚で終局
・敵行動後の自HP0を先に敗北判定、その後に12往復のDRAW
・12往復目の勝利優先、勝利経路、予告が無変化で実行と一致
・State全体の一往復Undo、終局から復帰、空履歴と無効入力、Rの履歴消去
・無入力、押しっぱなし、同時押し、押下/保持の不一致、解放ゲート
・手動actor、左右の予告だけでは進まない、選択札Space使用、終局Undoと再勝利
・段階別CLI拒否、CLIモード競合、原作不要import、実コードと説明snippet一致
・4つの隣接diff、5段階schema、ライセンス全文、Pyxel固定、明示UTF-8 IO
Controller試験はGUIが実際に使うクラスを通しますが、OSキーイベントではありません。

2. 原作Aとの直接比較と同梱fixture：PASS
原作フォルダー内のmodel.py/app.py/README.txt/LICENSE.txtは制作前に全文確認。
原作を-Bで読み取り専用import。原作フォルダー全56ファイルのSHA256を前後照合して不変。
qa/source_hashes.jsonに各hash、qa/provenance.jsonに派生範囲と省略を保存。
qa/canonical_comparison.jsonに件数、qa/canonical_reference.jsonに期待状態/イベントを同梱。

一致対象：A-01-g1、exchange=Trueの通常規則。
・Level全フィールド一致
・初期状態から合法往復を全分岐して終局まで列挙：273状態
  内訳：RUNNING 201、WON 21、LOST 18、DRAW 33
・所有する札を各状態で入力：525チェック
  実際に進む402往復遷移＋終局後の123無変化入力
・各状態の両actorと所有札について単独cast：1092チェック
・Undo、空Undo、無効札、bool/None、終局を含む3本のTimeline入力列
・04/05の往復結果とEvent、03のcast結果とEventを直接照合
・通常テストでは03/04/05のcast、04/05の往復、05の予告と履歴を同梱fixtureと照合

任意の再比較：python -B tools/verify_original.py --source "原作058フォルダー"
原作がなくても普通の43テストは実行可能です。上の任意ツールだけが原作を必要とします。
一致主張は原作Aの到達状態と記載した入力族です。B/C、固定所有比較、原作UI、
任意に破損した状態は対象外。02の所有固定実験は原作の比較版との同等性を主張しません。

3. 実Pyxel headless描画：18シナリオ PASS
再実行：python -B tools/render_qa.py
実Pyxelのupdate/drawを起動し、実Controller.tickへ合成キーを渡します。
コマンド/終了コード/期待状態：qa/render_runs.json
各PNGは実描画、各JSONは操作・フレーム・状態・表示用効果、各logは標準出力/エラー。
・01〜05の初期画面5枚：無入力、0往復、初期状態
・01_won：火球2回で敵HP0
・02_shield_expired：防壁→敵火球で自HP4、防壁0
・02_zero_heal：手当2回でHP7、最後の回復量0、所有権固定
・03_passed_fire：火球を渡して敵が同じ札で反撃、自HP4/敵3
・03_wall_expires_wall：防壁対防壁でも古い対象防壁が切れる
・04_enemy_heals：火球後に敵が手当、自札が火花/手当へ変わる
・04_won：2→1→2→1、自HP3、4往復、1枚対3枚で勝利
・05_preview_spark：右で予告のみ変更、現在状態は0往復のまま
・05_cast_preview：Space確定で予告通り自HP6/敵5、1往復
・05_won：4往復勝利、終局時は予告選択表示を消す
・05_undo：勝利から3往復目に戻り、火球予告は敵反撃なしの勝利
・05_lost：1→4→1→2→4→1、自HP0/敵2、6往復で敗北
・05_draw：1→2→1→4→4→1→4→1→4→4→1→4、両HP4、12往復で決着なし

全18PNGを実際に開いて観察。HP・防壁・所有者・手札・予告・終局・文言と枠内収まりを確認。
初回16画像の点検後、終局時の不要なPREVIEW選択表示を消す修正を実施。
全画面を再描画し、影響する勝利画面を再確認。追加の敗北・DRAW画面も確認。
独立監査の指摘を受け、敵HP・火球名/効果・敵予告の暗赤文字を明色7へ変更。
全18画面を再生成・再点検し、所有者などの意味ラベルとゲーム規則を保持しました。
qa/accessibility_fix.jsonに変更前後hashと非描画AST不変の検査を保存。
qa/visual_inspection.jsonに画像hashと観察内容を保存。
無音設計であり、dummy audioを使っています。音を聴いて確認したとは主張しません。

4. 単独ファイル・日本語/空白パス・独立テスト：PASS
全5本をそれぞれ「教材 日本語」フォルダーへ1本だけコピー。
別の「無関係 CWD」から実Pyxelで意味のある操作列を起動。
日本語と空白を含む親パス、相対出力relative.png/relative.jsonの生成を確認。
元の.pyフォルダーにはその1本以外が増えないことも確認。
qa/isolation_runs.jsonに起動コマンド・CWD・終状態・終了コードを保存。

教材のコード/説明/差分/tools/原作fixtureだけを別の隔離フォルダーへコピーし、
原作なし、site-packagesなしの -S で同じ43テストを実行してPASS。
qa/standalone_tests.txt、qa/standalone_tests.json。
同じ43件の別環境再実行であり、86種類の独立テストとは数えていません。

5. 配布と凍結
五つの.pyはPyxel以外に外部依存なし。相互import・原作参照・絶対runtimeパスなし。
全テキスト読み書きはencoding='utf-8'。外部素材やフォントの読み込みなし。
LICENSEは原作MITを全文保持。全Python（テストと補助ツールを含む）に同じ原文コメント。
原文のNoto例外文は保持していますが、教材にはNoto字形/フォントは含まれません。
qa/freeze_manifest.jsonが配布ファイルのSHA256一覧。自分自身を除く全ファイルを対象。
python -B tools/check_freeze.py は不足・変更・予期しない追加も検査します。
凍結後に実験用スクリーンショット等を足すと追加として報告されるため、実験はコピー先を推奨。

6. 未実施・別証拠
人による初見理解/面白さ/難易度/操作感の評価、Windows/macOS実機GUI、
Python 3.11実行、OSネイティブキーボード入力、ブラウザ、音声評価、Site公開、ユーザーPC操作。
原作側の35テストと新規自動デモは別の検証です。本教材の件数へ合算していません。
