Skip to main content

@eigeninteractive/rules

@eigeninteractive/rules — the contract a game implements. A GameModule bundles one GameRules unit per schema version; the engine calls its hooks and never inspects game state directly. This package is types plus a couple of helpers: it has no runtime dependencies and pulls in no engine code.

Classes

IllegalMoveError

Defined in: eigen-server/packages/rules/src/helpers.ts:14

Thrown by a game's applyAction to reject a move that breaks the rules — the expected failure of the hook (a mis-tap, a client bug), rendered to the caller as their error. Anything else a hook throws is treated as a game bug and surfaces as a server error. Domain-level on purpose: the game states "this move is illegal", the engine owns the transport mapping.

Extends

  • Error

Constructors

Constructor
new IllegalMoveError(message?): IllegalMoveError;

Defined in: eigen-web/node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1080

Parameters
ParameterType
message?string
Returns

IllegalMoveError

Inherited from
Error.constructor
Constructor
new IllegalMoveError(message?, options?): IllegalMoveError;

Defined in: eigen-web/node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts:1080

Parameters
ParameterType
message?string
options?ErrorOptions
Returns

IllegalMoveError

Inherited from
Error.constructor

Interfaces

ApplyActionArgs

Defined in: eigen-server/packages/rules/src/contract.ts:122

Extends

  • HookContext<TConfig>

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject
TAction extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:113

Inherited from
HookContext.config
data
data: TAction;

Defined in: eigen-server/packages/rules/src/contract.ts:125

pending
pending: number[];

Defined in: eigen-server/packages/rules/src/contract.ts:124

playerIndex
playerIndex: number;

Defined in: eigen-server/packages/rules/src/contract.ts:126

rng
rng: Rng;

Defined in: eigen-server/packages/rules/src/contract.ts:128

Deterministic per-transition RNG — see Rng.

state
state: TState;

Defined in: eigen-server/packages/rules/src/contract.ts:123


ApplyLifecycleArgs

Defined in: eigen-server/packages/rules/src/contract.ts:140

Extends

  • HookContext<TConfig>

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:113

Inherited from
HookContext.config
data
data: LifecycleAction;

Defined in: eigen-server/packages/rules/src/contract.ts:149

pending
pending: number[];

Defined in: eigen-server/packages/rules/src/contract.ts:146

Seats awaiting an action. For timeout these are exactly the seats that ran out of time — resolve the whole set in one envelope (you may declare a draw). For forfeit/autoForfeit, the target seat is in data.playerIndex.

rng
rng: Rng;

Defined in: eigen-server/packages/rules/src/contract.ts:151

Deterministic per-transition RNG — see Rng.

state
state: TState;

Defined in: eigen-server/packages/rules/src/contract.ts:141

type
type: LifecycleType;

Defined in: eigen-server/packages/rules/src/contract.ts:148

The trigger — always equal to data.type.


BotActionArgs

Defined in: eigen-server/packages/rules/src/contract.ts:221

A seated engine bot's turn to move, passed to the matching entry in GameRules.botActions. The brain runs inside the game's Durable Object post-commit and sees exactly what a human at this seat would (observation — the same fog-of-war projection, so a bot cannot read hidden state its seat may not); botConfig is that bot registry row's declared knob (difficulty, personality). The engine self-applies the returned move as this seat's action, validated against schemas.action exactly like a human move. rng is deterministic per (game, version, seat) for reproducible tests — but the chosen move is what gets logged, so the brain need not be pure (replay uses the recorded action, never re-runs the brain).

Extends

  • HookContext<TConfig>

Type Parameters

Type ParameterDefault type
TObservation extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

botConfig
botConfig: JsonObject;

Defined in: eigen-server/packages/rules/src/contract.ts:223

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:113

Inherited from
HookContext.config
observation
observation: ObservationSlice<TObservation>;

Defined in: eigen-server/packages/rules/src/contract.ts:222

playerIndex
playerIndex: number;

