
A browser-based fruit-merging puzzle game inspired by Suika Game. Drop fruits into a container ā when two identical fruits touch, they merge into the next fruit in the evolution chain. Combine your way up through 11 fruits (blueberry ā grape ā lemon ā orange ā apple ā dragonfruit ā pear ā peach ā pineapple ā honeydew ā watermelon) and chase the highest score.
The original game, "Merge Big Watermelon" (åę大脿ē), was created by Meadow Science (ē±³å ē§ę). This project was built as a fun memento of a team off-site where I accidentally got my entire team hooked on Suika Game. "Subak" (ģė°) is Korean for watermelon.
Play it live ā subak.kempf.dev

The project serves two roles:
| Mode | Description |
|---|---|
| Standalone app | A full SvelteKit single-page app deployed as a static site via @sveltejs/adapter-static. |
| Embeddable library | Published as an npm package exporting a SubakGame Svelte component (import { SubakGame } from 'subak-game'). |
src/
āāā lib/
ā āāā api/ # LeaderboardClient ā reactive Svelte 5 class managing
ā ā # session tokens, score submission, and global score fetching
ā āāā components/ # UI layer (Game, Leaderboard, modals, merge effects, etc.)
ā āāā game/ # Physics-layer classes
ā ā āāā Fruit.ts # Fruit rigid-body wrapper (Rapier colliders, merging)
ā ā āāā Boundary.ts # Wall / floor collider creation
ā ā āāā AudioManager.svelte.ts # Sound effect management via Howler
ā āāā hooks/ # Reactive utilities (useBoundingRect, useCursorPosition)
ā āāā stores/
ā ā āāā game.svelte.ts # Core GameState class ā physics loop, collision detection,
ā ā ā # score tracking, fruit spawning, game-over logic
ā ā āāā db.ts # Local high-score persistence (Dexie / IndexedDB)
ā ā āāā telemetry.svelte.ts # Session telemetry & anti-cheat payload builder
ā āāā icons/ & svg/ # Inline SVG fruit sprites and UI icons
ā āāā types/ # Shared TypeScript interfaces
āāā routes/ # SvelteKit page entry point
āāā utils/ # Web analytics (PostHog) initializer
| Layer | Tool | Why |
|---|---|---|
| UI | Svelte 5 | Fine-grained reactivity via $state runes, minimal runtime overhead |
| Physics | Rapier (rapier2d-compat) | WASM-powered 2D rigid-body simulation with deterministic collision events |
| Audio | Howler | Cross-browser sound playback with volume and pitch control |
| Local storage | Dexie | Ergonomic IndexedDB wrapper for persisting local high scores |
| Screenshots | modern-screenshot | DOM-to-image capture for sharing game-over screens |
| Telemetry | PostHog | Optional web analytics |
| Build | Vite + SvelteKit | Fast HMR, SSG via adapter-static, library mode via svelte-package |
| Linting | Biome | Formatting and linting in a single tool |
| Testing | Vitest (browser mode) + Playwright | Real-browser test execution with @testing-library/svelte |
| Type checking | TypeScript + svelte-check |
Full strict-mode type safety across .ts and .svelte files |
git clone https://github.com/Fauntleroy/subak-game.git
cd subak-game
npm install
cp .env.example .env
| Variable | Purpose | Default |
|---|---|---|
VITE_APP_VERSION |
Injected build version | $npm_package_version |
VITE_POSTHOG_TOKEN |
PostHog analytics token (optional) | ā |
PUBLIC_SHARED_CLIENT_SALT |
Shared salt for anti-cheat hash | "public_secret_hash_salt" |
PUBLIC_LEADERBOARD_URL |
Leaderboard API base URL | http://localhost:3001 |
Leaderboard is optional. The game runs fully offline ā the leaderboard client gracefully handles missing or unavailable servers.
npm run dev
Open http://localhost:4032 (port 4032 = PLU code of ģė° š).
| Command | Description |
|---|---|
npm run dev |
Start Vite dev server with HMR |
npm run build |
Production build ā build/ (static site) + dist/ (library package) |
npm run preview |
Preview the production build locally |
npm run check |
Run svelte-check for type errors |
npm run lint |
Lint with Biome |
npm run format |
Auto-format with Biome |
npm run test |
Run Vitest (browser mode, headless Chromium) |
npm run test:watch |
Run Vitest in watch mode |
npm run validate |
Full CI gate ā format ā lint ā test ā check |
Tests run in Vitest browser mode using Playwright (headless Chromium) for real DOM and browser API access.
npm run test # single run
npm run test:watch # watch mode
Test files live alongside their source code in __tests__/ directories, covering components, game engine classes, stores, and hooks.
npm run build
The SvelteKit static adapter outputs to build/. The docs/ directory contains a pre-built snapshot served via GitHub Pages at subak.kempf.dev.
npm run prepack
Outputs the publishable Svelte component library to dist/. Consumers import the game as:
<script>
import { SubakGame } from 'subak-game';
</script>
<SubakGame />
The game connects to an optional companion leaderboard API (subak-leaderboard) for global score tracking. Set PUBLIC_LEADERBOARD_URL in .env to point to a running instance. See the leaderboard repo for setup instructions.
subak-game/
āāā src/
ā āāā lib/ # Core library ā components, game engine, stores, hooks
ā āāā routes/ # SvelteKit app entry point
ā āāā utils/ # Web analytics setup
āāā static/ # Static assets (images, sounds, favicon)
āāā design/ # Source design files (Affinity Designer)
āāā docs/ # Pre-built static site for GitHub Pages
āāā dist/ # Compiled library package output
āāā biome.json # Biome linter/formatter config
āāā svelte.config.js # SvelteKit config (static adapter)
āāā vite.config.ts # Vite config (dev server, build)
āāā vitest.config.ts # Vitest config (browser mode, Playwright)
āāā tsconfig.json # TypeScript config
Contributions are welcome! If you have ideas for improvements, new features, or bug fixes, feel free to open an issue or submit a pull request.
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0). See LICENSE.md for details.