From a2e32401fc4b99ddbfa3801204aa11dd7a525152 Mon Sep 17 00:00:00 2001 From: Joshua Leshnick Date: Fri, 24 Jul 2026 16:26:13 -0500 Subject: [PATCH 1/4] refactor: remove dead save-session concept and dead-code cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete SaveSlot interface (types.ts) — was never imported or used - Remove Ctrl+S autosave-reminder handler (App.tsx) and its shortcut entry (SettingsDialog) - Fix stale SettingsDialog copy referencing non-existent '.json saved game files' - Correct docs (CLAUDE.md, CONTEXT.md, ARCHITECTURE.md) to remove labyrinth_saved_slots_list / named-slots claims; describe actual single-slot autosave - Remove unused _atlasUrl param from scanBoard() (boardScanner.ts) and update call site (BoardScanModal.tsx) - Replace undefined stone-850/stone-750 shades with valid stone-700/stone-800; drop inconsistent dark: variant prefixes (app uses [data-theme], not Tailwind dark:) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- CONTEXT.md | 2 +- docs/ARCHITECTURE.md | 2 +- src/App.tsx | 5 ----- src/components/board/Board.tsx | 4 ++-- src/components/modals/BoardScanModal.tsx | 2 +- src/components/modals/SettingsDialog.tsx | 8 +++----- src/components/panels/SetupPanel.tsx | 4 ++-- src/components/panels/SolverPanel.tsx | 2 +- src/lib/boardScanner.ts | 1 - src/types.ts | 6 ------ 11 files changed, 12 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 50157c4..1079d27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ This is the project root for Labyrinth Game Solver, a responsive, mobile-friendl - **Mobile Standalone / PWA**: Configured with Apple Web App meta tags to allow launching full-screen from the iPhone home screen. Max viewport bounds prevent bouncing and double-tap zoom. - **Path Resolving**: In `vite.config.ts`, `base: './'` is configured so that files compile with relative links to support hosting on GitHub Pages subpaths. Alias `@/` maps to the `src/` folder. - **Safe Deep Copy**: `useLabyrinthHistory.ts` uses a custom `deepClone` (via `JSON.parse(JSON.stringify(...))`) instead of solver's `cloneBoard()` to avoid crash errors with `null` grid values. -- **Auto-Save / Game Storage**: The state synchronizes to the browser's `localStorage` database under `labyrinth_saved_slots_list` and `labyrinth_strategist_state`. +- **Auto-Save / Game Storage**: The board layout and game state are autosaved to the browser's `localStorage` under `labyrinth_strategist_state` and restored on reload. User preferences (theme, accent color, mute, 3D, active players) use separate keys (`labyrinth_theme`, `labyrinth_accent_color`, etc.). ## Release Pipeline diff --git a/CONTEXT.md b/CONTEXT.md index 45e7aa2..9eaade3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -42,4 +42,4 @@ The Labyrinth project manages its interface states directly inside `App.tsx` and - **[src/components/Tile.tsx](src/components/Tile.tsx)**: Generates CSS-based corridors (straight, corner, t-junction) based on rotation degrees, and overlays pawn tokens. - **[src/solver.js](src/solver.js)**: Legacy pathfinder algorithm (implements graph BFS and slide shifts). - **[src/constants.ts](src/constants.ts)**: Configures diagnostic starts (colors, diagonal alignment presets, named treasures). -- **[src/hooks/useLabyrinthStorage.ts](src/hooks/useLabyrinthStorage.ts)**: Interacts with the browser's `LocalStorage` database to save and load named slots and the auto-save. +- **[src/hooks/useLabyrinthStorage.ts](src/hooks/useLabyrinthStorage.ts)**: Autosaves board and game state to `localStorage` (`labyrinth_strategist_state`) and restores it on reload. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e4cb603..b0ead2b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -50,7 +50,7 @@ Complex pathfinder searches run inside `solver.worker.js` (Web Worker). ### 2. State & History Synchronization - **`useLabyrinthHistory`**: Keeps deep cloned snapshots of `AppGameState` to manage custom undo/redo actions. -- **`useLabyrinthStorage`**: Provides quick read/write tools to standard `localStorage` to save game slots and sync setup options. +- **`useLabyrinthStorage`**: Autosaves board and game state to `localStorage` (`labyrinth_strategist_state`) and restores it on reload. ### 3. Native Web Audio Synth Audio feedback is synthesized dynamically using the Web Audio API in `src/utils/audio.ts` (minimizing app footprint by not packing static mp3/wav files). diff --git a/src/App.tsx b/src/App.tsx index 63a3259..0dea10c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -309,11 +309,6 @@ export default function App() { game.handleRedo(); return; } - if (ctrl && e.key === "s") { - e.preventDefault(); - showToast("Autosave keeps your layout ready next time ✨"); - return; - } if (e.key === "?" && !ctrl) { e.preventDefault(); setIsSettingsOpen(true); diff --git a/src/components/board/Board.tsx b/src/components/board/Board.tsx index 5489873..6eab376 100644 --- a/src/components/board/Board.tsx +++ b/src/components/board/Board.tsx @@ -272,9 +272,9 @@ export const Board: React.FC = ({ className={cn( "w-full h-full max-w-[70%] max-h-[70%] mx-auto p-0.5 rounded-lg transition-all focus:outline-none flex items-center justify-center", isForbidden - ? "opacity-20 cursor-not-allowed border border-stone-850 text-stone-600 bg-stone-950/60" + ? "opacity-20 cursor-not-allowed border border-stone-700 text-stone-600 bg-stone-950/60" : turnPhase === "move" - ? "opacity-25 cursor-not-allowed border border-stone-850 text-stone-600 bg-stone-950/60" + ? "opacity-25 cursor-not-allowed border border-stone-700 text-stone-600 bg-stone-950/60" : isStaged ? "neo-brutalism-button border-stone-950 bg-theme-primary text-stone-950 scale-105 translate-x-[1px] translate-y-[1px] shadow-[1px_1px_0_0_#000000] cursor-pointer" : isHighlighted diff --git a/src/components/modals/BoardScanModal.tsx b/src/components/modals/BoardScanModal.tsx index 5b40063..86dfe7a 100644 --- a/src/components/modals/BoardScanModal.tsx +++ b/src/components/modals/BoardScanModal.tsx @@ -324,7 +324,7 @@ export function BoardScanModal({ open, onClose, onApply }: Props) { y: c.y * scanH, })) as [CornerPoint, CornerPoint, CornerPoint, CornerPoint]; - const scanResults = await scanBoard(scanSource, absCorners, "", setProgress); + const scanResults = await scanBoard(scanSource, absCorners, setProgress); setResults(scanResults); setStep("results"); } catch (err) { diff --git a/src/components/modals/SettingsDialog.tsx b/src/components/modals/SettingsDialog.tsx index 6fab235..0e37c43 100644 --- a/src/components/modals/SettingsDialog.tsx +++ b/src/components/modals/SettingsDialog.tsx @@ -44,7 +44,6 @@ const KEYBOARD_SHORTCUTS = [ shortcuts: [ { keys: ["Ctrl", "Z"], desc: "Undo last action" }, { keys: ["Ctrl", "Y"], desc: "Redo last action" }, - { keys: ["Ctrl", "S"], desc: "Show autosave reminder" }, ], }, { @@ -529,9 +528,8 @@ export function SettingsDialog({ Troubleshooting