Defined in: eigen-server/packages/rules/src/contract.ts:224

rng
rng: Rng;

Defined in: eigen-server/packages/rules/src/contract.ts:225


BotSeatableArgs

Defined in: eigen-server/packages/rules/src/contract.ts:206

A candidate bot seating, passed to GameRules.botSeatable. gameConfig is parsed against the game's version schema; botConfig is the bot's declared capabilities — game-owned but unversioned by the game schemas, so it stays opaque.

Type Parameters

Type ParameterDefault type
TConfig extends JsonObjectJsonObject

Properties

botConfig
botConfig: JsonObject;

Defined in: eigen-server/packages/rules/src/contract.ts:208

gameConfig
gameConfig: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:207


ComputeObservationArgs

Defined in: eigen-server/packages/rules/src/contract.ts:170

Extends

  • HookContext<TConfig>

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject
TAction extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

cause
cause: TransitionCause<TAction>;

Defined in: eigen-server/packages/rules/src/contract.ts:182

What produced state — see TransitionCause. Shared across the per-seat fan-out; per-seat filtering of what it reveals is this hook's job.

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:113

Inherited from
HookContext.config
isReplay
isReplay: boolean;

Defined in: eigen-server/packages/rules/src/contract.ts:185

TRUE only when projecting a finished game for replay — hidden-info games may reveal opponent state.

participantCount
participantCount: number;

Defined in: eigen-server/packages/rules/src/contract.ts:178

pending
pending: number[];

Defined in: eigen-server/packages/rules/src/contract.ts:172

playerIndex
playerIndex: number | null;

Defined in: eigen-server/packages/rules/src/contract.ts:177

The seat this projection is for, or null for a viewer (a non-participant replaying a public game). A viewer projection only ever occurs with isReplay true (a public finished game), so a game may safely reveal the full post-game view for it.

state
state: TState;

Defined in: eigen-server/packages/rules/src/contract.ts:171


Envelope

Defined in: eigen-server/packages/rules/src/contract.ts:81

The result of advancing the game by one transition — the return of initialState, applyAction, and applyLifecycle.

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject

Properties

outcome?
optional outcome?: OutcomeEntry[];

Defined in: eigen-server/packages/rules/src/contract.ts:89

Present only when the game ends. Absent/undefined means ongoing.

pendingPlayers
pendingPlayers: number[];

Defined in: eigen-server/packages/rules/src/contract.ts:87

0-based seats that may act next. Empty ⇒ game over.

state
state: TState;

Defined in: eigen-server/packages/rules/src/contract.ts:85

New pure game payload (board, deck, fog…). Never carries whose-turn or winner info — those are engine-owned fields. Must match the game's schemaVersion schema — the engine validates it before committing.

turnSeconds?
optional turnSeconds?: number;

Defined in: eigen-server/packages/rules/src/contract.ts:92

