feat: implement Pixel Slide puzzle game with procedural art generation and Web Audio engine

This commit is contained in:
shancheas
2026-08-27 17:36:00 +07:00
commit 4170748438
6 changed files with 3466 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
# ๐Ÿงฉ PixelSlide โ€” 15-Puzzle Image Game with PixiJS
> A premium sliding puzzle game built with **PixiJS v8**, Vanilla HTML5/CSS3/JavaScript, procedural canvas artwork, soundtrack integration, and a persistent local leaderboard.
![PixiJS](https://img.shields.io/badge/PixiJS-v8.20.0-E72264?style=flat-square&logo=pixiv&logoColor=white)
![JavaScript](https://img.shields.io/badge/JavaScript-Vanilla%20ES6+-F7DF1E?style=flat-square&logo=javascript&logoColor=black)
![CSS3](https://img.shields.io/badge/CSS3-Glassmorphism%20Dark-1572B6?style=flat-square&logo=css3&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
---
## โœจ Features
- **๐Ÿ–ผ๏ธ Custom Image Slicing**:
- Upload any image (PNG, JPG, WebP, GIF) via file picker or drag-and-drop.
- Automatically center-crops and slices into grid tiles.
- Comes with 4 built-in procedural gallery presets (*Neon City*, *Deep Galaxy*, *Hex Prism*, *Aurora Peak*) for instant offline play.
- **๐ŸŽฎ 5 Difficulty Levels**:
- ๐Ÿ› **Debug Mode (3ร—3)**: Starts from solved state and performs 5 non-reversing valid moves (guaranteed solvable in **โ‰ค 5 moves**).
- ๐ŸŸข **Easy (3ร—3)**: 8 tiles + 1 blank, 30 valid shuffle moves.
- ๐Ÿ”ต **Medium (4ร—4)**: Classic 15-puzzle (15 tiles + 1 blank), 100 valid shuffle moves.
- ๐ŸŸฃ **Hard (5ร—5)**: 24 tiles + 1 blank, 200 valid shuffle moves.
- ๐Ÿ”ด **Insane (6ร—6)**: 35 tiles + 1 blank, 400 valid shuffle moves.
- **โšก 100% Solvability Guarantee**:
- Shuffling simulates valid legal moves from the solved board state to ensure the puzzle is always solvable without parity issues.
- **๐ŸŽจ Ultra-Smooth PixiJS v8 Rendering**:
- High-DPI canvas rendering with hardware acceleration.
- 60FPS smooth lerp/easing tile sliding animations.
- **๐ŸŽต Sound System & Soundtracks**:
- Background music ([`assets/sounds/backsound.mp3`](assets/sounds/backsound.mp3)) with auto-looping and browser autoplay-policy handling.
- Victory fanfare ([`assets/sounds/winner.mp3`](assets/sounds/winner.mp3)) upon solving.
- Native Web Audio synthesizer for tile slides, clicks, and invalid move bumps.
- Persistent sound toggle (`๐Ÿ”Š` / `๐Ÿ”‡`).
- **๐Ÿ† Persistent Leaderboard**:
- High scores saved in browser `localStorage`.
- Filterable by difficulty (All, Debug, Easy, Medium, Hard, Insane).
- Sorted by fastest completion time and least moves made.
- **๐Ÿ’ก Helper Tools & Accessibility**:
- `#๏ธโƒฃ` **Tile Numbers Toggle**: Overlay numbering (1 to N-1) for challenging abstract images.
- `๐Ÿ‘๏ธ` **Ghost Image Hint**: Semi-transparent reference overlay of the full image.
- `โฑ๏ธ` **Millisecond-Precision Timer**: Starts on the very first move.
- `๐Ÿ”„` **Quick Restart**: Re-shuffle the same image and reset stats with one click.
- `โธ๏ธ` **Pause Modal**: Stops the clock when taking a break.
- `๐ŸŽ‰` **Confetti Burst**: Physics-based canvas confetti explosion on victory.
---
## ๐Ÿ“ Project Structure
```
eigen_vibe_code_competition/
โ”œโ”€โ”€ index.html # Main HTML markup, HUD, modals, and screen containers
โ”œโ”€โ”€ style.css # Cyber dark theme, glassmorphism, responsive grid, animations
โ”œโ”€โ”€ game.js # PixiJS v8 setup, puzzle engine, Web Audio, leaderboard logic
โ”œโ”€โ”€ assets/
โ”‚ โ””โ”€โ”€ sounds/
โ”‚ โ”œโ”€โ”€ backsound.mp3 # Looping background soundtrack
โ”‚ โ””โ”€โ”€ winner.mp3 # Victory celebration audio track
โ””โ”€โ”€ README.md # Project documentation
```
---
## ๐Ÿš€ How to Run the Project
Since the project uses ES6 features and fetches audio assets / PixiJS canvas textures, it should be run through a local HTTP server.
### Option 1: Python (Recommended, no installation required)
If you have Python installed, run in your terminal:
```bash
# Python 3
python3 -m http.server 8080
```
Then open your browser at:
๐Ÿ‘‰ **[http://localhost:8080](http://localhost:8080)**
---
### Option 2: Node.js (`npx serve` or `npx http-server`)
If you have Node.js installed:
```bash
npx serve . -p 8080
```
or
```bash
npx http-server -p 8080
```
Then open **[http://localhost:8080](http://localhost:8080)**.
---
### Option 3: VS Code Live Server
1. Open the project folder in **Visual Studio Code**.
2. Install the **Live Server** extension (by Ritwick Dey).
3. Right-click [`index.html`](index.html) and select **"Open with Live Server"**.
---
## ๐ŸŽฎ Controls & Shortcuts
| Input / Key | Action |
| :--- | :--- |
| **Mouse Click / Tap** | Slide adjacent tile into the blank slot |
| <kbd>โ†‘</kbd> / <kbd>W</kbd> | Move tile below blank upwards |
| <kbd>โ†“</kbd> / <kbd>S</kbd> | Move tile above blank downwards |
| <kbd>โ†</kbd> / <kbd>A</kbd> | Move tile on right to the left |
| <kbd>โ†’</kbd> / <kbd>D</kbd> | Move tile on left to the right |
| <kbd>N</kbd> | Toggle tile number overlays (`#๏ธโƒฃ`) |
| <kbd>H</kbd> | Toggle ghost image hint (`๐Ÿ‘๏ธ`) |
| <kbd>R</kbd> | Quick restart level (`๐Ÿ”„`) |
| <kbd>M</kbd> | Toggle audio mute (`๐Ÿ”Š`/`๐Ÿ”‡`) |
| <kbd>Esc</kbd> | Pause / Resume game (`โธ๏ธ`) |
---
## ๐Ÿ› ๏ธ Technology Stack
- **Rendering Engine**: [PixiJS v8.20.0](https://pixijs.com/) via CDN
- **Structure & Logic**: Vanilla HTML5, Vanilla JavaScript (ES6+ Classes)
- **Styling**: Vanilla CSS3 (Custom Properties, Glassmorphism `backdrop-filter`, Flexbox, CSS Grid)
- **Typography**: Google Fonts (*Outfit*, *Inter*)
- **Audio**: HTML5 Audio API & Web Audio API Synthesizer
- **Storage**: Browser `localStorage` API
---
## ๐Ÿ“ License
Distributed under the MIT License. Feel free to use and customize for your own projects!
Binary file not shown.
Binary file not shown.
+1572
View File
File diff suppressed because it is too large Load Diff
+328
View File
@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Pixel Slide โ€” Premium 15-Puzzle Game</title>
<meta name="description" content="Experience the ultimate sliding puzzle game powered by PixiJS. Slices any custom image into dynamic sliding tiles across multiple difficulty levels." />
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700;800;900&display=swap" rel="stylesheet" />
<!-- PixiJS v8.20.0 -->
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.20.0/dist/pixi.min.js"></script>
<!-- Stylesheet -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Ambient background glow particles / decorative elements -->
<div class="ambient-bg">
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="glow-orb orb-3"></div>
<div class="grid-overlay"></div>
</div>
<div id="app-root">
<!-- TOP NAVIGATION BAR -->
<header class="app-header">
<div class="logo-group" id="btn-nav-home" role="button" tabindex="0" title="Return to Home">
<div class="logo-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1.5"></rect>
<rect x="14" y="3" width="7" height="7" rx="1.5"></rect>
<rect x="3" y="14" width="7" height="7" rx="1.5"></rect>
<rect x="14" y="14" width="7" height="7" rx="1.5" stroke-dasharray="2 2"></rect>
</svg>
</div>
<span class="logo-text">PIXEL<span class="logo-highlight">SLIDE</span></span>
</div>
<div class="header-controls">
<button id="btn-sound-toggle" class="icon-btn" title="Toggle Sound (M)" aria-label="Toggle Sound">
<span id="sound-icon">๐Ÿ”Š</span>
</button>
<button id="btn-header-leaderboard" class="glass-pill-btn" title="View Leaderboard">
<span>๐Ÿ†</span> Leaderboard
</button>
</div>
</header>
<!-- SCREEN: HOME -->
<main id="screen-home" class="screen active">
<div class="hero-section">
<h1 class="hero-title">
Master the <span class="gradient-text">Art of Sliding</span>
</h1>
<p class="hero-subtitle">
Upload any picture or choose from gallery presets. Sliced, shuffled, and rendered with ultra-smooth 60FPS physics.
</p>
<!-- Image source selection -->
<div class="upload-container">
<div class="upload-dropzone" id="upload-dropzone">
<input type="file" id="file-input" accept="image/*" class="visually-hidden" />
<div class="dropzone-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</div>
<div class="dropzone-text">
<strong>Click to upload</strong> or drag and drop an image
</div>
<div class="dropzone-hint">Supports PNG, JPG, WebP, GIF (Max 15MB)</div>
</div>
<div class="preset-divider">
<span>OR CHOOSE A PRESET IMAGE</span>
</div>
<div class="preset-gallery" id="preset-gallery">
<!-- Preset items will be injected by JS -->
</div>
</div>
</div>
</main>
<!-- SCREEN: LEVEL SELECT -->
<section id="screen-level-select" class="screen">
<div class="screen-header">
<button id="btn-back-to-home" class="back-btn">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Back
</button>
<div class="screen-title-group">
<h2>Select Difficulty</h2>
<p>Pick your grid complexity and test your spatial reasoning.</p>
</div>
</div>
<div class="level-selection-layout">
<div class="image-preview-card">
<div class="preview-header">
<span>Selected Image</span>
<button id="btn-change-image" class="text-link-btn">Change</button>
</div>
<div class="preview-canvas-wrapper">
<canvas id="preview-canvas"></canvas>
<div class="preview-grid-overlay" id="preview-grid-overlay"></div>
</div>
</div>
<div class="level-cards-grid" id="level-cards-grid">
<!-- Level Cards rendered by JS -->
</div>
</div>
</section>
<!-- SCREEN: GAMEPLAY -->
<section id="screen-game" class="screen">
<!-- HUD BAR -->
<div class="game-hud">
<div class="hud-left">
<button id="btn-game-pause" class="hud-btn pause-hud-btn" title="Pause Game (Esc)">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2">
<rect x="6" y="4" width="4" height="16" rx="1"></rect>
<rect x="14" y="4" width="4" height="16" rx="1"></rect>
</svg>
<span>Pause</span>
</button>
<div class="level-badge" id="hud-level-badge">Medium 4ร—4</div>
</div>
<div class="hud-center">
<div class="stat-box timer-box">
<span class="stat-label">TIME</span>
<span class="stat-value" id="hud-timer">00:00.00</span>
</div>
<div class="stat-box moves-box">
<span class="stat-label">MOVES</span>
<span class="stat-value" id="hud-moves">0</span>
</div>
</div>
<div class="hud-right">
<button id="btn-toggle-numbers" class="hud-btn icon-only" title="Toggle Tile Numbers (N)" aria-label="Toggle Numbers">
<span class="btn-icon">#๏ธโƒฃ</span>
</button>
<button id="btn-toggle-hint" class="hud-btn icon-only" title="Toggle Image Ghost Hint (H)" aria-label="Toggle Hint">
<span class="btn-icon">๐Ÿ‘๏ธ</span>
</button>
<button id="btn-game-restart-quick" class="hud-btn icon-only" title="Restart Level (R)" aria-label="Restart Level">
<span class="btn-icon">๐Ÿ”„</span>
</button>
</div>
</div>
<!-- MAIN PUZZLE CANVAS CONTAINER -->
<div class="puzzle-viewport-container">
<div class="puzzle-board-frame" id="puzzle-board-frame">
<div id="pixi-canvas-mount"></div>
<!-- Ghost image overlay for hints -->
<div id="ghost-overlay" class="ghost-overlay"></div>
<!-- Shuffling overlay -->
<div id="shuffle-overlay" class="shuffle-overlay">
<div class="shuffle-spinner"></div>
<span>Shuffling tiles...</span>
</div>
</div>
</div>
<!-- BOTTOM CONTROL BAR -->
<div class="game-footer-info">
<span class="tip-text">๐Ÿ’ก Tap adjacent tiles or use <kbd>Arrow Keys</kbd> / <kbd>WASD</kbd> to slide</span>
</div>
</section>
<!-- SCREEN: LEADERBOARD -->
<section id="screen-leaderboard" class="screen">
<div class="screen-header">
<button id="btn-back-from-leaderboard" class="back-btn">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Back
</button>
<div class="screen-title-group">
<h2>Hall of Fame</h2>
<p>Global high scores sorted by fastest completion time and move count.</p>
</div>
</div>
<div class="leaderboard-container glass-card">
<div class="leaderboard-tabs" id="leaderboard-tabs">
<button class="tab-btn active" data-level="all">All Levels</button>
<button class="tab-btn" data-level="debug">Debug</button>
<button class="tab-btn" data-level="easy">Easy (3ร—3)</button>
<button class="tab-btn" data-level="medium">Medium (4ร—4)</button>
<button class="tab-btn" data-level="hard">Hard (5ร—5)</button>
<button class="tab-btn" data-level="insane">Insane (6ร—6)</button>
</div>
<div class="leaderboard-table-wrapper">
<table class="leaderboard-table">
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Difficulty</th>
<th>Time</th>
<th>Moves</th>
<th>Date</th>
</tr>
</thead>
<tbody id="leaderboard-tbody">
<!-- Entries injected by JS -->
</tbody>
</table>
<div id="leaderboard-empty" class="leaderboard-empty">
<div class="empty-icon">๐Ÿ†</div>
<p>No records found for this category.</p>
<span class="empty-sub">Complete a puzzle to claim the first spot!</span>
</div>
</div>
<div class="leaderboard-footer">
<button id="btn-clear-leaderboard" class="danger-link-btn">Clear Records</button>
</div>
</div>
</section>
<!-- OVERLAY: PAUSE MODAL -->
<div id="overlay-pause" class="modal-overlay">
<div class="modal-card pause-card">
<div class="modal-icon-badge">โธ</div>
<h2>Game Paused</h2>
<p>Take a breath. The timer is currently stopped.</p>
<div class="modal-stats-row">
<div class="modal-stat">
<span class="m-label">Elapsed Time</span>
<span class="m-val" id="pause-modal-time">00:00.00</span>
</div>
<div class="modal-stat">
<span class="m-label">Moves Made</span>
<span class="m-val" id="pause-modal-moves">0</span>
</div>
</div>
<div class="modal-actions-stacked">
<button id="btn-pause-resume" class="btn btn-primary btn-glow">
<span>โ–ถ</span> Resume Game
</button>
<button id="btn-pause-restart" class="btn btn-secondary">
<span>๐Ÿ”„</span> Restart Puzzle
</button>
<button id="btn-pause-change-level" class="btn btn-outline">
<span>๐ŸŽฎ</span> Change Difficulty
</button>
<button id="btn-pause-quit" class="btn btn-danger-outline">
<span>๐Ÿ </span> Quit to Menu
</button>
</div>
</div>
</div>
<!-- OVERLAY: VICTORY MODAL -->
<div id="overlay-victory" class="modal-overlay">
<canvas id="confetti-canvas" class="confetti-canvas"></canvas>
<div class="modal-card victory-card">
<div class="victory-header">
<div class="crown-icon">๐Ÿ‘‘</div>
<h2>Puzzle Solved!</h2>
<p class="victory-tagline">Flawless spatial mastery. Here are your final stats:</p>
</div>
<div class="victory-stats-grid">
<div class="v-stat-card highlight">
<span class="v-label">TIME</span>
<span class="v-value" id="victory-time">00:00.00</span>
</div>
<div class="v-stat-card">
<span class="v-label">MOVES</span>
<span class="v-value" id="victory-moves">0</span>
</div>
<div class="v-stat-card">
<span class="v-label">DIFFICULTY</span>
<span class="v-value" id="victory-difficulty">Medium</span>
</div>
</div>
<!-- Name submit form -->
<form id="victory-form" class="victory-form" onsubmit="return false;">
<label for="player-name-input">Save to Leaderboard</label>
<div class="input-with-btn">
<input type="text" id="player-name-input" maxlength="20" placeholder="Enter your name" required autocomplete="off" />
<button type="submit" id="btn-save-score" class="btn btn-primary">
Save Score
</button>
</div>
<div id="save-feedback" class="save-feedback"></div>
</form>
<div class="victory-actions">
<button id="btn-victory-replay" class="btn btn-secondary">
<span>๐Ÿ”„</span> Play Again
</button>
<button id="btn-victory-levels" class="btn btn-primary">
<span>๐ŸŽฎ</span> Select Level
</button>
<button id="btn-victory-leaderboard" class="btn btn-outline">
<span>๐Ÿ†</span> Leaderboard
</button>
</div>
</div>
</div>
</div>
<!-- Game script -->
<script src="game.js"></script>
</body>
</html>
+1430
View File
File diff suppressed because it is too large Load Diff