Files
nntv/docs/codebase-summary.md
T
tiennm99 ba687ea130 feat(game): wilting tomato mechanic replaces static light
Static guards now emit a Manhattan aura that shrinks by 1 per turn until
harmless (clamped at -1 to keep solver state finite). Level data schema
change: 'static' guards now take 'initialRadius' instead of 'litCells'.
All 27 static-guard entries migrated in one pass.

L1 and L2 redesigned with real forced-zigzag walls — solver path is now
20 moves (vs Manhattan 14), so players must move AWAY from the goal to
reach it. L2 adds three wilting tomatoes parked on the zigzag path as
a gentle intro to the mechanic.

Tests: +3 new tests on StaticGuard wilting behavior; all 70 tests pass.
2026-04-21 22:40:53 +07:00

8.8 KiB
Raw Blame History

Codebase Summary - Night Ninja: Twilight Voyage

Overview

NNTV is a turn-based stealth puzzle game built with Svelte 5 and Vite 6.x. The codebase separates pure JS game engine (classes in lib/game/) from Svelte rendering (scenes + components). State flows one-way: game classes mutate internally, then renderVersion++ triggers Svelte re-derivation.

Module Inventory

Entry Point

  • src/main.js — Mounts App.svelte into #app

Scene Router

  • src/App.svelte{#key currentScene} with transition:fade, passes navigate function as prop to all scenes

Scenes (src/scenes/)

File Purpose
MainMenu.svelte Start game, level select, settings, guide
StoryIntro.svelte Scrolling narrative with skip button
LevelIntro.svelte Level name + story text + continue
LevelSelect.svelte 4x3 grid of level buttons (locked/unlocked/completed)
Game.svelte Main gameplay: state owner, input, turn loop, rendering
GameOver.svelte Loss/twist screen, retry/menu
Settings.svelte Language toggle (EN/VI)
Guide.svelte Rules, controls, enemy types

Components (src/components/)

File Purpose
Button.svelte Reusable styled button
GameBoard.svelte CSS grid of cells, each rendering a pixel-art tile
GameHud.svelte Level, lives (pixel hearts), turns, pixel-icon action buttons
PlayerSprite.svelte Pixel-art ninja rabbit sprite
GuardSprite.svelte Pixel-art veggie sprite dispatched by guard.type + direction indicator
DetectionPopup.svelte "Detected!" overlay with retry
LevelCompletePopup.svelte Star rating, best moves, next-level action
PauseMenu.svelte Resume / restart / main menu
ControlsOverlay.svelte Keyboard / tap / swipe reference overlay

Game Engine (src/lib/game/) — Pure JS, no Svelte

File Purpose
grid-system.js GridSystem class: cell state, walls, goals, lighting
player.js Player class: position, movement validation
guards.js Guard base + 6 subclasses (Static, Rotating, Blinking, Mirror, Patrolling, Chaser)
turn-manager.js TurnManager: turn cycle, guard updates, detection
level-manager.js loadLevel(): GUARD_REGISTRY factory pattern for guard instantiation
level-solver.js BFS solvability checker (test-only); reuses runtime AI via capture()/apply()
game-history.js GameHistory class: undo/redo snapshots (Z/Y keys)
princess-mechanic.js Princess detection logic: escalating light rings on level 12
touch-controls.js TouchControls class: pointer gesture detection (legacy; no mobile support claim)

Level Data (src/lib/levels/)

File Purpose
levels.js LEVELS array: 12 level definitions (grid, guards, walls, goals); L1-L11 solvable, L12 intentionally unsolvable
levels.solvability.test.js CI-enforced invariants: each L1-L11 solvable, L12 unsolvable, no wall/light overlaps, exactly 12 levels

Pixel-Art Pipeline (src/lib/pixel/)

File Purpose
Pixel.svelte SVG renderer: string-art + palette → run-length-merged <rect>s
palette.js NNTV color constants (mirrors theme.css guard colors; extended atmosphere palette)
art-characters.js 32×32 sprites: rabbit, princess, 6 guard veggies + GUARD_SPRITES map
art-tiles.js 16×16 board tiles: empty, wall, goal, lit, mirror, preview
art-ui.js Heart (full/empty), moon, logo, pixel icons (undo/redo/eye/pause/settings/lang/arrow)
art-scenes.js 80×N act backdrops + SCENE_BY_LEVEL mapping (garden/walls/fortress/underground/palace/chamber)

Audio System (src/lib/)

File Purpose
audio.js Web Audio API procedural sound: playTone, playMoveSound, playDetectionSound, playCompleteSound, toggleMute

Utilities (src/lib/)

File Purpose
localization.js getText/setLanguage/getLanguage/initLanguage
progress.js getProgress/completeLevel via localStorage

Localization (src/lib/locales/)

File Keys Purpose
en.json ~55 English translations
vi.json ~55 Vietnamese translations

Styles (src/styles/)

File Purpose
theme.css CSS variables: colors, fonts, guard colors, grid colors

Class Hierarchy

Guard Inheritance

Guard (abstract base: grid, row, col, type, direction, isOn)
├── StaticGuard     — wilting tomato: Manhattan aura shrinks by 1 per turn (initialRadius → currentRadius)
├── RotatingGuard   — rotates beam 90°/turn, castBeam with mirror bounce
├── BlinkingGuard   — toggles isOn, lights litCells when on
├── MirrorGuard     — lights own cell, stores reflectDirection (cw/ccw)
├── PatrollingGuard — follows path array, lights front + right cells
└── ChaserGuard     — BFS pathfinding to player, detectionRadius range

Key Data Structures

Level Definition

{
  id: 1, name: "Garden Path", storyKey: "level1Story",
  grid: { rows: 6, cols: 6 },
  player: { row: 0, col: 0 },
  goal: { row: 5, col: 5 },
  walls: [{ row: 1, col: 1 }, ...],
  guards: [
    { type: "static", position: { row: 2, col: 4 }, initialRadius: 2 },
    { type: "rotating", position: { row: 3, col: 3 }, startDirection: 0 },
    { type: "blinking", position: {...}, litCells: [...], startState: true },
    { type: "mirror", position: {...}, reflectDirection: "cw" },
    { type: "patrolling", startPosition: {...}, path: [...] },
    { type: "chaser", position: {...}, detectionRadius: 3 },
  ],
  isFinalLevel: false
}

Cell State

{ isWall: boolean, isGoal: boolean, isLight: boolean }

Progress (localStorage)

{ maxLevel: 1, completedLevels: [1, 2, 3] }

Public API Summary

GridSystem

new GridSystem(rows, cols, cellSize)
.isValidPosition(row, col), .isWall(row, col), .setWall(row, col, value)
.isGoal(row, col), .setGoal(row, col, value)
.isLight(row, col), .setLight(row, col, value)
.clearAllLight(), .getAllCells() → [{row, col, isWall, isGoal, isLight}]

Player

new Player(grid, row, col)
.move(direction) → boolean, .moveTo(row, col) → boolean
.isInLitCell() → boolean, .isAtGoal() → boolean

Guards

All: .updateLight(allGuards?), .onTurnChange(allGuards?)
All: .capture() → state, .apply(state)   # dynamic-state snapshot for undo/preview
RotatingGuard: .castBeam(dir, fromRow, fromCol, range, allGuards, depth)
PatrollingGuard: .checkIfCircularPath(), path traversal with reversing
MirrorGuard: .reflectDirection ('cw' or 'ccw')
ChaserGuard: .bfsNextStep(targetRow, targetCol), hunting/returning state

PrincessMechanic

new PrincessMechanic()
.update(grid, player, goalRow, goalCol) → { showMessage, detected }
.lightRing(grid, goalRow, goalCol, radius)
.capture() → { alerted, alertRadius, messageShown }, .apply(state)
.reset()

GameHistory

new GameHistory()
.createSnapshot(player, guards, turnCount, princessState) → snapshot
.pushSnapshot(snap), .undo(...), .redo(...)
.canUndo(), .canRedo(), .reset()

TurnManager

new TurnManager()
.nextTurn(grid, player, guards) → { detected, levelComplete }
.reset()

Dependencies

Package Version Purpose
svelte 5.x UI framework (runes mode)
vite 6.3.6 Build tool, dev server
@sveltejs/vite-plugin-svelte 6.x Svelte compiler for Vite

File Dependency Map

main.js → App.svelte
App.svelte → all scenes

Game.svelte (central hub)
  ├── lib/game/level-manager.js → grid-system, player, guards, levels
  ├── lib/game/turn-manager.js
  ├── lib/game/game-history.js, princess-mechanic.js, touch-controls.js
  ├── lib/audio.js, lib/progress.js, lib/localization.js
  ├── lib/pixel/Pixel.svelte, lib/pixel/art-scenes.js (act backdrop)
  ├── components/GameBoard, PlayerSprite, GuardSprite, GameHud
  ├── components/DetectionPopup, LevelCompletePopup, PauseMenu, ControlsOverlay
  └── renderVersion pattern for reactivity

LevelSelect.svelte → level-manager.getTotalLevels(), progress.js, localization.js
LevelIntro.svelte → levels.js, lib/pixel/art-scenes.js, localization.js
MainMenu.svelte → lib/pixel/{Pixel, art-ui, art-characters}
Pixel components (PlayerSprite/GuardSprite/GameBoard/GameHud) → lib/pixel/Pixel + art-*
All scenes → localization.js (for UI text)

Statistics

Metric Value
Total Source Files ~36 (8 scenes + 9 components + 8 engine + 6 pixel + audio + utils)
Number of Classes 10 (GridSystem, Player, Guard + 6 subclasses, TurnManager, GameHistory, TouchControls)
Number of Levels 12 (across 6 acts)
Guard Types 6 (Static, Rotating, Blinking, Mirror, Patrolling, Chaser)
Localization Keys ~67 per language
Max Grid Size 10x10
Max Guards/Level 8