# Game engine API

For TypeScript types, validation, and reusable integration helpers, see the [protocol library](./protocol-library.md).

Your game engine is an npm module with named function exports. In the signatures
below, `GameData` and `Move` are your game's types. BGS calls these exports directly;
there is no emitter or registration step. `defineEngine` can type-check an object,
but you must still export its methods (not just a default engine object). See the
[complete example](./protocol-library.md#engine-contract).

`inspectEngine` reports required and optional method coverage separately. A low
optional percentage is normal: implement only hooks your game needs.

To host the engine on the platform — from the npm registry or an uploaded `npm pack` tarball — see [Adding a game](./adding-a-game.md).

Methods such as `init`, `move`, and `dropPlayer` return `GameData`, the game state. You can alter the `GameData`
passed in the argument and return, or return a whole new object.

[[toc]]

## Required methods

### init

```ts
init(players: number, expansions: string[], options: Record<string, unknown>, seed: string, creator?: number): GameData | Promise<GameData>
```

Creates the initial game data. The function can be asynchronous - if you need to make an API call to an external
tool like a map generator, for example.

After initialization, the players are 0-indexed: a four-player game will have players `0`, `1`, `2` and `3`.

`creator` is the index of the player that created the game. It can be `undefined` if the creator of the game is not a player.

#### players

The number of players

#### expansions

The list of expansions activated

#### options

An object containing the options chosen for the game — the values the creator picked from the `options` declared
on the game info. See [Game options, preferences & settings](./game-options.md); always read defensively, keys may
be absent.

#### seed

The random seed for the game. Games with the same seed should always produce the same random results (map generation, card shuffling, dice throws...).

The seed is stored on the game document and visible to players. If your engine keeps secrets
(defines [stripSecret](#stripsecret)), the platform hashes it with a server-only key before
passing it here, so players cannot precompute the hidden random results (card order, dice
throws, ...) from the stored seed. Override with the optional `hashSeed` engine export:

```ts
export const hashSeed = true; // Force hashing when the seed drives hidden data.
```

Set it to `false` to force the raw seed. When omitted, hashing follows the presence of `stripSecret`.

### move

```ts
move(data: GameData, move: Move, player: number): GameData | Promise<GameData>
```

Execute a move by `player`.

Throw an error if the move is invalid.

For private tentative input, return `undefined` from [toSave](#tosave) until the
player confirms. For revisable choices shared with teammates, use
[isLiveUpdate](#isliveupdate). See [Undo and tentative moves](#undo-and-tentative-moves).

### ended

```ts
ended (data: GameData): boolean
```

Returns whether or not the game is ended.

### scores

```ts
scores (data: GameData): number[]
```

Give the score of each player. The scores are displayed in the sidebar next to player games. They are also used to determine
the player ranking at the end for elo calculation, unless [rankings](#rankings) is implemented.

### dropPlayer

```ts
dropPlayer(data: GameData, player: number): GameData | Promise<GameData>
```

Drop a player out. Called when a player quits or is dropped after running out of time (see
[Game clocks & timing](./timing.md)).

Let the remaining players continue (for example, with an engine-controlled replacement),
or end/cancel the game if it cannot continue. Preserve the original seat indices
for scores, factions, and all other per-player arrays. If the dropped player was
active and play continues, advance to the next participant who can act. This must
work at any point in the game — see [Bot players](./bots.md) for how this differs
from platform bot slots.

### currentPlayer

```ts
currentPlayer(data: GameData): number | number[] | undefined
```

Get the current player(s). Some games like 6nimmt can have multiple players that can play at the same time.

When the game is ended, the `currentPlayer` can be `undefined`.

### logLength

```ts
logLength(data: GameData): number
```

Returns the length of the log of the game.

It is used to send log slices to the viewer, especially when executing a move, when we want to send only the new log items to the viewer.

### logSlice

```ts
logSlice(data: GameData, options?: { player?: number; start?: number; end?: number }): unknown
```

Returns a log slice to be sent to [player](#options-player)'s viewer from [start](#options-start) to [end](#options-end) included.

The return value can have any structure. We recommend something like `{log: items[], availableMoves?: moves[]}` to show the log and the available moves
at the final state.

#### options.player

The player to whom to send the log.

If `undefined`, it is to a spectator. Otherwise it is the player's zero-based seat index.

The secrets should be stripped from the log items, depending on who receives the log info. For example,
in a card game, the spectators or other players should not see a player's card - unless the game is already ended.

#### options.start

The beginning of the log slice; default to `0` when omitted.

#### options.end

The end of the log slice. If `undefined`, it corresponds to the very end of the existing log.

## Optional methods

### moveAI

```ts
moveAI(data: GameData, player: number): Promise<GameData> | GameData
```

Play one move for `player`, chosen by the engine. This powers the platform's **bot players**:
when a bot slot becomes the current player, the game-server calls `moveAI` and stores the
result like a regular move. It should be exported from the engine's **entry point** (the
module the game-server loads), since that's what the bot driver calls. See
[Bot players](./bots.md) for the full platform behavior (scheduling, failure policy, requirements).

Optional — only games whose engine implements `moveAI` can have bot players. The game-server
**auto-detects** it when an engine version is installed (probing the entry point for a
`moveAI` export) and records it as `meta.bots` on the game info, which the creation UI uses to
offer bot seats. Reusing auto-play logic from [dropPlayer](#dropplayer) is a possible
starting point; the engine controls how the bot chooses its move.

### setPlayerMetaData

```ts
setPlayerMetaData(data: GameData, player: number, metaData: {name: string}): GameData
```

Set metadata on the player, from BGS.

- `name`: Name of the player

Other metadata such as an avatar, clan name, ... could be given in the future.

### setPlayerSettings

```ts
setPlayerSettings(data: GameData, player: number, settings: Record<string, unknown>): GameData
```

Update player settings - such as autocharge in Gaia Project. The available settings are declared on the game
info — see [Game options, preferences & settings](./game-options.md).

Only update settings for the given keys. Other settings are left unchanged.

### playerSettings

```ts
playerSettings(data: GameData, player: number): Record<string, unknown>
```

Get player settings.

### rankings

```ts
rankings (data: GameData): number[]
```

Rankings for the players.

Only necessary if [scores](#scores) does not give enough information to rank the players. For example, if the player with the smallest score is the winner, or if there are tie-breaking conditions not reflected in the score.

Return one rank per player, in the same order as `scores`. Rank `1` is best.
For players `[Alice, Bob]`, `[2, 1]` means Bob won and Alice came second;
`[1, 1]` means they tied.

### round

```ts
round (data: GameData): number | undefined
```

The current round in the game.

It is shown in the game listings, and used for statistic purposes.

### cancelled

```ts
cancelled (data: GameData): boolean
```

Returns true if the game is cancelled. For example if a player drops out too early in the game, during faction selection.

### factions

```ts
factions (data: GameData): Array<string | undefined>
```

Return the faction of each player, if applicable.

Used for thumbnails in game lists and sidebar - images can be defined for each faction.

Also used for statistics.

### stripSecret

```ts
stripSecret(data: GameData, player?: number): unknown
```

Middleware to process data to be sent to a player's viewer, strip secrets if needed.

In case of a spectator, `player` is undefined.

### toSave

```ts
toSave(data: GameData): unknown
```

Middleware to process data to be stored in the database.

Return `undefined` to NOT store the data. It can happen for example
when a player executes a move without confirming it, a dry run so to speak.

By default, saving a state credits the mover with `timePerMove` and publishes a turn summary.
Use [`isLiveUpdate`](#isliveupdate) to share intermediate changes and
[`timeIncrements`](#timeincrements) to control when players earn time.
Return `undefined` for private tentative input that should not be saved.

### isLiveUpdate

```ts
isLiveUpdate(data: GameData): boolean
```

Called on the saved state returned by `toSave`. Return `true` to share a change without publishing a turn summary:

- Update active players from `currentPlayer(data)`.
- Pause clocks for players who finish; resume them when they become active again.
- Add no turn summary or notification. Time increments follow `timeIncrements` when provided.

Return `false` (or omit this hook) for normal turn handling. Game endings always
use normal handling, regardless of this flag.

### timeIncrements

```ts
timeIncrements(data: GameData): number[]
```

Return each player's cumulative number of earned increments, for example `[3, 2, 3]`.
Each increase credits `timePerMove` immediately, on live and normal saves alike.
For cooperative choices, increase it on the first confirmation. Undoing and reconfirming
keeps the count unchanged. Confirming a new choice forced by a teammate increases it again.
Replay and admin edits never grant time. Without this hook, normal saves credit only the mover.

#### Undo and tentative moves

For private, undoable choices, keep the turn in the viewer and return `undefined`
from `toSave` until it is confirmed. Undo edits that local turn. For shared choices,
use live saves instead. Never remove published log entries; other viewers read
logs incrementally.

### canMoveOutOfTurn

```ts
canMoveOutOfTurn(data: GameData, move: Move, player: number): boolean
```

Return `true` to allow this move from an inactive player, for example **Change my choice**
after clicking **Ready**. The engine then validates the move normally.
Absent or `false`, only active players may move.

For repeatable ready/reopen actions, keep completion counts unchanged and coalesce the toggles in replay history.

### messages

```ts
messages(data: GameData): {messages: string[], data: GameData}
```

Important messages / game events to show in the game's chat.

`data` should be modified so that a subsequent call does not show the same messages.

### replay

```ts
replay(data: GameData, options?: { to?: number }): GameData | Promise<GameData>
```

Replays a game.

It can be called after the database is manually edited, or the game engine is updated.

`to` is optional and means replaying to that move, e.g. `replay(data, {to: 1})` will only redo the first move, and
the rest of the moves will not be played.

### canLaunchAnalysisMode

```ts
canLaunchAnalysisMode(data: GameData): boolean | Promise<boolean>
```

Controls whether players can start an analysis while the game is still running.

- With `createAnalysisScenario`, ongoing analysis is enabled by default. Return `false`
  here to disable it during phases your scenario builder cannot handle.
- Without `createAnalysisScenario`, return `true` only if the full game state is safe
  to reveal, including cards, bids and future draws. Gaia Project uses this approach,
  but disables analysis during its secret setup auctions.
- Without either hook, analysis is available only after the game ends.

Do not modify `data`. BGS checks this hook when opening an ongoing game's current or
historical position. It does not use it to restrict play inside an existing analysis.
Finished games do not need this permission. No admin setting is required.

### createAnalysisScenario

```ts
createAnalysisScenario(data: GameData, options: { player?: number; seed: string }): GameData | Promise<GameData>
```

Creates a playable analysis of an ongoing game without revealing its secrets.
For example, keep the player's own hand, but generate plausible opponent hands and
future draws instead of copying the real ones.

BGS supplies:

- `data`: the current game state. Leave it unchanged.
- `player`: the requesting player's seat, or `undefined` for a spectator.
- `seed`: fresh randomness for the simulation. Never reuse the live game's seed.

The server chooses `player` and `seed`; the client cannot override them.

To build the scenario:

1. Keep everything public and everything the requester already knows. Spectators
   should keep only public information.
2. Generate the unknown information from the game's card lists and rules. Respect
   known facts, such as revealed cards or bids. Do not just shuffle the real hidden
   deck: even which cards it contains may be secret.
3. Remove the original seed, private logs, old undo states, messages, premoves and
   automatic player actions. Allow every seat to be played manually.

Throw an error if you cannot build a valid scenario. Use `canLaunchAnalysisMode` to
hide the analysis option in unsupported phases.

BGS saves only your result. Undo and variations start from there; players cannot go
back into the live game's earlier history. **New scenario** calls this hook again
with the saved starting scenario, the same player's seat and a new seed. Keep any
known facts needed for that second call in your result, without keeping real secrets.
Sharing an analysis can reveal the original player's hand, so BGS asks the owner to
explicitly enable sharing. Finished games use `createAnalysis` instead.

Test that the result is playable, leaves the source unchanged and can be used to
create another scenario. Also test the key privacy rule: with the same supplied seed
and the same information visible to the requester, changing the real hidden cards,
bids or seed must not change the result. Run these checks for players and spectators.

### createAnalysis

```ts
createAnalysis(data: GameData, options: { to: number; sourceEnded: boolean }): GameData | Promise<GameData>
```

Optional **technical** override for creating a playable sandbox position. It does not
grant permission: BGS checks `canLaunchAnalysisMode` separately.

- `data` is the complete saved source state, including its history.
- `to` is the public history boundary, using the same indices as `logLength` and the
  viewer’s replay cursor. Adapt it here if the internal replay indexes a different history.
- `sourceEnded` describes the original game, not the historical position being restored.
- Return the complete playable state at that boundary, with legal actions ready for the
  normal viewer. Include the automatic consequences of the last included move; log
  entries describing those consequences belong to the same playable turn. Throw if
  the boundary cannot be restored.

Use this hook when the normal replay also runs live automation, or when the copied
state needs to discard queued moves, auto-play settings or dropped-player flags.
Work on a copy; do not write to the live game or retain references outside the call.

By default, BGS calls `replay(data, { to })` and checks that the returned `logLength`
matches `to`. If there is no replay implementation, only the current source position
can be copied. Engines whose ordinary replay already returns a playable position need
no `createAnalysis` export.

The same hook reconstructs positions in the saved source history, including previews
and copies of an analysis. BGS pins the source snapshot and engine package so later
changes to the live game do not change those positions.

### analysisMove / analysisView

```ts
analysisMove(data: GameData, move: Move, player: number): GameData | Promise<GameData>
analysisView(data: GameData, options: { player: number; start?: number; end?: number }): { state: unknown; log: unknown }
```

These optional adapters customize sandbox behavior, without controlling authorization.
Normally BGS uses `move` and the usual player-specific viewer payload: `stripSecret(data, player)`
(or `data` when absent), plus `logSlice(data, { player })`.
Use `analysisMove` when the regular move function also executes premoves or automatic
player decisions. Use `analysisView` when the viewer needs a different log/state shape
for an all-seat analysis.

`toSave` keeps its usual meaning: `undefined` returns a tentative position to the viewer
without adding a saved turn. If the engine has no `toSave`, BGS saves the move result.
The platform does not run its live-game bot driver, clocks, ratings or chat notifications
for sandbox moves.

The usual viewer receives `preferences.analysis = true`. Hide chat, live-game planning
controls and account-level automation settings in this mode; keep the journal and legal
move controls. The analysis host owns history navigation and the player selector.

### stats

```ts
stats (data: GameData): Record<string, Many<Record<string, number | string>>>
```

Gets stats on a game, to be used to write to CSV. The `Many` type is `type Many<T> = T | T[]`.

For example, here is what it could return for _one_ game:

```ts
{
  basic: {
    point: 120,
    turns: 500
  },
  detailed: [{
    player: 0,
    boosters: 5,
    turns: 120
  }, {
    player: 1,
    boosters: 9,
    turns: 100
  }]
}
```

Here two CSVs would be generated, `basic.csv` and `detailed.csv`.

## Clock, presence and tables (protocol 0.8)

`init` takes `EngineContext` as its sixth argument. `move`, `dropPlayer`, and `stripSecret` receive it after their usual arguments.

- `now`: trusted Unix milliseconds.
- `gameId`: this game's ID.
- `playerIds`: account IDs in starting seat order (in `init`).
- `preferredPlayerColors`: optional public custom colours in starting seat order, normalized to six-digit hex or `null` (in `init`). A game with colour selection can skip players who already have a custom colour, let the other players choose first, then assign unused game colours as fallbacks. Snapshot this input in game state if it affects setup, and use the snapshot during replay; never reconstruct setup from current profile preferences. This contains no private membership data. Viewer preferences separately provide live cosmetic updates.
- `present`: visible, connected participant seats, when the engine uses presence.
- `table`: table ID, round number, and optional versioned `progress` (in `init`).

Optional hooks:

```ts
presenceChanged(data, present: number[], context): GameData
phase(data): { name: "waiting" | "playing" | "between-rounds"; canLeave: boolean }
tableProgress(data): { version: number; data: unknown }
migrateTableProgress(progress): { version: number; data: unknown }
```

Presence requires the `advanceTime` / `nextWakeup` clock hooks. BGS processes presence changes before deadlines at the same timestamp, then advances the clock. Make `presenceChanged` idempotent. A disconnect need not pause an already running game.

`canLeave` explicitly allows ending the current attempt via `dropPlayer` without a quit penalty; it must be false while playing. A phase label alone does not grant permission.

`tableProgress` runs at each save. Return a safe checkpoint, including if this attempt is cancelled: no active mission, seed, hands, or full replay. Keep the JSON under 64 KiB and key characters by account ID, not seat. The next engine can upgrade its schema with `migrateTableProgress`; reject an unsupported version instead of resetting progress. Finished games retain their original state and replay.

See [game clocks and continuing tables](./timing.md#games-with-their-own-clock).
