はじめに
Pythonで作ったCLI(ターミナル)向けのテキストアドベンチャーゲームを、GitHub Pagesでブラウザからプレイできるように移植する取り組みを行いました。
このとき厄介なのは、単純な「言語の書き換え」では終わらないという点です。Python版とWeb版(JavaScript)を同じゲームとして両方保守し続ける必要があるため、片方だけを直しても片方が古いままになる「二重実装の同期崩れ」が随所で発生します。
本記事では、実際の移植作業で発生した5つの同期崩れパターンと、それぞれの対処法を紹介します。同じようにCLIツールやゲームをWeb移植する場合の参考になれば幸いです。
1. データ層をまず統一する:台詞データのJSON分離
最初に決めるべきは「シナリオデータをどちらの言語のコードにも埋め込まない」という方針です。
- 課題:台詞・シナリオデータをPythonコード内に直書きしていると、Web版に同じ内容を持たせるには手動でのコピーが必要になり、更新のたびに二重管理コストが発生する。
-
対処:台詞・シナリオデータを共通の
data/script.jsonに分離・抽出する。- Python版は
json.load()で読み込む。 - Web版はJavaScriptの
fetchAPIで動的に読み込む。 - 1つのJSONファイルを更新するだけで両環境に即時反映される。
- Python版は
リポジトリ構成も、別リポジトリに分けるのではなく同一リポジトリ内に web/(GitHub Pages公開用)と data/script.json を同居させるモノレポ構成にしました。データ更新とコミットが単一リポジトリで完結し、Conventional CommitsやSemVerによるリリースノート管理も一元化できます。
データ層さえ統一してしまえば、以降の問題は「ロジックの再実装」に伴うタイミングのズレに絞られます。
2. セーブタイミングのズレ:離脱時にカウントが巻き戻る
-
課題:戦闘システムの
/mercyコマンド実行時、カウンタ変数をインクリメントした後、ダイアログメッセージの表示アニメーション(awaitで待機する非同期処理)が完了してから状態保存処理を呼び出していました。このため、タイピングアニメーションの表示中にプレイヤーがタブを閉じたりリロードしたりすると、インクリメントされた値がLocalStorageに保存されず、カウントが巻き戻ってしまう可能性がありました。 -
対処:カウンタをインクリメントした直後(表示アニメーションの
awaitを呼ぶ前)に状態保存処理を実行するよう順序を入れ替えました。
CLIのPython版では「表示が終わってから保存する」処理順でも問題になりませんでした。しかしWeb版は非同期の表示待機中にユーザーがページ自体を離脱できてしまうため、**「状態変更とその永続化は、表示演出よりも先に完了させる」**という原則がWeb移植で初めて必要になったポイントです。
3. アニメーション待機がシステムログにまで及ぶ問題
-
課題:Python版の
print(...)に対応するシステムログ・アナウンス表示(「戦闘をしかけてきた」「HP表示」「コマンド未認識」等)が、Web版では通常の台詞と同じタイピングアニメーション+入力待ちのメソッドを流用していたため、本来は一瞬で表示されるべきシステムメッセージまでもっさりと表示され、キー入力待ちで止まってしまっていました。 -
対処:タイピングタイマーや文字送り待機のPromiseを一切発生させず、UI要素へ同期的に即時反映する専用メソッド(
printInstant)を新設し、システムログ系のみこちらに置き換えました。キャラクターの台詞やエンディング演出など、演出として意味のある表示は従来の待機付きメソッドを維持しています。
「表示に時間をかける処理」と「即座に反映すべき処理」を、呼び出し側で明確に使い分けるインターフェースを用意したことで解決しました。
4. delay定数の食い違い:同じはずの演出速度がズレる
-
課題:Web版の文字送り速度のデフォルト値が30msだったのに対し、Python版のデフォルト文字送り速度は40ms(
DEFAULT_DELAY = 0.04)でした。個別の演出シーン(撃破演出、謝罪セリフ、エンディングのタメ演出など)についても、Python版とWeb版でそれぞれ独立に数値を決め打ちしていたため、細部の間(ま)が両バージョンで微妙に異なっていました。 -
対処:Python版のソースコード(
display.py,story.py)内のdelayやsleepの秒数を1つずつ照合し、Web版の該当箇所を全て同じ値に合わせ込みました。
この手のズレは機能的には「バグ」ではなく、実行しても一見正常に動きます。だからこそレビューで指摘されるまで見過ごされやすく、「移植が完了した」の基準にタイミングの一致まで含めるかどうかを最初に決めておく必要があります。
5. UIが「AIっぽい」デザインになってしまう問題
-
課題:機能実装を優先すると、UIはSaaS的なシアンのネオングロー、グラスモーフィズム(
backdrop-filter: blurの多用)、汎用的な角丸、Inter/Fira Codeのような定番フォントといった、いわゆる量産型の「AIっぽいデザイン」に収束しがちでした。 -
対処:デザインを監査し、テキストアドベンチャーの世界観に合わせた「レトロCRTアンバー端末×シネマティック・ノワール」というテーマに作り直しました。
- ネオンシアンのグローとメッシュグラデーションを排除。
- 深いダーク背景(
#08080a)にアンバーゴールド(#f59e0b系)を差し色として使用。 - 見出し・コマンド入力には
Share Tech Mono、台詞・ログ本文にはShippori MinchoとNoto Serif JPを採用し、機械的な部分と物語的な部分でフォントの質感を分離。 - CRT風のビネットとスキャンラインのオーバーレイを追加。
-
JS側との互換性維持:HTML要素のIDはすべて維持しつつ、
app.jsがインラインで参照しているCSS変数(アクセントカラー用の変数)の値だけをアンバー系に差し替えることで、デザイン刷新とロジックの無変更を両立させました。
機能実装のスピードを優先すると、UIは後回しにされて「動くけど量産型」のまま固まりがちです。移植プロジェクトの終盤で一度デザイン監査の工程を挟むと、世界観の一貫性を取り戻しやすくなります。
6. 体験の入り口が違う:自動開始 vs START画面
- 課題:Python版はターミナルを起動した瞬間から物語が始まりますが、Web版でもページ読み込み直後に自動で台詞が流れ始める作りにしていました。ブラウザでの体験としては、ユーザーが心の準備をする間もなく始まってしまい、タイトル画面としての体裁もありませんでした。
- 対処:ページ読み込み時にはSTARTボタンのあるタイトル画面で待機し、ボタンクリックまたはEnterキー入力をトリガーに物語の実行(または戦闘状態・エンディングの再開)を開始する作りに変更しました。既存のログ表示エリアや操作エリアはデフォルトで非表示にしておき、START操作後に表示を切り替えます。
CLIとWebでは「起動」という体験の意味が異なります。CLIはコマンドを打つこと自体が能動的な開始行為ですが、Webはページを開いただけで受動的に読み込みが始まるため、明示的な開始トリガーをUIとして用意することが必要になりました。
まとめ
Python版とWeb版のような二重実装を同時に保守する際は、機能の移植だけでなく、次の観点まで一致させて初めて「移植完了」と言えます。
- データ:シナリオ・台詞データは共通フォーマットに分離し、両言語から参照する。
- 状態保存のタイミング:非同期の表示待機よりも先に、状態変更の永続化を完了させる。
- 表示速度の使い分け:即座に反映すべき処理と、演出として意図的に待たせる処理を、呼び出し側で明確に分岐させる。
- タイミング定数:文字送り速度やウェイト時間は、片方の実装を正として数値を照合し直す。
- UIの世界観:機能実装後に必ずデザイン監査の工程を挟み、量産型の見た目から離す。
- 体験の入り口:CLIとWebでは「開始」の意味が異なるため、Web側には明示的な開始トリガーを用意する。
どれも個別に見れば小さな修正ですが、見過ごすとプレイ体験の細部に積み重なって影響します。移植プロジェクトのチェックリストとして活用してもらえればと思います。