mirror of
https://github.com/tiennm99/try-claudekit.git
synced 2026-04-17 13:21:43 +00:00
chore: remove completed spec files
These implementation specs served their purpose and the features are all merged — no longer needed in the repo.
This commit is contained in:
@@ -1,160 +0,0 @@
|
|||||||
# GitHub Actions: Deploy to GitHub Pages
|
|
||||||
|
|
||||||
## Status
|
|
||||||
Draft
|
|
||||||
|
|
||||||
## Authors
|
|
||||||
Claude Code — 2026-04-12
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Add a GitHub Actions workflow to automatically build and deploy the Suika game to GitHub Pages on every push to `main`. This gives the game a publicly accessible URL with zero hosting cost.
|
|
||||||
|
|
||||||
## Background / Problem Statement
|
|
||||||
The game currently has no deployment pipeline. To share or play it, someone must clone the repo and run `npm run dev` locally. GitHub Pages is the natural fit: the repo is already hosted on GitHub (`tiennm99/try-claudekit`), the game is a static Vite build, and Pages is free for public repos.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
- Automated deploy on every push to `main`
|
|
||||||
- Game accessible at `https://tiennm99.github.io/try-claudekit/`
|
|
||||||
- Build failures block deployment (no broken deploys)
|
|
||||||
- Run existing tests before deploying
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
- Custom domain setup
|
|
||||||
- Preview deployments for pull requests
|
|
||||||
- CDN or caching configuration beyond GitHub Pages defaults
|
|
||||||
- Server-side rendering or API routes
|
|
||||||
|
|
||||||
## Technical Dependencies
|
|
||||||
- **GitHub Actions** — built-in CI/CD, no external service needed
|
|
||||||
- **actions/configure-pages** / **actions/upload-pages-artifact** / **actions/deploy-pages** — official GitHub Pages deployment actions
|
|
||||||
- **Vite** (already in devDeps) — needs `base` config for subdirectory hosting
|
|
||||||
|
|
||||||
## Detailed Design
|
|
||||||
|
|
||||||
### 1. Vite Base Path Configuration
|
|
||||||
|
|
||||||
GitHub Pages serves this repo at `/try-claudekit/`, so asset paths must be prefixed. Create a minimal `vite.config.js`:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { defineConfig } from "vite";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
base: "/try-claudekit/",
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
This affects only production builds (`npm run build`). Dev server (`npm run dev`) ignores `base` for local paths.
|
|
||||||
|
|
||||||
### 2. GitHub Actions Workflow
|
|
||||||
|
|
||||||
Create `.github/workflows/deploy.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: Deploy to GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
workflow_dispatch: # allow manual trigger
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: pages
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
cache: npm
|
|
||||||
|
|
||||||
- run: npm ci
|
|
||||||
|
|
||||||
- run: npm test
|
|
||||||
|
|
||||||
- run: npm run build
|
|
||||||
|
|
||||||
- uses: actions/configure-pages@v5
|
|
||||||
|
|
||||||
- uses: actions/upload-pages-artifact@v3
|
|
||||||
with:
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
- id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. File Changes Summary
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `vite.config.js` (new) | Set `base: "/try-claudekit/"` |
|
|
||||||
| `.github/workflows/deploy.yml` (new) | Build + test + deploy workflow |
|
|
||||||
|
|
||||||
### 4. GitHub Repository Setting
|
|
||||||
|
|
||||||
GitHub Pages source must be set to **"GitHub Actions"** (not branch-based). This is a one-time manual step in repo Settings > Pages > Source.
|
|
||||||
|
|
||||||
## User Experience
|
|
||||||
- **Developers**: Push to `main` triggers automatic deployment. Build/test failures show in the Actions tab and block deploy.
|
|
||||||
- **Players**: Visit `https://tiennm99.github.io/try-claudekit/` to play. No install required.
|
|
||||||
- **Manual trigger**: The `workflow_dispatch` event allows re-deploying from the Actions tab without a new commit.
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Pre-deploy gate
|
|
||||||
The workflow runs `npm test` before building. If any test fails, the job stops and nothing is deployed.
|
|
||||||
|
|
||||||
### Verification after implementation
|
|
||||||
- Push a commit to `main` and confirm the Actions run completes successfully
|
|
||||||
- Visit the Pages URL and verify the game loads and is playable
|
|
||||||
- Verify asset paths resolve correctly (no 404s for JS/CSS bundles)
|
|
||||||
- Test that a failing test prevents deployment by temporarily breaking a test
|
|
||||||
|
|
||||||
### Local validation
|
|
||||||
- Run `npm run build` locally and serve `dist/` with `npx vite preview` to verify the base path works
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
- Build is fast (~5s for a small Vite project). No caching beyond `npm ci` + Node module cache needed.
|
|
||||||
- GitHub Pages serves with its own CDN. No additional optimization required for this scale.
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
- Workflow uses minimal permissions (`contents: read`, `pages: write`, `id-token: write`)
|
|
||||||
- No secrets or tokens needed — Pages deployment uses OIDC via `id-token: write`
|
|
||||||
- `concurrency` group prevents race conditions between overlapping deploys
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
- Update README or docs to include the live URL once deployed
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Core (MVP)
|
|
||||||
1. Create `vite.config.js` with `base` setting
|
|
||||||
2. Create `.github/workflows/deploy.yml`
|
|
||||||
3. Set GitHub Pages source to "GitHub Actions" in repo settings
|
|
||||||
4. Push to `main` and verify deployment
|
|
||||||
|
|
||||||
### Phase 2: Optional Enhancements (if desired later)
|
|
||||||
- Add PR preview deployments
|
|
||||||
- Add build status badge to README
|
|
||||||
- Custom domain configuration
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
- Should the repo name change in the future, the `base` path in `vite.config.js` must be updated to match. Consider using an environment variable if this is likely.
|
|
||||||
|
|
||||||
## References
|
|
||||||
- [Vite — Deploying to GitHub Pages](https://vite.dev/guide/static-deploy#github-pages)
|
|
||||||
- [GitHub Actions — deploy-pages](https://github.com/actions/deploy-pages)
|
|
||||||
- [GitHub Pages documentation](https://docs.github.com/en/pages)
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
# Suika Game (Watermelon Game)
|
|
||||||
|
|
||||||
**Status**: Draft
|
|
||||||
**Authors**: Claude Code, 2026-04-12
|
|
||||||
**Type**: Feature
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
A browser-based clone of the Suika Game (Watermelon Game) -- a physics-based puzzle game where players drop fruits into a container. When two identical fruits collide, they merge into the next larger fruit in the chain. The goal is to create the largest fruit (watermelon) and achieve the highest score without overflowing the container.
|
|
||||||
|
|
||||||
## Background / Problem Statement
|
|
||||||
|
|
||||||
This is a greenfield project with no existing application code. The repository currently contains only ClaudeKit tooling configuration. The goal is to build a complete, playable Suika Game as a web application using modern frontend tooling.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Deliver a fully playable Suika Game in the browser
|
|
||||||
- Implement realistic 2D physics (gravity, collision, settling)
|
|
||||||
- Support the complete 11-fruit progression chain with merging
|
|
||||||
- Provide score tracking and game-over detection
|
|
||||||
- Responsive design that works on both desktop (mouse) and mobile (touch)
|
|
||||||
- Clean, maintainable JavaScript codebase
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
- Multiplayer or networked play
|
|
||||||
- Persistent leaderboards or backend/database integration
|
|
||||||
- Sound effects or music (can be added later)
|
|
||||||
- Animations beyond basic physics (no particle effects, screen shakes, etc.)
|
|
||||||
- User accounts or authentication
|
|
||||||
- PWA / offline support
|
|
||||||
|
|
||||||
## Technical Dependencies
|
|
||||||
|
|
||||||
| Dependency | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| [Matter.js](https://brm.io/matter-js/) | ^0.20.0 | 2D rigid-body physics engine |
|
|
||||||
| [Vite](https://vitejs.dev/) | ^6.x | Build tooling and dev server |
|
|
||||||
|
|
||||||
No framework (React, etc.) is needed -- this will be a vanilla JavaScript + Canvas2D application to keep it simple and performant.
|
|
||||||
|
|
||||||
## Detailed Design
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
main.js # Entry point: initializes game, attaches event listeners
|
|
||||||
game.js # Game class: orchestrates engine, renderer, input, state
|
|
||||||
physics.js # Matter.js engine setup, world configuration, walls
|
|
||||||
renderer.js # Canvas2D rendering: fruits, walls, UI overlay
|
|
||||||
input.js # Mouse/touch input handling, drop control
|
|
||||||
fruits.js # Fruit definitions (sizes, colors, labels, progression)
|
|
||||||
merger.js # Collision detection callback, merge logic, scoring
|
|
||||||
constants.js # Game dimensions, physics tuning, timing constants
|
|
||||||
index.html # Single HTML page with canvas element
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fruit Progression Chain
|
|
||||||
|
|
||||||
11 fruits from smallest to largest. Only the 5 smallest can be randomly selected for dropping.
|
|
||||||
|
|
||||||
| Tier | Fruit | Radius (px) | Color | Merge Points |
|
|
||||||
|------|-------|-------------|-------|-------------|
|
|
||||||
| 0 | Cherry | 12 | #E74C3C | 1 |
|
|
||||||
| 1 | Strawberry | 16 | #FF6B6B | 3 |
|
|
||||||
| 2 | Grapes | 20 | #9B59B6 | 6 |
|
|
||||||
| 3 | Dekopon | 26 | #F39C12 | 10 |
|
|
||||||
| 4 | Persimmon | 32 | #E67E22 | 15 |
|
|
||||||
| 5 | Apple | 36 | #E74C3C | 21 |
|
|
||||||
| 6 | Pear | 42 | #A8D648 | 28 |
|
|
||||||
| 7 | Peach | 48 | #FDCB6E | 36 |
|
|
||||||
| 8 | Pineapple | 57 | #F1C40F | 45 |
|
|
||||||
| 9 | Melon | 64 | #2ECC71 | 55 |
|
|
||||||
| 10 | Watermelon | 77 | #27AE60 | 66 |
|
|
||||||
|
|
||||||
When two tier-10 watermelons merge, they disappear (bonus points, no new fruit spawned).
|
|
||||||
|
|
||||||
### Container / World Setup
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────┐ ← danger line (Y threshold)
|
|
||||||
│ │
|
|
||||||
│ drop zone │
|
|
||||||
│ │
|
|
||||||
├─────────────────────────┤ ← top of container walls
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
│ ● ● │
|
|
||||||
│ ●●● ●● │
|
|
||||||
│ ●●●●●●●●● │
|
|
||||||
└─────────────────────────┘ ← floor
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Container**: U-shaped static body (left wall, right wall, floor)
|
|
||||||
- **Dimensions**: 400px wide x 600px tall container, centered on canvas
|
|
||||||
- **Canvas size**: 500px x 700px (with padding for UI)
|
|
||||||
- **Danger line**: ~50px below the top of the container walls
|
|
||||||
|
|
||||||
### Physics Configuration (Matter.js)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const engine = Engine.create({
|
|
||||||
gravity: { x: 0, y: 1.5 },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fruit body defaults
|
|
||||||
const fruitBodyOptions = {
|
|
||||||
restitution: 0.2, // low bounce
|
|
||||||
friction: 0.5, // moderate friction
|
|
||||||
frictionAir: 0.01, // slight air drag
|
|
||||||
density: 0.001, // consistent density
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Core Game Loop
|
|
||||||
|
|
||||||
1. **Idle state**: Next fruit shown at top, follows cursor/touch X position
|
|
||||||
2. **Drop**: On click/tap, fruit is released with gravity enabled
|
|
||||||
3. **Cooldown**: 500ms before the next fruit can be dropped
|
|
||||||
4. **Collision check**: On every `collisionStart` event from Matter.js:
|
|
||||||
- If two bodies share the same fruit tier label and neither is flagged for removal:
|
|
||||||
- Flag both bodies for removal
|
|
||||||
- Calculate midpoint of the two bodies
|
|
||||||
- If tier < 10: spawn new fruit at midpoint with tier + 1
|
|
||||||
- If tier == 10: no spawn (watermelons vanish)
|
|
||||||
- Add merge points to score
|
|
||||||
5. **Game-over check**: Each frame, check if any settled fruit (not newly dropped within the last 1s) has its top edge above the danger line
|
|
||||||
|
|
||||||
### Merge Logic (Critical Path)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Collision handler
|
|
||||||
Events.on(engine, "collisionStart", (event) => {
|
|
||||||
for (const pair of event.pairs) {
|
|
||||||
const { bodyA, bodyB } = pair;
|
|
||||||
if (bodyA.label === bodyB.label && !bodyA.removing && !bodyB.removing) {
|
|
||||||
bodyA.removing = true;
|
|
||||||
bodyB.removing = true;
|
|
||||||
|
|
||||||
const tier = bodyA.fruitTier;
|
|
||||||
const midX = (bodyA.position.x + bodyB.position.x) / 2;
|
|
||||||
const midY = (bodyA.position.y + bodyB.position.y) / 2;
|
|
||||||
|
|
||||||
World.remove(engine.world, [bodyA, bodyB]);
|
|
||||||
|
|
||||||
if (tier < 10) {
|
|
||||||
const newFruit = createFruitBody(tier + 1, midX, midY);
|
|
||||||
World.add(engine.world, newFruit);
|
|
||||||
}
|
|
||||||
|
|
||||||
score += FRUITS[tier + 1]?.points ?? 66;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Input Handling
|
|
||||||
|
|
||||||
- **Desktop**: `mousemove` on canvas for positioning, `click` to drop
|
|
||||||
- **Mobile**: `touchmove` for positioning, `touchend` to drop
|
|
||||||
- Clamp X position to stay within container walls minus fruit radius
|
|
||||||
- During cooldown, input is accepted for positioning but drop is disabled
|
|
||||||
|
|
||||||
### Rendering (Canvas2D)
|
|
||||||
|
|
||||||
Each frame via `requestAnimationFrame`:
|
|
||||||
|
|
||||||
1. Clear canvas
|
|
||||||
2. Draw container walls (gray rectangles)
|
|
||||||
3. Draw danger line (dashed red line)
|
|
||||||
4. For each fruit body in the world:
|
|
||||||
- Draw filled circle at body position with fruit color
|
|
||||||
- Draw fruit label text (emoji or name) centered
|
|
||||||
5. Draw "next fruit" preview at cursor position (semi-transparent during cooldown)
|
|
||||||
6. Draw score in top corner
|
|
||||||
7. If game over, draw overlay with final score and "Click to restart" prompt
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Game state object
|
|
||||||
const state = {
|
|
||||||
score: 0,
|
|
||||||
nextFruitTier: 0, // 0-4 (random from droppable set)
|
|
||||||
isDropCooldown: false,
|
|
||||||
isGameOver: false,
|
|
||||||
cursorX: 0, // current drop position
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
No external state library needed. The `Game` class owns the state and passes it to the renderer each frame.
|
|
||||||
|
|
||||||
## User Experience
|
|
||||||
|
|
||||||
1. Page loads -> canvas renders with empty container and a fruit preview following cursor
|
|
||||||
2. Player clicks/taps to drop the fruit
|
|
||||||
3. Fruit falls, settles, and may merge with matching neighbors
|
|
||||||
4. Merged fruit creates satisfying chain reactions as larger fruits collide
|
|
||||||
5. Score updates in real-time at the top
|
|
||||||
6. When a fruit settles above the danger line, "Game Over" overlay appears with final score
|
|
||||||
7. Clicking "Restart" resets the game
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests (Vitest)
|
|
||||||
|
|
||||||
- **`fruits.test.js`**: Verify fruit definitions are consistent (radii increase monotonically, all 11 tiers defined, points are correct)
|
|
||||||
- **`merger.test.js`**: Test merge logic in isolation -- given two same-tier fruits, verify correct tier+1 output, midpoint calculation, and score delta. Test watermelon+watermelon produces no new fruit.
|
|
||||||
- **`input.test.js`**: Test X-clamping logic keeps fruit within container bounds
|
|
||||||
- **`game.test.js`**: Test state transitions: drop sets cooldown, game-over triggers on threshold breach, restart resets state
|
|
||||||
|
|
||||||
### Integration Tests (Vitest)
|
|
||||||
|
|
||||||
- **Physics + merge**: Create a Matter.js engine, drop two same-tier fruits so they collide, verify the collision handler produces the correct merged fruit
|
|
||||||
- **Game-over detection**: Simulate a stack that crosses the danger line, verify game-over flag is set
|
|
||||||
|
|
||||||
### Manual / E2E Testing
|
|
||||||
|
|
||||||
- Play through a full game in the browser to verify feel, responsiveness, and chain reactions
|
|
||||||
- Test on mobile viewport (Chrome DevTools device mode)
|
|
||||||
- Verify no fruit escapes the container walls
|
|
||||||
|
|
||||||
### Test Documentation
|
|
||||||
|
|
||||||
Each test file should include a top-level comment explaining what module it validates and why. Each test case should have a descriptive name that explains the scenario being tested (e.g., `"merging two tier-3 dekopon produces a tier-4 persimmon"`).
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- **Physics step rate**: Matter.js default (60Hz) is sufficient; no need for sub-stepping
|
|
||||||
- **Rendering**: Canvas2D with simple circle drawing is very lightweight
|
|
||||||
- **Body count**: Maximum ~40-50 fruits in container at once. Matter.js handles this easily
|
|
||||||
- **Memory**: Remove merged bodies immediately from the world to prevent leaks
|
|
||||||
- **Mobile**: Touch events have no performance concern; canvas size is small enough for any device
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- No user input beyond mouse/touch coordinates -- no injection vectors
|
|
||||||
- No network requests, no backend, no data storage
|
|
||||||
- No third-party scripts loaded at runtime (Matter.js bundled via Vite)
|
|
||||||
- Score is client-side only -- no need to prevent tampering
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
- **README.md**: Update with project description, setup instructions (`npm install`, `npm run dev`), how to play, and screenshot
|
|
||||||
- No API docs needed (no backend)
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Core Game (MVP)
|
|
||||||
|
|
||||||
1. Initialize Vite project (`npm create vite@latest . -- --template vanilla`)
|
|
||||||
2. Install Matter.js
|
|
||||||
3. Implement `constants.js` and `fruits.js` (data definitions)
|
|
||||||
4. Implement `physics.js` (engine, walls, container)
|
|
||||||
5. Implement `renderer.js` (canvas drawing for fruits, walls, score)
|
|
||||||
6. Implement `input.js` (mouse/touch positioning and drop trigger)
|
|
||||||
7. Implement `merger.js` (collision-based merge logic and scoring)
|
|
||||||
8. Implement `game.js` (game loop, state management, game-over detection)
|
|
||||||
9. Wire everything together in `main.js`
|
|
||||||
10. Verify playability in browser
|
|
||||||
|
|
||||||
### Phase 2: Polish
|
|
||||||
|
|
||||||
1. Add "next fruit" preview indicator
|
|
||||||
2. Add drop cooldown visual feedback (e.g., dimmed preview)
|
|
||||||
3. Add game-over overlay with score and restart button
|
|
||||||
4. Responsive canvas scaling for different screen sizes
|
|
||||||
5. Add fruit emoji or simple sprite rendering instead of plain circles
|
|
||||||
|
|
||||||
### Phase 3: Testing and Quality
|
|
||||||
|
|
||||||
1. Set up Vitest
|
|
||||||
2. Write unit tests for fruit definitions, merge logic, clamping
|
|
||||||
3. Write integration tests for physics + merge flow
|
|
||||||
4. Manual playtesting and tuning (gravity, restitution, fruit sizes)
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **Fruit visuals**: Use colored circles with text labels (simpler) or emoji/sprites (prettier)? Recommend starting with colored circles, upgrading in Phase 2.
|
|
||||||
2. **Difficulty tuning**: The physics constants (gravity, restitution, fruit sizes) may need adjustment after playtesting. These should be easy to tweak in `constants.js`.
|
|
||||||
3. **Canvas scaling**: Fixed pixel size or scale to viewport? Recommend fixed size with CSS centering for Phase 1, responsive scaling in Phase 2.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Suika Game - Wikipedia](https://en.wikipedia.org/wiki/Suika_Game)
|
|
||||||
- [Matter.js Documentation](https://brm.io/matter-js/docs/)
|
|
||||||
- [Matter.js GitHub](https://github.com/liabru/matter-js)
|
|
||||||
- [Vite Getting Started](https://vitejs.dev/guide/)
|
|
||||||
- [Suika Game Fruits & Points](https://suikagame.com/games/suika-game-fruits-points-list/)
|
|
||||||
- [Example clone: sgbj/suika-clone (Phaser + Matter.js + TS)](https://github.com/sgbj/suika-clone)
|
|
||||||
- [Example clone: TomboFry/suika-game (plain JS + Matter.js)](https://github.com/TomboFry/suika-game)
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# Fix Collision Behavior and Physics Tuning
|
|
||||||
|
|
||||||
**Status**: Draft
|
|
||||||
**Authors**: Claude Code, 2026-04-12
|
|
||||||
**Type**: Bugfix / Improvement
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Fix the "weird collision" behavior in the Suika Game by addressing physics tuning issues and improving merge execution safety. The current implementation has unstable collision feel due to low density values, missing delta clamping, and a fragile merge removal pattern that can cause ghost bodies and unexpected cascading merges.
|
|
||||||
|
|
||||||
## Background / Problem Statement
|
|
||||||
|
|
||||||
The current game uses raw Matter.js (v0.20) with Canvas2D rendering. Players report that collisions feel "weird" — fruits appear floaty, merges can glitch, and physics behavior is inconsistent. Investigation reveals several root causes:
|
|
||||||
|
|
||||||
1. **Physics tuning**: Uniform density of 0.001 makes all fruits unrealistically light. Large fruits don't feel heavier than small ones.
|
|
||||||
2. **Merge timing**: Bodies flagged for removal (`removing: true`) remain physically active until `flushMerges()` runs. During that window, they can collide with other fruits, potentially triggering unintended merges.
|
|
||||||
3. **Frame delta unclamped**: Lag spikes can cause huge physics steps where bodies tunnel through walls or each other.
|
|
||||||
4. **No scaled density**: All fruits share the same density regardless of tier, so a Watermelon (radius 77) doesn't feel meaningfully heavier than a Cherry (radius 12) beyond area-based mass.
|
|
||||||
|
|
||||||
### Why Not Phaser?
|
|
||||||
|
|
||||||
Research confirms that Phaser 3 (v3.90.0) wraps Matter.js without adding deferred-removal or safer collision handling. The `collisionstart` event in Phaser has the exact same mid-step mutation risk. Phaser adds ~345 KB gzipped for features (sprite management, scene system, asset loading) that this game doesn't need. Fixing the issues in the current codebase is more appropriate.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Make fruit collisions feel physically satisfying and stable
|
|
||||||
- Eliminate ghost bodies and double-merge glitches
|
|
||||||
- Ensure larger fruits feel heavier and more impactful
|
|
||||||
- Prevent frame-spike physics tunneling
|
|
||||||
- Keep the existing vanilla JS + Matter.js architecture
|
|
||||||
|
|
||||||
## Non-Goals
|
|
||||||
|
|
||||||
- Migrating to Phaser or another game framework
|
|
||||||
- Adding visual effects (particles, screen shake) for merges
|
|
||||||
- Changing the fruit progression chain or scoring system
|
|
||||||
- Adding sound effects
|
|
||||||
|
|
||||||
## Technical Dependencies
|
|
||||||
|
|
||||||
| Dependency | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| [Matter.js](https://brm.io/matter-js/) | ^0.20.0 | 2D physics engine (already installed) |
|
|
||||||
|
|
||||||
No new dependencies required.
|
|
||||||
|
|
||||||
## Detailed Design
|
|
||||||
|
|
||||||
### 1. Tier-Scaled Density
|
|
||||||
|
|
||||||
Replace the uniform density with per-tier density scaling. Larger fruits should be denser, making them settle more firmly and push smaller fruits aside.
|
|
||||||
|
|
||||||
**File**: `src/constants.js`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Replace flat density with base + scaling
|
|
||||||
export const BASE_DENSITY = 0.002;
|
|
||||||
export const DENSITY_SCALE_PER_TIER = 0.0005;
|
|
||||||
```
|
|
||||||
|
|
||||||
**File**: `src/physics.js` — `createFruitBody()`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export function createFruitBody(tier, x, y) {
|
|
||||||
const fruit = FRUITS[tier];
|
|
||||||
const density = BASE_DENSITY + DENSITY_SCALE_PER_TIER * tier;
|
|
||||||
const body = Bodies.circle(x, y, fruit.radius, {
|
|
||||||
...FRUIT_BODY_OPTIONS,
|
|
||||||
density,
|
|
||||||
label: `fruit_${tier}`,
|
|
||||||
});
|
|
||||||
body.fruitTier = tier;
|
|
||||||
body.removing = false;
|
|
||||||
body.dropTime = Date.now();
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This gives Cherry ~0.002 density and Watermelon ~0.007 — a 3.5x density increase on top of the much larger area, making watermelons feel substantially heavier.
|
|
||||||
|
|
||||||
### 2. Improved Physics Constants
|
|
||||||
|
|
||||||
**File**: `src/constants.js`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
export const GRAVITY = { x: 0, y: 1.8 }; // Slightly stronger gravity
|
|
||||||
|
|
||||||
export const FRUIT_BODY_OPTIONS = {
|
|
||||||
restitution: 0.3, // Slightly bouncier for more satisfying collisions
|
|
||||||
friction: 0.3, // Lower friction so fruits slide and settle naturally
|
|
||||||
frictionAir: 0.02, // More air drag to dampen chaotic bouncing
|
|
||||||
frictionStatic: 0.5, // Higher static friction so stacked fruits don't slide
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Key changes:
|
|
||||||
- **restitution 0.2 → 0.3**: More responsive bounce on contact
|
|
||||||
- **friction 0.5 → 0.3**: Fruits slide into gaps more naturally
|
|
||||||
- **frictionAir 0.01 → 0.02**: Reduces floaty drifting
|
|
||||||
- **frictionStatic (new) 0.5**: Prevents settled stacks from sliding
|
|
||||||
- **gravity 1.5 → 1.8**: Faster settling, feels more weighty
|
|
||||||
|
|
||||||
### 3. Delta Time Clamping
|
|
||||||
|
|
||||||
**File**: `src/game.js` — `loop()`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
loop(time) {
|
|
||||||
const rawDelta = time - this.lastTime;
|
|
||||||
const delta = Math.min(rawDelta, 20); // Cap at ~50fps equivalent
|
|
||||||
this.lastTime = time;
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This prevents lag spikes from causing physics tunneling. At 20ms max, the physics engine never advances more than one "normal" frame's worth of simulation per step.
|
|
||||||
|
|
||||||
### 4. Safer Merge Execution with Collision Filtering
|
|
||||||
|
|
||||||
The current approach already defers removal to `flushMerges()` after `stepEngine()`, which is correct. The remaining issue is that bodies flagged `removing: true` can still physically collide with other bodies during the same frame.
|
|
||||||
|
|
||||||
**File**: `src/merger.js` — In the collision handler, after flagging bodies:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
Events.on(engine, 'collisionStart', (event) => {
|
|
||||||
for (const pair of event.pairs) {
|
|
||||||
const { bodyA, bodyB } = pair;
|
|
||||||
|
|
||||||
if (
|
|
||||||
bodyA.fruitTier !== undefined &&
|
|
||||||
bodyB.fruitTier !== undefined &&
|
|
||||||
bodyA.label === bodyB.label &&
|
|
||||||
!bodyA.removing &&
|
|
||||||
!bodyB.removing
|
|
||||||
) {
|
|
||||||
bodyA.removing = true;
|
|
||||||
bodyB.removing = true;
|
|
||||||
|
|
||||||
// Make bodies sensors so they stop physically interacting
|
|
||||||
bodyA.isSensor = true;
|
|
||||||
bodyB.isSensor = true;
|
|
||||||
|
|
||||||
pendingMerges.push({ bodyA, bodyB, tier: bodyA.fruitTier });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Setting `isSensor = true` on flagged bodies means Matter.js will still detect them for collision callbacks but won't apply physical forces to/from them. This prevents ghost collisions between a merging body and its neighbors.
|
|
||||||
|
|
||||||
### 5. Renderer: Skip Sensor Bodies
|
|
||||||
|
|
||||||
**File**: `src/renderer.js` — `drawFruits()`
|
|
||||||
|
|
||||||
The renderer already skips bodies with `body.removing === true`, so no change needed. The sensor flag is purely for physics behavior.
|
|
||||||
|
|
||||||
### Architecture Summary
|
|
||||||
|
|
||||||
```
|
|
||||||
No new files. Changes to existing modules:
|
|
||||||
|
|
||||||
src/constants.js — Updated physics values, add density scaling constants
|
|
||||||
src/physics.js — Per-tier density in createFruitBody()
|
|
||||||
src/game.js — Delta clamping in loop()
|
|
||||||
src/merger.js — Set isSensor on merging bodies
|
|
||||||
```
|
|
||||||
|
|
||||||
## User Experience
|
|
||||||
|
|
||||||
- Fruits drop faster and settle more quickly (stronger gravity)
|
|
||||||
- Large fruits feel heavier and push small fruits aside
|
|
||||||
- Collisions have slightly more bounce, feeling more responsive
|
|
||||||
- Merges are cleaner — no "phantom" collisions from disappearing fruits
|
|
||||||
- Stacked fruits stay stable instead of slowly sliding apart
|
|
||||||
- Lag spikes no longer cause fruits to teleport through walls
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
- **`constants.test.js`**: Verify BASE_DENSITY and DENSITY_SCALE_PER_TIER produce reasonable values for all 11 tiers (density always positive, monotonically increasing)
|
|
||||||
- **`physics.test.js`** (update existing): Verify `createFruitBody()` applies tier-scaled density. Test that tier-0 density < tier-10 density.
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
- **Delta clamping test**: Simulate a 100ms delta, verify the engine only advances by 20ms worth of physics
|
|
||||||
- **Sensor flag test**: Create two same-tier bodies, trigger merge detection, verify both bodies have `isSensor === true` after collision
|
|
||||||
- **No cascade merge test**: After flagging bodies as removing/sensor, verify they don't trigger additional merge events with neighboring bodies
|
|
||||||
|
|
||||||
### Manual Testing
|
|
||||||
|
|
||||||
- Play through a full game and verify:
|
|
||||||
- Fruits no longer feel floaty
|
|
||||||
- Large fruits settle firmly
|
|
||||||
- Merges are clean without visual glitches
|
|
||||||
- No fruits escape the container walls during lag
|
|
||||||
- Chain merges (merge produces a fruit that immediately merges again) still work correctly
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- Tier-scaled density has zero performance cost (one multiplication per fruit creation)
|
|
||||||
- Setting `isSensor` on merging bodies reduces physics workload (engine skips force resolution for sensors)
|
|
||||||
- Delta clamping prevents runaway physics computation on lag frames
|
|
||||||
- No new per-frame allocations or computation added
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
No security implications — all changes are to client-side physics tuning constants and game logic.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
No documentation changes needed. The README setup instructions and gameplay description remain the same.
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Core Fixes
|
|
||||||
|
|
||||||
1. Update `src/constants.js` with new physics values and density scaling constants
|
|
||||||
2. Update `src/physics.js` to apply per-tier density in `createFruitBody()`
|
|
||||||
3. Update `src/game.js` to clamp delta time in `loop()`
|
|
||||||
4. Update `src/merger.js` to set `isSensor = true` on merging bodies
|
|
||||||
5. Update existing tests and add new ones
|
|
||||||
6. Manual playtest to verify feel
|
|
||||||
|
|
||||||
### Phase 2: Fine-Tuning (if needed)
|
|
||||||
|
|
||||||
1. Adjust gravity, restitution, friction values based on playtesting
|
|
||||||
2. Consider adding `sleepThreshold` to Matter.js engine to let settled bodies sleep (performance optimization)
|
|
||||||
3. Tune density scaling curve if large fruits feel too heavy or too light
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. **Exact density curve**: Linear scaling (`BASE + SCALE * tier`) is proposed. Would exponential scaling feel better for the largest fruits? Needs playtesting.
|
|
||||||
2. **Sleep threshold**: Should settled fruits use Matter.js sleeping to improve performance with many bodies? This could cause "delayed wakeup" artifacts but would improve stability of stacks.
|
|
||||||
3. **Restitution per tier**: Should larger fruits be less bouncy (lower restitution) than smaller ones? This would make large fruits feel more "solid" but adds complexity.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Matter.js Body Properties](https://brm.io/matter-js/docs/classes/Body.html)
|
|
||||||
- [Matter.js isSensor documentation](https://brm.io/matter-js/docs/classes/Body.html#property_isSensor)
|
|
||||||
- [Matter.js Engine.update delta parameter](https://brm.io/matter-js/docs/classes/Engine.html#method_update)
|
|
||||||
- [Phaser 3 Suika implementation (reference)](https://github.com/Coteh/suika-clone) — Uses same Matter.js deferred-removal pattern
|
|
||||||
- [TomboFry/suika-game](https://github.com/TomboFry/suika-game) — Vanilla JS + Matter.js reference
|
|
||||||
- [Original Suika Game spec](./feat-suika-game.md)
|
|
||||||
Reference in New Issue
Block a user