Optional per-action deadline override for this action only (does not touch any player's bank). Omit to use the game's configured timing.


GameModule

Defined in: eigen-server/packages/rules/src/contract.ts:348

The complete game-specific surface — the same-named twin of the Dart GameModule (whose extras are client-only creation/about UI). Implement this once per app and pass it to createEngine; the engine owns all version dispatch — every request resolves the game's schemaVersion entry from versions and invokes that unit's hooks. Game code never branches on version.

Properties

versions
versions: Record<number, AnyGameRules>;

Defined in: eigen-server/packages/rules/src/contract.ts:356

The GameRules units keyed by schemaVersion — exactly the versions this build ships. Sparse on purpose: game creation rejects a version not present here, loading a stored game requires its version's entry, and a drained old version is retired by deleting its entry. The value type is AnyGameRules — each entry is authored against its concrete payload types and erased here; safe because the engine parses each payload with the same entry's schemas before invoking its hooks.


GameRules

Defined in: eigen-server/packages/rules/src/contract.ts:264

Everything one schemaVersion of a game needs: the payload contracts plus all six hooks, narrowly typed to that version's shapes.

The type parameters are the version's payload types, inferred from the schemas in schemas (z.infer<typeof stateSchema> etc. — use type aliases, not interfaces). The engine parses every payload with this unit's schemas before invoking its hooks, so hook bodies never see unvalidated JSON — and never another version's shape. When rules or shapes change incompatibly, ship a new GameRules under the next version key (reusing unchanged pieces by import) instead of branching inside hooks.

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject
TObservation extends JsonObjectJsonObject
TAction extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

botActions?
optional botActions?: Record<string, BotAction<TAction, TObservation, TConfig>>;

Defined in: eigen-server/packages/rules/src/contract.ts:319

Optional — the in-DO bot brains, keyed by bot username. When a seated engine-type bot's turn starts, the engine resolves its registry row's username, looks the move function up here, runs it post-commit, and self-applies the returned move — so a bot game needs no external service. Several bots that share behaviour point their usernames at the same function and differ by their per-row botConfig; distinct behaviour is a distinct entry. A seated engine bot whose username is absent here (or an external bot with no webhook_url) is rejected at seating. The returned move is validated against schemas.action and an illegal one is rejected exactly like a human's, so a buggy brain fails that seat's turn (the deadline backstops it) rather than corrupting the game.

schemas
schemas: GameSchemas<TState, TObservation, TAction, TConfig>;

Defined in: eigen-server/packages/rules/src/contract.ts:266

The payload contracts for this version.

Methods

applyAction()
applyAction(args): Envelope<TState>;

Defined in: eigen-server/packages/rules/src/contract.ts:277

Apply a player's move. The engine has already confirmed it is this seat's turn at the expected version, so do not re-check turn order — only validate move legality and throw IllegalMoveError if it fails; the engine renders it as the caller's error. Any other throw is a game bug and surfaces as a server error.

Parameters
ParameterType
argsApplyActionArgs<TState, TAction, TConfig>
Returns

Envelope<TState>

applyLifecycle()
applyLifecycle(args): Envelope<TState>;

Defined in: eigen-server/packages/rules/src/contract.ts:284

Resolve a lifecycle action (forfeit/timeout) into an envelope. Lifecycle actions operate on the game from outside its rules — they may be player-triggered (a resign) or engine-triggered (timeout, purge); either way the consequence is the game's to decide. Unlike applyAction it cannot be "illegal" — it always resolves.

Parameters
ParameterType
argsApplyLifecycleArgs<TState, TConfig>
Returns

Envelope<TState>

botSeatable()
botSeatable(args): boolean;

Defined in: eigen-server/packages/rules/src/contract.ts:306

Decide whether a bot's declared capabilities (botConfig) support a game with gameConfig. The engine gates seating on this before committing; the Dart GameRules twin filters the bot pickers locally. Return true to allow.

Parameters
ParameterType
argsBotSeatableArgs<TConfig>
Returns

boolean

computeObservation()
computeObservation(args): ObservationSlice<TObservation>;

Defined in: eigen-server/packages/rules/src/contract.ts:292

Project the state into one seat's view — including what that seat may see of the transition that produced it (args.cause), so the client can animate. Perfect-info games can use the passthroughObservation helper (which ignores the cause). What this hook reveals also implicitly sets the simultaneous-move policy: a stale submission survives exactly while the acting seat's projected view is unchanged (the same-view rule).

Parameters
ParameterType
argsComputeObservationArgs<TState, TAction, TConfig>
Returns

ObservationSlice<TObservation>

initialState()
initialState(args): Envelope<TState>;

Defined in: eigen-server/packages/rules/src/contract.ts:270

Starting envelope. Draw any setup randomness (deck shuffle, first player…) from args.rng.

Parameters
ParameterType
argsInitialStateArgs<TConfig>
Returns

Envelope<TState>

ratingPool()
ratingPool(args): string | null;

Defined in: eigen-server/packages/rules/src/contract.ts:300

Decide whether — and in which pool — a game with these settings is rated. Return the pool name (e.g. 'rapid') or null for unrated. The engine computes canBeRated = pool != null && !guest and validates the client's concrete rated assertion against it (rejecting a mismatch). The Dart GameRules keeps a twin of this so the create dialog can gate the Rated/Casual toggle and send the same value.

Parameters
ParameterType
argsRatingPoolArgs<TConfig>
Returns

string | null


GameSchemas

Defined in: eigen-server/packages/rules/src/contract.ts:241

The declarative payload contracts for one schemaVersion: the Standard Schemas the engine uses to parse (and validate) every game payload crossing the JSON boundary. Keep them transform-free — what parses is what persists, and the engine re-validates hook-returned state against state. Schemas must validate synchronously (every mainstream library does unless you opt into async refinements) — the engine rejects an async schema as a game bug.

Type Parameters

Type ParameterDefault type
TState extends JsonObjectJsonObject
TObservation extends JsonObjectJsonObject
TAction extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Properties

action
action: GamePayloadSchema<TAction>;

Defined in: eigen-server/packages/rules/src/contract.ts:247

A player move's data, as submitted by clients and bots.

config
config: GamePayloadSchema<TConfig>;

Defined in: eigen-server/packages/rules/src/contract.ts:249

The per-instance creation config stored on the game.

observation
observation: GamePayloadSchema<TObservation>;

Defined in: eigen-server/packages/rules/src/contract.ts:245

One participant's projected view, as returned by computeObservation.

state
state: GamePayloadSchema<TState>;

Defined in: eigen-server/packages/rules/src/contract.ts:243

The pure game payload stored per transition.


InitialStateArgs

Defined in: eigen-server/packages/rules/src/contract.ts:116

Extends

  • HookContext<TConfig>

Type Parameters

Type ParameterDefault type
TConfig extends JsonObjectJsonObject

Properties

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:113

Inherited from
HookContext.config
playerCount
playerCount: number;

Defined in: eigen-server/packages/rules/src/contract.ts:119

rng
rng: Rng;

Defined in: eigen-server/packages/rules/src/contract.ts:118

Deterministic RNG for this transition — see Rng.


JsonObject

Defined in: eigen-server/packages/rules/src/json.ts:23

A JSON object — the shape of state, config, data, and observation slices, and the constraint every game payload type must satisfy. An interface for the same lazy-resolution reason as JsonArray. Declare game payload types as type aliases (e.g. via your schema library's inference), not interfaces — a payload interface lacks the implicit index signature this constraint relies on.

Indexable

[key: string]: Json | undefined

ObservationSlice

Defined in: eigen-server/packages/rules/src/contract.ts:96

One participant's view of the state, produced by computeObservation.

Type Parameters

Type ParameterDefault type
TObservation extends JsonObjectJsonObject

Properties

data
data: TObservation;

Defined in: eigen-server/packages/rules/src/contract.ts:98

What this seat is permitted to see.

pendingPlayers
pendingPlayers: number[];

Defined in: eigen-server/packages/rules/src/contract.ts:103

Pending set as this seat sees it — may be narrowed from the true set for hidden-info games (e.g. a Nope window, or a simultaneous-commit round where revealing that the opponent moved would leak information). It must stay truthful about the seat itself — the engine enforces that.


RatingPoolArgs

Defined in: eigen-server/packages/rules/src/contract.ts:192

The chosen game settings, passed to GameRules.ratingPool at creation so the game can decide its rating pool (or that the game is unrated). config is already parsed against the requested version's config schema.

Type Parameters

Type ParameterDefault type
TConfig extends JsonObjectJsonObject

Properties

access
access: GameAccess;

Defined in: eigen-server/packages/rules/src/contract.ts:193

budgetSeconds
budgetSeconds: number | null;

Defined in: eigen-server/packages/rules/src/contract.ts:195

config
config: TConfig;

Defined in: eigen-server/packages/rules/src/contract.ts:199

incrementSeconds
incrementSeconds: number | null;

Defined in: eigen-server/packages/rules/src/contract.ts:196

maxPlayers
maxPlayers: number;

Defined in: eigen-server/packages/rules/src/contract.ts:198

minPlayers
minPlayers: number;

Defined in: eigen-server/packages/rules/src/contract.ts:197

turnSeconds
turnSeconds: number | null;

Defined in: eigen-server/packages/rules/src/contract.ts:194


Rng

Defined in: eigen-server/packages/rules/src/contract.ts:53

Deterministic per-transition random source, derived by the engine from the game's stored base seed and the state version the envelope commits as. Draw freely (next() → float in [0, 1), stateful within the invocation); replaying the transition re-derives the identical sequence, so the game stays a pure function of (base seed, action log) — provided the hook draws in deterministic code order.

Methods

next()
next(): number;

Defined in: eigen-server/packages/rules/src/contract.ts:54

Returns

number

Type Aliases

ActionKind

type ActionKind = "game" | "lifecycle";

Defined in: eigen-server/packages/rules/src/contract.ts:43

Which species a logged action is. Everything that transitions state is an action; the two species differ by contract: a game action is rules-scoped (game-defined payload, validated by applyAction, rejectable as illegal), a lifecycle action is engine-scoped (a LifecycleAction payload, resolved unconditionally by applyLifecycle). Stamped on every logged transition, so replay classifies the log structurally, never by payload shape.


ActionType

type ActionType = "user" | "bot" | "system";

Defined in: eigen-server/packages/rules/src/contract.ts:34

Who performed a logged action.


AnyGameRules

type AnyGameRules = GameRules<any, any, any, any>;

Defined in: eigen-server/packages/rules/src/contract.ts:338

A GameRules unit with its payload types erased — the type of a rules entry once it is stored in a GameModule.versions registry that holds many games'/versions' rules whose concrete TState/TAction/TConfig genuinely differ. That container needs "a GameRules for some payload types", an existential TypeScript cannot spell; any is the one sanctioned escape for it (unknown cannot — the config/action params are contravariant input positions). It is safe here because the engine re-validates every payload against that entry's own schemas before invoking a hook, so the static type was only ever an authoring aid — redundant once the unit is registered. Authors keep full type-checking by writing class X implements GameRules<State, Observation, Action, Config> (or annotating a literal : GameRules<…>); assigning that into a versions map just works, with no as-cast, because any disables the variance check at this seam.


BotAction

type BotAction<TAction, TObservation, TConfig> = (args) => TAction;

Defined in: eigen-server/packages/rules/src/contract.ts:230

One engine bot's move function — the value type in GameRules.botActions.

Type Parameters

Type ParameterDefault type
TAction extends JsonObjectJsonObject
TObservation extends JsonObjectJsonObject
TConfig extends JsonObjectJsonObject

Parameters

ParameterType
argsBotActionArgs<TObservation, TConfig>

Returns

TAction


GameAccess

type GameAccess = "public" | "private" | "friends";

Defined in: eigen-server/packages/rules/src/contract.ts:31

Game visibility.


GamePayloadSchema

type GamePayloadSchema<Payload> = StandardSchemaV1<Payload, Payload> & StandardJSONSchemaV1<Payload, Payload>;

Defined in: eigen-server/packages/rules/src/standard-json-schema.ts:8

One game payload declaration: runtime validation plus portable schema emission from the same transform-free object. Input and output deliberately share one type: what parses is what persists and what Dart generates.

Type Parameters

Type Parameter
Payload

GameResult

type GameResult = "win" | "loss" | "draw" | "eliminated";

Defined in: eigen-server/packages/rules/src/contract.ts:28

Per-player result of a finished game.


Json

type Json = string | number | boolean | null | JsonArray | JsonObject;

Defined in: eigen-server/packages/rules/src/json.ts:10

Any JSON value. undefined is allowed inside objects (treated as an absent key, matching how schema libraries model optional fields); it never survives serialization.


LifecycleAction

type LifecycleAction =
| {
type: "timeout";
}
| {
playerIndex: number;
type: "forfeit" | "autoForfeit";
};

Defined in: eigen-server/packages/rules/src/contract.ts:138

The engine-constructed payload of a lifecycle action, recorded verbatim in the action log (with kind = 'lifecycle'). Engine-owned and version-independent: every game gets these transitions for free, without declaring them in its schemas. forfeit carries the forfeiting seat (a voluntary resign); autoForfeit is the engine-driven variant (account purge); timeout carries no seat — the affected seats are ApplyLifecycleArgs.pending.


LifecycleType

type LifecycleType = "timeout" | "forfeit" | "autoForfeit";

Defined in: eigen-server/packages/rules/src/contract.ts:25

The trigger of a lifecycle action, resolved by the game's applyLifecycle hook. forfeit is a voluntary resign; autoForfeit the engine-driven variant (account-deletion purge); timeout is the clock. The two forfeits share a shape (both target data.playerIndex) and most games resolve them identically — but the hook receives the real trigger, so a game may choose different consequences (e.g. a draw rather than a loss when the seat was purged).


OutcomeEntry

type OutcomeEntry = {
placement: number;
playerIndex: number;
result: GameResult;
score?: number | null;
teamIndex: number;
};

Defined in: eigen-server/packages/rules/src/contract.ts:68

One participant's result, recorded when the game ends. placement (1 = best, ties share a value) feeds OpenSkill directly; teamIndex groups players rated together (use playerIndex for individual games).

A type alias, not an interface, on purpose: outcomes are JSON payloads (persisted, compared by fixture runners), and only a type alias gets the implicit index signature that makes it assignable to Json.

Properties

placement
placement: number;

Defined in: eigen-server/packages/rules/src/contract.ts:71

playerIndex
playerIndex: number;

Defined in: eigen-server/packages/rules/src/contract.ts:69

result
result: GameResult;

Defined in: eigen-server/packages/rules/src/contract.ts:70

score?
optional score?: number | null;

Defined in: eigen-server/packages/rules/src/contract.ts:74

Optional raw game score, for display or score-based variants.

teamIndex
teamIndex: number;

Defined in: eigen-server/packages/rules/src/contract.ts:72


TransitionCause

type TransitionCause<TAction> =
| {
data: TAction;
kind: "game";
playerIndex: number;
}
| {
data: LifecycleAction;
kind: "lifecycle";
}
| null;

Defined in: eigen-server/packages/rules/src/contract.ts:168

The action that produced the state being projected — a game action (applyAction), a lifecycle action (applyLifecycle), or null for the initial frame (initialState), which no action produced.

This is how a game tells each seat what happened — pure frame diffing can't recover causality (identical footprints, hidden-info moves, composite resolutions). Embed whatever animation/narration cues a seat is permitted to see into that seat's slice data (e.g. a lastMove field); visibility stays game-controlled because the embedding happens inside computeObservation. Cues describe a transition: a client should render them as animation only when it has the frame's predecessor, and as static "last move" info otherwise.

Type Parameters

Type ParameterDefault type
TAction extends JsonObjectJsonObject

Functions

passthroughObservation()

function passthroughObservation<TState, TAction, TConfig>(args): ObservationSlice<TState>;

Defined in: eigen-server/packages/rules/src/helpers.ts:24

Default computeObservation for perfect-information games: every seat sees the full state and the true pending set. Ignores args.cause — a perfect-info client can usually infer the transition from consecutive frames; embed explicit cues in the slice instead when it can't. Note that under the same-view rule a passthrough game is automatically strict about simultaneous submissions: any opponent move changes every seat's view.

Type Parameters

Type Parameter
TState extends JsonObject
TAction extends JsonObject
TConfig extends JsonObject

Parameters

ParameterType
argsComputeObservationArgs<TState, TAction, TConfig>

Returns

ObservationSlice<TState>