- If you encounter issues with saved games or stale settings, clearing the - app cache will reset all local data. Your disk-saved game files (.json) - remain untouched. + If you encounter issues with stale settings or a corrupted board layout, + clearing the app cache will reset your saved board and all preferences.

diff --git a/src/components/panels/SolverPanel.tsx b/src/components/panels/SolverPanel.tsx index f933b0d..45dc399 100644 --- a/src/components/panels/SolverPanel.tsx +++ b/src/components/panels/SolverPanel.tsx @@ -332,7 +332,7 @@ export function SolverPanel({ key={p.id} variant={activePawn === p.id ? "default" : "outline"} onClick={() => { if (!isMuted) playClickSound(); setActivePawn(p.id); }} - className={`neo-brutalism-button bg-card hover:bg-stone-100 dark:hover:bg-stone-850 text-foreground border-stone-950 h-11 md:h-9 ${activePawn === p.id ? p.colorClass + " text-stone-950 font-extrabold translate-x-[1px] translate-y-[1px] shadow-[1px_1px_0_0_#000000]" : ""}`} + className={`neo-brutalism-button bg-card hover:bg-stone-100 hover:bg-stone-800 text-foreground border-stone-950 h-11 md:h-9 ${activePawn === p.id ? p.colorClass + " text-stone-950 font-extrabold translate-x-[1px] translate-y-[1px] shadow-[1px_1px_0_0_#000000]" : ""}`} > {p.name} diff --git a/src/lib/boardScanner.ts b/src/lib/boardScanner.ts index b32fe0b..1bd47d5 100644 --- a/src/lib/boardScanner.ts +++ b/src/lib/boardScanner.ts @@ -262,7 +262,6 @@ export function isFixedCell(row: number, col: number): boolean { export async function scanBoard( boardImageElement: HTMLImageElement | HTMLCanvasElement, corners: [CornerPoint, CornerPoint, CornerPoint, CornerPoint], - _atlasUrl: string, // kept for API compatibility, no longer used onProgress?: (pct: number) => void ): Promise { const templates = await loadTileTemplates(); diff --git a/src/types.ts b/src/types.ts index 1babc83..a73bbad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,12 +48,6 @@ export interface AppGameState { pawnPositions: PawnPositions; } -export interface SaveSlot { - name: string; - key: string; - timestamp: number; -} - export interface BoardScanCell { row: number; col: number; From 4f11c61939d3581d809c36fd011443a56889e434 Mon Sep 17 00:00:00 2001 From: Joshua Leshnick Date: Fri, 24 Jul 2026 16:31:53 -0500 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20UX=20improvements=20=E2=80=94=20Sta?= =?UTF-8?q?ts=20button,=20tablet=20toolbar,=20split=20timer,=20turn-model?= =?UTF-8?q?=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppHeader: lower desktop action toolbar breakpoint from lg: to md: so tablets (iPad, small laptops) no longer lose undo/redo/rotate/mute controls - AppHeader: Stats is now reachable on desktop — adds a BarChart2 button in the game-mode toolbar wired to onToggleStats (was previously aliased unused _onToggleStats; the only working open path was the mobile compact panel Gauge button) - AppHeader: split timer control out of the overloaded Play/pause/resume step-nav button — Play Game now only starts/ends the game; timer shows as a separate pill with its own pause/resume toggle and Pause/Clock icon - AppHeader: activate showStats prop to highlight the Stats button when open - SetupPanel: swap RotateCw → RefreshCcw for Reset Layout to disambiguate from the Rotate Board button in the header (RotateCw now unambiguously means board rotation) - WelcomeGuide: add step 4 explaining the 2-phase slide→move turn model and how to set a custom target by tapping any board tile Co-Authored-By: Claude Sonnet 4.6 --- src/components/AppHeader.tsx | 59 +++++++++++++++++--------- src/components/modals/WelcomeGuide.tsx | 20 ++++++--- src/components/panels/SetupPanel.tsx | 4 +- 3 files changed, 56 insertions(+), 27 deletions(-) diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 88ba3a2..b07c864 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -19,6 +19,8 @@ import { Settings2, HelpCircle, Clock, + Pause, + BarChart2, } from "lucide-react"; export interface AppHeaderProps { @@ -81,7 +83,7 @@ export function AppHeader({ canUndo, canRedo, isMuted, - showStats: _showStats, + showStats, baseTheme: _baseTheme, activePlayers, activePawn, @@ -96,7 +98,7 @@ export function AppHeader({ onRedo, onOpenHistory, onRotateBoard, - onToggleStats: _onToggleStats, + onToggleStats, onStartGame, onEndGame, onToggleMute, @@ -135,30 +137,23 @@ export function AppHeader({ - {/* Center — Step Nav (timer baked into Play Game button) */} + {/* Center — Step Nav + timer pill */}
{STEPS.map((s) => { const isActive = s.id === currentStep; const isDisabled = s.id === "game" && !isGameStarted && !canStartGame; - const showTimer = s.id === "game" && isActive && elapsedTime; return ( ); })}
+ + {/* Timer pill — visible during game, separate from step nav */} + {isGameStarted && elapsedTime && onToggleTimer && ( + + + + )}
{/* Right — toolbar */} @@ -272,8 +278,8 @@ export function AppHeader({ )} - {/* Desktop actions toolbar */} -
+ {/* Tablet/desktop actions toolbar — visible at md+ */} +
+ {isGameStarted && onToggleStats && ( + <> +
+ + + + + )}
))} diff --git a/src/components/panels/SetupPanel.tsx b/src/components/panels/SetupPanel.tsx index 6813c66..cf1d5d2 100644 --- a/src/components/panels/SetupPanel.tsx +++ b/src/components/panels/SetupPanel.tsx @@ -2,7 +2,7 @@ // sheet (< md) and the tablet/desktop side column (md+). Unprefixed classes // target the phone sheet; `md:` targets the tablet column; `lg:` targets the // wider desktop column. Interactive controls get a 44px phone floor. -import { Sparkles, Layers, Users, Compass, Play, RotateCw, Camera } from "lucide-react"; +import { Sparkles, Layers, Users, Compass, Play, RefreshCcw, Camera } from "lucide-react"; import { SidePanel } from "./SidePanel"; import { Button } from "../ui/button"; import { PAWNS, TREASURES } from "../../constants"; @@ -170,7 +170,7 @@ export function SetupPanel({ aria-label="Reset layout" className="border-stone-800 text-stone-400 hover:text-stone-200 hover:bg-stone-900 min-h-11 w-11 shrink-0 px-0 cursor-pointer" > - +
From 13634de26a1c4af11de2e614a32a4bc959f4f0b0 Mon Sep 17 00:00:00 2001 From: Joshua Leshnick Date: Fri, 24 Jul 2026 16:36:33 -0500 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20solver=20improvements=20=E2=80=94?= =?UTF-8?q?=20depth=20control,=201-move=20targets=20on=20by=20default,=20f?= =?UTF-8?q?ull=20sequence=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Default showOneMoveTargets to true so reachable-in-one-move targets are visible immediately without needing to toggle them on - Replace hardcoded SOLVER_MAX_TURNS=3 constant with state (solverDepth) initialized from localStorage (labyrinth_solver_depth); defaults to 3 and re-triggers the solver effect when changed so results update instantly - Add solver depth selector (1–5) in SettingsDialog Experimental section, wired through AppHeaderProps → AppHeader → SettingsDialog; shows (default)/(faster)/(slower) hint - Remove multi-card sequence truncation in SolverPanel: show full card order with → arrows instead of slicing at 3 with an ellipsis Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 27 ++++++++++++++------ src/components/AppHeader.tsx | 6 +++++ src/components/modals/SettingsDialog.tsx | 32 ++++++++++++++++++++++++ src/components/panels/SolverPanel.tsx | 5 ++-- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0dea10c..e5b5837 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,11 +43,8 @@ import { } from "lucide-react"; import { cn } from "./lib/utils"; -// Search depth for the solver worker. Always searches this many turns ahead -// rather than exposing it as a user-facing option — solveAllHand already -// ranks shorter/safer paths first, so a wider search can only surface better -// suggestions, never worse ones. -const SOLVER_MAX_TURNS = 3; +// Default solver depth. Can be overridden via the Advanced settings panel. +const DEFAULT_SOLVER_DEPTH = 3; export default function App() { const sensors = useSensors( @@ -91,7 +88,11 @@ export default function App() { const [turnPhase, setTurnPhase] = useState<"slide" | "move">("slide"); const [stagedArrow, setStagedArrow] = useState(null); const [stagedRotation, setStagedRotation] = useState<0 | 90 | 180 | 270>(0); - const [showOneMoveTargets, setShowOneMoveTargets] = useState(false); + const [showOneMoveTargets, setShowOneMoveTargets] = useState(true); + const [solverDepth, setSolverDepthState] = useState(() => { + const saved = parseInt(localStorage.getItem("labyrinth_solver_depth") ?? ""); + return [1, 2, 3, 4, 5].includes(saved) ? saved : DEFAULT_SOLVER_DEPTH; + }); const [mobilePanelStop, setMobilePanelStop] = useState<"peek" | "expanded">("expanded"); const [hasShownSetupHint, setHasShownSetupHint] = useState(false); const [showWelcomeGuide, setShowWelcomeGuide] = useState( @@ -152,6 +153,15 @@ export default function App() { } }, [baseTheme]); + const setSolverDepth = useCallback((depth: number) => { + setSolverDepthState(depth); + try { + localStorage.setItem("labyrinth_solver_depth", String(depth)); + } catch { + /* storage full */ + } + }, []); + // ── Accent color ────────────────────────────────────────────────────────────── const applyAccentColor = useCallback((hex: string) => { if (!hex) return; @@ -436,7 +446,7 @@ export default function App() { pawnPos: currentPawnCoord, handCards, lastShiftArrowId: game.lastShiftArrowId, - maxTurns: SOLVER_MAX_TURNS, + maxTurns: solverDepth, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -450,6 +460,7 @@ export default function App() { game.getSolverFormattedBoard, game.getSolverFormattedSpare, game.customTargetCoords, + solverDepth, ]); // ── Drag and Drop ───────────────────────────────────────────────────────────── @@ -784,6 +795,8 @@ export default function App() { onToggleTimer={toggleTimer} is3D={is3D} onToggle3D={toggle3D} + solverDepth={solverDepth} + onSetSolverDepth={setSolverDepth} />
diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index b07c864..9756e6a 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -58,6 +58,8 @@ export interface AppHeaderProps { onToggleTimer?: () => void; is3D?: boolean; onToggle3D: () => void; + solverDepth?: number; + onSetSolverDepth?: (depth: number) => void; } const iconBtnCls = @@ -113,6 +115,8 @@ export function AppHeader({ onToggleTimer, is3D: _is3D, onToggle3D: _onToggle3D, + solverDepth, + onSetSolverDepth, }: AppHeaderProps) { const [showEndGameConfirm, setShowEndGameConfirm] = useState(false); @@ -392,6 +396,8 @@ export function AppHeader({ setAccentColor={setAccentColor} is3D={_is3D} onToggle3D={_onToggle3D} + solverDepth={solverDepth} + onSetSolverDepth={onSetSolverDepth} />
diff --git a/src/components/modals/SettingsDialog.tsx b/src/components/modals/SettingsDialog.tsx index 0e37c43..4a5ba18 100644 --- a/src/components/modals/SettingsDialog.tsx +++ b/src/components/modals/SettingsDialog.tsx @@ -24,6 +24,8 @@ interface SettingsDialogProps { setAccentColor: (hex: string) => void; is3D?: boolean; onToggle3D?: () => void; + solverDepth?: number; + onSetSolverDepth?: (depth: number) => void; } const ACCENT_PRESETS = [ @@ -66,6 +68,8 @@ export function SettingsDialog({ setAccentColor, is3D = false, onToggle3D, + solverDepth = 3, + onSetSolverDepth, }: SettingsDialogProps) { const [settingsTab, setSettingsTab] = useState<"preferences" | "appearance" | "application">("preferences"); @@ -287,6 +291,34 @@ export function SettingsDialog({ {is3D ? "On" : "Off"}
+ {onSetSolverDepth && ( +
+
+
Solver Search Depth
+
+ Turns looked ahead. Higher values find more creative solutions but take longer to compute. +
+
+
+ {[1, 2, 3, 4, 5].map((d) => ( + + ))} + + {solverDepth === 3 ? "(default)" : solverDepth > 3 ? "(slower)" : "(faster)"} + +
+
+ )} )} diff --git a/src/components/panels/SolverPanel.tsx b/src/components/panels/SolverPanel.tsx index 45dc399..af4345d 100644 --- a/src/components/panels/SolverPanel.tsx +++ b/src/components/panels/SolverPanel.tsx @@ -299,9 +299,8 @@ export function SolverPanel({ {TREASURES.find((t) => t.id === (sol as { cardId?: string }).cardId)?.name ?? (sol as { cardId?: string }).cardId} {(sol as { sequenceOrder?: string[] }).sequenceOrder && (sol as { sequenceOrder?: string[] }).sequenceOrder!.length > 1 && ( - - → then {(sol as { sequenceOrder?: string[] }).sequenceOrder!.slice(1, 3).map((id) => TREASURES.find((t) => t.id === id)?.name ?? id).join(", ")} - {(sol as { sequenceOrder?: string[] }).sequenceOrder!.length > 3 ? "…" : ""} + + → then {(sol as { sequenceOrder?: string[] }).sequenceOrder!.slice(1).map((id) => TREASURES.find((t) => t.id === id)?.name ?? id).join(" → ")} )} From 4bcf0d312e5dad285655a13b510e906df9051acc Mon Sep 17 00:00:00 2001 From: Joshua Leshnick Date: Fri, 24 Jul 2026 16:38:49 -0500 Subject: [PATCH 4/4] fix: restore timer inside Game button, rename Play Game -> Game Re-integrates the stopwatch display and pause/resume behavior back into the step-nav button (matching the original design). Clicking the active Game button toggles the timer; clicking when the game hasn't started begins the game. Also renames the button label from "Play Game" to "Game" for brevity. Co-Authored-By: Claude Sonnet 4.6 --- src/components/AppHeader.tsx | 39 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 9756e6a..4b05c24 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -19,7 +19,6 @@ import { Settings2, HelpCircle, Clock, - Pause, BarChart2, } from "lucide-react"; @@ -74,8 +73,8 @@ const STEPS = [ }, { id: "game" as const, - label: "Play Game", - shortLabel: "Play", + label: "Game", + shortLabel: "Game", icon: , }, ]; @@ -141,23 +140,30 @@ export function AppHeader({ - {/* Center — Step Nav + timer pill */} + {/* Center — Step Nav (timer integrated in Game button) */}
{STEPS.map((s) => { const isActive = s.id === currentStep; const isDisabled = s.id === "game" && !isGameStarted && !canStartGame; + const showTimer = s.id === "game" && isActive && elapsedTime; return ( ); })}
- - {/* Timer pill — visible during game, separate from step nav */} - {isGameStarted && elapsedTime && onToggleTimer && ( - - - - )}
{/* Right — toolbar */}