Skip to content

Commit

Permalink
Add dropdowns for algorithms
Browse files Browse the repository at this point in the history
Temporary scroll solution for too many settings

Closes #1
  • Loading branch information
imericxu committed Jul 10, 2024
1 parent c91fe4b commit bafd70a
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 52 deletions.
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 74 additions & 0 deletions src/app/components/GlassSelect.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Select
name={name}
className="flex flex-col items-start"
defaultSelectedKey={defaultSelectedKey}
>
<Label className="px-3 text-sm">{label}</Label>

{/* Select Button */}
<Button className="glass-tube-container h-8 w-48 text-nowrap pe-9 ps-3 text-start transition-colors hover:bg-blue-500/20">
<SelectValue className="block w-full overflow-hidden overflow-ellipsis" />
{/* Down arrow. Flips over on open */}
<ChevronDown className="absolute end-3 top-1/2 -translate-y-1/2 stroke-1 transition-transform group-open/select:rotate-180" />
</Button>

{/* Dropdown */}
<Popover>
<ListBox
items={items}
className="glass-surface flex flex-col gap-1 rounded-md bg-slate-50/20 p-2"
>
{(item) => (
<ListBoxItem
key={item[0]}
id={item[0]}
textValue={item[1]}
className="group/item relative flex h-8 cursor-pointer select-none items-center gap-1 ps-7 focus:outline-none"
>
{/* Blurred background on focus */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 bg-gradient-to-r from-blue-500/30 opacity-0 blur transition duration-200 group-focus/item:opacity-100"
></div>

{/* Checkmark */}
<Check
aria-hidden
className="invisible absolute left-0 top-1/2 shrink-0 -translate-y-1/2 stroke-1 group-selected/item:visible"
/>

{/* Label */}
<span>{item[1]}</span>
</ListBoxItem>
)}
</ListBox>
</Popover>
</Select>
);
}
45 changes: 43 additions & 2 deletions src/app/components/OptionsForm.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -12,6 +17,20 @@ import {
TextField,
} from "react-aria-components";
import { twMerge } from "tailwind-merge";
import GlassSelect from "./GlassSelect";

const GENERATION_ALGORITHMS: Record<GenerationAlgorithm, string> = {
random: "Surprise Me!",
prims: "Prim’s Algorithm",
wilsons: "Wilson’s Algorithm",
backtracker: "Recursive Backtracker",
} as const;

const SOLVE_ALGORITHMS: Record<SolveAlgorithm, string> = {
random: "Surprise Me!",
bfs: "Flood Fill (BFS)",
tremaux: "Trémaux’s Algorithm (DFS)",
} as const;

export interface OptionsFormProps {
onAction: (action: FormActionType, settings: MazeSettings) => void;
Expand Down Expand Up @@ -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";

Expand All @@ -51,7 +76,9 @@ export default function OptionsForm({
cols,
cellWallRatio,
},
generationAlgorithm,
doAnimateGenerating,
solveAlgorithm,
doAnimateSolving,
};

Expand Down Expand Up @@ -138,6 +165,14 @@ export default function OptionsForm({
</div>
</TextField>

{/* Generation Algorithm Dropdown */}
<GlassSelect
name="generationAlgorithm"
defaultSelectedKey="random"
label="Generation Algorithm"
items={Object.entries(GENERATION_ALGORITHMS)}
/>

{/* Animate Generating Switch */}
<Switch
name="doAnimateGeneration"
Expand All @@ -153,7 +188,13 @@ export default function OptionsForm({
</div>
</Switch>

{/* Generation Algorithms Select */}
{/* Solve Algorithm Dropdown */}
<GlassSelect
name="solveAlgorithm"
defaultSelectedKey="random"
label="Solve Algorithm"
items={Object.entries(SOLVE_ALGORITHMS)}
/>

{/* Animate Solving Switch */}
<Switch
Expand Down
9 changes: 6 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import { MazeController, type MazeSettings } from "@/lib/MazeController";
import type { MazeSettings } from "@/lib/maze";
import { MazeController } from "@/lib/MazeController";
import { type RectSize } from "@/lib/twoDimens";
import {
useCallback,
Expand Down Expand Up @@ -60,8 +61,10 @@ export default function Home(): ReactElement {

return (
<main className="flex flex-col items-center gap-8 p-4">
<div className="flex flex-col items-center gap-4">
<OptionsForm onAction={onAction} solvable={solvable} />
<div className="flex flex-col items-center gap-2">
<div className="w-screen overflow-x-scroll p-2">
<OptionsForm onAction={onAction} solvable={solvable} />
</div>

{/* Zoom settings island */}
<RadioGroup
Expand Down
69 changes: 37 additions & 32 deletions src/lib/MazeController.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,27 @@
import { Mutex } from "async-mutex";
import { match } from "ts-pattern";
import {
type GenerationAlgorithm,
type SolveAlgorithm,
} from "./algorithms/algorithmTypes";
import { MazeGenerator } from "./algorithms/generating/MazeGenerator";
import { Wilsons } from "./algorithms/generating/Wilsons";
import { BFS } from "./algorithms/solving/BFS";
import { MazeSolver } from "./algorithms/solving/MazeSolver";
Backtracker,
BFS,
MazeGenerator,
MazeSolver,
Prims,
Tremaux,
Wilsons,
} from "./algorithms/algorithms";
import { AnimationPromise } from "./AnimationPromise";
import {
GENERATION_ALGORITHMS,
GenerationAlgorithm,
SOLVE_ALGORITHMS,
SolveAlgorithm,
type MazeDimensions,
type MazeSettings,
} from "./maze";
import { type MazeCell } from "./MazeCell";
import MazeDrawer from "./MazeDrawer";
import { type RectSize } from "./twoDimens";
import { clamp, deepEqual } from "./utils";

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;
generatingAlgorithm?: GenerationAlgorithm;
doAnimateGenerating: boolean;
solvingAlgorithm?: SolveAlgorithm;
doAnimateSolving: boolean;
}
import type { RectSize } from "./twoDimens";
import { clamp, deepEqual, randomFromArray } from "./utils";

export class MazeController {
/** Range of rows/cols allowed. */
Expand All @@ -51,7 +39,9 @@ export class MazeController {
cols: 20,
cellWallRatio: MazeDrawer.DEFAULT_VALUES.cellWallRatio,
},
generationAlgorithm: "random",
doAnimateGenerating: true,
solveAlgorithm: "random",
doAnimateSolving: true,
} as const;

Expand Down Expand Up @@ -97,7 +87,15 @@ export class MazeController {
const { rows, cols } = this.dimensions;

await this.stopMazeAnimation();
const alg: MazeGenerator = new Wilsons({ rows, cols });
const algType: Exclude<GenerationAlgorithm, "random"> =
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;

Expand Down Expand Up @@ -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<SolveAlgorithm, "random"> =
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) {
Expand Down
9 changes: 2 additions & 7 deletions src/lib/MazeDrawer.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down
7 changes: 0 additions & 7 deletions src/lib/algorithms/algorithmTypes.ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/lib/algorithms/algorithms.ts
Original file line number Diff line number Diff line change
@@ -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 };
2 changes: 1 addition & 1 deletion src/lib/algorithms/generating/Backtracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MazeCell>[]] {
Expand Down
29 changes: 29 additions & 0 deletions src/lib/maze.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit bafd70a

Please sign in to comment.