A browser-based level builder and arcade puzzle game where players can create maps, play custom levels, share exports, and compete on leaderboards.
Demo Video: https://youtu.be/cHKOrDZPw-U?si=1hhYFDztobo06hrW
Live Site: https://level-crafter.vercel.app/
Repository: https://github.com/codenamed22/LevelCrafter
- About The Project
- What You Can Do
- Core Features
- Gameplay Rules
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- Available Scripts
- How The App Works
- Database And Leaderboards
- Level Format
- Important Source Files
- Testing And Verification
- Future Improvements
- Documentation
- License
LevelCrafter is a React and TypeScript web app for building and playing custom grid-based puzzle levels.
The project has two main experiences:
- Create levels with a visual tile editor.
- Play levels with arcade-style movement, hazards, scoring, and leaderboards.
It is designed like a small game platform. Players can make maps, save them, import maps, export playable HTML files, customize their character, complete levels, and see profile or leaderboard progress.
The app uses Supabase for authentication, level storage, user profiles, completed runs, best scores, and global rankings.
With LevelCrafter, a user can:
- Create a new level by choosing a difficulty.
- Paint tiles on a grid using a visual editor.
- Place walls, coins, bombs, vents, exits, moving hazards, and the player start.
- Generate starter maps or maze maps.
- Undo and redo editor changes.
- Validate whether a level is playable.
- Save levels to Supabase.
- Import saved levels into the editor.
- Paste JSON level data.
- Export levels as JSON.
- Copy a share code for a level.
- Export a standalone playable HTML file.
- Play levels by difficulty.
- Move with keyboard controls or pointer dragging.
- Collect coins and reach the exit.
- Avoid bombs, fire, and moving hazards.
- Use vents to teleport to another vent.
- Track moves, time, score, and completion stats.
- View per-level leaderboards.
- View global rankings.
- Register, log in, and view a profile dashboard.
- Change username and player avatar.
The editor lets users build levels directly in the browser.
Supported editor features:
- Difficulty-based board sizes:
- Easy:
5x5 - Medium:
8x8 - Hard:
12x12
- Easy:
- Click-and-drag painting.
- Tile palette.
- Undo and redo history.
- Starter level generation.
- DFS maze generation.
- Bomb preview timer configuration.
- Level validation before save/export.
- Saved-level import.
- JSON paste import.
- JSON export.
- Share-code export.
- Standalone HTML export.
The play screen loads levels by difficulty and lets the player complete them.
Gameplay features:
- WASD movement.
- Pointer drag movement from the player tile.
- Wall collision.
- Coin collection.
- Hidden bombs after a preview countdown.
- Moving danger tiles.
- Vent teleport selection.
- Explosion feedback on death.
- Win screen with score breakdown.
- Per-level leaderboard modal.
Authenticated users get a profile dashboard.
Profile features:
- Username display and edit.
- Player avatar selection.
- Created map list.
- Recent play history.
- Global rank display.
- Logout support.
LevelCrafter supports two ranking views:
- Per-level leaderboards based on each user's best score for a specific level.
- Global rankings based on the total of best scores across completed levels.
Repeated runs are saved as play history, but only a user's best score for each level contributes to leaderboards.
The game is played on a square tile grid.
| Tile | Meaning |
|---|---|
empty |
Walkable floor tile |
wall |
Blocks movement |
coin |
Collectible bonus item |
hazard |
Bomb tile that causes restart |
movingFireHorizontal |
Moving fire hazard on the horizontal axis |
movingFireVertical |
Moving fire hazard on the vertical axis |
movingHazardHorizontal |
Moving bomb-style hazard on the horizontal axis |
movingHazardVertical |
Moving bomb-style hazard on the vertical axis |
enemyHorizontal |
Horizontal moving danger tile |
enemyVertical |
Vertical moving danger tile |
vent |
Teleport tile |
player |
Player start position |
exit |
Win tile |
W: move upA: move leftS: move downD: move right
The board also supports pointer dragging. Start dragging from the player tile and move into adjacent cells.
The player wins by reaching the exit tile.
The player restarts if they touch:
- A visible or hidden bomb.
- Moving fire.
- Any dynamic danger tile.
When the player steps on a vent, the game shows available vent destinations. The player can click a destination vent to teleport there.
Bombs are visible for a short countdown at the beginning of a level. After the countdown ends, bomb art fades out, but the hazard still exists and still causes restart.
- React
- TypeScript
- Vite
- Tailwind CSS
- React Router
- Supabase
- PostgreSQL
- Supabase Auth
- Supabase RPC for completed run recording
- ESLint
- TypeScript compiler
- Vite production build
LevelCrafter/
├── avatar/ # Player avatar image assets
├── public/ # Public static files
├── readme/ # Project documentation
├── src/
│ ├── audio/ # Retro music and sound effects
│ ├── components/ # Shared app-level components
│ ├── constants/ # Difficulty and tile constants
│ ├── context/ # Auth context
│ ├── features/
│ │ ├── editor/ # Grid editor and tile palette
│ │ ├── mazeGenerator/ # Maze generation logic
│ │ ├── play/ # Gameplay hooks and play components
│ │ ├── playerAvatar/ # Avatar UI and storage
│ │ └── tiles/ # Tile artwork and asset mapping
│ ├── game/ # Movement, collision, rules, spawn, engine logic
│ ├── hooks/ # Shared React hooks
│ ├── lib/ # Supabase client setup
│ ├── pages/ # Route-level screens
│ ├── services/ # Supabase, scoring, level, and profile services
│ ├── shared/ # Shared UI components
│ ├── types/ # TypeScript app and database types
│ ├── App.tsx # Router and top-level app composition
│ └── main.tsx # React entry point
├── supabase/
│ └── migrations/ # Database schema and RPC migration
├── tiles/ # Tile image assets
├── package.json
├── vite.config.ts
└── tsconfig.json
Follow these steps to run the project locally.
Install Node.js and npm.
Recommended:
Node.js 20+
npm 10+
Clone the repository:
git clone <your-repository-url>
cd LevelCrafterInstall dependencies:
npm installCreate a .env file in the project root:
VITE_SUPABASE_URL=<your-supabase-project-url>
VITE_SUPABASE_ANON_KEY=<your-supabase-anon-key>Start the development server:
npm run devOpen the local Vite URL shown in the terminal. It is usually:
http://localhost:5173
The app requires Supabase configuration.
| Variable | Required | Description |
|---|---|---|
VITE_SUPABASE_URL |
Yes | Supabase project URL |
VITE_SUPABASE_ANON_KEY |
Yes | Supabase public anon key |
These values are read in src/lib/supabase.ts.
Do not commit private service-role keys to the frontend. The frontend should only use the public anon key.
npm run devRuns the app locally with Vite.
npm run buildRuns TypeScript build checks and creates the production bundle.
npm run lintRuns ESLint across the project.
npm run previewServes the built production output locally.
Routes are defined in src/App.tsx.
Main routes:
| Route | Screen |
|---|---|
/ |
Home |
/create |
Create level |
/play |
Play levels |
/login |
Login |
/register |
Register |
/profile |
Profile dashboard |
/global-leaderboard |
Global leaderboard |
The profile route is protected. Users without a session are redirected to login.
Choose difficulty
|
v
Create empty/starter/maze grid
|
v
Paint tiles in editor
|
v
Validate level
|
v
Save, export, or playtest
Important rules:
- A level must have exactly one player start.
- A level must have exactly one exit.
- A level with vents must have at least two vents.
- The exit must be reachable from the player start.
- The grid size must match the selected difficulty.
Choose difficulty
|
v
Load levels
|
v
Select level
|
v
Preview bombs
|
v
Move, collect, avoid hazards
|
v
Win or restart
The active player position is stored separately from the static level grid. This keeps movement logic clean and prevents stale player tiles from staying in the board after reset or replay.
The score calculation rewards completion, coins, difficulty, and shorter bomb-preview timers.
Current scoring model:
baseScore = 1000
coinBonus = coinsCollected * 100
movePenalty = moves * 5
timePenalty = timeSeconds * 2
rawScore = max(0, baseScore + coinBonus - movePenalty - timePenalty)
finalScore = rawScore * difficultyMultiplier * bombPreviewMultiplier
Difficulty multipliers:
| Difficulty | Multiplier |
|---|---|
| Easy | 1.0 |
| Medium | 1.25 |
| Hard | 1.5 |
Shorter bomb preview timers use higher multipliers because they make the level harder.
The database is defined in:
supabase/migrations/001_leaderboards.sql
Main data areas:
users: public profile information.levels: saved level metadata and grid data.play_sessions: completed run history.best_scores: best score per user per level.global_rankings: view that aggregates global scores.
Player wins level
|
v
Calculate score
|
v
Insert play session
|
v
Update best score if score improved
|
v
Leaderboard/global ranking reads use best_scores
The preferred persistence path uses the record_completed_run Supabase RPC so play session creation and best-score updates happen together.
Every completed run is useful for history, but a leaderboard should show each player only once per level.
That is why:
play_sessionsstores all completions.best_scoresstores only the best completion for each(user_id, level_id).global_rankingssums best scores, not every run.
A level is stored as a TypeScript object like this:
interface Level {
id: string;
name: string;
difficulty: "easy" | "medium" | "hard";
bombPreviewSeconds: number;
createdAt: number;
width: number;
height: number;
grid: Tile[][];
}Example simplified level:
{
"name": "Starter Easy",
"difficulty": "easy",
"bombPreviewSeconds": 3,
"width": 5,
"height": 5,
"grid": [
["wall", "wall", "wall", "wall", "wall"],
["wall", "player", "coin", "exit", "wall"],
["wall", "empty", "wall", "empty", "wall"],
["wall", "hazard", "empty", "empty", "wall"],
["wall", "wall", "wall", "wall", "wall"]
]
}Imported levels are treated as untrusted input. The app validates the grid, tile names, difficulty, player count, exit count, and reachable path before saving imported data.
| File | Purpose |
|---|---|
src/game/movement.ts |
Calculates the next position for a direction |
src/game/collision.ts |
Reads tiles and handles wall collision |
src/game/rules.ts |
Converts tiles into gameplay events |
src/game/spawn.ts |
Finds the player start |
src/game/gameEngine.ts |
Resolves movement and vent destinations |
src/features/play/hooks/useGame.ts |
Main gameplay state machine |
| File | Purpose |
|---|---|
src/pages/CreateLevelPage.tsx |
Main level creation screen |
src/features/editor/components/GridEditor.tsx |
Paintable grid UI |
src/features/editor/components/TilePalette.tsx |
Tile selection UI |
src/features/mazeGenerator/mazeGenerator.ts |
DFS maze generator |
src/services/levelValidation.ts |
Level validation rules |
| File | Purpose |
|---|---|
src/lib/supabase.ts |
Supabase client setup |
src/services/levelStorage.ts |
Save, load, import, export level data |
src/services/gameplayService.ts |
Persist completed gameplay runs |
src/services/scoreService.ts |
Calculate scores and update best scores |
src/services/leaderboardService.ts |
Read per-level leaderboards |
src/services/rankingService.ts |
Read global rankings |
src/services/profileService.ts |
Auth user, profile, avatar, dashboard data |
| File | Purpose |
|---|---|
src/App.tsx |
Main routes |
src/context/AuthContext.tsx |
Session context |
src/components/ProtectedRoute.tsx |
Auth-only route guard |
src/pages/PlayPage.tsx |
Main play screen |
src/pages/ProfilePage.tsx |
Profile dashboard |
src/pages/GlobalLeaderboardPage.tsx |
Global leaderboard screen |
This project currently uses linting and production build checks.
Run lint:
npm run lintRun production build:
npm run buildRecommended manual checks after major gameplay/editor changes:
- Create each difficulty level.
- Place every tile type.
- Validate a playable level.
- Save a level.
- Import a saved level.
- Export JSON.
- Copy a share code and import it.
- Export standalone HTML.
- Play a level to completion.
- Confirm bombs hide after the countdown.
- Confirm moving hazards restart the player.
- Confirm vents let the player choose a destination.
- Confirm score appears after win.
- Confirm leaderboard/profile data updates after completion.
Possible next steps:
- Add automated unit tests for movement, validation, maze generation, and scoring.
- Add end-to-end tests for create/play flows.
- Add route-level code splitting to reduce the production bundle size.
- Add more tile types and puzzle mechanics.
- Add creator analytics.
- Add level search and filtering.
- Add public level pages.
- Add comments or ratings for levels.
- Add better mobile-specific editor controls.
- Add richer standalone export visuals.
This project is licensed under the MIT License. See readme/LICENSE.md for details.