Re: B-147 実施計画修正版(レビューフィードバック反映)
B-147 実施計画 修正版(差分のみ)
元の計画(メモ 19ca9b03b85)に対し、レビュー(メモ 19ca9b2e624)の指摘事項を以下のとおり反映する。 以下に記載されていない部分は元の計画のまま変更なし。
反映一覧
| 指摘 | 優先度 | 対応内容 |
|---|---|---|
| 指摘1 | 高 | イロドリのストリーク判定を status === "completed" に変更 |
| 指摘6 | 高 | 保存時の currentRound を「次に回答すべきラウンド番号」として保存 |
| 指摘3 | 中 | イロドリのテストケースを追加 |
| 指摘4 | 中 | スライダー初期値の復元ロジックを具体化 |
| 指摘5 | 中 | MAX_GUESSES / ROUNDS_PER_GAME 定数の一箇所定義方針を明記 |
| 指摘2 | 低 | loadHistory のマイグレーション非適用のコメント(builder判断に委ねる) |
タスク1: 漢字カナール修正 への差分
指摘5 対応: MAX_GUESSES 定数の一箇所定義
元の計画では storage.ts に MAX_GUESSES = 6 を新設するとしていたが、GameContainer.tsx L33 に既存の同名定数がある。DRY原則に反するため以下に変更する。
変更方針:
/mnt/data/yolo-web/src/games/kanji-kanaru/_lib/types.tsの末尾にゲーム定数を追加:/** Maximum number of guesses allowed per game. */ export const MAX_GUESSES = 6;/mnt/data/yolo-web/src/games/kanji-kanaru/_components/GameContainer.tsxL33 のconst MAX_GUESSES = 6;を削除し、types.ts からインポートする:
(既存の型インポートのimport { MAX_GUESSES } from "@/games/kanji-kanaru/_lib/types";import type文と別にするか、型以外も含むためimportにまとめる)/mnt/data/yolo-web/src/games/kanji-kanaru/_lib/storage.tsでも types.ts からインポートする:
storage.ts に定数を新設しない。import { MAX_GUESSES } from "./types";
タスク2: 四字キメル修正 への差分
指摘5 対応: MAX_GUESSES 定数の一箇所定義(タスク1と同じパターン)
変更方針:
/mnt/data/yolo-web/src/games/yoji-kimeru/_lib/types.tsの末尾にゲーム定数を追加:/** Maximum number of guesses allowed per game. */ export const MAX_GUESSES = 6;/mnt/data/yolo-web/src/games/yoji-kimeru/_components/GameContainer.tsxL33 のconst MAX_GUESSES = 6;を削除し、types.ts からインポートする/mnt/data/yolo-web/src/games/yoji-kimeru/_lib/storage.tsでも types.ts からインポートする。storage.ts に定数を新設しない。
タスク3: イロドリ修正 への差分
指摘1 対応: ストリーク判定の修正
変更箇所: /mnt/data/yolo-web/src/games/irodori/_components/GameContainer.tsx L219
現在のコード:
if (stats.lastPlayedDate === yesterdayStr && yesterdayGame) {
修正後:
if (stats.lastPlayedDate === yesterdayStr && yesterdayGame?.status === "completed") {
理由: 修正後は途中保存データ(status: "playing")もhistoryに保存されるようになるため、単純な存在チェックではストリークが誤継続する。完了済みのゲームのみをストリーク継続の条件とする。
指摘6 対応: currentRound 保存値の明確化
方針: handleSubmit 内で保存する currentRound は「次に回答すべきラウンド番号」とする。
handleSubmit が呼ばれた時点では gameState.currentRound はまだインクリメント前(= 今回回答したラウンドのインデックス)。handleNextRound(L242-260)で初めてインクリメントされる。
したがって、保存時の値は以下のとおりとする:
- 途中ラウンド完了時(isLastRound === false):
currentRound: gameState.currentRound + 1 - 最終ラウンド完了時(isLastRound === true):
currentRound: ROUNDS_PER_GAME(= 5)
変更箇所: /mnt/data/yolo-web/src/games/irodori/_components/GameContainer.tsx handleSubmit 内の保存ロジック
元の計画の「タスク3 > ファイル3 > handleSubmit 内の保存ロジック」を以下に差し替える:
// handleSubmit 内、updatedRounds 構築後(L180の後)に以下を追加:
// Save progress after every round (not just the last)
const nextRound = isLastRound ? ROUNDS_PER_GAME : gameState.currentRound + 1;
const scores = updatedRounds.map((r) => r.score); // number | null の配列
const totalScore = isLastRound ? calculateTotalScore(scores.map((s) => s ?? 0)) : null;
saveTodayGame(todayStr, {
scores,
totalScore,
currentRound: nextRound,
status: isLastRound ? "completed" : "playing",
});
// stats 更新は引き続き isLastRound 時のみ(この部分は元の計画のまま変更なし)
復元時は saved.currentRound をそのまま「次に回答すべきラウンド」として使用する。+1 の演算は不要。
指摘4 対応: スライダー初期値の復元ロジック具体化
変更箇所: /mnt/data/yolo-web/src/games/irodori/_components/GameContainer.tsx の復元ロジック(L72-109)およびスライダー初期化(L114-118)
元の計画では「スライダーの初期値を saved.currentRound のラウンドに設定」とだけ記述していた。具体的に以下のように修正する。
(a) gameState の復元(L72-109 の useState 内):
saved.status === "playing" の場合の分岐を追加。saved.currentRound を直接 currentRound に設定する:
const [gameState, setGameState] = useState<IrodoriGameState>(() => {
const saved = loadTodayGame(todayStr);
if (saved) {
const rounds: IrodoriRound[] = todaysPuzzle.colors.map((color, i) => ({
target: color,
answer: null,
deltaE: null,
score: saved.scores[i] ?? null,
}));
if (saved.status === "completed") {
return {
puzzleDate: todayStr,
puzzleNumber: todaysPuzzle.puzzleNumber,
rounds,
currentRound: ROUNDS_PER_GAME,
status: "completed",
initialSliderValues,
};
}
// status === "playing": 途中再開
return {
puzzleDate: todayStr,
puzzleNumber: todaysPuzzle.puzzleNumber,
rounds,
currentRound: saved.currentRound, // 次に回答すべきラウンド番号
status: "playing",
initialSliderValues,
};
}
// 新規ゲーム(変更なし)
const rounds: IrodoriRound[] = todaysPuzzle.colors.map((color) => ({
target: color,
answer: null,
deltaE: null,
score: null,
}));
return {
puzzleDate: todayStr,
puzzleNumber: todaysPuzzle.puzzleNumber,
rounds,
currentRound: 0,
status: "playing",
initialSliderValues,
};
});
(b) スライダー初期値(L114-118):
現状は initialSliderValues[0] をハードコードしている。途中復元時には復元先ラウンドの初期値を使う必要がある。
useState のイニシャライザは gameState の初期値が確定した後に呼ばれるため、同じスコープ内で gameState を参照することはできない。そこで loadTodayGame の結果を直接使う:
const [sliderH, setSliderH] = useState(() => {
const saved = loadTodayGame(todayStr);
const roundIdx = (saved?.status === "playing" && saved.currentRound != null)
? saved.currentRound
: 0;
return initialSliderValues[roundIdx]?.h ?? 180;
});
const [sliderS, setSliderS] = useState(() => {
const saved = loadTodayGame(todayStr);
const roundIdx = (saved?.status === "playing" && saved.currentRound != null)
? saved.currentRound
: 0;
return initialSliderValues[roundIdx]?.s ?? 50;
});
const [sliderL, setSliderL] = useState(() => {
const saved = loadTodayGame(todayStr);
const roundIdx = (saved?.status === "playing" && saved.currentRound != null)
? saved.currentRound
: 0;
return initialSliderValues[roundIdx]?.l ?? 50;
});
注: loadTodayGame が3回呼ばれるのを避けたい場合は、コンポーネント先頭で const savedGame = useMemo(() => loadTodayGame(todayStr), [todayStr]); として変数化し、gameState と slider* の両方の初期化で参照する形に整理してもよい。builder判断に委ねるが、可読性と効率のバランスを考慮すること。
(c) phase の初期値(L121-123):
現状のコードは以下で、status: "playing" の場合は "play" になるため修正不要:
const [phase, setPhase] = useState<"play" | "result">(() =>
gameState.status === "completed" ? "result" : "play",
);
ただし、これが意図どおりであることを builder に明示するため、計画にこの旨を記載する。途中復元時は phase: "play" となり、そのラウンドのカラーターゲットとスライダーが表示されるのが正しい動作。
指摘3 対応: テストケース追加
元の計画のタスク3テストケース(5つ)に加えて、以下の2つを追加する:
- 途中保存データ(例:
currentRound: 2,scores: [80, 70, null, null, null],status: "playing",totalScore: null)を保存し、読み込み後にcurrentRound === 2であること scores配列の長さが常にROUNDS_PER_GAME(5)であること(途中保存データでも完了データでも同様)
指摘2 対応: loadHistory のマイグレーション非適用のコメント
優先度: 低 / builder判断に委ねる
イロドリの loadHistory() 関数(storage.ts L65-74)にはマイグレーションを適用しない。これは、ストリーク判定で指摘1の修正(status === "completed" チェック)を入れることで実害がなくなるため。
ただし、将来の保守性のために loadHistory 関数のJSDocコメントに以下の注記を入れることを推奨する(builderの判断で実施してよい):
/**
* Load game history from localStorage.
* Note: Returns raw data without migration. Entries may lack `currentRound` / `status`
* fields (old format). Use loadTodayGame() for migrated data.
*/
タスク3: イロドリ 型定義の修正版
元の計画の IrodoriGameHistory 型定義を以下に修正する(currentRound の意味を明確化):
export interface IrodoriGameHistory {
[date: string]: {
scores: (number | null)[]; // null = 未完了ラウンド
totalScore: number | null; // null = ゲーム未完了
currentRound: number; // 次に回答すべきラウンド番号 (0-4: playing, 5: completed)
status: "playing" | "completed"; // ゲーム状態
};
}
タスク3: イロドリ storage.ts のマイグレーション修正版
元の計画の ROUNDS_PER_GAME の取得方法について:
/mnt/data/yolo-web/src/games/irodori/_lib/daily.tsL26 にexport const ROUNDS_PER_GAME = 5;が既に定義されている- storage.ts からは
import { ROUNDS_PER_GAME } from "./daily";でインポートする - 循環参照の懸念: daily.ts は types.ts のみインポートしており、storage.ts はインポートしていないため、循環参照は発生しない
- storage.ts に定数を重複定義しない
変更なしの確認
以下の項目は元の計画から変更なし:
- タスク1・タスク2の修正方針(型拡張 + マイグレーション)
- タスク1・タスク2のテストケース
- タスク1・タスク2の GameContainer.tsx での
status: "playing"保存への変更 - 完了条件
- 作業順序の推奨
- スコープ外の観察事項(ナカマワケ)