Files
nntv/docs/code-standards.md
T
tiennm99 7233310662 feat: add undo/redo, audio, mobile controls, BFS pathfinding, and accessibility
- Fix ChaserGuard with BFS pathfinding and proper chase/return states
- Add undo/redo system (Z/Y keys) with full state snapshots
- Add procedural audio via Web Audio API (move, wait, detection, complete)
- Add mobile swipe controls with touch gesture detection
- Add detection feedback (cell flash, player shake animation)
- Add CSS transitions on grid cells for smooth lighting changes
- Add ARIA accessibility labels on game board and cells
- Add controls overlay ("?" button) showing all keyboard/touch shortcuts
- Add mute toggle in HUD
- Update Guide scene with chaser/mirror guard descriptions and tips
- Replace guard switch statement with factory registry pattern
- Extract princess mechanic and touch controls into separate modules
- Localize all UI strings (EN/VI) including new controls and tips
- Update README for Svelte 5 architecture with all current features
- Update project docs (architecture, code standards, codebase summary)
2026-04-13 18:24:46 +07:00

7.9 KiB

Code Standards - Night Ninja: Twilight Voyage

Naming Conventions

Files

  • Svelte components: PascalCase (Game.svelte, GameBoard.svelte, PlayerSprite.svelte)
  • JS modules: kebab-case (grid-system.js, turn-manager.js, level-manager.js)
  • JSON/config: kebab-case (en.json, vi.json, theme.css)

Variables & Functions

  • Classes: PascalCase (GridSystem, Player, RotatingGuard)
  • Methods & Functions: camelCase (updateLight(), nextTurn(), getText())
  • Constants: UPPER_CASE for arrays (LEVELS), camelCase for others
  • Svelte state: camelCase (renderVersion, isPaused, detected)
  • Props: camelCase (cellSize, oncellclick, onplayagain)

Grid Coordinates

  • row: Vertical axis (0 = top, increases downward)
  • col: Horizontal axis (0 = left, increases rightward)
  • Position objects: { row: num, col: num }

Code Organization

Directory Structure

src/
├── main.js                          # Entry point, mounts App.svelte
├── App.svelte                       # Scene router ({#key} + fade)
│
├── scenes/                          # Full-page scene components
│   ├── MainMenu.svelte
│   ├── StoryIntro.svelte
│   ├── LevelIntro.svelte
│   ├── LevelSelect.svelte
│   ├── Game.svelte                  # Main gameplay (state owner)
│   ├── GameOver.svelte
│   ├── Settings.svelte
│   └── Guide.svelte
│
├── components/                      # Reusable UI components
│   ├── Button.svelte
│   ├── GameBoard.svelte             # CSS grid rendering
│   ├── GameHud.svelte               # Level/lives/turns display
│   ├── PlayerSprite.svelte          # Positioned player div
│   ├── GuardSprite.svelte           # Colored guard circle/diamond
│   ├── PauseMenu.svelte
│   └── DetectionPopup.svelte
│
├── lib/
│   ├── game/                        # Pure JS game engine (no framework)
│   │   ├── grid-system.js
│   │   ├── player.js
│   │   ├── guards.js                # Base + 6 guard subclasses (including ChaserGuard)
│   │   ├── turn-manager.js
│   │   ├── level-manager.js         # GUARD_REGISTRY factory pattern
│   │   ├── game-history.js          # Undo/redo system
│   │   ├── princess-mechanic.js     # Level 12 escalating detection
│   │   └── touch-controls.js        # Mobile swipe support
│   │
│   ├── levels/
│   │   └── levels.js                # 12 level definitions
│   │
│   ├── locales/
│   │   ├── en.json
│   │   └── vi.json
│   ├── audio.js                     # Web Audio API sounds
│   ├── localization.js
│   └── progress.js
│
└── styles/
    └── theme.css                    # CSS variables (colors, fonts)

File Size Limits

  • Maximum 200 lines per file before considering split
  • Scenes with heavy logic → extract to lib/game/
  • Shared UI → extract to components/

Svelte 5 Patterns

State Management

// Primitive state
let isPaused = $state(false);

// Class instances (NOT auto-proxied — use renderVersion pattern)
let grid = $state(null);
let player = $state(null);
let renderVersion = $state(0);

// Derived values depend on renderVersion to pick up class mutations
let cells = $derived((renderVersion, grid ? grid.getAllCells() : []));

