chore: AGENTS.md updated
This commit is contained in:
parent
6984e54bdf
commit
976ee43ed3
121
AGENTS.md
121
AGENTS.md
|
|
@ -2,92 +2,71 @@
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Prefer `bash.exe`(C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps\bash.exe) over Powershell.
|
Shell environment: git bash (bash.exe) on Windows.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build # Build ESM bundle + declarations to dist/ (via tsup)
|
npm run build # ESM bundle + declarations to dist/ (tsup)
|
||||||
npm run test # Run vitest in watch mode
|
npm run build:samples # Build samples separately (tsup.samples.config.ts)
|
||||||
npm run test:run # Run vitest once (no watch)
|
npm run test # vitest in watch mode
|
||||||
npm run typecheck # Type-check without emitting (tsc --noEmit)
|
npm run test:run # vitest once (no watch)
|
||||||
|
npm run typecheck # tsc --noEmit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running a single test file
|
Run a single test file: `npx vitest run tests/core/game.test.ts`
|
||||||
```bash
|
Run tests matching a name: `npx vitest run -t "should create"`
|
||||||
npx vitest run tests/samples/tic-tac-toe.test.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running a single test by name
|
`npm run prepare` runs `npm run build` on install — if `inline-schema` isn't available, installs will fail.
|
||||||
```bash
|
|
||||||
npx vitest run -t "should detect horizontal win for X"
|
## Local Dependency
|
||||||
```
|
|
||||||
|
`inline-schema` is a **local peer dependency** at `file:../inline-schema` — must exist as a sibling directory before `npm install`. Both vitest and tsup load custom plugins from it (`inline-schema/csv-loader/rollup` and `inline-schema/csv-loader/esbuild`). `yarn-spinner-loader` is also a local dependency at `file:../yarn-spinner-loader` — it can be changed and published if needed. It provides vitest and tsup/esbuild plugins for `.yarnproject` dialogue files.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **MutableSignal\<T\>**: extends Preact Signal — `.value` to read, `.produce(draft => ...)` to mutate (uses `mutative`). `.produceAsync()` awaits interruption promises first, then mutates.
|
||||||
|
- **GameHost**: lifecycle manager — `start(seed?)`, `tryInput(string)`, `tryAnswerPrompt(def, ...args)`, `dispose()`. Reactive signals: `state`, `status`, `activePromptSchema`, `activePromptPlayer`, `activePromptHint`.
|
||||||
|
- **GameModule**: `{ registry?, createInitialState, start }`. `registry` defaults to `createGameCommandRegistry()` if omitted.
|
||||||
|
- **Command system**: CLI-style parsing with schema validation via `inline-schema`. `CommandRunner.run()` uses `this` bound to a `CommandRunnerContext`.
|
||||||
|
- **Prompt system**: `game.prompt(def, validator, currentPlayer?)` — validator **throws a string** to reject, **returns a value** to accept. `PromptEvent.tryCommit(Command | string)` returns `null` on success, error string on failure. `promptEnd` event fires after resolve or cancel.
|
||||||
|
- **Part collections**: `Record<string, Part<TMeta>>` keyed by ID. `Object.values()` for iteration, `Object.keys()` for count/IDs.
|
||||||
|
- **Region system**: `createRegion()` factory; `partMap` maps position keys to part IDs.
|
||||||
|
- **Barrel exports**: `src/index.ts` is the single public API surface.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
core/ # Core game primitives (game, part, region)
|
core/ # game.ts, game-host.ts, part.ts, part-factory.ts, region.ts
|
||||||
samples/ # Example games (tic-tac-toe, boop)
|
samples/ # tic-tac-toe.ts, boop/, onitama/, regicide/, slay-the-spire-like/
|
||||||
utils/ # Shared utilities (mutable-signal, command, rng, async-queue)
|
utils/ # mutable-signal.ts, rng.ts, async-queue.ts, command/ (8 files)
|
||||||
index.ts # Single public API barrel export
|
index.ts # barrel export
|
||||||
tests/ # Mirrors src/ structure with *.test.ts files
|
global.d.ts # *.yarnproject module declaration
|
||||||
|
tests/ # mirrors src/ with *.test.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`src/utils/command/` is a directory: types.ts, command-parse.ts, command-registry.ts, command-runner.ts, command-validate.ts, command-apply.ts, schema-parse.ts, index.ts.
|
||||||
|
|
||||||
|
## Samples Build
|
||||||
|
|
||||||
|
`tsup.samples.config.ts` auto-discovers entries from `src/samples/` (directories → `index.ts`, files → direct `.ts`). It rewrites `@/core/*`, `@/utils/*`, and `@/index` imports to external `boardgame-core`.
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
### Imports
|
- Double quotes for local imports, single quotes for npm packages
|
||||||
- Use **double quotes** for local imports, **single quotes** for npm packages
|
- `@/*` alias maps to `src/*` (configured in tsconfig, vitest, and tsup)
|
||||||
- Use `@/**/*` for `./src/**/*` import alias (configured in tsconfig.json)
|
|
||||||
- Use `@/index` when importing from the public API surface in samples
|
|
||||||
- Group imports: npm packages first, then local `@/` imports, separated by blank line
|
- Group imports: npm packages first, then local `@/` imports, separated by blank line
|
||||||
|
- 4-space indentation, semicolons, trailing commas in multi-line
|
||||||
### Formatting
|
- Arrow functions for callbacks; `function` keyword for methods needing `this` (e.g. command handlers)
|
||||||
- **4-space indentation** (no tabs)
|
- Strict TypeScript — no `any`; type aliases for object shapes (not interfaces)
|
||||||
- **Semicolons** at statement ends
|
- Discriminated unions for results: `{ success: true; result: T } | { success: false; error: string }`
|
||||||
- **Arrow functions** for callbacks; `function` keyword for methods needing `this` (e.g. command handlers)
|
|
||||||
- No trailing whitespace
|
|
||||||
- Trailing commas in multi-line objects/arrays
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- **Types/Interfaces**: `PascalCase` — `Part`, `Region`, `Command`, `IGameContext`, `GameHost`
|
|
||||||
- **Classes**: `PascalCase` — `MutableSignal`, `AsyncQueue`, `Mulberry32RNG`, `GameHost`
|
|
||||||
- **Functions**: `camelCase`, verb-first — `createGameContext`, `createGameHost`, `parseCommand`, `isValidMove`
|
|
||||||
- **Variables**: `camelCase`
|
|
||||||
- **Constants**: `UPPER_SNAKE_CASE` — `BOARD_SIZE`, `WINNING_LINES`
|
|
||||||
- **Test files**: `*.test.ts` mirroring `src/` structure under `tests/`
|
|
||||||
- **Factory functions**: prefix with `create` or `mutable` — `createGameContext`, `createGameHost`, `mutableSignal`
|
|
||||||
|
|
||||||
### Types
|
|
||||||
- **Strict TypeScript** is enabled — no `any`
|
|
||||||
- Use **generics** heavily: `MutableSignal<T>`, `CommandRunner<TContext, TResult>`
|
|
||||||
- Use **type aliases** for object shapes (not interfaces)
|
|
||||||
- Use **discriminated unions** for results: `{ success: true; result: T } | { success: false; error: string }`
|
|
||||||
- Use `unknown` for untyped values, narrow with type guards or `as`
|
|
||||||
- Derive state types via `ReturnType<typeof createInitialState>`
|
- Derive state types via `ReturnType<typeof createInitialState>`
|
||||||
|
- Factory function prefix: `create` or `mutable` — `createGameHost`, `mutableSignal`
|
||||||
|
- Validation error messages in Chinese (e.g. `"参数不足"`)
|
||||||
|
|
||||||
### Error Handling
|
## Testing
|
||||||
- Prefer **result objects** over throwing: return `{ success, result/error }`
|
|
||||||
- When catching: `const error = e as Error;` then use `error.message`
|
|
||||||
- Use `throw new Error(...)` only for truly exceptional cases
|
|
||||||
- Validation error messages are in Chinese (e.g. `"参数不足"`)
|
|
||||||
- Prompt validators return `null` for valid input, `string` error message for invalid
|
|
||||||
|
|
||||||
### Testing
|
- Vitest with globals enabled, but **test files import explicitly**: `import { describe, it, expect } from 'vitest'`
|
||||||
- **Vitest** with globals enabled (`describe`, `it`, `expect` available without import)
|
- Use `@/` imports in test files (vitest resolve alias)
|
||||||
- Use `async/await` for async tests
|
- Command/prompt test pattern: `waitForPrompt(ctx)` → `promptEvent.tryCommit(Command)` → returns `null` | error string
|
||||||
- Narrow result types with `if (result.success)` before accessing `result.result`
|
- No mocking — real implementations. Define inline helpers (`createTestContext()`, `waitForPrompt()`) per file.
|
||||||
- Define inline helper functions in test files when needed (e.g. `createTestContext()`)
|
- Narrow result types: `if (result.success)` before accessing `result.result`
|
||||||
- No mocking — use real implementations
|
|
||||||
- For command tests: use `waitForPrompt()` + `promptEvent.tryCommit()` pattern
|
|
||||||
- Test helpers: `createTestContext()`, `createTestRegion()`
|
|
||||||
|
|
||||||
## Architecture Notes
|
|
||||||
|
|
||||||
- **Reactivity**: `MutableSignal<T>` extends Preact Signal — access state via `.value`, mutate via `.produce(draft => ...)`
|
|
||||||
- **Command system**: CLI-style parsing with schema validation via `inline-schema`
|
|
||||||
- **Prompt system**: Commands prompt for input via `PromptEvent` with `resolve`/`reject`; `tryCommit` accepts `Command | string` and validates against schema before custom validator; `promptEnd` event fires when prompt completes (success or cancel)
|
|
||||||
- **GameHost**: Lifecycle manager for game sessions — provides `setup()`, `onInput()`, `dispose()`, reactive `state`/`status`/`activePromptSchema` signals
|
|
||||||
- **Barrel exports**: `src/index.ts` is the single public API surface
|
|
||||||
- **Game modules**: export `registry` and `createInitialState` for use with `createGameContextFromModule` or `createGameHost`
|
|
||||||
- **Region system**: plain `Region` type with `createRegion()` factory; `parts` are stored as `Record<string, Part>` keyed by ID, with `partMap` in regions mapping position keys to part IDs
|
|
||||||
- **Part collections**: Game state uses `Record<string, Part<TMeta>>` (not arrays) for O(1) lookup by ID. Use `Object.values(parts)` when iteration is needed, `Object.keys(parts)` for count/IDs
|
|
||||||
- **Mutative**: used for immutable state updates inside `MutableSignal.produce()`
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
# slay-the-spire-like
|
||||||
|
|
||||||
|
A Slay the Spire + Backpack Heroes hybrid roguelike sample. Players explore a point-crawl map, manage a tetris-style grid inventory, and fight enemies using cards generated from their equipment.
|
||||||
|
|
||||||
|
## Game Design Docs
|
||||||
|
|
||||||
|
Design docs are in the markdown files at this level:
|
||||||
|
- `01-overview.md` — core game concept, zones, encounter structure, combat rules, buff/debuff system
|
||||||
|
- `02-fighter.md` — Fighter class items (weapons, armor, tools, consumables, relics)
|
||||||
|
- `03-desert.md` — Desert zone enemies (minions, elites, boss)
|
||||||
|
- `data/rules.md` — combat state machine, turn order, effect timing rules
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
This is **not** a `GameModule` yet — there is no `createInitialState`/`start`/`registry` wired up to `createGameHost`. The code is a library of subsystems that can be composed into a game module.
|
||||||
|
|
||||||
|
### Subsystems
|
||||||
|
|
||||||
|
| Directory | Purpose | Key exports |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `progress/` | Run state, player HP/gold, inventory management, map progression | `createRunState`, `moveToNode`, `resolveEncounter`, `damagePlayer`, `healPlayer`, `addItemFromCsv`, `removeItem`, `getReachableChildren` |
|
||||||
|
| `map/` | Point-crawl map generation and traversal | `generatePointCrawlMap`, `getNode`, `getChildren`, `getParents`, `hasPath`, `findAllPaths` |
|
||||||
|
| `grid-inventory/` | Tetris-style grid placement (place, move, rotate, flip items) | `createGridInventory`, `placeItem`, `removeItem`, `moveItem`, `rotateItem`, `flipItem`, `validatePlacement`, `getAdjacentItems` |
|
||||||
|
| `deck/` | Card/deck system (draw pile, hand, discard, exhaust) | `generateDeckFromInventory`, `createStatusCard`, `createDeckRegions`, `createPlayerDeck` |
|
||||||
|
| `data/` | CSV game data loaded via `inline-schema/csv-loader`. `.d.ts` files are auto-generated by the csv-loader plugin — do not edit by hand. | `heroItemFighter1Data`, `encounterDesertData`, `enemyDesertData`, `enemyIntentDesertData`, `effectDesertData`, `statusCardDesertData` |
|
||||||
|
| `dialogue/` | Yarn Spinner dialogue files (placeholder). Loaded via `yarn-spinner-loader`, a local peer dependency at `../yarn-spinner-loader` (like `inline-schema`, it can be changed and published if needed). | `encounters` yarnproject |
|
||||||
|
| `utils/` | Shape parsing and collision math | `parseShapeString`, `checkCollision`, `checkBounds`, `transformShape`, `rotateTransform`, `flipXTransform`, `flipYTransform` |
|
||||||
|
|
||||||
|
### Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
CSV files (data/)
|
||||||
|
→ inline-schema/csv-loader → typed JS objects (e.g. HeroItemFighter1)
|
||||||
|
→ parseShapeString() converts shape strings → ParsedShape
|
||||||
|
→ GridInventory<GameItemMeta> holds placed items
|
||||||
|
→ generateDeckFromInventory() generates cards per occupied cell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key types
|
||||||
|
|
||||||
|
- **`RunState`** — top-level state: seed, map, player, inventory, currentNodeId, encounter state, resolved set. Designed for `MutableSignal.produce()` mutation.
|
||||||
|
- **`GridInventory<TMeta>`** — `items: Map<string, InventoryItem<TMeta>>` + `occupiedCells: Set<CellKey>` for O(1) collision. Mutated directly inside `.produce()`.
|
||||||
|
- **`InventoryItem<TMeta>`** — id, shape (ParsedShape), transform (Transform2D), meta. Shape + transform determines which cells are occupied.
|
||||||
|
- **`GameCard`** — a `Part<GameCardMeta>` bridging inventory items to the deck system. `sourceItemId` links back to the inventory item; `null` for status cards.
|
||||||
|
- **`PointCrawlMap`** — layered DAG: 10 layers (start → wild×2 → settlement → wild×2 → settlement → wild×2 → end). Wild = 3 nodes, Settlement = 4 nodes.
|
||||||
|
- **`MapNode`** — id, type (MapNodeType enum), childIds, optional encounter data from CSV.
|
||||||
|
|
||||||
|
### Map generation
|
||||||
|
|
||||||
|
`generatePointCrawlMap(seed?)` produces a deterministic map:
|
||||||
|
- 10 layers: Start → Wild(3) → Wild(3) → Settlement(4) → Wild(3) → Wild(3) → Settlement(4) → Wild(3) → Wild(3) → End
|
||||||
|
- Settlement layers guarantee ≥1 camp, ≥1 shop, ≥1 curio (4th slot random)
|
||||||
|
- Wild pair types are optimized to minimize same-type repetition
|
||||||
|
- Edge patterns avoid crossings: Start→all wild, Wild→Wild 1:1, Wild↔Settlement 3:4 or 4:3, Wild→all End
|
||||||
|
|
||||||
|
### Shape system
|
||||||
|
|
||||||
|
Items have shapes defined as movement strings parsed by `parseShapeString`:
|
||||||
|
- `o` = origin cell, `n/s/e/w` = move + fill, `r` = return to previous position
|
||||||
|
- Example: `"oesw"` = 2×2 block (origin, east, south, west = full square)
|
||||||
|
- Example: `"oe"` = 1×2 horizontal
|
||||||
|
- Example: `"onrersrw"` = cross/X shape
|
||||||
|
|
||||||
|
Shapes are positioned via `Transform2D` (offset, rotation, flipX, flipY) and validated against the 6×4 grid.
|
||||||
|
|
||||||
|
### Grid inventory
|
||||||
|
|
||||||
|
All mutation functions (`placeItem`, `removeItem`, `moveItem`, `rotateItem`, `flipItem`) mutate the `GridInventory` **directly** — they must be called inside `produce()` callbacks. `validatePlacement` checks bounds + collisions before placement.
|
||||||
|
|
||||||
|
### Card generation
|
||||||
|
|
||||||
|
`generateDeckFromInventory(inventory)` creates one card per occupied cell in each item's shape. Cards carry `GameCardMeta` linking back to the source item and cell position. Status cards (wound, venom, etc.) are created separately via `createStatusCard`.
|
||||||
|
|
||||||
|
## CSV data format
|
||||||
|
|
||||||
|
All CSVs use `inline-schema` typed headers. The first row is a comment header, the second row is the schema row with types and references:
|
||||||
|
- `'energy'|'uses'` — union type
|
||||||
|
- `@enemyDesert` — foreign key reference to another CSV
|
||||||
|
- `[effect: @effectDesert; number][]` — array of structured references
|
||||||
|
|
||||||
|
### heroItemFighter1.csv columns
|
||||||
|
|
||||||
|
| Column | Type | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| type | `'weapon'|'armor'|'consumable'|'tool'` | |
|
||||||
|
| name | string | Display name (Chinese) |
|
||||||
|
| shape | string | Movement string for `parseShapeString` |
|
||||||
|
| costType | `'energy'|'uses'` | Energy = per-turn cost; Uses = limited uses |
|
||||||
|
| costCount | int | Cost amount |
|
||||||
|
| targetType | `'single'|'none'` | |
|
||||||
|
| price | int | Shop price |
|
||||||
|
| desc | string | Ability description (Chinese) |
|
||||||
|
| effects | `['self'|'target'|'all'|'random'; @effectDesert; number][]` | Effect references |
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Chinese is used for all user-facing strings (item names, error messages, effect descriptions)
|
||||||
|
- Discriminated union result types: `{ success: true } | { success: false, reason: string }`
|
||||||
|
- Mutation functions mutate state directly (inside `produce()`); validation is separate
|
||||||
|
- `Map` and `Set` are used in `GridInventory` and `PointCrawlMap` (not plain objects) — requires careful handling with `mutative` since it drafts Maps/Sets differently than plain objects
|
||||||
|
- Starter items defined in `progress/index.ts`: `['治疗药剂', '绷带', '水袋', '短刀', '剑']`
|
||||||
|
- Default player stats: 50 HP, 50 gold, 6×4 inventory
|
||||||
Loading…
Reference in New Issue