ログインなしのブラウザゲーム記録を壊さず保存する
localStorage schema・migration・corruption recovery
なぜバックエンドを使わないか
『60秒後の彼女』の記録(streak・日別ベスト・履歴)はすべてブラウザの localStorage のみに保存する。サーバー側のアカウントも、分析も、外部送信もない。理由はプライバシーと配備の単純さ。記録は端末に留まり、ユーザーが消せば完全に消える。
スキーマバージョンを分ける
「チャレンジプロトコルのバージョン」と「保存データのスキーマバージョン」は別物だ。本作は STORAGE_SCHEMA_VERSION = 2 を持ち、保存Blobに schemaVersion を埋め込む。将来フォーマットを変えるとき、古いデータを新形式へ変換(migration)する根拠になる。
壊れたデータからの復旧
localStorage は信頼できない入力源だ。JSONが壊れている、未知のフィールドがある、quota超過、プライベートブラウズで書けない——こうした状況でもクラッシュしてはならない。本作の parseStore は:
- 空文字/
null→ デフォルトストア - JSONパース失敗 → デフォルトストア(例外を外に出さない)
- スキーマ不一致 →
migrateStoreで既知フィールドだけ持ち上げ、未知フィールドは無視 - 履歴の不正な要素(
nullや形違い)はフィルタで落とす - 履歴は直近50件にcap
単体テストで「不正なJSON」「数値でないstreak」「壊れた履歴要素」を投入し、クラッシュせずデフォルトに近い状態になることを確認済み。
streak の意味(失敗→成功も含む)
「その日に1回以上クリアしたか」でstreakを判定する。重要な点:同じ日の失敗だけで、それまでのstreakを壊さない。例えば前日クリア済みで、今日まず失敗し、その後クリアした場合、streakは前日から繋がる。同日の複数クリアで加算しない。
export function computeStreak(prev, todayKey, success) {
const current = Number(prev?.current) || 0;
const best = Number(prev?.best) || 0;
const last = prev?.lastSuccessDate || null;
if (!success) return { current, best, lastSuccessDate: last }; // 失敗はstreakを壊さない
let next;
if (last === todayKey) next = current; // 同日2回目以降は加算しない
else if (last === yesterday(todayKey)) next = current + 1; // 連日
else next = 1; // ギャップ後または初回
return { current: next, best: Math.max(best, next), lastSuccessDate: todayKey };
}
失敗時の挙動は単体テストで確認済み(fail then success same day keeps connecting to previous success)。
daily best の比較関数
同じ日の記録は「最良1件」だけ残す。比較順位は明確にする:
- 成功 > 失敗
- 完了フェーズ数が多い
- 救助率(rescue gauge)が高い
- 残り時間が長い
- ミスが少ない
export function compareResults(a, b) {
const sa = a.success ? 1 : 0, sb = b.success ? 1 : 0;
if (sa !== sb) return sa - sb;
if ((a.phaseCount||0) !== (b.phaseCount||0)) return (a.phaseCount||0) - (b.phaseCount||0);
if ((a.rescueGauge||0) !== (b.rescueGauge||0)) return (a.rescueGauge||0) - (b.rescueGauge||0);
if ((a.timeLeft||0) !== (b.timeLeft||0)) return (a.timeLeft||0) - (b.timeLeft||0);
return (b.misses||0) - (a.misses||0);
}
単体テストで「成功は救助率が低くても失敗より上」「各優先度が正しく効く」を確認済み。
clear data と quota
「データを消す」ボタンは localStorage.removeItem で該当キーを消し、ストアをデフォルトに戻す(dismiss設定もリセットされる)。quota超過やプライベートブラウズで書き込みが失敗しても、ゲーム自体は続行できる(保存失敗は致命にしない try/catch)。
制限(limitations)
- 記録は端末間で同期しない。別のPCやブラウザでは初期状態から。
localStorageは「安全なバックアップ」ではない。OSのクリアやブラウザ削除で失われる。- スキーマ変更時、未知フィールドは意図的に破棄される。