Props

let { navigate, level = 1, lives = 3 } = $props();

Event Handlers

<!-- Window-level events -->
<svelte:window onkeydown={onKeyDown} />

<!-- Component events via callback props -->
<Button text="Resume" onclick={() => isPaused = false} />

Scene Navigation

// Parent passes navigate function as prop
navigate('Game', { level: 3, lives: 2 });

// App.svelte routes via {#key currentScene}

Class & Inheritance Patterns

Guard Hierarchy

Guard (abstract base)
├── StaticGuard      — fixed lit cells
├── RotatingGuard    — rotating beam + mirror reflection
├── BlinkingGuard    — toggle on/off
├── MirrorGuard      — redirects beams
├── PatrollingGuard  — path movement + directional light
└── ChaserGuard      — BFS pathfinding + detection radius

Base Guard contract:

  • Constructor: grid, row, col, type
  • updateLight(allGuards?): Set lit cells on grid
  • onTurnChange(allGuards?): Update state then call updateLight

Level Manager Pattern: Use GUARD_REGISTRY factory pattern in level-manager.js:

const GUARD_REGISTRY = {
    static: (grid, g) => new StaticGuard(...),
    chaser: (grid, g) => new ChaserGuard(...),
    // ... etc
};

This eliminates switch statements and allows easy guard type registration.

Key rule: Game engine classes are pure JS with no Svelte dependency. They operate on raw object references, not proxied state.

Grid & Coordinate System

  • Origin: Top-left (0, 0)
  • Row: 0 = top, increases downward
  • Col: 0 = left, increases rightward
  • Cell Data: grid[row][col] = { isWall, isGoal, isLight }
  • Pixel Position: row * cellSize, col * cellSize

Theme & Styling

All visual constants centralized in src/styles/theme.css as CSS variables:

  • --bg-dark, --bg-panel — backgrounds
  • --grid-empty, --grid-wall, --grid-goal, --grid-lit — cell colors
  • --guard-static, --guard-rotating, --guard-blinking, --guard-patrolling, --guard-mirror — guard colors
  • --font-title, --font-body, --font-button — typography
  • --text-primary, --text-accent, --text-danger — text colors

Component-scoped <style> blocks reference these variables. No inline color values.

Error Handling

  • Try-catch: localStorage operations (progress.js, localization.js)
  • Null checks: if (!player || !grid) return in input handlers
  • Validation: Grid bounds checked before all cell operations
  • Fallback: Missing translations return key string itself

Localization Pattern

import { getText } from '../lib/localization.js';

const message = getText('level1Story');  // Returns EN or VI string

Key naming: camelCase matching JSON structure (levelSelectTitle, enemyTypesContent).

Git & Commit Conventions

  • Format: Conventional commits (feat:, fix:, refactor:, test:, docs:)
  • Branch: main for production
  • Messages: Descriptive, explain "why" not just "what"
  • Example: fix: resolve Svelte 5 reactivity for class instances

New Patterns

Undo/Redo System

import { GameHistory } from '../lib/game/game-history.js';
const history = new GameHistory();

// Before player move
history.snapshot(player, guards, turnCount, princessAlerted, alertRadius);

// Z/Y key handlers
if (event.key === 'z') history.undo(player, guards);
if (event.key === 'y') history.redo(player, guards);

Mobile Touch Controls

import { TouchControls } from '../lib/game/touch-controls.js';
const touch = new TouchControls();

// In Game.svelte
<svelte:window ontouchstart={e => touch.onTouchStart(e)} 
               ontouchend={e => { const dir = touch.onTouchEnd(e); handleMove(dir); }} />

Audio Feedback

import * as audio from '../lib/audio.js';
audio.playMoveSound();
audio.playDetectionSound();
audio.toggleMute();

Code Review Checklist

  • Follows naming conventions (PascalCase components, kebab-case JS modules)
  • No dead code or commented-out blocks
  • Class mutations followed by renderVersion++
  • No hardcoded colors (use CSS variables)
  • Localization keys used for all user-facing strings
  • File size under 200 lines
  • Pure JS game logic has no Svelte imports
  • Guard types registered in GUARD_REGISTRY (not switch statements)
  • Touch input debounced/throttled if needed
  • Audio context lazily initialized (autoplay policy compliance)