# Writing a module

> How to add a new game module to the engine, following the file layout, define function, config types, tests, docs, skill and example that the four existing modules share.

Write a module when one genre should be quick and reliable for every new game: a config in, a complete game out. If only one game needs the genre, write that game from the engine pieces instead (see [Game modules](https://docs.moonspell.app/modules/overview.md#when-no-module-fits)). A module is the same code moved into `src/engine/modules/<name>/` with its knobs pulled out into a config.

This page lists the conventions the racing, puzzle, shooter and kingdom modules share. Follow them so the module reads like the others to people and to coding agents.

## File layout

Everything lives in `templates/crescent-game/src/engine/modules/<name>/`:

| File | Holds | In the existing modules |
| --- | --- | --- |
| `index.ts` | the public exports, and nothing else | every module |
| `types.ts` | the `Config` the game writes and the `ResolvedConfig` with every default filled in | `racing/types.ts`, `shooter/types.ts`, `kingdom/types.ts` (puzzle keeps them in `config.ts`) |
| `config.ts` | `resolve<Name>Config(config)`: fills defaults, clamps ranges, drops bad references | `resolveRaceConfig`, `resolvePuzzleConfig`, `resolveShooterConfig`, `resolveKingdomConfig` |
| pure rule files | the game's rules as plain TypeScript, no React, no React Native | `raceState.ts`, `vehicle.ts`, `track.ts`; `packs/`, `session.ts`, `goals.ts`; `match.ts`, `maps.ts`, `pickups.ts`, `snapshot.ts`; `town.ts`, `sortie.ts`, `objectives.ts` |
| `<Name>Game.tsx` | the screen and `define<Name>Game` | `RaceGame.tsx`, `PuzzleGame.tsx`, `ShooterGame.tsx`, `KingdomGame.tsx` |
| `<Name>Hud.tsx`, screens | the HUD and the title, pause and results screens | `RaceHud.tsx`, `PuzzleScreens.tsx`, `ShooterHud.tsx`, `KingdomHud.tsx` |
| meshes or render | the scene pieces | `TrackMeshes.tsx`, `Car.tsx`; `render.ts`; `ShooterMeshes.tsx`; `meshes.tsx`, `TownScene.tsx`, `SortieScene.tsx` |
| `*.test.ts` | unit tests for the pure files | `puzzle/levels.test.ts`, `puzzle/packs.test.ts`, `shooter/shooter.test.ts`, `kingdom/kingdom.test.ts` |

Inside the module, import engine pieces by relative path (`../../render/EngineCanvas`, `../../input/TouchControls`). Games import the module by its own path, `@/engine/modules/<name>`; modules are not re-exported from `@/engine`.

## The define function

One entry point, named `define<Name>Game`, takes the config and returns a `GameDefinition`. Both types come from `src/engine/core/types.ts` (inside a module, `import type { GameDefinition, GameScreenProps } from '../../core/types'`; games get them from `@/engine`).

The shape every module uses, shown here with the shooter's:

```tsx
export const defineShooterGame = (config: ShooterConfig): GameDefinition => {
  const resolved = resolveShooterConfig(config);
  const Component = (props: GameScreenProps) => <ShooterGame config={resolved} {...props} />;
  Component.displayName = 'ShooterGameScreen';
  return {
    id: resolved.id,
    title: resolved.title,
    description: resolved.description,
    badge: resolved.badge,
    accent: resolved.accent,
    Component,
  };
};
```

- Resolve the config once, outside the component.
- `Component` receives `onExit` from `GameScreenProps`. Every exit control in the module calls it.
- Pick a sensible `accent` from the config (the racing module uses the car's accent, the puzzle module `theme.accent`).
- Keep anything that must exist once per game (a save slot definition, as the kingdom module does with `defineSave`) next to the resolve call, not inside the component.

The game then writes one file:

```tsx
// src/game/index.tsx
import { defineShooterGame } from '@/engine/modules/shooter';

export default defineShooterGame({ id: 'yard-brawl', title: 'Yard Brawl', description: 'Everyone against everyone.' });
```

## Config types

- Only identity fields are required (`id`, `title`, `description`) where the genre allows. The kingdom module also requires its content tables, because a kingdom with no buildings or story is not a game.
- Put the range and the default in each field's doc comment, for example `/** Road width in metres. 8-16. Default 11. */`. Agents read these comments.
- Use string unions for choices (`'ffa' | 'tdm' | ...`) and export the list when it is useful at runtime (`SHOOTER_MODES`, `SHOOTER_MAPS`).
- Group fields by topic (`track`, `car`, `opponents`, `theme`; `rules`, `map`, `player`, `bots`, `pickups`) rather than one flat object.
- The resolver clamps every number into a range that plays well and falls back on unknown values, so a config written in a hurry still runs. Drop references that point nowhere (the kingdom resolver warns about them in development) instead of crashing.
- Assets come in as `require(...)` numbers on optional fields (`model`, `sounds`, `art.sheet`). The module always has a fallback: a procedural shape, placeholder sounds or silence, an icon instead of a sprite.

## The screen

- A 3D module renders an `EngineCanvas` inside an `EngineErrorBoundary`, runs its simulation in `useFrame`, and writes per-frame numbers to Reanimated shared values so React does not re-render every frame. A 2D module renders `GameCanvas2D`. See [Canvas and rendering](https://docs.moonspell.app/engine/canvas-and-rendering.md) and [2D layer](https://docs.moonspell.app/engine/2d.md).
- Use the engine's controls (`TouchControls`, `CharacterControls`, `Joystick`). They already answer to the keyboard in a desktop browser. See [Input](https://docs.moonspell.app/engine/input.md).
- Leaderboards, presence and rooms go through the [net hooks](https://docs.moonspell.app/engine/net.md) with `GAME_BACKEND_URL`, so the module works with and without a backend. Progress goes through [saves](https://docs.moonspell.app/engine/saves.md).
- Nothing platform-specific in the module. What differs between web, iOS and Android belongs in the engine's platform layer (`*.web.ts` files, `Platform.OS`).

## Dev and test hooks

Every module has the same three, so automation can drive any of them:

- A telemetry line with `testID` `hud-debug`, shown only when `__DEV__` is true.
- An **AUTO** button (`btn-autopilot`, or `btn-autoplay` in the puzzle module) that plays the game by itself, also only in development.
- A `testID` and an `accessibilityLabel` on every control, and `game-<id>` on the root view.

## Tests

Put the rules in pure files and test them with Node's test runner next to the code, as `*.test.ts`:

```ts
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolveKingdomConfig } from './config';
```

`npm test` in the template runs every `src/engine/**/*.test.ts`. Test at least: the resolver's defaults and clamps, each rule, the end-of-round conditions, and that the AUTO player reaches the end. The shooter's tests also walk every map preset to check that bots can reach every objective.

## Docs, skill and example

A module is not done until people and agents can find it and use it:

| Piece | Where | What |
| --- | --- | --- |
| Agent reference | `templates/crescent-game/docs/<name>-module.md` | every config field with type, range and default, a copy-ready config, backend needs |
| Skill | `templates/crescent-game/.claude/skills/<genre>-game/SKILL.md` | how to turn a player's words into a config: tables of "player says" to fields, naming, difficulty, a checklist |
| Router row | `templates/crescent-game/.claude/skills/game-router/SKILL.md` | the trigger words, the route, what to start from, and what the module cannot do |
| Agent instructions | the template's `CLAUDE.md` and `AGENTS.md` ("What you can build") and `docs/engine-api.md` (the module list) | one entry pointing at the doc, the skill and the example |
| Example | `templates/crescent-game/examples/<game>/index.tsx` | a complete, self-contained game on the module that a game can copy over `src/game/index.tsx` |
| Public page | `docs/modules/<name>.md` on this site | the page format the other module pages use |

If the module uses the backend, document which leaderboard names it submits to (and add them, or a `"prefix-*"` family, to the scaffold's `BOARDS` if every game of the module uses them), whether it needs rooms, and what it saves. See the [backend overview](https://docs.moonspell.app/backend/overview.md).

## Checklist

1. `src/engine/modules/<name>/` with `index.ts`, the config types, `config.ts`, the pure rules, the screen and `define<Name>Game`.
2. Fallbacks for every asset field.
3. `hud-debug`, AUTO, `testID`s, an exit that calls `onExit`.
4. Tests for the pure parts; `npm test` and `npm run typecheck` pass.
5. The game runs on the web (`npm run web`) and on a phone build.
6. Agent doc, skill, router row, `CLAUDE.md` / `AGENTS.md` / `engine-api.md` entries, an example, a public page.
7. Backend entries if the module uses leaderboards or rooms.

For the Moonspell app, the hosted builder (Nova) also has a list of what it can build in `agent/instructions.ts`; add the module there too if you contribute to the app. How the app picks up engine changes is on the [Moonspell pages](https://docs.moonspell.app/moonspell/architecture.md).
