commit c8eb84ce7e89b3e0e47de6e87726753a6946a9be Author: Andre Padez Date: Tue Oct 14 13:33:51 2025 +0100 first diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6051801 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules/ +bower_components/ + +# Build outputs +dist/ + +# TypeScript +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +*.log + +# Coverage & reports +coverage/ +*.lcov + +# Caches +.vite/ +.next/ +.nuxt/ +.svelte-kit/ +.angular/ +.cache/ +.parcel-cache/ +.turbo/ +.eslintcache +.stylelintcache + +# IDE & editor +.vscode/ +.idea/ +*.sw? +*.sublime-workspace +*.sublime-project + +# OS files +.DS_Store +Thumbs.db + +# Env files +.env +.env.* + +# Misc +*.pem +*.pfx +*.p12 +*.crt +*.key +*.cer + +# Bun +.bun/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a2cf60a --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +src/ +node_modules/ +tsconfig.json +bun.lockb +*.log + diff --git a/README.md b/README.md new file mode 100644 index 0000000..94c27ad --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# @vimazing/vim-maze + +![VIMazing maze demo](./vim-maze.gif) + +Lightweight, typed **React hooks** and utilities for building interactive maze games with VIM-inspired controls. + +Part of the [VIMazing](https://github.com/andrepadez/vimazing-vimaze) project. + +--- + +## Contents +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Core Hooks](#core-hooks) +- [Utilities](#utilities) +- [Example App](#example-app) +- [Build & Release](#build--release) +- [License](#license) +- [Acknowledgements](#acknowledgements) + +--- + +## Features +- **Drop-in hooks** for maze generation, player control, scoring, and key logging. +- **Typed API** with generated declaration files for editor IntelliSense. +- **VIM-style navigation** (`h`, `j`, `k`, `l`, counts, `i` to start, etc.). +- **Composable architecture** โ€“ bring your own rendering and platform-specific bindings. +- **Tree-shakeable exports** to keep bundles lean. + +--- + +## Installation + +Using **npm**: + +```bash +npm install @vimazing/vim-maze +``` + +Or with **bun**: + +```bash +bun add @vimazing/vim-maze +``` + +Optionally pull in the default maze styles: + +```ts +import "@vimazing/vim-maze/maze.css"; +``` + +--- + +## Quick Start + +```tsx +import { useEffect } from "react"; +import { useGame } from "@vimazing/vim-maze"; +import "@vimazing/vim-maze/maze.css"; + +export function MazeGame() { + const { containerRef, gameStatus, startGame } = useGame(20, 16); + + useEffect(() => { + startGame(); + }, [startGame]); + + return ( +
+

VIMazing Maze

