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..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; @@ -309,11 +319,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); @@ -441,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 }, [ @@ -455,6 +460,7 @@ export default function App() { game.getSolverFormattedBoard, game.getSolverFormattedSpare, game.customTargetCoords, + solverDepth, ]); // ── Drag and Drop ───────────────────────────────────────────────────────────── @@ -789,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 88ba3a2..4b05c24 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -19,6 +19,7 @@ import { Settings2, HelpCircle, Clock, + BarChart2, } from "lucide-react"; export interface AppHeaderProps { @@ -56,6 +57,8 @@ export interface AppHeaderProps { onToggleTimer?: () => void; is3D?: boolean; onToggle3D: () => void; + solverDepth?: number; + onSetSolverDepth?: (depth: number) => void; } const iconBtnCls = @@ -70,8 +73,8 @@ const STEPS = [ }, { id: "game" as const, - label: "Play Game", - shortLabel: "Play", + label: "Game", + shortLabel: "Game", icon: , }, ]; @@ -81,7 +84,7 @@ export function AppHeader({ canUndo, canRedo, isMuted, - showStats: _showStats, + showStats, baseTheme: _baseTheme, activePlayers, activePawn, @@ -96,7 +99,7 @@ export function AppHeader({ onRedo, onOpenHistory, onRotateBoard, - onToggleStats: _onToggleStats, + onToggleStats, onStartGame, onEndGame, onToggleMute, @@ -111,6 +114,8 @@ export function AppHeader({ onToggleTimer, is3D: _is3D, onToggle3D: _onToggle3D, + solverDepth, + onSetSolverDepth, }: AppHeaderProps) { const [showEndGameConfirm, setShowEndGameConfirm] = useState(false); @@ -135,7 +140,7 @@ export function AppHeader({ - {/* Center — Step Nav (timer baked into Play Game button) */} + {/* Center — Step Nav (timer integrated in Game button) */}
{STEPS.map((s) => { @@ -272,8 +277,8 @@ export function AppHeader({
)} - {/* Desktop actions toolbar */} -
+ {/* Tablet/desktop actions toolbar — visible at md+ */} +
+ {isGameStarted && onToggleStats && ( + <> +
+ + + + + )}
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..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 = [ @@ -44,7 +46,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" }, ], }, { @@ -67,6 +68,8 @@ export function SettingsDialog({ setAccentColor, is3D = false, onToggle3D, + solverDepth = 3, + onSetSolverDepth, }: SettingsDialogProps) { const [settingsTab, setSettingsTab] = useState<"preferences" | "appearance" | "application">("preferences"); @@ -288,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)"} + +
+
+ )}
)} @@ -529,9 +560,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.

@@ -285,7 +285,7 @@ export function SetupPanel({ size="sm" variant={alreadyInHand ? "secondary" : "outline"} onClick={() => (alreadyInHand ? onRemoveCard(t.id) : onAddCard(t.id))} - className={`text-[10px] md:text-xs py-1 justify-start h-11 md:h-9 lg:h-8 px-2 truncate neo-brutalism-button bg-card hover:bg-stone-100 dark:hover:bg-stone-850 text-foreground border-2 border-stone-950 shadow-[2px_2px_0_0_#000000] ${alreadyInHand ? "bg-theme-primary text-stone-950 shadow-[1px_1px_0_0_#000000] translate-x-[1px] translate-y-[1px]" : ""}`} + className={`text-[10px] md:text-xs py-1 justify-start h-11 md:h-9 lg:h-8 px-2 truncate neo-brutalism-button bg-card hover:bg-stone-800 text-foreground border-2 border-stone-950 shadow-[2px_2px_0_0_#000000] ${alreadyInHand ? "bg-theme-primary text-stone-950 shadow-[1px_1px_0_0_#000000] translate-x-[1px] translate-y-[1px]" : ""}`} > {t.name} diff --git a/src/components/panels/SolverPanel.tsx b/src/components/panels/SolverPanel.tsx index f933b0d..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(" → ")} )}
@@ -332,7 +331,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;