Skip to content

Latest commit

 

History

History
134 lines (107 loc) · 6.75 KB

File metadata and controls

134 lines (107 loc) · 6.75 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

Blockbuster is a client-side TypeScript playground for travelling-salesman route-finding through non-uniform space: a hex grid is laid over a fictitious 50 km × 30 km landscape, each cell gets a composite risk cost, and the app proposes 3 diverse Courses of Action (COAs) between chosen waypoint cells. Everything runs in the browser — no backend.

The full build spec lives in docs/spec/ (read docs/spec/README.md first). It is the authoritative reference for behaviour and acceptance criteria; consult the relevant docs/spec/NN-*.md before implementing an engine module.

New to the project? Open docs/approaches.html in a browser for an interactive guide to the algorithms — terrain generation, hex grids, risk modelling, and route optimisation with hands-on widgets. Good first read before diving into the spec or engine code.

Commands

npm install          # required first; node >= 20
npm run dev          # Vite dev server → http://localhost:5173
npm run typecheck    # tsc --noEmit (strict)
npm run lint         # eslint .
npm test             # vitest (watch)
npm run test:run     # vitest run (one-shot, use in CI/checks)
npm run build        # tsc --noEmit && vite build
npm run format       # prettier --write over src + docs

Run a single test file or filter by name:

npx vitest run src/engine/risk/risk.test.ts
npx vitest run -t "name of the test"

There is no PR check workflow — CI only deploys (see below), so run typecheck + lint + test:run + build locally before pushing; they are all expected to stay green.

Real engine vs. the retained mock

The app runs on the real engine. The four engine modules under src/engine/* (mapgen, hexgrid, risk, routing) are implemented, and createEngine() (src/engine/index.ts) wires them together. src/state/store.ts injects that into the app singleton — createBlockbusterStore(createEngine()).

  • The deterministic mock under src/mocks/* (mockEngine.ts + fixtures.ts) has been dropped from the app's runtime path. It is retained as the living reference and as fixture/test data — store.test.ts, the routing tests and the golden fixtures still build on it.
  • The mock stays the behavioural yardstick: a change to a real module should keep the app behaving identically or better. Each engine module ships a colocated *.test.ts with its acceptance criteria — keep those green.
  • To swap an implementation for debugging, compose an Engine by hand, e.g. { ...createEngine(), gridBuilder: createMockEngine().gridBuilder }.

Architecture — the rules that matter

The whole design exists to let modules be built and tested in isolation. Honour these invariants; breaking them defeats the point.

  1. The shared kernel is src/domain (alias @domain). It holds only types, units and the module-boundary ports — no meaningful behaviour. Everything imports from @domain; @domain imports nothing from src. Treat the @domain barrel (src/domain/index.ts) as a public API: additive changes only, announce edits to existing types.

  2. Each engine module implements exactly one port (src/domain/ports.ts: MapGenerator, GridBuilder, RiskEngine, RoutePlanner). Engine code is pure, deterministic, framework-free — no DOM, no React, no Math.random() (seed via src/domain/rng.ts). The four engine modules must not import each other; they meet only through @domain types, which is why they parallelize.

  3. The UI never imports the engine. UI ↔ engine traffic goes through the Zustand store (src/state/store.ts), which depends on the engine only via the injected Engine port. The store owns the world, controls and routing output and is the only thing both UI and engine touch. UI components subscribe to narrow selectors (useBlockbusterStore(s => s.x)).

  4. The cost function is shared kernel (src/domain/cost.ts). The routing worker and the COA stacked-bar charts both call riskCostBreakdown / cellRiskCost, so every chart segment is exactly a per-risk cost the planner optimised. Never duplicate the cost formula — there must be no private copy. The optimised total also includes a per-step movement cost (movementCost) — a constant per hex step that keeps the three COAs distinct by trading distance against risk — but the charts deliberately omit it: being constant per cell it adds no per-cell signal and would read as a stray non-risk colour, so the bars show the risk breakdown only.

  5. Routing runs in a Web Worker (src/engine/routing/worker.ts), so its request/response must be plain structured-clone-friendly data — no closures, no class instances. HexGrid (which has methods) is projected to HexGridDto via toHexGridDto before sending; the worker reconstructs costs from the per-cell RiskProfile + CostParams. Messages are typed (RouteWorkerRequest / RouteWorkerResponse). createRoutePlanner({ useWorker: false }) runs the same planRoutes core synchronously for tests.

Dependency DAG (no cycles): domain → (nothing); engine/* → domain; mocks → domain; state → domain + an injected Engine; ui/* → domain, state; app/* → domain, state, ui.

Data flow (one cycle)

regenerate(seed)MapGenerator.generateGridBuilder.build + sampleTerrainRiskEngine.baseProfile per cell → store holds CellRiskState { base, overrides }. User edits (appetite slider / cell override / waypoints) → store recomputes effective profiles + CostParamsdebounced ~150 ms call to RoutePlanner.planRoutePlan { coas }. Replans are stale-guarded: a result whose waypoints no longer match live state is dropped. Determinism is a hard requirement — same seed + inputs ⇒ same output.

Conventions

  • TypeScript is strict with extras on: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noUnusedLocals/Parameters, verbatimModuleSyntax (use import type for type-only imports). Prefix intentionally-unused vars with _.
  • Aliases: @src, @domainsrc/domain.
  • Prettier: single quotes, trailing commas, semicolons, width 100.
  • Tests are colocated *.test.ts(x), run under Vitest + jsdom with globals and Testing Library (src/setupTests.ts); fixtures under src/mocks/.

Deployment

Two GitHub Actions workflows publish to the gh-pages branch: deploy.yml builds main to the site root; pr-preview.yml builds each PR to pr-preview/pr-<number>/ and comments a preview link. The Vite base is './' so the same build works at the root and under a PR sub-path.