034 三手の剣舞 教材QA
検証日：2026-10-05 UTC
対象：01_distance.py / 02_slash.py / 03_history.py / 04_simultaneous.py / 05_undo.py
実行環境：cloud Linux、Python 3.12.14、Pyxel 2.9.9、SDL_AUDIODRIVER=dummy。
対応仕様はPython 3.11以上。3.11本体、Windows/macOS実GUIは未検証。
新規インストールをせず、指定済みの既存仮想環境だけを使用しました。

1. 純粋モデル・合成Controller：31テスト PASS
コマンド：python -B -m unittest -v test_stages.py
実行には既存Pyxel仮想環境のPythonを使用。テスト自体は標準ライブラリだけで動きます。
全文：qa/model_tests.txt。原作本体はテスト実行に不要です。
・01のマス、距離、接触前進は1手、盤外入力は不受理
・02の剣の距離1、1点ずつの練習台、勝利後の固定
・03の3手切り詰め、Wも履歴1枠、同数最新、空振りSと2点連続斬り、読み表示のみ
・04/05の多数決170フィクスチャ、現在入力をpolicyへ渡さない設計と公開済み構え固定
・移動候補衝突で双方を戻す、退く相手の旧位置へ入れる、後退端の切り詰め
・最終距離で両ダメージ、相打ち、空振りSも履歴、3連以上も2点
・攻撃が外れても次は1手だけ息継ぎ、追い足は自動息継ぎなし
・Undo全状態一致、やり直し一致、終了後Undo、不正/盤外手は保存しない、Rで履歴消去
・無入力、長押し、同一行動の別名キー、異なる同時入力拒否、R/X後の解放ゲート
・教材の実在コード断片、各段階の攻略チェックポイント
Controller試験は合成の押下/保持リストであり、ネイティブOSキー試験ではありません。
初回試験で「B B FのあとWならCHASE継続」とした試験側の期待値を修正しました。
実際は履歴B F Wの同票で新しいFを読みGUARD。BREATHにならない点が目的です。
ルール本体は原作との一致を保っています。

2. 原作Aの直接比較：PASS
参照：games/034_three_move_dance/model.py の LEVELS[0]、window=3。
原作model SHA256：f1e70936f01db9b730ecd235dd4b8a0881ba537060dec4fa172e8cd703724849
コマンド：python -B qa/compare_original.py --original ORIGINAL_GAME_FOLDER
原作を-Bかつsys.dont_write_bytecodeで読み、原作34ファイルの前後ハッシュ一致を確認。
04と05を別々に、原作Aから到達可能な45,165状態（うち終端2,637）を有限探索。
各段階170,112個の非終端からの全4行動Resultを直接比較し、全項目一致。
Resultには状態、受理/不受理、理由、与/被ダメージ、衝突を含みます。
手数は規則へ影響しないため、重複状態の同一視からだけ除外しています。
各代表経路のResultでは手数も比較します。終端の不変処理は別の単体試験と再生列で確認。
同じ有限グラフのダイジェストをqa/canonical_reference.jsonに保持し、
原作なしのtest_stages.pyでも04/05をそれぞれ再探索して照合します。
他に11種類の再生列と各手の全状態hash、多数決/回復170例を同梱。
結果：qa/canonical_comparison.json / qa/canonical_compare_log.txt。
01〜03は意図的な縮小ルール。B/C、窓1、原作UIとの同等性は主張しません。
無制限に延びるUndoスタックの全可能列は有限比較の対象外で、個別復元試験をしています。

3. 実Pyxel headless描画：12シナリオ PASS
コマンド：python -B qa/run_render_checks.py
qa/render_runs.jsonへ正規化コマンドと終了コード、各JSONへ合成入力と状態を保存。
全5本の初期画面は無入力12描画フレーム、TURN 0。
01_contact：FFFで距離1、前進衝突でもTURN 3。
02_won：FFSSSSSで練習台を倒す。
03_latest_vote：SFWBで履歴F W B、最新Bを読みCHASE、相手は6に静止。
04_breath：Wの後、攻撃が空振りでもBREATH。
04_won：SFFSSSでWON、HP1。
05_undo：SFFSSSXで勝利を戻し、TURN 5、PLAYING、HP1/相手HP2。
05_safe_win：SBBSSFSFSでWON、HP3。
12枚のPNGを実際に開き、ASCII文字の切れ、マス番号、両者位置、HP、状態、
履歴、構え、候補位置、攻撃距離、段階の縮小表示、キー案内を目視確認しました。
04の衝突説明を「双方停止し、最終距離でダメージ」に明確化してから再描画しました。
無音の実Pyxel描画です。音声を聞いた、OSキーを押した、人が遊んだという主張はしません。

4. 各1ファイルの独立起動：5本すべて PASS
各.pyだけを日本語・空白の一時フォルダーへコピーし、別cwdから12フレーム起動。
相対--screenshot/--reportが呼出元に保存され、原作/他段階へ依存しないことを確認。
LinuxのUTF-8ロケール、Python UTF-8モード/locale coercion無効、stdoutをcp932指定。
これはcp932標準出力設定の確認で、Windowsファイルシステム・キー入力の実機試験ではありません。
結果：qa/isolation_runs.json。

既知の環境制約：LC_ALL=C、PYTHONUTF8=0、PYTHONCOERCECLOCALE=0のASCIIロケールに
日本語パスを組み合わせると、Pyxel.initがsurrogateをUTF-8化できずUnicodeEncodeErrorで失敗。
この試験は不合格として区別し、隠して成功へ数えていません。通常のUTF-8ロケールへ戻して
5本の独立起動を再試験しPASSしました。READMEにUTF-8ロケール要件を追記しています。
追加確認：同じASCIIロケールのまま、そのプロセスだけPYTHONUTF8=1とすると
日本語・空白パス/別cwdの5本すべて12フレーム起動PASS。qa/utf8_workaround_runs.json。
既存の5件とは別の回避策確認です。グローバル設定・原作は変更していません。
実行環境の一時切断がありましたが、不明な結果を成功とは扱わず、再接続後に実施しました。

5. 未実施
人の初見理解・面白さ・難易度、実OSキーボードイベント、Windows/macOS実GUI、
ブラウザ上の操作、音声聴取、Sites公開。原作変更やパッケージ新規導入もしていません。
原作側の既存テストや動画の検証数をこの教材へ合算していません。

6. 配布整合性
5本の独立プログラム、MIT原文LICENSE、固定Pyxel要求、全OS .venv手順、
日本語LESSON、5件のlessons、比較メタデータ、4差分、独立テスト、フィクスチャを同梱。
全配布.pyにMIT全文。Noto案内は原作LICENSEのままですがNotoデータは未同梱・未使用。
qa/provenance.jsonは参照元と範囲。qa/freeze_manifest.jsonは最終ファイルSHA256。
凍結後の編集は再検証・再凍結が必要です。
