内部の仕組み
ターン進行の排他制御、島の放棄、OGP 画像の生成など、実装の要点を説明します。
ターン進行
ターンは「最終更新時刻から 1 ターン分の時間が経過しているか」で判定します。判定はリクエスト時 (turn-check ミドルウェア) に行われるほか、次の経路でも進みます。
- Node.js サーバー:
HAKONIWA_TURN_CHECK_INTERVAL_SEC(既定 60 秒) 間隔のタイマー - Cloudflare Workers:
wrangler.jsoncのtriggers.crons(既定*/15 * * * *) から Worker のscheduledハンドラが呼ばれ、DO の RPCcheckTurn()(turnService.advanceTurnIfDue) を実行します。ターンを進めるべきかどうかは DB に保存された 1 ターンの長さと最終更新時刻から判定するため、Cron 側はターンの境界を意識しません
二重進行の防止
リクエスト時の判定と Cron / タイマーが同時に走っても、ターンは二重に進みません。
- 1 ターン分の処理 (
TurnService.#advanceOnce) はawaitを含まない同期処理として、リポジトリの 1 トランザクション内で実行されます - ゲームの状態更新は
tryBumpTurn(UPDATE games ... WHERE id = ? AND turn = ?) の楽観ロックで、進行前のターン番号を条件にします。先に進めた側が勝ち、負けた側はロールバックして何もしません - Cloudflare Workers では Durable Object 自体が単一スレッドで、
transactionSyncの中で同期的に完結するため、そもそも 2 つのトリガーが同時に同じ DO で処理されることはありません
島の放棄
開発画面の「島を放棄する」(POST /games/:gameId/my-island/abandon) で、islands.abandoned_at が記録され、町のヘックスは荒地に、計画はすべて資金繰りに戻ります。
- 所有判定 (
findIslandByOwner) は放棄されていない島だけを返すため、放棄直後から新しい島を作成できます - 放棄島はターン処理の収入・計画・成長・災害の各フェーズをスキップし、ターン末の死滅判定で除去されます (ログは「放棄され、無人島になりました」)
- 放棄回数は
GameConfig.maxAbandonsPerGame(既定 3) までで、abandonments表に (game_id, user_id) ごとに記録が残ります
OGP 画像
島のページの og:image は GET /games/:gameId/islands/:id/ogp.png (800×420 の PNG) です。
- 地図は観光者向けの表示 (ミサイル基地→森、海底基地→海、ハリボテ→防衛施設に見える偽装ルールを含む) をそのまま敷き詰めたもので、文字は描画しません。島名やターン・人口・面積・順位は
og:title/og:descriptionに載せます - 外部サービスやネイティブライブラリを使わず、
packages/core/src/ogp/の純粋な TypeScript (自前の PNG エンコーダ + 事前生成したタイル画像データ) で毎回組み立てます - 画像の URL には現在のターン (
?turn=N) を付け、ターンが進むと URL が変わるようにしています
バックアップ
- Node.js:
HAKONIWA_BACKUP_DIR配下にファイルとしてバックアップを作成・復元・削除します - Cloudflare Workers: SQLite backend の DO が持つ Point-in-Time Recovery のブックマークを使います。
restoreは DO を再起動する (ctx.abort()) ため、実行後は再読み込みを促す案内を表示します