+
+ {gameStatus === "game-won" &&

๐ŸŽ‰ You escaped!

} +
+ ); +} +``` + +Default controls: + +| Key | Action | +| ------------------- | --------------- | +| `i` | Start game | +| `h` / `j` / `k` / `l` | Move player | +| `` | Move with count | +| `q` | Quit game | +| `Esc` | Cancel / pause | + +--- + +## Core Hooks + +| Hook | Description | +| ---- | ----------- | +| `useGame(cols, rows, platformHook?)` | One-stop hook that wires maze generation, rendering, player state, scoring, and keyboard bindings. Returns refs, status helpers, and managers you can compose with your UI. | +| `useMaze()` | Generates maze paths, keeps references to the current maze instance, and exposes helpers to reset or rebuild. | +| `useKeyBindings()` | Provides VIM-style key bindings, including repeat counts and movement helpers. | +| `useScore()` | Tracks timers, optimal steps, efficiency, and final score calculation. | + +Each hook is exported individually, so you can cherry-pick only what you need: + +```ts +import { useScore } from "@vimazing/vim-maze/useScore"; +``` + +--- + +## Utilities + +Besides the hooks, the library exports: + +- `MazeGenerator` โ€“ procedural maze generation class. +- `MazeRenderer` โ€“ canvas renderer tailored for the generated mazes. +- `types` โ€“ shared TypeScript types such as `GameStatus`, `MazeCell`, and `KeyLogEntry`. + +```ts +import { MazeGenerator, GameStatus } from "@vimazing/vim-maze"; +``` + +--- + +## Example App + +A demo application lives under `example/` and consumes the package directly. + +```bash +cd example +bun install +bun run dev +``` + +During local development the Vite config aliases `@vimazing/vim-maze` to the source folder so you can iterate without rebuilding. When publishing, run the build first (see below) so editors consume the generated declarations in `dist/`. + +--- + +## Build & Release + +Build the distributable bundle and type declarations: + +```bash +bun run build +``` + +This writes JavaScript, type definitions, and styles to `dist/`. The `prepublishOnly` hook reuses the same command to guarantee fresh artifacts before publishing. + +--- + +## License + +MIT ยฉ [Andrรฉ Padez](https://github.com/andrepadez) + +--- + +## Acknowledgements + +Inspired by [The Art of Web โ€“ Random Maze Generator](https://www.the-art-of-web.com/javascript/maze-game/) and extended for the VIMazing platform. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..935fccc --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@vimazing/vim-maze", + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.2.2", + "typescript": "~5.9.3", + }, + "peerDependencies": { + "react": "^19.1.1", + }, + }, + }, + "packages": { + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..a98e6ab --- /dev/null +++ b/example/README.md @@ -0,0 +1,15 @@ +# example + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.23. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/example/bun.lock b/example/bun.lock new file mode 100644 index 0000000..b5febc2 --- /dev/null +++ b/example/bun.lock @@ -0,0 +1,348 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "vimazing-vim-maze-example", + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1", + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.4", "", {}, "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw=="], + + "@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], + + "@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.4", "", { "dependencies": { "@babel/types": "^7.28.4" }, "bin": "./bin/babel-parser.js" }, "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], + + "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.38", "", {}, "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.14", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.5.1" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.14", "@tailwindcss/oxide-darwin-arm64": "4.1.14", "@tailwindcss/oxide-darwin-x64": "4.1.14", "@tailwindcss/oxide-freebsd-x64": "4.1.14", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", "@tailwindcss/oxide-linux-x64-musl": "4.1.14", "@tailwindcss/oxide-wasm32-wasi": "4.1.14", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" } }, "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.14", "", { "os": "android", "cpu": "arm64" }, "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14", "", { "os": "linux", "cpu": "arm" }, "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.14", "", { "os": "linux", "cpu": "x64" }, "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.14", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.5", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.14", "", { "os": "win32", "cpu": "x64" }, "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], + + "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], + + "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], + + "@vimazing/vim-maze": ["@vimazing/vim-maze@file:..", { "devDependencies": { "typescript": "~5.9.3" }, "peerDependencies": { "react": "^19.1.1" } }], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="], + + "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], + + "browserslist": ["browserslist@4.26.3", "", { "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", "electron-to-chromium": "^1.5.227", "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.235", "", {}, "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], + + "normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + + "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.1.14", "", {}, "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "vite": ["vite@5.4.20", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + } +} diff --git a/example/global.d.ts b/example/global.d.ts new file mode 100644 index 0000000..72f5ca9 --- /dev/null +++ b/example/global.d.ts @@ -0,0 +1,2 @@ +declare module "@vitejs/plugin-react"; +declare module "@tailwindcss/vite"; diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..691ef74 --- /dev/null +++ b/example/index.html @@ -0,0 +1,13 @@ + + + + + + VIMazing Maze Demo + + +
+ + + + diff --git a/example/package-lock.json b/example/package-lock.json new file mode 100644 index 0000000..939a090 --- /dev/null +++ b/example/package-lock.json @@ -0,0 +1,1166 @@ +{ + "name": "vimazing-vim-maze-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vimazing-vim-maze-example", + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10" + } + }, + "..": { + "name": "@vimazing/vim-maze", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "~5.9.3" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/node/node_modules/jiti": { + "version": "2.6.1", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.14", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vimazing/vim-maze": { + "resolved": "..", + "link": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001750", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "3.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.23", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..b571444 --- /dev/null +++ b/example/package.json @@ -0,0 +1,27 @@ +{ + "name": "vimazing-vim-maze-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.1.1", + "@types/react-dom": "^19.1.1", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.41", + "tailwindcss": "^4.1.14", + "typescript": "~5.9.3", + "vite": "^5.4.10" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.14", + "@vimazing/vim-maze": "file:../", + "react": "^19.1.1", + "react-dom": "^19.1.1" + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx new file mode 100644 index 0000000..01945f0 --- /dev/null +++ b/example/src/App.tsx @@ -0,0 +1,24 @@ +import { useGame } from "@vimazing/vim-maze"; +import { useKeyBindings } from "./useKeyBindings"; +import { useMounted } from "./useMounted"; +import { useIsMobile } from "./useIsMobile"; +import { GameOverlay } from "./GameOverlay"; + +function App() { + const isMobile = useIsMobile(); + const mounted = useMounted(); + const cols = isMobile ? 12 : 32; + const rows = isMobile ? 8 : 24; + + const gameManager = useGame(cols, rows, useKeyBindings); + const { containerRef, gameStatus, scoreManager } = gameManager; + + return ( +
+
+ {mounted && } +
+ ); +}; + +export default App; diff --git a/example/src/GameOverlay.tsx b/example/src/GameOverlay.tsx new file mode 100644 index 0000000..d9d8c59 --- /dev/null +++ b/example/src/GameOverlay.tsx @@ -0,0 +1,111 @@ +import type { GameStatus } from "@vimazing/vim-maze"; +import type { UseScoreType } from "@vimazing/vim-maze"; +import { useIsMobile } from "./useIsMobile"; +import { MobileGameOverlay } from "./MobileGameOverlay"; + +type GameOverlayProps = { + gameStatus: GameStatus; + scoreManager: UseScoreType; +}; + +export const GameOverlay = ({ gameStatus, scoreManager }: GameOverlayProps) => { + const isMobile = useIsMobile(); + if (isMobile) return + + // ๐Ÿง  Only show overlay when the game has ended + if (['started', 'hasKey'].includes(gameStatus)) return null; + // console.log('gameStatus:', gameStatus) + + return ( + + {gameStatus === "waiting" && } + {gameStatus === "game-over" && } + {gameStatus === "game-won" && } + + ); +}; + +export const GameWaitingContent = () => { + return ( +
+
+

Press i to play...

+

h,j,k,l to move around

+

try other vim motions
you're familiar with

+

+ $,^,0,G,gg only valid if
+ the edges are reachable +

+
+
+ ); +}; + +export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => { + const finalScore = scoreManager.finalScore ?? 0; + + return ( +
+

Congratulations!

+

Final Score:

+

{finalScore.toLocaleString()}

+

+ min + ( + 100000 + , ( + optimalTime + / + actualTime + ) * + 100000 + ) +

+

Press i to play again

+
+ ); +}; + +export const GameOverContent = () => { + return ( +
+

Game Over

+

Press i to play again

+
+ ); +}; + +export const GameOverlayContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + + + diff --git a/example/src/MobileGameOverlay.tsx b/example/src/MobileGameOverlay.tsx new file mode 100644 index 0000000..f804e3f --- /dev/null +++ b/example/src/MobileGameOverlay.tsx @@ -0,0 +1,99 @@ +import type { GameStatus } from "@vimazing/vim-maze"; +import type { UseScoreType } from "@vimazing/vim-maze"; + +type MobileGameOverlayProps = { + gameStatus: GameStatus; + scoreManager: UseScoreType; +}; + +export const MobileGameOverlay = ({ gameStatus, scoreManager }: MobileGameOverlayProps) => { + // ๐Ÿง  Only show overlay when the game has ended + if (['started', 'hasKey'].includes(gameStatus)) return null; + // console.log('gameStatus:', gameStatus) + + return ( + + {gameStatus === "waiting" && } + {gameStatus === "game-over" && } + {gameStatus === "game-won" && } + + ); +}; + +export const GameWaitingContent = () => { + return ( +
+
+

Press here to play

+

all the vim motions
available on desktop

+
+
+ ); +}; + +export const GameWonContent = ({ scoreManager }: { scoreManager: UseScoreType }) => { + const finalScore = scoreManager.finalScore ?? 0; + + return ( +
+

Congratulations!

+

Final Score:

+

{finalScore.toLocaleString()}

+

+ min + ( + 100000 + , ( + optimalTime + / + actualTime + ) * + 100000 + ) +

+

Press to play again

+
+ ); +}; + +export const GameOverContent = () => { + return ( +
+ Game Over +
+ ); +}; + +export const GameOverlayContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + + diff --git a/example/src/index.css b/example/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/example/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/example/src/main.tsx b/example/src/main.tsx new file mode 100644 index 0000000..d9f0159 --- /dev/null +++ b/example/src/main.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +); + diff --git a/example/src/useIsMobile.ts b/example/src/useIsMobile.ts new file mode 100644 index 0000000..6b77170 --- /dev/null +++ b/example/src/useIsMobile.ts @@ -0,0 +1,30 @@ +import * as React from "react"; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState( + typeof window !== "undefined" + ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches + : false + ); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + + const onChange = (event: MediaQueryListEvent) => { + setIsMobile(event.matches); + }; + + // Set initial value + setIsMobile(mql.matches); + + // Add listener + mql.addEventListener("change", onChange); + + // Cleanup + return () => mql.removeEventListener("change", onChange); + }, []); + + return isMobile; +} diff --git a/example/src/useKeyBindings.ts b/example/src/useKeyBindings.ts new file mode 100644 index 0000000..91e6895 --- /dev/null +++ b/example/src/useKeyBindings.ts @@ -0,0 +1,46 @@ +import { useEffect } from "react"; +import type { UseGameType } from "@vimazing/vim-maze"; + +export const useKeyBindings = (gameManager: UseGameType) => { + const { gameStatus, initGame, stopGame, clearLog, resetCount } = gameManager; + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + // start game + if (e.key === "i" || e.key === "I") { + if (["waiting", "game-over", "game-won"].includes(gameStatus)) { + clearLog(); + resetCount(); + if (gameStatus === "game-over" || gameStatus === "game-won") { + stopGame(); + setTimeout(() => initGame(true), 0); + } else { + initGame(true); + } + return; + } + } + + // ignore other keys while waiting + if (gameStatus === "waiting") return; + + // cancel count or stop game + if (e.key === "Escape") { + stopGame(); + resetCount(); + return; + } + + // quit + if (e.key === "q" || e.key === "Q") { + stopGame(); + resetCount(); + return; + } + }; + + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [gameStatus, initGame, stopGame, clearLog, resetCount]); +}; + diff --git a/example/src/useMounted.ts b/example/src/useMounted.ts new file mode 100644 index 0000000..7da354e --- /dev/null +++ b/example/src/useMounted.ts @@ -0,0 +1,11 @@ +import { useState, useEffect } from 'react'; + +export const useMounted = () => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return mounted; +}; diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..8538eed --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + /* === Environment === */ + "target": "ESNext", + "lib": ["ESNext", "DOM"], + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + + /* === Bundler / Vite === */ + "noEmit": true, + "allowJs": true, + "skipLibCheck": true, + "isolatedModules": true, + "types": ["vite/client"], + + /* === Code Style & Safety === */ + "strict": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + + /* === Module Behavior === */ + "allowImportingTsExtensions": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + /* === Path aliases (optional) === */ + "baseUrl": ".", + "paths": { + "@vimazing/vim-maze": ["../dist/index.d.ts"], + "@vimazing/vim-maze/*": ["../dist/*"] + } + }, + "include": ["src", "vite.config.ts", "global.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/example/vite.config.ts b/example/vite.config.ts new file mode 100644 index 0000000..f2794ab --- /dev/null +++ b/example/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import tailwindcss from '@tailwindcss/vite' +import react from "@vitejs/plugin-react"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const libSource = resolve(__dirname, "../src"); + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@vimazing/vim-maze": libSource, // use source during dev + }, + }, + server: { + port: 5173, + open: true, + }, +}); + diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..30085cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "@vimazing/vim-maze", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vimazing/vim-maze", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "~5.9.3" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9621916 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "@vimazing/vim-maze", + "version": "1.0.0", + "description": "VIMazing VIM Maze engine โ€” a lightweight, typed React hook set for maze-based games.", + "author": "Andrรฉ Padez ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./useGame": { + "import": "./dist/useGame/index.js", + "types": "./dist/useGame/index.d.ts" + }, + "./useScore": { + "import": "./dist/useScore/index.js", + "types": "./dist/useScore/index.d.ts" + }, + "./*": { + "import": "./dist/*.js", + "types": "./dist/*.d.ts" + }, + "./*.css": "./dist/*.css" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --outDir dist --declaration --emitDeclarationOnly false && node -e \"const fs = require('node:fs'); fs.mkdirSync('dist', { recursive: true }); fs.copyFileSync('src/maze.css', 'dist/maze.css');\"", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "react": "^19.1.1" + }, + "devDependencies": { + "@types/node": "^24.7.2", + "@types/react": "^19.2.2", + "typescript": "~5.9.3" + } +} diff --git a/src/MazeGenerator.ts b/src/MazeGenerator.ts new file mode 100644 index 0000000..611629c --- /dev/null +++ b/src/MazeGenerator.ts @@ -0,0 +1,346 @@ +import type { PositionTag, Coord, CellTag, MazeCell, MazeData } from "./types"; + +export class MazeGenerator { + width: number; + height: number; + cols: number; + rows: number; + maze: MazeCell[][]; + totalSteps: number = 0; + + constructor(userCols: number, userRows: number) { + const cellsWide = Math.max(1, Math.floor(userCols / 2)); + const cellsHigh = Math.max(1, Math.floor(userRows / 2)); + + this.width = cellsWide; + this.height = cellsHigh; + + this.cols = 2 * this.width + 1; + this.rows = 2 * this.height + 1; + + this.maze = this.initArray([]); + + this.maze.forEach((row, r) => { + row.forEach((_cell, c) => { + switch (r) { + case 0: + case this.rows - 1: + this.maze[r][c] = ["wall"]; + break; + + default: + if ((r % 2) === 1) { + if ((c === 0) || (c === this.cols - 1)) { + this.maze[r][c] = ["wall"]; + } + } else if (c % 2 === 0) { + this.maze[r][c] = ["wall"]; + } + } + }); + + if (r === 0) { + const doorPos = this.posToSpace(this.rand(1, this.width)); + this.maze[r][doorPos] = ["door", "exit"]; + } + + if (r === this.rows - 1) { + const doorPos = this.posToSpace(this.rand(1, this.width)); + this.maze[r][doorPos] = ["door", "entrance"]; + } + }); + + this.partition(1, this.height - 1, 1, this.width - 1); + } + + private initArray(value?: T): T[][] { + return new Array(this.rows) + .fill(null) + .map(() => new Array(this.cols).fill(value as T)); + } + + private rand(min: number, max: number): number { + return min + Math.floor(Math.random() * (1 + max - min)); + } + + private posToSpace(x: number): number { + return 2 * (x - 1) + 1; + } + + private posToWall(x: number): number { + return 2 * x; + } + + private inBounds(r: number, c: number): boolean { + if ( + typeof this.maze[r] === "undefined" || + typeof this.maze[r]?.[c] === "undefined" + ) { + return false; + } + return true; + } + + private shuffle(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; + } + + private partition(r1: number, r2: number, c1: number, c2: number): void { + let horiz: number, vert: number; + let x: number, y: number, start: number, end: number; + + if (r2 < r1 || c2 < c1) { + return; + } + + if (r1 === r2) { + horiz = r1; + } else { + x = r1 + 1; + y = r2 - 1; + start = Math.round(x + (y - x) / 4); + end = Math.round(x + (y - x) * 3 / 4); + horiz = this.rand(start, end); + } + + if (c1 === c2) { + vert = c1; + } else { + x = c1 + 1; + y = c2 - 1; + start = Math.round(x + (y - x) / 3); + end = Math.round(x + (y - x) * 2 / 3); + vert = this.rand(start, end); + } + + for (let i = this.posToWall(r1) - 1; i <= this.posToWall(r2) + 1; i++) { + for (let j = this.posToWall(c1) - 1; j <= this.posToWall(c2) + 1; j++) { + if (i === this.posToWall(horiz) || j === this.posToWall(vert)) { + this.maze[i][j] = ["wall"]; + } + } + } + + const gaps = this.shuffle([true, true, true, false]); + + if (gaps[0]) { + const gapPosition = this.rand(c1, vert); + this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; + } + + if (gaps[1]) { + const gapPosition = this.rand(vert + 1, c2 + 1); + this.maze[this.posToWall(horiz)][this.posToSpace(gapPosition)] = []; + } + + if (gaps[2]) { + const gapPosition = this.rand(r1, horiz); + this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; + } + + if (gaps[3]) { + const gapPosition = this.rand(horiz + 1, r2 + 1); + this.maze[this.posToSpace(gapPosition)][this.posToWall(vert)] = []; + } + + this.partition(r1, horiz - 1, c1, vert - 1); + this.partition(horiz + 1, r2, c1, vert - 1); + this.partition(r1, horiz - 1, vert + 1, c2); + this.partition(horiz + 1, r2, vert + 1, c2); + } + + private isGap(...cells: Array<[number, number]>): boolean { + return cells.every(([row, col]) => { + const cell = this.maze[row][col]; + if (cell.length > 0) { + if (!cell.includes("door")) return false; + } + return true; + }); + } + + private countSteps( + array: (number | undefined)[][], + r: number, + c: number, + val: number, + stop: CellTag + ): boolean { + if (!this.inBounds(r, c)) { + return false; + } + + if (array[r][c] !== undefined && (array[r][c] as number) <= val) { + return false; + } + + if (!this.isGap([r, c])) { + return false; + } + + array[r][c] = val; + + if (this.maze[r][c].includes(stop)) { + return true; + } + + this.countSteps(array, r - 1, c, val + 1, stop); + this.countSteps(array, r, c + 1, val + 1, stop); + this.countSteps(array, r + 1, c, val + 1, stop); + this.countSteps(array, r, c - 1, val + 1, stop); + + return false; + } + + private getKeyLocation(): Coord { + const fromEntrance = this.initArray(); + const fromExit = this.initArray(); + + this.totalSteps = -1; + + for (let j = 1; j < this.cols - 1; j++) { + if (this.maze[this.rows - 1][j].includes("entrance")) { + this.countSteps(fromEntrance, this.rows - 1, j, 0, "exit"); + } + if (this.maze[0][j].includes("exit")) { + this.countSteps(fromExit, 0, j, 0, "entrance"); + } + } + + let fc = -1, + fr = -1; + + this.maze.forEach((_row, r) => { + _row.forEach((_cell, c) => { + if (typeof fromEntrance[r]?.[c] === "undefined") { + return; + } + const stepCount = + (fromEntrance[r][c] ?? Infinity) + (fromExit[r][c] ?? Infinity); + if (stepCount > this.totalSteps) { + fr = r; + fc = c; + this.totalSteps = stepCount; + } + }); + }); + + return [fr, fc]; + } + + placeKey(): void { + const [fr, fc] = this.getKeyLocation(); + this.maze[fr][fc] = ["key"]; + } + + shortestPath( + fromTag: PositionTag, + toTag: PositionTag, + currentPosition?: Coord + ): Coord[] { + let start: Coord | undefined; + let end: Coord | undefined; + + if (fromTag === "hero" && currentPosition) { + start = currentPosition; + } else { + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + if (this.maze[r][c].includes(fromTag)) start = [r, c]; + } + } + } + + for (let r = 0; r < this.rows; r++) { + for (let c = 0; c < this.cols; c++) { + if (this.maze[r][c].includes(toTag)) end = [r, c]; + } + } + + if (!start || !end) return []; + + const visited = this.initArray(false); + const prev = this.initArray(null); + const queue: Coord[] = [[...start]]; + visited[start[0]][start[1]] = true; + + const dirs: Coord[] = [ + [-1, 0], + [1, 0], + [0, -1], + [0, 1], + ]; + + while (queue.length > 0) { + const [r, c] = queue.shift() as Coord; + + if (r === end[0] && c === end[1]) { + const path: Coord[] = []; + let curr: Coord | null = end; + while (curr) { + path.unshift(curr); + curr = prev[curr[0]][curr[1]]; + } + return path; + } + + for (const [dr, dc] of dirs) { + const nr = r + dr, + nc = c + dc; + if (!this.inBounds(nr, nc)) continue; + if (visited[nr][nc]) continue; + if (this.maze[nr][nc].includes("wall")) continue; + + visited[nr][nc] = true; + prev[nr][nc] = [r, c]; + queue.push([nr, nc]); + } + } + + return []; + } + + shortestPathToKey(currentPosition?: Coord): Coord[] { + return this.shortestPath( + currentPosition ? "hero" : "entrance", + "key", + currentPosition + ); + } + + shortestPathToExit(currentPosition?: Coord): Coord[] { + return this.shortestPath( + currentPosition ? "hero" : "entrance", + "exit", + currentPosition + ); + } + + getDistance( + fromTag: PositionTag, + toTag: PositionTag, + currentPosition?: Coord + ): number { + const path = this.shortestPath(fromTag, toTag, currentPosition); + return path.length ? path.length - 1 : Infinity; + } + + getData(): MazeData { + return { + maze: this.maze, + width: this.width, + height: this.height, + cols: this.cols, + rows: this.rows, + totalSteps: this.totalSteps, + }; + } + + toJSON(): MazeData { + return this.getData(); + } +} diff --git a/src/MazeRenderer.ts b/src/MazeRenderer.ts new file mode 100644 index 0000000..acf74f7 --- /dev/null +++ b/src/MazeRenderer.ts @@ -0,0 +1,116 @@ +import type { MazeData } from "./types"; +import "./maze.css"; + +export class MazeRenderer { + private mazeData: MazeData; + private numbered: boolean; + private parentDiv?: HTMLElement; + + constructor(mazeData: MazeData, numbered: boolean = false) { + this.mazeData = mazeData; + this.numbered = numbered; + } + + display(parentDiv: HTMLElement | null): boolean { + this.parentDiv = parentDiv ?? undefined; + + if (!this.parentDiv) { + console.error("Cannot initialise maze - parent element not provided."); + return false; + } + + while (this.parentDiv.firstChild) { + this.parentDiv.removeChild(this.parentDiv.firstChild); + } + + const container = document.createElement("div"); + container.id = "maze"; + container.dataset.steps = String(this.mazeData.totalSteps); + + const makeCell = (cls = "", text = "", attrs?: Record) => { + const d = document.createElement("div"); + d.className = cls; + if (text) d.textContent = text; + if (attrs) for (const k in attrs) d.setAttribute(k, attrs[k]); + return d; + }; + + const rowLabel = (r: number) => (r > 0 && r < this.mazeData.rows ? String(r) : ""); + const colLabel = (c: number) => (c > 0 && c < this.mazeData.cols ? String(c) : ""); + + if (this.numbered) { + const topStrip = document.createElement("div"); + topStrip.className = "number-strip number-strip-top"; + topStrip.appendChild(makeCell("number corner", "")); + for (let c = 0; c < this.mazeData.cols; c++) { + topStrip.appendChild(makeCell("number col", colLabel(c), { "data-c": String(c) })); + } + topStrip.appendChild(makeCell("number corner", "")); + container.appendChild(topStrip); + } + + this.mazeData.maze.forEach((row, r) => { + const rowDiv = document.createElement("div"); + rowDiv.className = "maze-row"; + rowDiv.setAttribute("data-r", String(r)); + + if (this.numbered) { + rowDiv.appendChild(makeCell("number row", rowLabel(r), { "data-r": String(r) })); + } + + row.forEach((cell, c) => { + const cellDiv = document.createElement("div"); + if (cell) cellDiv.className = cell.join(" "); + cellDiv.setAttribute("data-r", String(r)); + cellDiv.setAttribute("data-c", String(c)); + rowDiv.appendChild(cellDiv); + }); + + if (this.numbered) { + rowDiv.appendChild(makeCell("number row", rowLabel(r), { "data-r": String(r) })); + } + + container.appendChild(rowDiv); + }); + + if (this.numbered) { + const bottomStrip = document.createElement("div"); + bottomStrip.className = "number-strip number-strip-bottom"; + bottomStrip.appendChild(makeCell("number corner", "")); + for (let c = 0; c < this.mazeData.cols; c++) { + bottomStrip.appendChild(makeCell("number col", colLabel(c), { "data-c": String(c) })); + } + bottomStrip.appendChild(makeCell("number corner", "")); + container.appendChild(bottomStrip); + } + + this.parentDiv.appendChild(container); + return true; + } + + getMazeEl(container: HTMLElement): HTMLElement | null { + return container.querySelector("#maze"); + } + + getCellSelector(r: number, c: number): string { + return `.maze-row > div[data-r="${r}"][data-c="${c}"]`; + } + + getCellEl(container: HTMLElement, r: number, c: number): HTMLElement | null { + const maze = this.getMazeEl(container); + if (!maze) return null; + return maze.querySelector(this.getCellSelector(r, c)) as HTMLElement | null; + } + + addCellClass(container: HTMLElement, r: number, c: number, cls: string): void { + this.getCellEl(container, r, c)?.classList.add(cls); + } + + removeCellClass(container: HTMLElement, r: number, c: number, cls: string): void { + this.getCellEl(container, r, c)?.classList.remove(cls); + } + + toggleCellClass(container: HTMLElement, r: number, c: number, cls: string): void { + this.getCellEl(container, r, c)?.classList.toggle(cls); + } +} diff --git a/src/hooks/useTimer.ts b/src/hooks/useTimer.ts new file mode 100644 index 0000000..fb766d5 --- /dev/null +++ b/src/hooks/useTimer.ts @@ -0,0 +1,57 @@ +import { useState, useRef, useCallback, useEffect } from "react"; + +/** + * useTimer โ€“ a low-level timer that updates with requestAnimationFrame. + * + * @returns { timeValue, startTimer, stopTimer, resetTimer } + */ +export function useTimer() { + const [timeValue, setTimeValue] = useState(0); + const startTimeRef = useRef(null); + const frameRef = useRef(null); + const runningRef = useRef(false); + + // ๐Ÿ•’ Start timer + const startTimer = useCallback(() => { + if (runningRef.current) return; runningRef.current = true; + startTimeRef.current = performance.now(); + + const tick = () => { + if (!runningRef.current) return; + const now = performance.now(); + const elapsed = now - (startTimeRef.current ?? now); + setTimeValue(elapsed); + frameRef.current = requestAnimationFrame(tick); + }; + + frameRef.current = requestAnimationFrame(tick); + }, []); + + // โน Stop/pause timer + const stopTimer = useCallback(() => { + runningRef.current = false; + if (frameRef.current) { + cancelAnimationFrame(frameRef.current); + frameRef.current = null; + } + }, []); + + // ๐Ÿ”„ Reset timer + const resetTimer = useCallback(() => { + stopTimer(); + setTimeValue(0); + startTimeRef.current = null; + }, [stopTimer]); + + // ๐Ÿงน Cleanup on unmount + useEffect(() => { + return () => { + if (frameRef.current) cancelAnimationFrame(frameRef.current); + }; + }, []); + + return { timeValue, startTimer, stopTimer, resetTimer }; +} + +export type UseTimerReturn = ReturnType; + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6c39bd9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,5 @@ +export { MazeGenerator } from "./MazeGenerator"; +export { MazeRenderer } from "./MazeRenderer"; +export * from './useGame'; +export * from './types'; + diff --git a/src/maze.css b/src/maze.css new file mode 100644 index 0000000..ef8c9d6 --- /dev/null +++ b/src/maze.css @@ -0,0 +1,230 @@ +/* ========================================================= + ๐Ÿ”ง Controls (tweak these two) + ========================================================= */ +:root { + --cell-size: 24px; /* size of every grid box (maze cells + number cells) */ + --number-font-size: 14px; /* size of the digits around the maze */ +} + +/* ========================================================= + Container & Maze Base + ========================================================= */ +#maze_container { + position: relative; + margin: 1em auto; + width: fit-content; +} + +#maze { + position: relative; + background-color: #1e2030; /* Tokyo Night card background */ + background-image: + radial-gradient(circle at 0% 0%, transparent 50%, rgba(130, 170, 255, 0.03) 50%), + radial-gradient(circle at center, rgba(130, 170, 255, 0.03) 50%, transparent 50%), + radial-gradient(circle at 100% 100%, transparent 50%, rgba(130, 170, 255, 0.03) 50%), + radial-gradient(circle at 0% 100%, transparent 50%, rgba(130, 170, 255, 0.03) 50%), + radial-gradient(circle at 100% 0%, transparent 50%, rgba(130, 170, 255, 0.03) 50%); + background-size: 8em 8em; + border-radius: 0.5rem; + box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px rgba(130, 170, 255, 0.05); +} + +/* ========================================================= + Layout primitives + ========================================================= */ +#maze .maze-row, +#maze .number-strip { + display: flex; /* rows of equal-width cells */ +} + +/* Every grid box (maze cells + number cells) shares the same size */ +#maze .maze-row > div, +#maze .number { + width: var(--cell-size); + height: var(--cell-size); + display: flex; + align-items: center; + justify-content: center; +} + +/* Number cells (rows/cols) */ +#maze .number { + color: #828bb8; /* Tokyo Night muted foreground */ + font-weight: 600; + font-family: "Fira Code", monospace; + font-size: var(--number-font-size); + line-height: 1; + opacity: 0.85; + user-select: none; +} + +#maze .number.corner { /* top-left/top-right/bottom corners */ + width: var(--cell-size); +} + +/* Relative highlight (your tweak) */ +#maze.is-relative .number.is-active { + background: #ffc777; /* Tokyo Night warning/yellow */ + border-radius: 2px; + font-size: 1.5rem; + color: #1b1d2b; /* Tokyo Night black */ +} + +/* ========================================================= + Maze cells visuals (โš ๏ธ scoped to real cells only) + ========================================================= */ +/* Base cell box */ +#maze .maze-row > div[data-r][data-c] { + position: relative; + transition: background-color 0.2s ease; +} + +/* ๐ŸŸข Corridors (non-wall/door/sentinel) */ +#maze .maze-row > div[data-r][data-c]:not(.wall):not(.door):not(.sentinel) { + background-color: #2f334d; /* Tokyo Night muted */ + background-image: + linear-gradient(135deg, rgba(130,170,255,0.08) 25%, transparent 25%), + linear-gradient(225deg, rgba(130,170,255,0.06) 25%, transparent 25%), + linear-gradient(45deg, rgba(130,170,255,0.04) 25%, transparent 25%), + linear-gradient(315deg, rgba(130,170,255,0.03) 25%, transparent 25%); + background-size: var(--cell-size) var(--cell-size); /* scale with cell size */ + box-shadow: inset 0 0 3px rgba(27,29,43,0.4); +} + +/* ๐Ÿงฑ Walls and exits */ +#maze .maze-row > div[data-r][data-c].wall, +#maze .maze-row > div[data-r][data-c].nubbin.wall, +#maze .maze-row > div[data-r][data-c].door.exit { + background-color: #222436; /* Tokyo Night background */ + background-image: + linear-gradient(45deg, rgba(27,29,43,0.5) 45%, transparent 55%), + linear-gradient(to bottom, rgba(27,29,43,0.4) 45%, transparent 55%); + background-size: calc(var(--cell-size) * 0.5) calc(var(--cell-size) * 0.5); + box-shadow: inset 0 0 3px rgba(27,29,43,0.6); +} + +/* ๐Ÿ”’ Door (exit) โ€” FIXED BACKGROUND */ +#maze .maze-row > div[data-r][data-c].door.exit { + background: radial-gradient(circle at center, #4fd6be 10%, #1e2030 90%) !important; + box-shadow: + 0 0 6px rgba(79, 214, 190, 0.5), + inset 0 0 4px rgba(79, 214, 190, 0.3); + border-radius: 3px; + z-index: 2; +} + +#maze .maze-row > div[data-r][data-c].door.exit::after { + content: "\1F6AA"; + color: #4fd6be; /* Tokyo Night teal */ + text-shadow: 0 0 6px rgba(79, 214, 190, 0.6); + position: absolute; + top: -4px; + left: -2px; + z-index: 3; +} + +/* ๐Ÿ’Ž Decorations (nubbins) */ +#maze .maze-row > div[data-r][data-c]::after { + position: absolute; + left: -3px; + top: -4px; + text-align: center; + text-shadow: 0 0 3px rgba(0,0,0,0.6); + font-size: calc(var(--cell-size) * 0.75); /* scale with cell */ + z-index: 10; +} + +#maze .maze-row > div[data-r][data-c].nubbin::after { content: "\1F33C"; } +#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(3n)::after { content: "\1F344"; } +#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(5n)::after { content: "\1F33B"; } +#maze .maze-row > div[data-r][data-c].nubbin:nth-of-type(7n)::after { content: "\1F48E"; } +#maze .maze-row > div[data-r][data-c].nubbin.wall::after { content: ""; } + +/* ๐Ÿง Hero */ +#maze .maze-row > div[data-r][data-c].hero { + background-color: #82aaff; /* Tokyo Night primary blue */ + border-radius: 50%; + box-shadow: 0 0 8px rgba(130, 170, 255, 0.7); +} +#maze.face-right .maze-row > div[data-r][data-c].hero::after { transform: scale(-1, 1); } +#maze .maze-row > div[data-r][data-c].hero::after { content: "\1F6B6" !important; } + +/* ๐Ÿ Sentinels */ +#maze .maze-row > div[data-r][data-c].sentinel.wall { background: transparent; } +#maze .maze-row > div[data-r][data-c].sentinel.wall::after { + content: "\1F40A"; + color: #828bb8; /* Tokyo Night muted foreground */ +} +#maze .maze-row > div[data-r][data-c].sentinel.wall:nth-of-type(3n)::after { content: "\1F40D"; } + +/* ๐Ÿ—๏ธ Key */ +#maze .maze-row > div[data-r][data-c].key::after { + content: "\1F511"; + color: #ffc777; /* Tokyo Night warning/yellow */ + text-shadow: 0 0 6px rgba(255, 199, 119, 0.8); +} + +/* ========================================================= + Score / message bar (unchanged) + ========================================================= */ +#maze_output { + display: flex; + margin: 0.5em auto; +} +#maze_score, +#maze_message { + font-family: "Fira Code", monospace; + font-weight: 600; + font-size: 1em; + color: #c8d3f5; /* Tokyo Night foreground */ +} +#maze_score { + flex: 1; + text-align: left; +} +#maze_score::before { + content: "Score: "; + color: #828bb8; /* Tokyo Night muted foreground */ +} +#maze_score.has-key::after { + content: "\00a0\1F511"; + color: #ffc777; /* Tokyo Night warning/yellow for key */ +} +#maze_message { + flex: 3; + text-align: right; +} + +/* Subtle step trail effect for counted moves */ +#maze .maze-row > div[data-r][data-c].trail { + box-shadow: inset 0 0 6px rgba(130, 170, 255, 0.6), 0 0 6px rgba(130, 170, 255, 0.35); + animation: maze-trail-fade 16ms ease-out; +} + +/* ===== Invalid move "red mist" around hero ===== */ +#maze .maze-row > div[data-r][data-c].invalid-move { + animation: maze-invalid-pulse 180ms ease-out; + box-shadow: + 0 0 12px rgba(255, 117, 127, 0.85), + 0 0 24px rgba(255, 117, 127, 0.45); +} + +@keyframes maze-invalid-pulse { + 0% { + transform: scale(1.02); + box-shadow: + 0 0 16px rgba(255, 117, 127, 0.95), + 0 0 28px rgba(255, 117, 127, 0.55); + } + 100% { + transform: none; + box-shadow: none; + } +} + + +@keyframes maze-trail-fade { + from { background-color: rgba(130, 170, 255, 0.25); } + to { background-color: transparent; } +} + diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2e6c5d4 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,54 @@ +import { useScore } from "./useScore"; +import { useGame } from "./useGame"; +export type UseGameType = ReturnType; +export type UseScoreType = ReturnType; + +export type GamePhase = "idle" | "playing"; +export type PlayStatus = "started" | "hasKey" | "game-over" | "game-won"; +export type GameStatus = "waiting" | "started" | "hasKey" | "game-over" | "game-won"; + +export const isPlaying = (status: GameStatus): status is PlayStatus => + status === "started" || status === "hasKey" || status === "game-over" || status === "game-won"; + +export const getGamePhase = (status: GameStatus): GamePhase => + status === "waiting" ? "idle" : "playing"; + +export type KeyLogEntry = { key: string; timestamp: number }; +export type KeyLogProvider = { keyLog: KeyLogEntry[] }; + +export type GameScoreContext = { + gameStatus: GameStatus; + setGameStatus: (status: GameStatus) => void; + stopGame: () => void; + + // Optional โ€” only Maze-type games provide these + mazeManager?: { + mazeInstanceRef?: { current?: any }; + }; + playerManager?: { + playerPos?: { r: number; c: number } | null; + }; +}; + +export type UseScoreParams = { + gameContext: GameScoreContext; + keyManager?: KeyLogProvider; +}; + +export type PositionTag = "entrance" | "exit" | "key" | "hero"; + +export type Coord = [number, number]; + +export type CellTag = "wall" | "door" | PositionTag; + +export type MazeCell = CellTag[]; + +export interface MazeData { + maze: MazeCell[][]; + width: number; + height: number; + cols: number; + rows: number; + totalSteps: number; +} + diff --git a/src/useGame/index.ts b/src/useGame/index.ts new file mode 100644 index 0000000..5b2f75f --- /dev/null +++ b/src/useGame/index.ts @@ -0,0 +1,2 @@ +export * from './useGame'; +export * from './useKeyBindings'; diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts new file mode 100644 index 0000000..425d680 --- /dev/null +++ b/src/useGame/useGame.ts @@ -0,0 +1,86 @@ +import { useEffect } from "react"; +import { useMaze } from "./useMaze"; +import { usePlayer } from "./usePlayer"; +import { useGameStatus } from "./useGameStatus"; +import { useHeroRender } from "./useHeroRender"; +import { useScore } from "../useScore"; +import type { UseScoreType } from '../types' +import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings"; + +export function useGame(cols: number, rows: number, platformHook?: unknown) { + const mazeManager = useMaze(cols, rows); + const playerManager = usePlayer(mazeManager); + const gameManager = useGameStatus(mazeManager, playerManager); + useHeroRender({ mazeManager, playerManager }); + + const { initMaze } = mazeManager; + const { movePlayer } = playerManager; + const { gameStatus, setGameStatus, startGame, stopGame } = gameManager; + const { containerRef } = mazeManager; + + // Initialize maze on mount + useEffect(() => { + initMaze(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const startNewGame = (autoStart = false) => { + if (autoStart) { + initMaze(true, startGame); + } else { + initMaze(); + } + }; + + // ๐ŸŽฎ key bindings integrated + const keyBindings: UseKeyBindingsType = useKeyBindings({ + gameManager: { + movePlayer: (dr: number, dc: number, count: number = 1) => + movePlayer(dr, dc, count, gameStatus, setGameStatus), + gameStatus, + mazeManager, + playerManager, + } as any, // minimal shape needed by useKeyBindings + }); + + // ๐Ÿ† scoring integrated โ€” pass the full context it needs (incl. maze/player) + const scoreManager: UseScoreType = useScore({ + gameContext: { + gameStatus, + setGameStatus, + stopGame, + mazeManager, // <-- provide for useScorePaths + playerManager, // <-- provide for step counting + }, + keyManager: { keyLog: keyBindings.keyLog }, // minimal info; avoids type cycles + }); + + const fullGameManager = { + // core game + containerRef, + gameStatus, + setGameStatus, + mazeManager, + playerManager, + startGame, + initGame: startNewGame, + movePlayer: (dr: number, dc: number, count: number = 1) => + movePlayer(dr, dc, count, gameStatus, setGameStatus), + stopGame, + reset: () => initMaze(), + + // key bindings + ...keyBindings, + + // scoring + scoreManager, + }; + + if (typeof platformHook === 'function') { + platformHook(fullGameManager); + } + + return fullGameManager; +} + +export type { GameStatus } from "../types"; diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts new file mode 100644 index 0000000..8db99a4 --- /dev/null +++ b/src/useGame/useGameStatus.ts @@ -0,0 +1,55 @@ +import { useCallback, useState, useMemo } from "react"; +import type { UseMazeType } from "./useMaze"; +import type { UsePlayerType } from "./usePlayer"; +import type { GameStatus } from "../types"; +import { getGamePhase, isPlaying } from "../types"; + +export type { GameStatus } from "../types"; + +export function useGameStatus(mazeManager: UseMazeType, playerManager: UsePlayerType) { + const { mazeRef, containerRef } = mazeManager; + const { setPlayerPos } = playerManager; + const [gameStatus, setGameStatus] = useState("waiting"); + + const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]); + const playStatus = useMemo( + () => (isPlaying(gameStatus) ? gameStatus : null), + [gameStatus] + ); + + // ๐Ÿ•น Start the game (place hero) + const startGame = useCallback(() => { + const m = mazeRef.current; + if (!m.length) return; + + for (let c = 0; c < m[0].length; c++) { + if (m[m.length - 1][c].includes("entrance")) { + setPlayerPos({ r: m.length - 1, c }); + break; + } + } + + setGameStatus("started"); + }, [mazeRef, setPlayerPos]); + + // ๐Ÿง Stop / exit game + const stopGame = useCallback(() => { + setPlayerPos(null); + setGameStatus("waiting"); + const container = containerRef.current; + container?.querySelector("#maze")?.classList.remove("finished"); + container?.querySelectorAll(".hero").forEach((el) => el.classList.remove("hero")); + }, [setPlayerPos, containerRef]); + + return { + gameStatus, + gamePhase, + playStatus, + setGameStatus, + startGame, + stopGame, + }; +} + +export type UseGameStatusType = ReturnType; + diff --git a/src/useGame/useHeroRender.ts b/src/useGame/useHeroRender.ts new file mode 100644 index 0000000..4c0ea88 --- /dev/null +++ b/src/useGame/useHeroRender.ts @@ -0,0 +1,81 @@ +import { useEffect } from "react"; +import type { UseMazeType } from "./useMaze"; +import type { UsePlayerType } from "./usePlayer"; + +type UseHeroRenderParams = { + mazeManager: UseMazeType; + playerManager: UsePlayerType; + relative?: boolean; +}; + +export function useHeroRender(params: UseHeroRenderParams) { + const { mazeManager, playerManager, relative = true } = params; + const { containerRef } = mazeManager; + const { playerPos } = playerManager; + + const applyRelativeNumbers = (mazeDiv: HTMLElement, R: number, C: number) => { + // counts provided by caller + const rows = Number(mazeDiv.dataset.rows || 0); + const cols = Number(mazeDiv.dataset.cols || 0); + + // rows: borders blank; active row shows its ABSOLUTE number (r), others show |r-R| + const rowNums = mazeDiv.querySelectorAll(".number.row[data-r]"); + rowNums.forEach((el) => { + const r = Number(el.getAttribute("data-r")); + if (r <= 0 || r >= rows) { + el.textContent = ""; + el.classList.remove("is-active"); + return; + } + el.textContent = r === R ? String(r) : String(Math.abs(r - R)); + el.classList.toggle("is-active", r === R); + }); + + // cols (top & bottom): borders blank; active col shows ABSOLUTE number (c), others show |c-C| + const colNums = mazeDiv.querySelectorAll(".number.col[data-c]"); + colNums.forEach((el) => { + const c = Number(el.getAttribute("data-c")); + if (c <= 0 || c >= cols) { + el.textContent = ""; + el.classList.remove("is-active"); + return; + } + el.textContent = c === C ? String(c) : String(Math.abs(c - C)); + el.classList.toggle("is-active", c === C); + }); + + mazeDiv.classList.add("is-relative"); + }; + + useEffect(() => { + if (!playerPos) return; + + const container = containerRef.current; + if (!container) return; + + const mazeDiv = container.querySelector("#maze") as HTMLElement | null; + if (!mazeDiv) return; + + // Remove any previous hero marker + mazeDiv.querySelectorAll(".hero").forEach((el) => el.classList.remove("hero")); + + // Place hero (select exact cell by coordinates) + const selector = `.maze-row > div[data-r="${playerPos.r}"][data-c="${playerPos.c}"]`; + const cell = mazeDiv.querySelector(selector) as HTMLElement | null; + if (cell) cell.classList.add("hero"); + + // Relative numbering (absolute on the active line/col) + if (relative) { + // Provide counts so helper can know borders + mazeDiv.dataset.rows = String(mazeDiv.querySelectorAll(".maze-row").length); + mazeDiv.dataset.cols = String( + mazeDiv + .querySelector(".maze-row > div[data-r][data-c]") + ?.parentElement?.querySelectorAll(':scope > div[data-r][data-c]').length || 0 + ); + applyRelativeNumbers(mazeDiv, playerPos.r, playerPos.c); + } + }, [playerPos, containerRef]); +} + +export type UseHeroRenderType = ReturnType; diff --git a/src/useGame/useKeyBindings.ts b/src/useGame/useKeyBindings.ts new file mode 100644 index 0000000..87cf73d --- /dev/null +++ b/src/useGame/useKeyBindings.ts @@ -0,0 +1,259 @@ +import type { GameStatus } from "../types"; +import { useEffect, useRef, useState } from "react"; +import type { KeyLogEntry } from "../types"; + +export type GameBindingContext = { + movePlayer: (dr: number, dc: number, count?: number) => void; + gameStatus: GameStatus; + mazeManager: any; + playerManager: any; +}; + +type UseMazeKeyBindingsParams = { + gameManager: GameBindingContext; +}; + +type Motion = { dr: number; dc: number; steps: number }; + +const RELEVANT_KEYS = new Set([ + "h", "H", "j", "J", "k", "K", "l", "L", ".", "^", "$", "g", "G", "0", + "1", "2", "3", "4", "5", "6", "7", "8", "9" +]); + +export const useKeyBindings = ({ gameManager }: UseMazeKeyBindingsParams) => { + const { movePlayer, gameStatus } = gameManager; + + const [keyLog, setKeyLog] = useState([]); + const logRef = useRef([]); + const countRef = useRef(""); + const lastMotionRef = useRef(null); + const lastKeyRef = useRef(""); + + const relevantKeys = RELEVANT_KEYS; + + const clearLog = () => { + logRef.current = []; + setKeyLog([]); + }; + + const recordKey = (key: string) => { + const entry = { key, timestamp: performance.now() }; + logRef.current.push(entry); + setKeyLog([...logRef.current]); + }; + + const resetCount = () => { countRef.current = ""; }; + + const findAnchorTarget = (dir: "left" | "right") => { + const m = gameManager.mazeManager.mazeRef.current; + const pos = gameManager.playerManager.playerPos; + if (!m.length || !pos) return null; + + const colsCount = m[0]?.length ?? 0; + const r = pos.r; + const c = pos.c; + + if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; + + const step = dir === "left" ? -1 : 1; + let cc = c; + + while (true) { + const next = cc + step; + if (next < 0 || next >= colsCount) return null; + if (m[r][next].includes("wall")) { + const wallIdx = next; + const expectedWallIdx = dir === "left" ? 0 : colsCount - 1; + const expectedCellIdx = dir === "left" ? 1 : colsCount - 2; + + if (wallIdx !== expectedWallIdx) return null; + if (cc !== expectedCellIdx) return null; + if (cc === c) return null; + + return { r, cTarget: cc }; + } + cc = next; + } + }; + + const findColumnAnchorTarget = (dir: "top" | "bottom") => { + const m = gameManager.mazeManager.mazeRef.current; + const pos = gameManager.playerManager.playerPos; + if (!m.length || !pos) return null; + + const rowsCount = m.length; + const r = pos.r; + const c = pos.c; + + if (!m[r] || !m[r][c] || m[r][c].includes("wall")) return null; + + const step = dir === "top" ? -1 : 1; + let rr = r; + + while (true) { + const next = rr + step; + if (next < 0 || next >= rowsCount) return null; + if (m[next][c].includes("wall")) { + const wallIdx = next; + const expectedWallIdx = dir === "top" ? 0 : rowsCount - 1; + const expectedCellIdx = dir === "top" ? 1 : rowsCount - 2; + + if (wallIdx !== expectedWallIdx) return null; + if (rr !== expectedCellIdx) return null; + if (rr === r) return null; + + return { rTarget: rr, c }; + } + rr = next; + } + }; + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (["waiting", "game-over", "game-won"].includes(gameStatus)) return; + + // "0" behaves like Vim: beginning of line + if (e.key === "0" && countRef.current === "") { + recordKey("0"); + resetCount(); + + const anchor = findAnchorTarget("left"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = 0; + const dc = anchor.cTarget > pos.c ? 1 : -1; + const steps = Math.abs(anchor.cTarget - pos.c); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + + const isDigit = e.key >= "0" && e.key <= "9"; + if (isDigit) { + if (e.key === "0" && countRef.current === "") return; + recordKey(e.key); + countRef.current += e.key; + return; + } + + if (e.key === ".") { + recordKey("."); + const last = lastMotionRef.current; + if (!last) return; + const steps = Math.max(1, parseInt(countRef.current || String(last.steps), 10)); + resetCount(); + movePlayer(last.dr, last.dc, steps); + lastMotionRef.current = { dr: last.dr, dc: last.dc, steps }; + return; + } + + if (e.key === "^" || e.key === "$") { + recordKey(e.key); + resetCount(); + + const dir = e.key === "^" ? "left" : "right"; + const anchor = findAnchorTarget(dir); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = 0; + const dc = anchor.cTarget > pos.c ? 1 : -1; + const steps = Math.abs(anchor.cTarget - pos.c); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + + if (e.key === "g" || e.key === "G") { + if (e.key === "g") { + if (lastKeyRef.current === "g") { + recordKey(e.key); + resetCount(); + lastKeyRef.current = ""; + + const anchor = findColumnAnchorTarget("top"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = anchor.rTarget > pos.r ? 1 : -1; + const dc = 0; + const steps = Math.abs(anchor.rTarget - pos.r); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } else { + recordKey(e.key); + lastKeyRef.current = "g"; + return; + } + } else { + recordKey(e.key); + resetCount(); + lastKeyRef.current = ""; + + const anchor = findColumnAnchorTarget("bottom"); + if (!anchor) { + window.dispatchEvent(new Event("maze-invalid")); + return; + } + + const pos = gameManager.playerManager.playerPos!; + const dr = anchor.rTarget > pos.r ? 1 : -1; + const dc = 0; + const steps = Math.abs(anchor.rTarget - pos.r); + + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + return; + } + } + + if (!relevantKeys.has(e.key)) return; + + recordKey(e.key); + + let dr = 0, dc = 0; + switch (e.key) { + case "h": + case "H": dr = 0; dc = -1; break; + case "j": + case "J": dr = 1; dc = 0; break; + case "k": + case "K": dr = -1; dc = 0; break; + case "l": + case "L": dr = 0; dc = 1; break; + default: return; + } + + const steps = Math.max(1, parseInt(countRef.current || "1", 10)); + resetCount(); + movePlayer(dr, dc, steps); + lastMotionRef.current = { dr, dc, steps }; + }; + + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [movePlayer, gameStatus, gameManager]); + + return { + keyLog, + clearLog, + resetCount, + getLog: () => logRef.current, + }; +}; + +export type UseKeyBindingsType = ReturnType; diff --git a/src/useGame/useMaze.ts b/src/useGame/useMaze.ts new file mode 100644 index 0000000..e037e61 --- /dev/null +++ b/src/useGame/useMaze.ts @@ -0,0 +1,45 @@ +import { useRef, useCallback } from "react"; +import { MazeGenerator } from "../MazeGenerator"; +import { MazeRenderer } from "../MazeRenderer"; +import type { MazeCell } from "../types"; + +export function useMaze(cols: number, rows: number) { + const containerRef = useRef(null); + const mazeRef = useRef([]); + const mazeInstanceRef = useRef(null); + const rendererRef = useRef(null); + + const initMaze = useCallback( + (autoStart = false, startCallback?: () => void) => { + const container = containerRef.current; + if (!container) return; + + container.innerHTML = ""; + + const generator = new MazeGenerator(cols, rows); + generator.placeKey(); + + const renderer = new MazeRenderer(generator.getData(), true); + renderer.display(container); + + mazeRef.current = generator.maze; + mazeInstanceRef.current = generator; + rendererRef.current = renderer; + + if (autoStart && startCallback) { + setTimeout(() => startCallback(), 0); + } + }, + [cols, rows] + ); + + return { + containerRef, + mazeRef, + mazeInstanceRef, + rendererRef, + initMaze, + }; +} + +export type UseMazeType = ReturnType; diff --git a/src/useGame/usePlayer.ts b/src/useGame/usePlayer.ts new file mode 100644 index 0000000..3098699 --- /dev/null +++ b/src/useGame/usePlayer.ts @@ -0,0 +1,202 @@ +import { useRef, useState, useCallback, useEffect } from "react"; +import type { UseMazeType } from "./useMaze"; +import type { GameStatus } from "./useGame"; + +export function usePlayer(mazeManager: UseMazeType) { + const { mazeRef, containerRef } = mazeManager; + const [playerPos, setPlayerPos] = useState<{ r: number; c: number } | null>(null); + + // animation gate so we don't accept new motions mid-anim + const animatingRef = useRef(false); + + // Helper: get the maze cell element by [r,c] via data attributes + const getCellEl = useCallback((r: number, c: number): HTMLElement | null => { + const container = containerRef.current; + if (!container) return null; + const mazeDiv = container.querySelector("#maze"); + if (!mazeDiv) return null; + const selector = `.maze-row > div[data-r="${r}"][data-c="${c}"]`; + return mazeDiv.querySelector(selector) as HTMLElement | null; + }, [containerRef]); + + // Visual: briefly highlight a visited cell + const leaveTrail = useCallback((r: number, c: number) => { + const el = getCellEl(r, c); + if (!el) return; + el.classList.add("trail"); + setTimeout(() => el.classList.remove("trail"), 220); + }, [getCellEl]); + + // Visual: flash an invalid move on the hero's current cell + const flashInvalid = useCallback(() => { + if (!playerPos) return; + const el = getCellEl(playerPos.r, playerPos.c); + if (!el) return; + el.classList.add("invalid-move"); + setTimeout(() => el.classList.remove("invalid-move"), 180); + }, [playerPos, getCellEl]); + + // Allow other modules (e.g., keybindings) to signal an invalid attempt + useEffect(() => { + const onInvalid = () => flashInvalid(); + window.addEventListener("maze-invalid", onInvalid); + return () => window.removeEventListener("maze-invalid", onInvalid); + }, [flashInvalid]); + + /** + * movePlayer โ€” supports counted motions with an all-or-nothing rule. + * If steps > 1 and the path is valid, animates quickly (many steps per frame), + * then applies interactions (key/exit) on the final tile. + */ + const movePlayer = useCallback( + ( + dr: number, + dc: number, + steps: number | undefined, + gameStatus: GameStatus, + setGameStatus: (s: GameStatus) => void + ) => { + const m = mazeRef.current; + if (!playerPos || !m.length) return; + if (gameStatus !== "started" && gameStatus !== "hasKey") return; + if (animatingRef.current) return; // ignore input mid-animation + + const n = Math.max(1, steps ?? 1); + + // ----- Preflight: simulate n steps; abort if any step is invalid ----- + const r0 = playerPos.r; + const c0 = playerPos.c; + let r = r0; + let c = c0; + let valid = true; + + for (let i = 1; i <= n; i++) { + r += dr; + c += dc; + if (!m[r] || !m[r][c] || m[r][c].includes("wall")) { + valid = false; + break; + } + if (m[r][c].includes("exit") && gameStatus !== "hasKey") { + valid = false; + break; + } + } + if (!valid) { + flashInvalid(); // โŒ all-or-nothing: show feedback and do nothing + return; + } + + // ----- Animated multi-step (RAF, fast: multiple steps per frame, short duration) ----- + if (n > 1) { + animatingRef.current = true; + + let i = 0; + let rNow = r0; + let cNow = c0; + let cancelled = false; + let rafId: number | null = null; + + // Target total duration (tweak): snappy/fast + const TOTAL_MS = 64; // 48 or 32 for ultra-fast + const framesTarget = Math.max(1, Math.round(TOTAL_MS / 16)); // ~60fps frame estimate + const stepsPerFrame = Math.max(1, Math.ceil(n / framesTarget)); + + const cancel = () => { + cancelled = true; + if (rafId !== null) cancelAnimationFrame(rafId); + window.removeEventListener("keydown", onEsc); + animatingRef.current = false; + }; + const onEsc = (ev: KeyboardEvent) => { + if (ev.key === "Escape") cancel(); + }; + window.addEventListener("keydown", onEsc); + + const tick = () => { + if (cancelled) return; + + // churn through multiple steps in one frame + let processed = 0; + while (processed < stepsPerFrame && i < n) { + i += 1; + rNow += dr; + cNow += dc; + leaveTrail(rNow, cNow); // visual only + processed += 1; + } + + // move hero to the latest stepped cell once per frame + setPlayerPos({ r: rNow, c: cNow }); + window.dispatchEvent(new CustomEvent("maze-logic-tick", { detail: { t: performance.now() } })); + + if (i < n) { + rafId = requestAnimationFrame(tick); + } else { + // final-tile interactions + if (m[rNow][cNow].includes("key")) { + setGameStatus("hasKey"); + m[rNow][cNow] = []; + const container = containerRef.current; + const mazeDiv = container?.querySelector("#maze"); + const keyCell = mazeDiv?.querySelector( + `.maze-row > div[data-r="${rNow}"][data-c="${cNow}"]` + ); + keyCell?.classList.remove("key"); + } + if (m[rNow][cNow].includes("exit")) { + setGameStatus("game-won"); + } + window.removeEventListener("keydown", onEsc); + animatingRef.current = false; + } + }; + + rafId = requestAnimationFrame(tick); + return; + } + + // ----- Single-step (no animation) ----- + const newR = r0 + dr; + const newC = c0 + dc; + + if (!m[newR] || !m[newR][newC]) { + flashInvalid(); + return; + } + if (m[newR][newC].includes("wall")) { + flashInvalid(); + return; + } + if (m[newR][newC].includes("exit") && gameStatus !== "hasKey") { + flashInvalid(); + return; + } + + if (m[newR][newC].includes("key")) { + setGameStatus("hasKey"); + m[newR][newC] = []; + const container = containerRef.current; + const mazeDiv = container?.querySelector("#maze"); + const cell = mazeDiv?.querySelector( + `.maze-row > div[data-r="${newR}"][data-c="${newC}"]` + ); + cell?.classList.remove("key"); + } + if (m[newR][newC].includes("exit")) { + setGameStatus("game-won"); + } + + setPlayerPos({ r: newR, c: newC }); + }, + [playerPos, mazeRef, containerRef, flashInvalid, leaveTrail] + ); + + return { + playerPos, + setPlayerPos, + movePlayer, + }; +} + +export type UsePlayerType = ReturnType; diff --git a/src/useScore/hooks/useTimer.ts b/src/useScore/hooks/useTimer.ts new file mode 100644 index 0000000..44b0e9e --- /dev/null +++ b/src/useScore/hooks/useTimer.ts @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +export type UseTimerReturn = { + timeValue: number; + startTimer: () => void; + stopTimer: () => void; + resetTimer: () => void; +}; + +export type UseTimerOptions = { + /** + * Interval (in ms) between updates. Defaults to 100ms. + */ + interval?: number; +}; + +export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { + const { interval = 100 } = options; + const [timeValue, setTimeValue] = useState(0); + + const intervalIdRef = useRef | null>(null); + const offsetRef = useRef(0); + const startTimeRef = useRef(null); + + const update = useCallback(() => { + if (startTimeRef.current === null) { + return; + } + + const now = performance.now(); + setTimeValue(offsetRef.current + (now - startTimeRef.current)); + }, []); + + const startTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + return; + } + + if (startTimeRef.current === null) { + startTimeRef.current = performance.now(); + } + + update(); + intervalIdRef.current = setInterval(update, Math.max(16, interval)); + }, [interval, update]); + + const stopTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + } + + if (startTimeRef.current !== null) { + update(); + offsetRef.current += performance.now() - startTimeRef.current; + startTimeRef.current = null; + } + }, [update]); + + const resetTimer = useCallback(() => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + intervalIdRef.current = null; + } + + startTimeRef.current = null; + offsetRef.current = 0; + setTimeValue(0); + }, []); + + useEffect(() => { + return () => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); + } + }; + }, []); + + return { + timeValue, + startTimer, + stopTimer, + resetTimer, + }; +} diff --git a/src/useScore/index.ts b/src/useScore/index.ts new file mode 100644 index 0000000..525ce75 --- /dev/null +++ b/src/useScore/index.ts @@ -0,0 +1 @@ +export * from './useScore'; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts new file mode 100644 index 0000000..8514393 --- /dev/null +++ b/src/useScore/useScore.ts @@ -0,0 +1,143 @@ +import { useEffect, useRef, useState } from "react"; +import { useTimer } from "../hooks/useTimer"; +import { useScoreTime } from "./useScoreTime"; +import { useScorePaths } from "./useScorePaths"; +import type { KeyLogProvider, GameScoreContext } from "../types"; + +type UseScoreParams = { + gameContext: GameScoreContext; + keyManager?: KeyLogProvider; +}; + +export function useScore({ gameContext, keyManager }: UseScoreParams) { + const timer = useTimer(); + const { gameStatus, setGameStatus } = gameContext; + const { timeValue, startTimer, stopTimer, resetTimer } = timer; + + // โœ… useScorePaths always returns distances under these names + const { + entranceToKey = 0, + entranceToExit = 0, + heroToKey = 0, + heroToExit = 0, + } = gameContext.mazeManager + ? useScorePaths({ gameManager: gameContext as any }) + : {}; + + // โœ… use unified naming for internal logic + const distToKey = entranceToKey || heroToKey; + const distToExit = entranceToExit || heroToExit; + + const [finalScore, setFinalScore] = useState(null); + const optimalRef = useRef(0); + const keystrokes = keyManager ? keyManager.keyLog.length : 0; + + // 1๏ธโƒฃ Manage visible wall-clock timer + useScoreTime({ gameStatus, timer }); + + // 2๏ธโƒฃ Compute static optimal path when maze (re)starts + useEffect(() => { + const maze = gameContext.mazeManager?.mazeInstanceRef?.current; + if (!maze) return; + + const k = maze.getDistance("entrance", "key"); + const e = maze.getDistance("key", "exit"); + const total = k + e; + + if (Number.isFinite(total) && total > 0) { + optimalRef.current = total; + } + }, [gameStatus, gameContext.mazeManager?.mazeInstanceRef]); + + const optimalSteps = optimalRef.current; + + // 3๏ธโƒฃ Efficiency (keystrokes vs optimal steps) + const efficiency = + optimalSteps > 0 ? Math.round((keystrokes / optimalSteps) * 100) : 0; + + // 4๏ธโƒฃ Logic time (internal motion time) + const [logicFirst, setLogicFirst] = useState(null); + const [logicLast, setLogicLast] = useState(null); + + useEffect(() => { + if (gameStatus === "started") { + setLogicFirst(null); + setLogicLast(null); + setPlayerSteps(0); + prevPosRef.current = null; + } + }, [gameStatus]); + + useEffect(() => { + const onTick = (e: Event) => { + const t = + (e as CustomEvent<{ t?: number }>).detail?.t ?? performance.now(); + setLogicFirst((prev) => prev ?? t); + setLogicLast(t); + }; + window.addEventListener("maze-logic-tick", onTick as EventListener); + return () => + window.removeEventListener("maze-logic-tick", onTick as EventListener); + }, []); + + // 5๏ธโƒฃ Actual player steps + const [playerSteps, setPlayerSteps] = useState(0); + const prevPosRef = useRef<{ r: number; c: number } | null>(null); + + useEffect(() => { + const pos = gameContext.playerManager?.playerPos; + if (!pos) { + prevPosRef.current = null; + return; + } + + if (!["started", "hasKey"].includes(gameStatus)) { + prevPosRef.current = pos; + return; + } + + const prev = prevPosRef.current; + prevPosRef.current = pos; + if (!prev) return; + + const delta = Math.abs(pos.r - prev.r) + Math.abs(pos.c - prev.c); + if (delta > 0) setPlayerSteps((s) => s + delta); + }, [gameStatus, gameContext.playerManager?.playerPos]); + + // 6๏ธโƒฃ Auto game-over on inefficiency + useEffect(() => { + if (gameStatus === "started" && efficiency > 150) { + setGameStatus("game-over"); + } + }, [efficiency, gameStatus, setGameStatus]); + + // 7๏ธโƒฃ Final score on win + useEffect(() => { + if (gameStatus !== "game-won") return; + + const logicMs = + logicFirst != null && logicLast != null + ? Math.max(1, logicLast - logicFirst) + : timeValue; + + const optimalMs = optimalSteps * 200; + const ratio = Math.max(0.1, optimalMs / logicMs); + const score = Math.min(100000, Math.round(ratio * 100000)); + + setFinalScore(score); + }, [gameStatus, logicFirst, logicLast, timeValue, optimalSteps, playerSteps]); + + // โœ… Return clean, typed object + return { + timeValue, + startTimer, + stopTimer, + resetTimer, + distToKey, + distToExit, + keystrokes, + optimalSteps, + efficiency, + finalScore, + }; +} diff --git a/src/useScore/useScorePaths.ts b/src/useScore/useScorePaths.ts new file mode 100644 index 0000000..880dc6b --- /dev/null +++ b/src/useScore/useScorePaths.ts @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import type { GameStatus } from "../types"; + +// explicit so as not to break with circular deps, but should be UseGameType +type UseScorePathsParams = { + gameManager: { + gameStatus: GameStatus; + mazeManager: { + mazeInstanceRef: { current: any }; + }; + playerManager: { + playerPos: { r: number; c: number } | null; + }; + }; +}; + +export function useScorePaths({ gameManager }: UseScorePathsParams) { + const { gameStatus } = gameManager; + const { mazeInstanceRef } = gameManager.mazeManager; + const { playerPos } = gameManager.playerManager; + + const [distances, setDistances] = useState({ + entranceToKey: Infinity, + entranceToExit: Infinity, + heroToKey: Infinity, + heroToExit: Infinity, + }); + + // ๐Ÿงญ 1๏ธโƒฃ Calculate static distances (entrance-based) when game starts + useEffect(() => { + if (gameStatus !== "started") return; + + const maze = mazeInstanceRef.current; + if (!maze) return; + + setDistances((prev) => ({ + ...prev, + entranceToKey: maze.getDistance("entrance", "key"), + entranceToExit: maze.getDistance("entrance", "exit"), + })); + }, [gameStatus, mazeInstanceRef]); + + // ๐Ÿšถ 2๏ธโƒฃ Update dynamic distances (hero-based) on every move + useEffect(() => { + const maze = mazeInstanceRef.current; + if (!maze || !playerPos) return; + + setDistances((prev) => ({ + ...prev, + heroToKey: maze.getDistance("hero", "key", [playerPos.r, playerPos.c]), + heroToExit: maze.getDistance("hero", "exit", [playerPos.r, playerPos.c]), + })); + }, [playerPos, mazeInstanceRef]); + + return distances; +} + +export type UseScorePathsReturn = ReturnType; diff --git a/src/useScore/useScoreTime.ts b/src/useScore/useScoreTime.ts new file mode 100644 index 0000000..fe2514b --- /dev/null +++ b/src/useScore/useScoreTime.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef } from "react"; +import type { UseTimerReturn } from "./hooks/useTimer"; +import type { GameStatus } from "../types"; + +type UseScoreTimeParams = { + gameStatus: GameStatus; + timer: UseTimerReturn; +}; + +export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { + const prevStatusRef = useRef(null); + const { startTimer, stopTimer, resetTimer } = timer; + + useEffect(() => { + // const prevStatus = prevStatusRef.current; + prevStatusRef.current = gameStatus; + + switch (gameStatus) { + case "started": + // Always reset and start a fresh timer when a new game begins + resetTimer(); + startTimer(); + break; + + case "hasKey": + // Keep running + break; + + case "game-over": + case "game-won": + case "waiting": + // Stop timer when game finishes or is exited + stopTimer(); + break; + } + }, [gameStatus, startTimer, stopTimer, resetTimer]); +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..14cd137 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src", "global.d.ts"], + "exclude": ["example"] +} diff --git a/vim-maze.gif b/vim-maze.gif new file mode 100644 index 0000000..e61fdad Binary files /dev/null and b/vim-maze.gif differ