import React, { useEffect, useState, useMemo } from "react";
// EscapePuzzle_500Levels_React
// Single-file React app (default export) ready to paste into a Create React App, Vite + React, or preview in the canvas.
// Uses Tailwind utility classes for styling (no imports needed). Designed to be production-ready scaffold.
// Features implemented:
// - Procedural generation of 500 levels with multiple puzzle types
// - Progress save/load via localStorage
// - Hints, skip (limited), level timer, star rating
// - Level selector and compact map view
// - Simple, extensible puzzle engine where new puzzle types can be added
// - Minimal asset footprint (no external images)
// - Instructions, accessibility-friendly labels
// How to use:
// - Drop this file into a React project and render in index.js
// - Tailwind recommended for styling. If Tailwind is not available, the app will still work but styling will differ.
// - Customize number of levels by changing LEVEL_COUNT constant.
const LEVEL_COUNT = 500;
const STORAGE_KEY = "escape_puzzle_progress_v1";
// Utility helpers
const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const shuffle = (arr) => arr.sort(() => Math.random() - 0.5);
// Level generator: produces an object describing the puzzle type and parameters
function generateLevel(i) {
// seed-ish behaviour derived from index to keep repeatability per run
const difficulty = Math.min(1 + Math.floor(i / 50), 10); // 1..10
const types = ["find-code", "pattern", "slider", "sequence", "riddle"];
// bias: early levels easy, later levels mixed
let type = types[(i + Math.floor(i / 7)) % types.length];
if (i < 10) type = types[i % types.length];
switch (type) {
case "find-code": {
// grid where some tiles show digits/letters; correct code is formed by following clue
const gridSize = 3 + Math.min(3, Math.floor(difficulty / 3));
const symbols = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".split("");
const grid = Array.from({ length: gridSize }, () =>
Array.from({ length: gridSize }, () => symbols[rand(0, symbols.length - 1)])
);
// place a hidden word (3-6 chars)
const wordLen = 3 + (difficulty % 4);
const word = Array.from({ length: wordLen }, () => symbols[rand(0, symbols.length - 1)]).join("");
// choose a path for the word
const path = [];
let r = rand(0, gridSize - 1), c = rand(0, gridSize - 1);
for (let k = 0; k < wordLen; k++) {
path.push([r, c]);
// move
r = Math.min(gridSize - 1, Math.max(0, r + rand(-1, 1)));
c = Math.min(gridSize - 1, Math.max(0, c + rand(-1, 1)));
}
// overwrite grid along path with letters from word
for (let k = 0; k < path.length; k++) {
const [rr, cc] = path[k];
grid[rr][cc] = word[k];
}
return { type, gridSize, grid, wordLength: wordLen, clue: `Find the ${wordLen}-char escape code hidden in the grid.` };
}
case "pattern": {
// show pattern with shapes/colors; player must click the missing piece number
const seqLen = 3 + (difficulty % 5);
const base = rand(1, 6);
const step = rand(1, 4);
const sequence = Array.from({ length: seqLen }, (_, idx) => base + idx * step);
const missingIndex = rand(0, seqLen - 1);
const choices = shuffle([
...Array.from({ length: 5 }, () => base + rand(-3, 10)),
sequence[missingIndex],
]).slice(0, 5);
if (!choices.includes(sequence[missingIndex])) choices[0] = sequence[missingIndex];
return { type, sequence, missingIndex, choices, clue: `Find the missing number in the pattern.` };
}
case "slider": {
// a simple scrambled phrase slider where player must rearrange tiles
const words = ["OPEN", "DOOR", "GOLD", "KEY", "SAFE", "TIME", "LOCK", "CODE"];
const len = 3 + (difficulty % 4);
const phrase = shuffle(words).slice(0, len);
const scrambled = shuffle([...phrase]);
return { type, phrase, scrambled, clue: `Rearrange tiles to form the escape phrase (${len} words).` };
}
case "sequence": {
// arithmetic or geometric sequence with missing next value
const a = rand(1, 6);
const r = rand(2, 5);
const len = 4 + (difficulty % 3);
const seq = Array.from({ length: len }, (_, k) => a * Math.pow(r, k));
const askIndex = len - 1;
return { type, seq: seq.slice(0, len - 1), answer: seq[askIndex], clue: `Continue the sequence.` };
}
case "riddle": {
const riddles = [
{ q: "I have keys but no locks, I have space but no rooms. What am I?", a: "keyboard" },
{ q: "What has hands but cannot clap?", a: "clock" },
{ q: "What runs but never walks?", a: "water" },
{ q: "I speak without a mouth and hear without ears. What am I?", a: "echo" },
];
const r = riddles[i % riddles.length];
return { type, riddle: r.q, answer: r.a, clue: `Answer the riddle to escape.` };
}
default:
return { type: "riddle", riddle: "An enigma for this level.", answer: "mystery", clue: "Solve the enigma." };
}
}
// Pre-generate level metadata (lightweight) - procedural so file size small
const LEVELS = Array.from({ length: LEVEL_COUNT }, (_, i) => ({ id: i + 1, meta: generateLevel(i) }));
// Save / Load progress
function loadProgress() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
function saveProgress(data) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
// Small components
function Button({ children, onClick, className = "" }) {
return (
);
}
function Header({ title, subtitle }) {
return (