// SDK contract for the proposed engine. Types only; no production runtime. import type { z } from 'zod'; import type { action, json, lockSchema, presentationPlanSchema, MatchOutcome, GamePackage, publicEvent, ActionOffer, } from './schema.ts'; export type Json = z.infer; export type JsonObject = { [key: string]: Json }; export type SeatId = `p${number}`; export type DeepReadonly = T extends (...args: never[]) => unknown ? T : T extends readonly (infer V)[] ? readonly DeepReadonly[] : T extends object ? { readonly [K in keyof T]: DeepReadonly } : T; export interface RuntimeSchema { parse(input: unknown): T; } export type GameAction = z.infer; export type DomainEvent = { type: string; payload: JsonObject }; export type VisibleEvent = DomainEvent; export type DeliveredEvent = z.infer; export interface Participant { seat: SeatId; teamId: string | null; role: string; status: 'active' | 'withdrawn'; } export interface Flow { phase: string; round: number; turn: number; actors: Partial>; // seat -> stage } export interface PhaseDefinition { participation: 'single' | 'multiple' | 'automatic'; stages: Readonly>; // stage -> action types } export interface Position { matchId: string; config: Config; participants: Participant[]; data: Data; flow: Flow; outcome: MatchOutcome | null; } export type Recipient = { kind: 'participant'; seat: SeatId; role: string } | { kind: 'observer' }; // Only when the platform explicitly authorizes it. export interface CardDefinition { id: string; suit: string; rank: number; attributes: JsonObject; } export interface CardInstance { id: string; definitionId: string; copy: number; componentId: string; } export interface Zone { id: string; owner: SeatId | null; ordered: boolean; visibility: 'public' | 'owner' | 'hidden'; items: string[]; } export interface DeckState { instances: CardInstance[]; zones: Zone[]; } export interface DieFace { id: string; value: Json; weight: number; } export interface WheelSector { id: string; weight: number; } export interface RuleServices { readonly nowMs: number; // Captured once and recorded with this command. readonly definitions: DeepReadonly>; readonly random: { int(tag: string, minInclusive: number, maxExclusive: number): number; shuffle(tag: string, items: readonly T[]): T[]; weighted( tag: string, options: readonly { id: string; weight: number }[], ): string; }; readonly cards: { instantiate( componentId: string, definitions: readonly CardDefinition[], copies: number, ): DeckState; move( state: DeepReadonly, ids: readonly string[], source: string, destination: string, index: number, ): DeckState; }; } export type DeadlineIntent = | { type: 'schedule'; id: string; dueAtMs: number; payload: JsonObject } | { type: 'cancel'; id: string }; export interface Transition { data: Data; flow: Flow; outcome: MatchOutcome | null; events: Event[]; deadlines: DeadlineIntent[]; } export type RuleResult = | { ok: true; transition: Transition } | { ok: false; code: 'ILLEGAL_ACTION' }; export interface ActionHandler { schema: RuntimeSchema; conflictPolicy: 'match' | 'actor'; apply( position: DeepReadonly>, command: DeepReadonly<{ actor: SeatId; action: Action }>, services: RuleServices, ): RuleResult; } export type OfferDraft = | { kind: 'concrete'; action: Action } | { kind: 'parameterized'; actionType: Action['type']; inputSchemaId: string; constraints: JsonObject; }; export interface Projection { view: View; context: JsonObject; // Explicit projection; never the raw Flow or config. outcome: MatchOutcome | null; } export type LifecycleEvent = | { type: 'participant.withdrawn'; seat: SeatId } | { type: 'deadline.reached'; deadlineId: string; payload: JsonObject }; export interface GameDefinition< Config, Data, View, Action extends GameAction, Event extends DomainEvent, PublicEvent extends VisibleEvent, > { id: string; rulesVersion: string; schemas: { config: RuntimeSchema; data: RuntimeSchema; view: RuntimeSchema; context: RuntimeSchema; event: RuntimeSchema; visibleEvent: RuntimeSchema; }; phases: Readonly>; setup( input: DeepReadonly<{ matchId: string; config: Config; participants: Participant[]; }>, services: RuleServices, ): Transition; validatePosition(position: DeepReadonly>): void; actions: { [K in Action['type']]: ActionHandler< Config, Data, Extract, Event >; }; availableActions( position: DeepReadonly>, recipient: DeepReadonly, ): OfferDraft[]; project( position: DeepReadonly>, recipient: DeepReadonly, ): Projection; projectEvent( input: DeepReadonly<{ before: Position | null; after: Position; event: Event; recipient: Recipient; }>, ): PublicEvent[]; onLifecycle( position: DeepReadonly>, event: DeepReadonly, services: RuleServices, ): Transition; } export interface PersistedEnvelope { schemaVersion: 1; game: { id: string; rulesVersion: string; stateSchemaVersion: number; packageDigest: string; dependencyLockDigest: string; }; revision: number; phaseEpoch: number; actorRevisions: Partial>; position: Position; } export type PackageLock = z.infer; export type PresentationPlan = z.infer; export type PresentationStep = PresentationPlan['steps'][number]; export type StepTrigger = PresentationStep['trigger']; export interface Preferences { reducedMotion: boolean; sound: boolean; volume: number; announcements: boolean; } // The module at a `graphic` resource path exports this definition as default. // Renderers own insertion and removal; a graphic creates no timers or commands. export interface GraphicDefinition { render( input: DeepReadonly<{ resourceId: string; slotId: string | null; definition: Json | null; locale: string; }>, ): SVGElement; } export type FinishReason = 'completed' | 'skipped' | 'cancelled' | 'failed' | 'timeout'; export interface EffectContext { signal: AbortSignal; preferences: DeepReadonly; resource(id: string): Promise; frame(key: string, value: Json): void; marker(name: string): void; audio(input: { resource: string; channel: string; policy: 'mix' | 'replace' | 'skip-if-busy'; maxLateMs: number; }): void; } export interface EffectHandle { play(): Promise; dispose(reason: FinishReason): void; } export interface EffectDefinition { params: RuntimeSchema; prepare( input: DeepReadonly, context: EffectContext, ): Promise; } export interface RenderFrame { presented: Projection; availableActions: ActionOffer[]; presentation: JsonObject; busy: boolean; connected: boolean; } export interface Renderer { update(frame: DeepReadonly>): void; dispose(): void; } export interface PresentationDefinition< View, Action extends GameAction, PublicEvent extends VisibleEvent, > { apiVersion: 1; mount( host: HTMLElement, frame: DeepReadonly>, intent: (action: Action) => void, ): Renderer; buildTransitionPlan( input: DeepReadonly<{ from: Projection; to: Projection; fromRevision: number; toRevision: number; events: (PublicEvent & { eventId: string })[]; manifest: GamePackage; preferences: Preferences; }>, ): PresentationPlan; effects: Readonly>; }