diff --git a/package-lock.json b/package-lock.json index 53a8655..d38dc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@datastructures-js/deque": "^1.0.4", "async-mutex": "^0.5.0", "immer": "^10.1.1", + "lucide-react": "^0.407.0", "next": "14.2.4", "react": "^18", "react-aria-components": "^1.2.1", @@ -7105,6 +7106,15 @@ "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.407.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.407.0.tgz", + "integrity": "sha512-+dRIu9Sry+E8wPF9+sY5eKld2omrU4X5IKXxrgqBt+o11IIHVU0QOfNoVWFuj0ZRDrxr4Wci26o2mKZqLGE0lA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/package.json b/package.json index b531615..b3ca13b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@datastructures-js/deque": "^1.0.4", "async-mutex": "^0.5.0", "immer": "^10.1.1", + "lucide-react": "^0.407.0", "next": "14.2.4", "react": "^18", "react-aria-components": "^1.2.1", diff --git a/src/app/components/GlassSelect.tsx b/src/app/components/GlassSelect.tsx new file mode 100644 index 0000000..b45fbce --- /dev/null +++ b/src/app/components/GlassSelect.tsx @@ -0,0 +1,74 @@ +import { ChevronDown, Check } from "lucide-react"; +import { ReactElement } from "react"; +import { + Select, + Label, + Button, + SelectValue, + Popover, + ListBox, + ListBoxItem, +} from "react-aria-components"; + +export interface GlassSelectProps { + name: string; + defaultSelectedKey: string; + label: string; + items: [string, string][]; +} + +export default function GlassSelect({ + name, + defaultSelectedKey, + label, + items, +}: GlassSelectProps): ReactElement { + return ( + + ); +} diff --git a/src/app/components/OptionsForm.tsx b/src/app/components/OptionsForm.tsx index 8c10e6a..bf2e9cc 100644 --- a/src/app/components/OptionsForm.tsx +++ b/src/app/components/OptionsForm.tsx @@ -1,5 +1,10 @@ import { type FormActionType } from "@/app/page"; -import { MazeController, type MazeSettings } from "@/lib/MazeController"; +import type { + GenerationAlgorithm, + MazeSettings, + SolveAlgorithm, +} from "@/lib/maze"; +import { MazeController } from "@/lib/MazeController"; import { getFloatFromForm, getIntFromForm } from "@/lib/utils"; import { useCallback, useRef, type ReactElement } from "react"; import { @@ -12,6 +17,20 @@ import { TextField, } from "react-aria-components"; import { twMerge } from "tailwind-merge"; +import GlassSelect from "./GlassSelect"; + +const GENERATION_ALGORITHMS: Record = { + random: "Surprise Me!", + prims: "Prim’s Algorithm", + wilsons: "Wilson’s Algorithm", + backtracker: "Recursive Backtracker", +} as const; + +const SOLVE_ALGORITHMS: Record = { + random: "Surprise Me!", + bfs: "Flood Fill (BFS)", + tremaux: "Trémaux’s Algorithm (DFS)", +} as const; export interface OptionsFormProps { onAction: (action: FormActionType, settings: MazeSettings) => void; @@ -40,8 +59,14 @@ export default function OptionsForm({ const cellWallRatio: number = getFloatFromForm(formData, "cellWallRatio") ?? MazeController.DEFAULTS.dimensions.cellWallRatio; + const generationAlgorithm: GenerationAlgorithm = formData.get( + "generationAlgorithm", + ) as GenerationAlgorithm; const doAnimateGenerating: boolean = formData.get("doAnimateGeneration") === "on"; + const solveAlgorithm: SolveAlgorithm = formData.get( + "solveAlgorithm", + ) as SolveAlgorithm; const doAnimateSolving: boolean = formData.get("doAnimateSolving") === "on"; @@ -51,7 +76,9 @@ export default function OptionsForm({ cols, cellWallRatio, }, + generationAlgorithm, doAnimateGenerating, + solveAlgorithm, doAnimateSolving, }; @@ -138,6 +165,14 @@ export default function OptionsForm({ + {/* Generation Algorithm Dropdown */} + + {/* Animate Generating Switch */} - {/* Generation Algorithms Select */} + {/* Solve Algorithm Dropdown */} + {/* Animate Solving Switch */} -
- +
+
+ +
{/* Zoom settings island */} = + settings.generationAlgorithm === "random" + ? randomFromArray(GENERATION_ALGORITHMS.filter((x) => x !== "random")) + : settings.generationAlgorithm; + const alg: MazeGenerator = match(algType) + .with("backtracker", () => new Backtracker({ rows, cols })) + .with("wilsons", () => new Wilsons({ rows, cols })) + .with("prims", () => new Prims({ rows, cols })) + .exhaustive(); this.maze = alg.maze; this.drawer.maze = this.maze; @@ -165,7 +163,14 @@ export class MazeController { const [start, end] = this.randomStartEnd(); this.drawer.startEnd = [start, end]; - const alg: MazeSolver = new BFS(this.maze, start, end); + const algType: Exclude = + settings.solveAlgorithm === "random" + ? randomFromArray(SOLVE_ALGORITHMS.filter((x) => x !== "random")) + : settings.solveAlgorithm; + const alg: MazeSolver = match(algType) + .with("bfs", () => new BFS(this.maze!, start, end)) + .with("tremaux", () => new Tremaux(this.maze!, start, end)) + .exhaustive(); if (settings.doAnimateSolving) { if (this.shouldSweep) { diff --git a/src/lib/MazeDrawer.ts b/src/lib/MazeDrawer.ts index 00f552a..7f89900 100644 --- a/src/lib/MazeDrawer.ts +++ b/src/lib/MazeDrawer.ts @@ -1,14 +1,9 @@ import colors from "tailwindcss/colors"; import { match } from "ts-pattern"; import { AnimationPromise } from "./AnimationPromise"; +import type { MazeDimensions, MazeEvent } from "./maze"; import { type MazeCell } from "./MazeCell"; -import { type MazeDimensions, type MazeEvent } from "./MazeController"; -import { - type RectSize, - type Coord, - type GridSize, - type Idx2d, -} from "./twoDimens"; +import type { Coord, GridSize, Idx2d, RectSize } from "./twoDimens"; import { clamp, easeOutQuad, type Direction } from "./utils"; /** Colors used by the renderer. */ diff --git a/src/lib/algorithms/algorithmTypes.ts b/src/lib/algorithms/algorithmTypes.ts deleted file mode 100644 index 0b73c49..0000000 --- a/src/lib/algorithms/algorithmTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type GenerationAlgorithm = - | "backtracking" - | "prims" - | "wilsons" - | "kruskals"; - -export type SolveAlgorithm = "tremaux" | "astar" | "breadth"; diff --git a/src/lib/algorithms/algorithms.ts b/src/lib/algorithms/algorithms.ts new file mode 100644 index 0000000..4a289ca --- /dev/null +++ b/src/lib/algorithms/algorithms.ts @@ -0,0 +1,9 @@ +import { Backtracker } from "./generating/Backtracking"; +import { MazeGenerator } from "./generating/MazeGenerator"; +import { Prims } from "./generating/Prims"; +import { Wilsons } from "./generating/Wilsons"; +import { BFS } from "./solving/BFS"; +import { MazeSolver } from "./solving/MazeSolver"; +import { Tremaux } from "./solving/Tremaux"; + +export { Backtracker, BFS, MazeGenerator, MazeSolver, Prims, Tremaux, Wilsons }; diff --git a/src/lib/algorithms/generating/Backtracking.ts b/src/lib/algorithms/generating/Backtracking.ts index 6235576..dea2721 100644 --- a/src/lib/algorithms/generating/Backtracking.ts +++ b/src/lib/algorithms/generating/Backtracking.ts @@ -8,7 +8,7 @@ import { randomFromArray } from "@/lib/utils"; * Randomly connects to unvisited nodes until it hits a dead end, then it * backtracks. This is a recursive algorithm, but it's implemented iteratively. */ -export class Backtracking extends MazeGenerator { +export class Backtracker extends MazeGenerator { private exploreStack: MazeCell[] = []; protected _step(): [boolean, Readonly[]] { diff --git a/src/lib/maze.ts b/src/lib/maze.ts new file mode 100644 index 0000000..2045f5f --- /dev/null +++ b/src/lib/maze.ts @@ -0,0 +1,29 @@ +export const GENERATION_ALGORITHMS = [ + "random", + "wilsons", + "backtracker", + "prims", +] as const; +export type GenerationAlgorithm = (typeof GENERATION_ALGORITHMS)[number]; +export const SOLVE_ALGORITHMS = ["random", "bfs", "tremaux"] as const; +export type SolveAlgorithm = (typeof SOLVE_ALGORITHMS)[number]; +export type MazeEvent = "generate" | "solve"; + +export interface MazeDimensions { + rows: number; + cols: number; + /** + * Width of cell vs wall. + * + * E.g., 0.5 means a cell is half the width of a wall. + */ + cellWallRatio: number; +} + +export interface MazeSettings { + dimensions: MazeDimensions; + generationAlgorithm: GenerationAlgorithm; + doAnimateGenerating: boolean; + solveAlgorithm: SolveAlgorithm; + doAnimateSolving: boolean; +}