t1k:designer:base:tool-architecture
| Field | Value |
|---|---|
| Module | base |
| Version | 1.13.2 |
| Effort | medium |
| Tools | — |
Keywords: content pipeline, curriculum, design tool, design tool replication, family classification, GameDesignToolTemplate, level editor, level pool, polycube, progression, schema migration, schema-driven, shape pool, solver, telemetry feedback, voxel surface
How to invoke
Section titled “How to invoke”/t1k:designer:base:tool-architectureGame Design Tool Architecture
Section titled “Game Design Tool Architecture”Build a replicable schema-driven design tool for level/content-driven games (puzzle, RPG, tower defense, card, match-3, platformer). Captures the toolchain pattern proven on Arrow3D — schema-driven UI, shape-first content pipeline, content-blind curriculum picker, two-phase content injection, load-time validation, and the hardening playbook (solver-driven difficulty, telemetry feedback, schema migrations, stats dashboard).
When This Skill Triggers
Section titled “When This Skill Triggers”- Building a new designer-facing tool from
GameDesignToolTemplateor scratch - Replicating an existing tool’s patterns for a new game
- Hardening an MVP design tool with feedback loops, validation, migrations
- Reviewing whether a design-tool architecture decision fits or fights the playbook
- Architecting curriculum / level-pool / progression / comparison modules
Maturity Tiers
Section titled “Maturity Tiers”A design tool walks through three tiers. Skipping ahead causes pain; skipping behind causes throwaway work.
| Tier | Goal | Outcome when missing |
|---|---|---|
| 1. Foundation | Tool runs; designers can edit data | Hand-rolled CRUD per module → maintenance hell |
| 2. Core pipeline | Content can be generated, picked, injected, validated | Manual content authoring fails past 50 levels |
| 3. Hardening | Closed feedback loops; data-driven balance | Curriculum drifts from intent; bugs reach players |
Tier 1 — Foundation
Section titled “Tier 1 — Foundation”Patterns
Section titled “Patterns”| Pattern | Why |
|---|---|
Schema-driven UI — Zod schemas auto-generate tables and forms via extractColumns / extractFormFields; extractFormFields also extracts Zod .default() values so fields pre-populate correctly, and strips label markers ([color], [textarea], [emoji], [moduleRef:…]) before rendering | Add a module by declaring schema; UI is free |
Local-first storage — IndexedDB scoped by {projectId, moduleId}; writes to each key are serialized through a per-key mutex (prevents read-modify-write races when rapid saves fire concurrently) | No backend; offline iteration; export on demand |
Self-registering modules — registerModule({...}) via side-effect import | Sidebar populates automatically; no central manifest to drift |
| Single engine, two callsites — generation/validation lib called from in-editor Web Worker AND batch CLI | One source of truth for compute logic |
| Cross-tree boundary — design tool emits JSON; engine consumes; tool never edits engine code | Two codebases ship independently; engine bugs become audit reports, not silent fixes |
DataTable controlled pagination + stable row IDs — getRowId callback returns a domain key (not array index); pagination state is controlled so filter/sort changes don’t silently deselect rows | Row selection survives search and column sort; required for multi-select bulk actions |
Arrow3D worked snippets
Section titled “Arrow3D worked snippets”| What | Where |
|---|---|
| Module registration | src/modules/level-editor/index.ts + side-effect import in src/App.tsx |
| Schema → UI + defaults + marker strip | src/core/schema-utils.ts (extractColumns, extractFormFields) |
| Storage + write serialization | src/core/data-store.ts (useDataStore per module; serialize() mutex per key) |
| Worker + CLI parity | src/modules/level-editor/workers/generator.worker.ts calls same generateLevel() as scripts/level-generate-arrow.mjs |
| Cross-tree boundary | tool writes to ../UnityArrow3D/Assets/Level Json/, never edits Unity source |
| Controlled pagination + getRowId | src/components/data-table/data-table.tsx |
Anti-patterns
Section titled “Anti-patterns”- ❌ Hand-rolled CRUD per module — defeats schema-driven design
- ❌ Backend service for “easier sync” — kills local-first iteration speed
- ❌ Two parallel engines for “interactive vs batch” paths — silent divergence
- ❌ Tool that auto-fixes engine bugs — cross-tree silent regressions
- ❌ Storing derived fields (e.g.,
SizewhenCells.bboxcomputes it) — drift guaranteed
Tier 2 — Core pipeline
Section titled “Tier 2 — Core pipeline”The five-stage pipeline that scales content production from 10 to 1000+ levels.
Stage 1: Shape-first generation
Section titled “Stage 1: Shape-first generation”Generate geometry only. Empty content array. Instant (~ms per shape).
data/<batch-name>/Shape_001.json: { Cells: [...], Tiles: [] } ← Tiles intentionally emptyWhy: content placement is slow (often seconds per shape). Curriculum tuning iterates many times — baking content during shape gen wastes hours regenerating shapes that never make the cut.
Stage 2: Curriculum picker (content-blind)
Section titled “Stage 2: Curriculum picker (content-blind)”Score abstract metrics per L-slot from a progression curve:
- Geometry size targets / voxel-count buckets
- Family/category variety rules (e.g., 1-2 RECT → 1 SPECIAL rotation)
Picker has zero content awareness. Output: 1 shape per L-slot. Just geometry. Picker can be re-run cheaply since shape pool already exists.
Stage 3: Content injection (two-phase)
Section titled “Stage 3: Content injection (two-phase)”| Phase | Type | Example |
|---|---|---|
| 3a | Auto primitives | Arrows, paths, mandatory placements — produced by generator engine; heavy compute |
| 3b | Designer-hinted/manual | Special elements, modifiers — placed by designer using per-L target counts surfaced as banner hints |
Why two-phase: auto path is deterministic and fits batch generation; manual path keeps designer authorship for rule-bending mechanics.
Stage 4: Load-time validation (hard gate)
Section titled “Stage 4: Load-time validation (hard gate)”Two validators run before any level can be used:
- Geometry validator — every cell exists, every face is exposed, no occlusions
- Reachability/solvability validator — every primitive can resolve its goal
Failure → throw, never silent fallback. Same validators in tool AND engine — same JSON, same rules, same outcome.
Stage 5: Engine output
Section titled “Stage 5: Engine output”Write JSON to engine’s asset folder. Optional encrypted mirror. Engine consumes; tool is done. Schema agreed in writing.
Arrow3D worked example
Section titled “Arrow3D worked example”| Stage | File |
|---|---|
| 1. Shape gen | scripts/gen-symbol-shapes.mjs → data/prod-batch-v*/ |
| 2. Picker | scripts/build-level-curriculum.mjs reads src/modules/progression/config/default-progression.ts |
| 3a. Auto inject | scripts/level-generate-arrow.mjs (parallel CLI) + workers/generator.worker.ts (in-editor) — both call generator/index.ts |
| 3b. Designer inject | scripts/inject-elements.mjs + manual editor placement |
| 4. Validate | LevelShapeValidator + ArrowReachabilityValidator (run in tool AND in Unity LevelParamsLoader) |
| 5. Output | Arrow3D/UnityArrow3D/Assets/Level Json/Level_*.json |
Anti-patterns
Section titled “Anti-patterns”- ❌ Picker that knows about content → couples curriculum tuning to content algorithms
- ❌ Baking content during shape gen → kills iteration speed
- ❌ Validating only at play time → bugs surface in player sessions
- ❌ Tool validators ≠ engine validators → silent dev/prod divergence
- ❌ Single-phase injection (everything auto) → templates lack rule-bending mechanics
Tier 3 — Hardening
Section titled “Tier 3 — Hardening”Graduate from MVP. Closed feedback loops; data-driven balance; production-grade.
3.1 Solver / auto-difficulty estimator
Section titled “3.1 Solver / auto-difficulty estimator”Run a play-simulator over each generated level. Score:
- States explored (proxy for puzzle space size)
- Decision branches (proxy for choice complexity)
- Dead-end frequency (proxy for trap density)
- Min-moves-to-solve
Compare actual score vs progression-curve intent at the level’s L-slot. Reject outliers; auto-grade newly generated content; validate that “L=50” actually plays at L=50 difficulty target.
MVP scope: count states + branches + dead-ends. Defer A* / IDA* gold-plating.
3.2 Telemetry feedback loop
Section titled “3.2 Telemetry feedback loop”Engine emits per-level playtest logs (completion %, retries, fail-locations). Tool ingests JSON → progression module overlays “intended vs actual” per L-slot.
Unlocks data-driven curve adjustments; identifies rage-quit levels.
Cross-tree caveat: needs engine-side log emission. Plan with engine team before tool work begins.
3.3 Schema versioning + migration framework
Section titled “3.3 Schema versioning + migration framework”migrations/NNN-*.mjs registry. Version stamp on every level JSON (schemaVersion: N). Auto-apply on data load.
Levels evolve without breaking historical content. Fully portable; bake into the template.
3.4 Stats dashboard module
Section titled “3.4 Stats dashboard module”Read-only module. Bar charts of:
- L-slot coverage (gaps highlighted)
- Family/category histogram
- Element/primitive count distribution
- Difficulty score distribution per L-slot (consumes 3.1 output)
Surfaces holes designers can’t see in raw JSON.
3.5 A/B variant generator
Section titled “3.5 A/B variant generator”Output 2-3 variants per L-slot at the same target difficulty. Tag with variant ID. Engine reads variant per cohort. Feeds telemetry loop (3.2) to compare cohort outcomes.
3.6 On-disk JSON variants
Section titled “3.6 On-disk JSON variants”Save named curve/curriculum snapshots as data/<module>-variants/<name>.json — git-tracked files, not IndexedDB blobs.
Why: IndexedDB blobs are invisible to version control; variants need to be diffable, shareable, and auditable by the whole team.
File pointer: data/progression-variants/, data/curriculum-variants/ (one file per named variant).
3.7 Chart-metric chip toggles
Section titled “3.7 Chart-metric chip toggles”Declarative STAT_CHIPS table maps each metric to its toggle chip, curve target, and actual-achieved value. Dashboard code iterates the table — adding a new metric is one table row, not a conditional block.
Why: without the table, each new metric requires four scattered code sites to stay in sync; the table collapses them to one.
File pointer: src/modules/stats-dashboard/config/stat-chips.ts (or equivalent module config).
Refs: GameDesignToolTemplate PR #8, Issue #7.
Arrow3D status (worked example)
Section titled “Arrow3D status (worked example)”| Pattern | Status |
|---|---|
| 3.1 Solver | Planned (reuses walker.ts + escape-animation engine) |
| 3.2 Telemetry | Deferred — needs Unity-side log emission |
| 3.3 Schema migrations | Shipped — migrations/NNN-*.mjs registry with schemaVersion stamp; auto-apply on load |
| 3.4 Stats dashboard | Planned |
| 3.5 A/B variants | Foundation present in matchmaker/ (selector, slot-curve, output-writer) |
| 3.6 On-disk JSON variants | Shipped — progression/curriculum variants saved as data/<module>-variants/<name>.json; git-tracked instead of IndexedDB blobs |
| 3.7 Chart-metric chip toggles | Shipped — declarative STAT_CHIPS table drives per-metric toggle chips on the stats dashboard; no hardcoded conditionals per metric (template PR #8) |
Anti-patterns
Section titled “Anti-patterns”- ❌ Solver that aims for “perfect” — gold-plate kills MVP timeline; ship cheap heuristic first
- ❌ Telemetry without dashboard — raw logs don’t drive design decisions
- ❌ Schema changes without migrations — breaks all existing level data
- ❌ Stats dashboard that writes — must be read-only to avoid drift between dashboard and SSOT modules
Replication Recipe (5 steps)
Section titled “Replication Recipe (5 steps)”- Start from
GameDesignToolTemplate— schema-driven shell + IndexedDB - Define your geometry primitive — grid / voxel / hex / graph. SSOT data type with computed bounds (no derived fields)
- Define your content primitives — auto-place vs designer-place per primitive
- Build the pipeline — Stage 1 (shape gen) → Stage 2 (picker) → Stage 3a/3b (inject) → Stage 4 (validate) → Stage 5 (engine writer). Single shared engine, two callsites
- Wire the engine consumer — JSON schema agreed; engine runs same validators on load; tool never edits engine source
→ See references/replication-recipe.md for the full walkthrough with Arrow3D as worked example, including what to copy vs adapt vs skip.
Hardening graduation checklist
Section titled “Hardening graduation checklist”Move Tier 2 → Tier 3 when ANY of these triggers:
- Curriculum picker output feels “off” (gut says easier/harder than intended)
- Designers complain about unseen L-slot coverage gaps
- A schema change broke historical level data
- Engine playtests reveal levels that “should” be L50 but everyone fails at L20
Order to ship:
- Schema migrations (3.3) — cheap, portable, ship first
- Stats dashboard (3.4) — surfaces gaps; designers self-serve
- Solver (3.1) — validates curriculum intent; closes loop on (1)
- Telemetry (3.2) — closes loop on (3); needs engine-side work
- A/B variants (3.5) — once telemetry exists, multiplies throughput
Cross-References
Section titled “Cross-References”puzzle-game-design— puzzle mechanics theory (combine for puzzle-genre tools)game-level-design— spatial flow and encounter designgame-procedural-generation— generation algorithms feeding Stage 1game-balance-tools— DPS/EHP/curve formulas feeding Tier 3.1 solver scoringgame-design-document— GDD/wiki sync patterns for the docs that wrap the tool
Common Mistakes
Section titled “Common Mistakes”| # | Mistake | Fix |
|---|---|---|
| 1 | Skipping Tier 1, going straight to procgen | No tool to drive procgen with — designers can’t edit knobs |
| 2 | Picker that injects content while picking | Decouple — picker scores geometry only; content injection is later stage |
| 3 | Validators only in engine, not tool | Mirror them; tool catches bugs at edit time, not play time |
| 4 | Storing Size when Cells.bbox computes it | No derived fields — compute at use site |
| 5 | Per-game tool repo from scratch | Start from GameDesignToolTemplate; only add game-specific bits |
| 6 | Telemetry ingest without dashboard | Logs without visualization don’t drive decisions |
| 7 | Solver gold-plating before MVP | Ship state-count heuristic; iterate later |
Reference Files
Section titled “Reference Files”| File | Coverage |
|---|---|
references/replication-recipe.md | Full 5-step walkthrough with Arrow3D worked example, what to copy / adapt / skip |
Gotchas
Section titled “Gotchas”- Solver-driven difficulty needs a determinism contract — same seed + same content = same output. If your solver uses
Random.valueinstead of a seeded RNG, replays diverge. - Schema-driven tools rot if validation runs only at runtime — load-time validation catches 80% of regressions on save; trust nothing without it.
- Telemetry feedback ≠ telemetry firehose — pipe only the metrics that drive design changes back into the tool; everything else is dashboard noise.
- IndexedDB write races corrupt module data —
useDataStoreruns multiple concurrentsetAllDatacalls when rapid edits fire (e.g., inline-cell save while an import is in flight). Without a per-key mutex, the second write reads stale state and overwrites the first write’s changes. Serialize writes viadata-store.ts → serialize(key, fn). Bug class: silently lost data, no thrown error. - Zod
.default()values are not honored unlessextractFormFieldsreads them —z.string().default("normal")on a schema field does nothing unless the form generator extracts the.default()from the Zod type and passes it asdefaultValuesto the form. Without this, new-record forms show blank fields even when defaults are declared. - Label markers survive into rendered UI if not stripped —
extractFormFieldsmust strip[color:red],[textarea],[emoji:🗡️],[moduleRef:items]from field label strings before passing to the form renderer. Unstripped markers show as literal text in field labels.