@puzzmo/sdk 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/sdk.ts","../src/keyboard.ts","../src/editor.ts"],"sourcesContent":["import type {\n MessagesSentFromEmbed,\n MessagesReceived,\n GamePlay,\n AugmentationConfig,\n CheckpointConfig,\n Theme,\n Deed,\n KeyboardConfig,\n} from \"./types\"\n\nexport type SDK = ReturnType<typeof createPuzzmoSDK>\n\nexport interface PuzzmoSDKOptions {\n /** Optional timeout in ms to wait for READY_DATA (default: 5000) */\n timeout?: number\n}\n\ntype SupportedOutgoingMessages = Pick<\n MessagesSentFromEmbed,\n | \"READY\"\n | \"READY_GAME_LOADED\"\n | \"GAME_COMPLETED\"\n | \"SHOW_GAME_COMPLETE_SCREEN\"\n | \"TIMER_TICK\"\n | \"TIMER_SYNC\"\n | \"UPLOAD_NEW_GAME_STATE\"\n | \"HIT_CHECKPOINT\"\n | \"KEYBOARD_UPDATE_CONFIG\"\n>\n\ntype SupportedIncomingMessages = Pick<\n MessagesReceived,\n | \"READY_DATA\"\n | \"START_GAME\"\n | \"PAUSE_GAME\"\n | \"RESUME_GAME\"\n | \"SETTINGS_UPDATE\"\n | \"RETRY_PUZZLE\"\n | \"KEYBOARD_KEY_PRESS\"\n | \"KEYBOARD_CURSOR_CHANGE\"\n | \"KEYBOARD_CURSOR_END\"\n>\n\ntype MessageHandler<T extends keyof SupportedIncomingMessages> = (data: SupportedIncomingMessages[T]) => void\n\nexport type SDKEventMap = {\n start: void\n pause: void\n resume: void\n retry: void\n settingsUpdate: any\n /** A key on the on-screen keyboard was tapped. */\n keyboardKeyPress: { key: string }\n /** The drag cursor moved across the keyboard. Only fires when `supportsDragCursor` is true. */\n keyboardCursorChange: { position: [number, number] }\n /** The drag cursor was released. Only fires when `supportsDragCursor` is true. */\n keyboardCursorEnd: void\n}\n\nexport type SDKEventType = keyof SDKEventMap\n\nexport interface SDKTimer {\n /** Get elapsed time in milliseconds */\n timeMs: () => number\n /** Get elapsed time in seconds */\n timeSecs: () => number\n /** Get added/penalty time in milliseconds */\n addedTimeMs: () => number\n /** Get added/penalty time in seconds */\n addedTimeSecs: () => number\n /** Get elapsed time without penalties in seconds */\n timeWithoutPenaltySecs: () => number\n /** Get formatted display strings [elapsed, added] */\n display: () => [string, string]\n /** Add penalty time in milliseconds */\n addPenalty: (ms: number) => void\n /** Check if timer is paused */\n isPaused: () => boolean\n /** Check if timer has been started */\n isRunning: () => boolean\n}\n\nfunction formatTime(timeMs: number): string {\n const isAnHourOrMore = timeMs >= 60 * 60 * 1000\n return new Date(timeMs)\n .toISOString()\n .slice(isAnHourOrMore ? 11 : 14, -1)\n .split(\".\")[0]\n}\n\nfunction createTimer(\n initialTimeMs = 0,\n initialAddedTimeMs = 0,\n): SDKTimer & {\n _init: () => void\n _pause: () => void\n _resume: () => void\n _reset: (initialTimeMs?: number, initialAddedTimeMs?: number) => void\n _conclude: () => void\n} {\n let baseTime = initialTimeMs\n let addedTime = initialAddedTimeMs\n let pausedTime = 0\n let concludeTime: number | undefined\n\n let startDate: number | undefined = undefined\n let pausedDate: number | undefined = undefined\n\n const getTime = (): number => {\n if (startDate === undefined) return baseTime + addedTime\n if (concludeTime !== undefined) return concludeTime\n const now = pausedDate ?? performance.now()\n const elapsed = now - startDate - pausedTime\n return baseTime + addedTime + elapsed\n }\n\n return {\n _init: () => {\n if (startDate === undefined) {\n startDate = performance.now()\n pausedDate = undefined\n }\n },\n _pause: () => {\n if (pausedDate !== undefined || startDate === undefined) return\n pausedDate = performance.now()\n },\n _resume: () => {\n if (pausedDate === undefined) return\n pausedTime += performance.now() - pausedDate\n pausedDate = undefined\n },\n _reset: (newInitialTimeMs = 0, newInitialAddedTimeMs = 0) => {\n baseTime = newInitialTimeMs\n addedTime = newInitialAddedTimeMs\n pausedTime = 0\n concludeTime = undefined\n startDate = undefined\n pausedDate = undefined\n },\n _conclude: () => {\n if (pausedDate !== undefined) {\n concludeTime = getTime()\n return\n }\n if (startDate === undefined) {\n concludeTime = baseTime + addedTime\n return\n }\n concludeTime = getTime()\n },\n timeMs: () => getTime(),\n timeSecs: () => getTime() / 1000,\n addedTimeMs: () => addedTime,\n addedTimeSecs: () => addedTime / 1000,\n timeWithoutPenaltySecs: () => (getTime() - addedTime) / 1000,\n addPenalty: (ms: number) => {\n addedTime += ms\n },\n isPaused: () => pausedDate !== undefined || startDate === undefined,\n isRunning: () => startDate !== undefined && pausedDate === undefined,\n display: () => {\n const elapsed = getTime() - addedTime\n const elapsedStr = formatTime(Math.max(0, elapsed))\n const addedStr = addedTime === 0 ? \"\" : formatTime(addedTime)\n return [elapsedStr, addedStr]\n },\n }\n}\n\nfunction createHostAPI() {\n const messageHandlers = new Map<string, Set<(data: any) => void>>()\n\n const sendMessage = <T extends keyof SupportedOutgoingMessages>(type: T, json: SupportedOutgoingMessages[T]) => {\n const message = { type, json, _: \"p\", __: \"mp\", private: true }\n\n if (\"parent\" in window && window.parent !== window) window.parent.postMessage(message, \"*\")\n\n window.postMessage(message, \"*\")\n\n if (\"webkit\" in window && (window as any).webkit?.messageHandlers?.app) (window as any).webkit.messageHandlers.app.postMessage(message)\n\n if (\"puzzmoMessageString\" in window) (window as any).puzzmoMessageString(JSON.stringify(message))\n\n if (\"ReactNativeWebView\" in window && (window as any).ReactNativeWebView?.postMessage)\n (window as any).ReactNativeWebView.postMessage(JSON.stringify(message))\n\n if (type !== \"TIMER_TICK\" && type !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] sent:\", type, json)\n }\n\n const onMessage = <T extends keyof SupportedIncomingMessages>(type: T, handler: MessageHandler<T>) => {\n if (!messageHandlers.has(type)) messageHandlers.set(type, new Set())\n messageHandlers.get(type)!.add(handler)\n\n return () => {\n messageHandlers.get(type)?.delete(handler)\n }\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", (event) => {\n if (!event?.data?.type) return\n const msgType = event.data.type as string\n const handlers = messageHandlers.get(msgType)\n if (handlers) {\n const msgData = event.data.data ?? event.data.json ?? {}\n if (msgType !== \"TIMER_TICK\" && msgType !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] received:\", msgType, msgData)\n handlers.forEach((handler) => handler(msgData))\n }\n })\n }\n\n return { sendMessage, onMessage }\n}\n\nconst hostAPI = createHostAPI()\n\n/** Creates a Puzzmo SDK instance for communicating with the Puzzmo host */\nexport const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {\n let readyData: MessagesReceived[\"READY_DATA\"] | null = null\n let readyDataResolve: ((data: MessagesReceived[\"READY_DATA\"]) => void) | null = null\n\n const getGameplay = () => readyData?.startOrFindGameplay?.gamePlayed\n const getGameplayID = () => getGameplay()?.id ?? null\n const getPuzzleString = () => getGameplay()?.puzzle.puzzle ?? null\n const getBoardState = () => getGameplay()?.boardState ?? null\n const getTheme = () => readyData?.theme ?? null\n const getCompleted = () => getGameplay()?.completed ?? false\n\n const eventListeners = new Map<SDKEventType, Set<(data?: any) => void>>()\n\n const internalTimer = createTimer()\n let timerTickInterval: ReturnType<typeof setInterval> | null = null\n let timerSyncInterval: ReturnType<typeof setInterval> | null = null\n\n const startTimerIntervals = () => {\n if (timerTickInterval) return\n\n timerTickInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n const [elapsed, added] = internalTimer.display()\n hostAPI.sendMessage(\"TIMER_TICK\", { display: [elapsed, added] })\n }, 500)\n\n timerSyncInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n hostAPI.sendMessage(\"TIMER_SYNC\", Math.floor(internalTimer.timeWithoutPenaltySecs()))\n }, 10000)\n }\n\n const stopTimerIntervals = () => {\n if (timerTickInterval) {\n clearInterval(timerTickInterval)\n timerTickInterval = null\n }\n if (timerSyncInterval) {\n clearInterval(timerSyncInterval)\n timerSyncInterval = null\n }\n }\n\n const emit = <T extends SDKEventType>(event: T, data?: SDKEventMap[T]) => {\n const listeners = eventListeners.get(event)\n if (listeners) listeners.forEach((listener) => listener(data))\n }\n\n hostAPI.onMessage(\"START_GAME\", () => {\n internalTimer._init()\n startTimerIntervals()\n emit(\"start\")\n })\n\n hostAPI.onMessage(\"PAUSE_GAME\", () => {\n internalTimer._pause()\n emit(\"pause\")\n })\n\n hostAPI.onMessage(\"RESUME_GAME\", () => {\n internalTimer._resume()\n emit(\"resume\")\n })\n\n hostAPI.onMessage(\"SETTINGS_UPDATE\", (data) => emit(\"settingsUpdate\", data))\n\n hostAPI.onMessage(\"KEYBOARD_KEY_PRESS\", (data) => emit(\"keyboardKeyPress\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_CHANGE\", (data) => emit(\"keyboardCursorChange\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_END\", () => emit(\"keyboardCursorEnd\"))\n\n hostAPI.onMessage(\"RETRY_PUZZLE\", () => {\n internalTimer._reset()\n stopTimerIntervals()\n emit(\"retry\")\n })\n\n hostAPI.onMessage(\"READY_DATA\", (data) => {\n const bootstrapData = data as MessagesReceived[\"READY_DATA\"]\n readyData = bootstrapData\n\n const gamePlayed = bootstrapData.startOrFindGameplay?.gamePlayed\n if (gamePlayed) {\n const existingTime = (gamePlayed.elapsedTimeSecs ?? 0) * 1000\n const existingAddedTime = (gamePlayed.additionalTimeAddedSecs ?? 0) * 1000\n internalTimer._reset(existingTime, existingAddedTime)\n }\n\n if (readyDataResolve) {\n readyDataResolve(bootstrapData)\n readyDataResolve = null\n }\n })\n\n const timer: SDKTimer = {\n timeMs: () => internalTimer.timeMs(),\n timeSecs: () => internalTimer.timeSecs(),\n addedTimeMs: () => internalTimer.addedTimeMs(),\n addedTimeSecs: () => internalTimer.addedTimeSecs(),\n timeWithoutPenaltySecs: () => internalTimer.timeWithoutPenaltySecs(),\n display: () => internalTimer.display(),\n addPenalty: (ms: number) => internalTimer.addPenalty(ms),\n isPaused: () => internalTimer.isPaused(),\n isRunning: () => internalTimer.isRunning(),\n }\n\n return {\n timer,\n\n gameReady: async (): Promise<{\n puzzleString: string\n inputString: string | null\n theme: Theme | null\n completed: boolean\n readyData: MessagesReceived[\"READY_DATA\"] | null\n }> => {\n hostAPI.sendMessage(\"READY\", {})\n\n if (getPuzzleString()) {\n const inputString = getBoardState()\n return {\n puzzleString: getPuzzleString()!,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n }\n\n const timeout = options.timeout ?? 5000\n const readyDataPromise = new Promise<MessagesReceived[\"READY_DATA\"]>((resolve, reject) => {\n readyDataResolve = resolve\n setTimeout(() => {\n if (readyDataResolve) {\n readyDataResolve = null\n reject(new Error(`Timeout waiting for READY_DATA after ${timeout}ms`))\n }\n }, timeout)\n })\n\n await readyDataPromise\n\n const puzzleString = getPuzzleString()\n if (!puzzleString) throw new Error(\"READY_DATA received but no puzzle data found\")\n\n const inputString = getBoardState()\n return {\n puzzleString,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n },\n\n gameLoaded: (state: any = {}) => {\n hostAPI.sendMessage(\"READY_GAME_LOADED\", {\n state,\n gameRuntimeContract: \"1.0\",\n embedRuntimeContract: \"1.0\",\n })\n },\n\n on: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void): (() => void) => {\n if (!eventListeners.has(event)) eventListeners.set(event, new Set())\n eventListeners.get(event)!.add(listener)\n return () => {\n eventListeners.get(event)?.delete(listener)\n }\n },\n\n off: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void) => {\n eventListeners.get(event)?.delete(listener)\n },\n\n updateGameState: (inputString: string, play?: Partial<GamePlay>) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n hostAPI.sendMessage(\"UPLOAD_NEW_GAME_STATE\", {\n id: gameplayID,\n input: {\n boardState: inputString,\n elapsedTimeSecs: play?.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play?.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n collabUserReferences: [],\n },\n })\n },\n\n gameCompleted: (play: Partial<GamePlay>, config?: AugmentationConfig) => {\n internalTimer._conclude()\n stopTimerIntervals()\n\n const finalPlay: Partial<GamePlay> = {\n ...play,\n elapsedTimeSecs: play.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n }\n\n const deeds: Deed[] = (config?.deeds as any) ?? []\n deeds.push({ id: \"points\", value: play.pointsAwarded })\n deeds.push({\n id: \"time\",\n value: Math.round(finalPlay.elapsedTimeSecs ?? 0) + Math.round(finalPlay.additionalTimeAddedSecs ?? 0),\n })\n\n const gameplayID = getGameplayID()\n if (gameplayID) {\n hostAPI.sendMessage(\"GAME_COMPLETED\", {\n id: gameplayID,\n input: finalPlay,\n config,\n })\n }\n },\n\n showCompletionScreen: (results: any[], gameplay: GamePlay, showRetry = true) => {\n hostAPI.sendMessage(\"SHOW_GAME_COMPLETE_SCREEN\", {\n results,\n showRetry,\n gameplay,\n })\n },\n\n hitCheckpoint: (checkpointName: string, checkpointConfig: CheckpointConfig, config?: AugmentationConfig) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n const inputStr = getBoardState() ?? \"\"\n const play: Partial<GamePlay> = {\n elapsedTimeSecs: internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: internalTimer.addedTimeSecs(),\n }\n\n hostAPI.sendMessage(\"HIT_CHECKPOINT\", {\n checkpointName,\n gameplay: { inputStr, play },\n checkpointConfig,\n augConfig: config ?? {},\n })\n },\n\n keyboard: {\n /** Show the on-screen keyboard with the given config. Call again to update state (e.g. to change disabled keys). */\n show: (config: KeyboardConfig) => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", config)\n },\n /** Hide the on-screen keyboard. */\n hide: () => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", {\n layout: [],\n symbols: {},\n highlight: [],\n disabled: [],\n xl: [],\n l: [],\n supportsDragCursor: false,\n })\n },\n },\n\n _hostAPI: hostAPI,\n }\n}\n","import type { KeyboardConfig } from \"./types\"\n\n/**\n * A standard QWERTY layout with Enter and Backspace — a reasonable default for\n * any game that needs text input. Customize from here by spreading and overriding.\n *\n * @example\n * // Use as-is\n * sdk.keyboard.show(defaultKeyboardConfig)\n *\n * @example\n * // Extend with dynamic disabled letters\n * sdk.keyboard.show({ ...defaultKeyboardConfig, disabled: usedLetters })\n */\nexport const defaultKeyboardConfig: KeyboardConfig = {\n layout: [\"qwertyuiop\", \"asdfghjkl\", \"↵zxcvbnm⌫\", undefined],\n symbols: { \"↵\": \"Enter\", \"⌫\": \"bsp\" },\n highlight: [\"↵\", \"⌫\"],\n disabled: [],\n xl: [],\n l: [\"↵\", \"⌫\"],\n supportsDragCursor: false,\n}\n","/** Severity level for validation issues */\nexport type ValidationLevel = \"error\" | \"warning\" | \"info\"\n\n/** Represents a single validation issue found during puzzle validation */\nexport interface ValidationIssue {\n level: ValidationLevel\n message: string\n line?: number\n col?: number\n length?: number\n}\n\n/** Complete validation report for a puzzle */\nexport interface ValidationReport {\n success: boolean\n issues: ValidationIssue[]\n}\n\nexport type ImportErrorType = \"invalid_format\" | \"parsing_error\" | \"unknown\"\n\n/** Custom error class for workshop import failures */\nexport class EditorImportError extends Error {\n constructor(\n public type: ImportErrorType,\n message: string,\n public originalError?: unknown,\n ) {\n super(message)\n this.name = \"EditorImportError\"\n }\n}\n\n/** Result of a successful puzzle import operation */\nexport interface ImportResult {\n data: string\n warnings?: ValidationIssue[]\n title?: string\n authors?: string[]\n editors?: string[]\n}\n\n/** Settings UI descriptor returned by an editor bundle */\nexport interface EditorBundleSettings<TComponent = unknown> {\n components: TComponent[]\n defaults: Record<string, unknown>\n}\n\n/** Main interface for a Workshop bundle */\nexport interface EditorBundle<TSettingsComponent = unknown> {\n validator: {\n validate(data: string): Promise<ValidationReport> | ValidationReport\n }\n importer?: {\n onImport(filename: string, contents: string | ArrayBuffer): Promise<ImportResult> | ImportResult\n }\n /** Embed-level settings UI, populated from the bundle's declared settings */\n settings?: EditorBundleSettings<TSettingsComponent>\n /** Editor-level settings UI, populated from the bundle's declared editor settings */\n editorSettings?: EditorBundleSettings<TSettingsComponent>\n /** Custom puzzle editor, if provided by the bundle */\n editor?: {\n mount(...args: unknown[]): unknown\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/sdk.ts","../src/keyboard.ts","../src/editor.ts"],"sourcesContent":["import type {\n MessagesSentFromEmbed,\n MessagesReceived,\n GamePlay,\n AugmentationConfig,\n CheckpointConfig,\n Theme,\n Deed,\n KeyboardConfig,\n} from \"./types\"\n\nexport type SDK = ReturnType<typeof createPuzzmoSDK>\n\nexport interface PuzzmoSDKOptions {\n /** Optional timeout in ms to wait for READY_DATA (default: 5000) */\n timeout?: number\n}\n\ntype SupportedOutgoingMessages = Pick<\n MessagesSentFromEmbed,\n | \"READY\"\n | \"READY_GAME_LOADED\"\n | \"GAME_COMPLETED\"\n | \"SHOW_GAME_COMPLETE_SCREEN\"\n | \"TIMER_TICK\"\n | \"TIMER_SYNC\"\n | \"UPLOAD_NEW_GAME_STATE\"\n | \"HIT_CHECKPOINT\"\n | \"KEYBOARD_UPDATE_CONFIG\"\n>\n\ntype SupportedIncomingMessages = Pick<\n MessagesReceived,\n | \"READY_DATA\"\n | \"START_GAME\"\n | \"PAUSE_GAME\"\n | \"RESUME_GAME\"\n | \"SETTINGS_UPDATE\"\n | \"RETRY_PUZZLE\"\n | \"KEYBOARD_KEY_PRESS\"\n | \"KEYBOARD_CURSOR_CHANGE\"\n | \"KEYBOARD_CURSOR_END\"\n>\n\ntype MessageHandler<T extends keyof SupportedIncomingMessages> = (data: SupportedIncomingMessages[T]) => void\n\nexport type SDKEventMap = {\n start: void\n pause: void\n resume: void\n retry: void\n settingsUpdate: any\n /** A key on the on-screen keyboard was tapped. */\n keyboardKeyPress: { key: string }\n /** The drag cursor moved across the keyboard. Only fires when `supportsDragCursor` is true. */\n keyboardCursorChange: { position: [number, number] }\n /** The drag cursor was released. Only fires when `supportsDragCursor` is true. */\n keyboardCursorEnd: void\n}\n\nexport type SDKEventType = keyof SDKEventMap\n\nexport interface SDKTimer {\n /** Get elapsed time in milliseconds */\n timeMs: () => number\n /** Get elapsed time in seconds */\n timeSecs: () => number\n /** Get added/penalty time in milliseconds */\n addedTimeMs: () => number\n /** Get added/penalty time in seconds */\n addedTimeSecs: () => number\n /** Get elapsed time without penalties in seconds */\n timeWithoutPenaltySecs: () => number\n /** Get formatted display strings [elapsed, added] */\n display: () => [string, string]\n /** Add penalty time in milliseconds */\n addPenalty: (ms: number) => void\n /** Check if timer is paused */\n isPaused: () => boolean\n /** Check if timer has been started */\n isRunning: () => boolean\n}\n\nfunction formatTime(timeMs: number): string {\n const isAnHourOrMore = timeMs >= 60 * 60 * 1000\n return new Date(timeMs)\n .toISOString()\n .slice(isAnHourOrMore ? 11 : 14, -1)\n .split(\".\")[0]\n}\n\nfunction createTimer(\n initialTimeMs = 0,\n initialAddedTimeMs = 0,\n): SDKTimer & {\n _init: () => void\n _pause: () => void\n _resume: () => void\n _reset: (initialTimeMs?: number, initialAddedTimeMs?: number) => void\n _conclude: () => void\n} {\n let baseTime = initialTimeMs\n let addedTime = initialAddedTimeMs\n let pausedTime = 0\n let concludeTime: number | undefined\n\n let startDate: number | undefined = undefined\n let pausedDate: number | undefined = undefined\n\n const getTime = (): number => {\n if (startDate === undefined) return baseTime + addedTime\n if (concludeTime !== undefined) return concludeTime\n const now = pausedDate ?? performance.now()\n const elapsed = now - startDate - pausedTime\n return baseTime + addedTime + elapsed\n }\n\n return {\n _init: () => {\n if (startDate === undefined) {\n startDate = performance.now()\n pausedDate = undefined\n }\n },\n _pause: () => {\n if (pausedDate !== undefined || startDate === undefined) return\n pausedDate = performance.now()\n },\n _resume: () => {\n if (pausedDate === undefined) return\n pausedTime += performance.now() - pausedDate\n pausedDate = undefined\n },\n _reset: (newInitialTimeMs = 0, newInitialAddedTimeMs = 0) => {\n baseTime = newInitialTimeMs\n addedTime = newInitialAddedTimeMs\n pausedTime = 0\n concludeTime = undefined\n startDate = undefined\n pausedDate = undefined\n },\n _conclude: () => {\n if (pausedDate !== undefined) {\n concludeTime = getTime()\n return\n }\n if (startDate === undefined) {\n concludeTime = baseTime + addedTime\n return\n }\n concludeTime = getTime()\n },\n timeMs: () => getTime(),\n timeSecs: () => getTime() / 1000,\n addedTimeMs: () => addedTime,\n addedTimeSecs: () => addedTime / 1000,\n timeWithoutPenaltySecs: () => (getTime() - addedTime) / 1000,\n addPenalty: (ms: number) => {\n addedTime += ms\n },\n isPaused: () => pausedDate !== undefined || startDate === undefined,\n isRunning: () => startDate !== undefined && pausedDate === undefined,\n display: () => {\n const elapsed = getTime() - addedTime\n const elapsedStr = formatTime(Math.max(0, elapsed))\n const addedStr = addedTime === 0 ? \"\" : formatTime(addedTime)\n return [elapsedStr, addedStr]\n },\n }\n}\n\nfunction createHostAPI() {\n const messageHandlers = new Map<string, Set<(data: any) => void>>()\n\n const sendMessage = <T extends keyof SupportedOutgoingMessages>(type: T, json: SupportedOutgoingMessages[T]) => {\n const message = { type, json, _: \"p\", __: \"mp\", private: true }\n\n if (\"parent\" in window && window.parent !== window) window.parent.postMessage(message, \"*\")\n\n window.postMessage(message, \"*\")\n\n if (\"webkit\" in window && (window as any).webkit?.messageHandlers?.app) (window as any).webkit.messageHandlers.app.postMessage(message)\n\n if (\"puzzmoMessageString\" in window) (window as any).puzzmoMessageString(JSON.stringify(message))\n\n if (\"ReactNativeWebView\" in window && (window as any).ReactNativeWebView?.postMessage)\n (window as any).ReactNativeWebView.postMessage(JSON.stringify(message))\n\n if (type !== \"TIMER_TICK\" && type !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] sent:\", type, json)\n }\n\n const onMessage = <T extends keyof SupportedIncomingMessages>(type: T, handler: MessageHandler<T>) => {\n if (!messageHandlers.has(type)) messageHandlers.set(type, new Set())\n messageHandlers.get(type)!.add(handler)\n\n return () => {\n messageHandlers.get(type)?.delete(handler)\n }\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", (event) => {\n if (!event?.data?.type) return\n const msgType = event.data.type as string\n const handlers = messageHandlers.get(msgType)\n if (handlers) {\n const msgData = event.data.data ?? event.data.json ?? {}\n if (msgType !== \"TIMER_TICK\" && msgType !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] received:\", msgType, msgData)\n handlers.forEach((handler) => handler(msgData))\n }\n })\n }\n\n return { sendMessage, onMessage }\n}\n\nconst hostAPI = createHostAPI()\n\n/** Creates a Puzzmo SDK instance for communicating with the Puzzmo host */\nexport const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {\n let readyData: MessagesReceived[\"READY_DATA\"] | null = null\n let readyDataResolve: ((data: MessagesReceived[\"READY_DATA\"]) => void) | null = null\n\n const getGameplay = () => readyData?.startOrFindGameplay?.gamePlayed\n const getGameplayID = () => getGameplay()?.id ?? null\n const getPuzzleString = () => getGameplay()?.puzzle.puzzle ?? null\n const getBoardState = () => getGameplay()?.boardState ?? null\n const getTheme = () => readyData?.theme ?? null\n const getCompleted = () => getGameplay()?.completed ?? false\n\n const eventListeners = new Map<SDKEventType, Set<(data?: any) => void>>()\n\n const internalTimer = createTimer()\n let timerTickInterval: ReturnType<typeof setInterval> | null = null\n let timerSyncInterval: ReturnType<typeof setInterval> | null = null\n\n const startTimerIntervals = () => {\n if (timerTickInterval) return\n\n timerTickInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n const [elapsed, added] = internalTimer.display()\n hostAPI.sendMessage(\"TIMER_TICK\", { display: [elapsed, added] })\n }, 500)\n\n timerSyncInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n hostAPI.sendMessage(\"TIMER_SYNC\", Math.floor(internalTimer.timeWithoutPenaltySecs()))\n }, 10000)\n }\n\n const stopTimerIntervals = () => {\n if (timerTickInterval) {\n clearInterval(timerTickInterval)\n timerTickInterval = null\n }\n if (timerSyncInterval) {\n clearInterval(timerSyncInterval)\n timerSyncInterval = null\n }\n }\n\n const emit = <T extends SDKEventType>(event: T, data?: SDKEventMap[T]) => {\n const listeners = eventListeners.get(event)\n if (listeners) listeners.forEach((listener) => listener(data))\n }\n\n hostAPI.onMessage(\"START_GAME\", () => {\n internalTimer._init()\n startTimerIntervals()\n emit(\"start\")\n })\n\n hostAPI.onMessage(\"PAUSE_GAME\", () => {\n internalTimer._pause()\n emit(\"pause\")\n })\n\n hostAPI.onMessage(\"RESUME_GAME\", () => {\n internalTimer._resume()\n emit(\"resume\")\n })\n\n hostAPI.onMessage(\"SETTINGS_UPDATE\", (data) => emit(\"settingsUpdate\", data))\n\n hostAPI.onMessage(\"KEYBOARD_KEY_PRESS\", (data) => emit(\"keyboardKeyPress\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_CHANGE\", (data) => emit(\"keyboardCursorChange\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_END\", () => emit(\"keyboardCursorEnd\"))\n\n hostAPI.onMessage(\"RETRY_PUZZLE\", () => {\n internalTimer._reset()\n stopTimerIntervals()\n emit(\"retry\")\n })\n\n hostAPI.onMessage(\"READY_DATA\", (data) => {\n const bootstrapData = data as MessagesReceived[\"READY_DATA\"]\n readyData = bootstrapData\n\n const gamePlayed = bootstrapData.startOrFindGameplay?.gamePlayed\n if (gamePlayed) {\n const existingTime = (gamePlayed.elapsedTimeSecs ?? 0) * 1000\n const existingAddedTime = (gamePlayed.additionalTimeAddedSecs ?? 0) * 1000\n internalTimer._reset(existingTime, existingAddedTime)\n }\n\n if (readyDataResolve) {\n readyDataResolve(bootstrapData)\n readyDataResolve = null\n }\n })\n\n const timer: SDKTimer = {\n timeMs: () => internalTimer.timeMs(),\n timeSecs: () => internalTimer.timeSecs(),\n addedTimeMs: () => internalTimer.addedTimeMs(),\n addedTimeSecs: () => internalTimer.addedTimeSecs(),\n timeWithoutPenaltySecs: () => internalTimer.timeWithoutPenaltySecs(),\n display: () => internalTimer.display(),\n addPenalty: (ms: number) => internalTimer.addPenalty(ms),\n isPaused: () => internalTimer.isPaused(),\n isRunning: () => internalTimer.isRunning(),\n }\n\n return {\n timer,\n\n gameReady: async (): Promise<{\n puzzleString: string\n inputString: string | null\n theme: Theme | null\n completed: boolean\n readyData: MessagesReceived[\"READY_DATA\"] | null\n }> => {\n hostAPI.sendMessage(\"READY\", {})\n\n if (getPuzzleString()) {\n const inputString = getBoardState()\n return {\n puzzleString: getPuzzleString()!,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n }\n\n const timeout = options.timeout ?? 5000\n const readyDataPromise = new Promise<MessagesReceived[\"READY_DATA\"]>((resolve, reject) => {\n readyDataResolve = resolve\n setTimeout(() => {\n if (readyDataResolve) {\n readyDataResolve = null\n reject(new Error(`Timeout waiting for READY_DATA after ${timeout}ms`))\n }\n }, timeout)\n })\n\n await readyDataPromise\n\n const puzzleString = getPuzzleString()\n if (!puzzleString) throw new Error(\"READY_DATA received but no puzzle data found\")\n\n const inputString = getBoardState()\n return {\n puzzleString,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n },\n\n gameLoaded: (state: any = {}) => {\n hostAPI.sendMessage(\"READY_GAME_LOADED\", {\n state,\n gameRuntimeContract: \"1.0\",\n embedRuntimeContract: \"1.0\",\n })\n },\n\n on: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void): (() => void) => {\n if (!eventListeners.has(event)) eventListeners.set(event, new Set())\n eventListeners.get(event)!.add(listener)\n return () => {\n eventListeners.get(event)?.delete(listener)\n }\n },\n\n off: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void) => {\n eventListeners.get(event)?.delete(listener)\n },\n\n updateGameState: (inputString: string, play?: Partial<GamePlay>) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n hostAPI.sendMessage(\"UPLOAD_NEW_GAME_STATE\", {\n id: gameplayID,\n input: {\n boardState: inputString,\n elapsedTimeSecs: play?.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play?.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n collabUserReferences: [],\n },\n })\n },\n\n gameCompleted: (play: Partial<GamePlay>, config?: AugmentationConfig) => {\n internalTimer._conclude()\n stopTimerIntervals()\n\n const finalPlay: Partial<GamePlay> = {\n ...play,\n elapsedTimeSecs: play.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n }\n\n const deeds: Deed[] = (config?.deeds as any) ?? []\n deeds.push({ id: \"points\", value: play.pointsAwarded })\n deeds.push({\n id: \"time\",\n value: Math.round(finalPlay.elapsedTimeSecs ?? 0) + Math.round(finalPlay.additionalTimeAddedSecs ?? 0),\n })\n\n const gameplayID = getGameplayID()\n if (gameplayID) {\n hostAPI.sendMessage(\"GAME_COMPLETED\", {\n id: gameplayID,\n input: finalPlay,\n config,\n })\n }\n },\n\n showCompletionScreen: (results: any[], gameplay: GamePlay, showRetry = true) => {\n hostAPI.sendMessage(\"SHOW_GAME_COMPLETE_SCREEN\", {\n results,\n showRetry,\n gameplay,\n })\n },\n\n hitCheckpoint: (checkpointName: string, checkpointConfig: CheckpointConfig, config?: AugmentationConfig) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n const inputStr = getBoardState() ?? \"\"\n const play: Partial<GamePlay> = {\n elapsedTimeSecs: internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: internalTimer.addedTimeSecs(),\n }\n\n hostAPI.sendMessage(\"HIT_CHECKPOINT\", {\n checkpointName,\n gameplay: { inputStr, play },\n checkpointConfig,\n augConfig: config ?? {},\n })\n },\n\n keyboard: {\n /** Show the on-screen keyboard with the given config. Call again to update state (e.g. to change disabled keys). */\n show: (config: KeyboardConfig) => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", config)\n },\n /** Hide the on-screen keyboard. */\n hide: () => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", {\n layout: [],\n symbols: {},\n highlight: [],\n disabled: [],\n xl: [],\n l: [],\n supportsDragCursor: false,\n })\n },\n },\n\n _hostAPI: hostAPI,\n }\n}\n","import type { KeyboardConfig } from \"./types\"\n\n/**\n * A standard QWERTY layout with Enter and Backspace — a reasonable default for\n * any game that needs text input. Customize from here by spreading and overriding.\n *\n * @example\n * // Use as-is\n * sdk.keyboard.show(defaultKeyboardConfig)\n *\n * @example\n * // Extend with dynamic disabled letters\n * sdk.keyboard.show({ ...defaultKeyboardConfig, disabled: usedLetters })\n */\nexport const defaultKeyboardConfig: KeyboardConfig = {\n layout: [\"qwertyuiop\", \"asdfghjkl\", \"↵zxcvbnm⌫\", undefined],\n symbols: { \"↵\": \"Enter\", \"⌫\": \"bsp\" },\n highlight: [\"↵\", \"⌫\"],\n disabled: [],\n xl: [],\n l: [\"↵\", \"⌫\"],\n supportsDragCursor: false,\n}\n","/** Severity level for validation issues */\nexport type ValidationLevel = \"error\" | \"warning\" | \"info\"\n\n/** Represents a single validation issue found during puzzle validation */\nexport interface ValidationIssue {\n level: ValidationLevel\n message: string\n line?: number\n col?: number\n length?: number\n}\n\n/** Complete validation report for a puzzle */\nexport interface ValidationReport {\n success: boolean\n issues: ValidationIssue[]\n}\n\nexport type ImportErrorType = \"invalid_format\" | \"parsing_error\" | \"unknown\"\n\n/** Custom error class for workshop import failures */\nexport class EditorImportError extends Error {\n constructor(\n public type: ImportErrorType,\n message: string,\n public originalError?: unknown,\n ) {\n super(message)\n this.name = \"EditorImportError\"\n }\n}\n\n/** Result of a successful puzzle import operation */\nexport interface ImportResult {\n data: string\n warnings?: ValidationIssue[]\n title?: string\n authors?: string[]\n editors?: string[]\n}\n\n/** Settings UI descriptor returned by an editor bundle */\nexport interface EditorBundleSettings<TComponent = unknown> {\n components: TComponent[]\n defaults: Record<string, unknown>\n}\n\n/** Main interface for a Workshop bundle */\nexport interface EditorBundle<TSettingsComponent = unknown> {\n validator: {\n validate(data: string): Promise<ValidationReport> | ValidationReport\n }\n importer?: {\n onImport(filename: string, contents: string | ArrayBuffer): Promise<ImportResult> | ImportResult\n }\n /** Embed-level settings UI, populated from the bundle's declared settings */\n settings?: EditorBundleSettings<TSettingsComponent>\n /** Editor-level settings UI, populated from the bundle's declared editor settings */\n editorSettings?: EditorBundleSettings<TSettingsComponent>\n /** Custom puzzle editor, if provided by the bundle */\n editor?: {\n mount(...args: unknown[]): unknown\n }\n}\n"],"mappings":"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"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/sdk.ts","../src/keyboard.ts","../src/editor.ts"],"sourcesContent":["import type {\n MessagesSentFromEmbed,\n MessagesReceived,\n GamePlay,\n AugmentationConfig,\n CheckpointConfig,\n Theme,\n Deed,\n KeyboardConfig,\n} from \"./types\"\n\nexport type SDK = ReturnType<typeof createPuzzmoSDK>\n\nexport interface PuzzmoSDKOptions {\n /** Optional timeout in ms to wait for READY_DATA (default: 5000) */\n timeout?: number\n}\n\ntype SupportedOutgoingMessages = Pick<\n MessagesSentFromEmbed,\n | \"READY\"\n | \"READY_GAME_LOADED\"\n | \"GAME_COMPLETED\"\n | \"SHOW_GAME_COMPLETE_SCREEN\"\n | \"TIMER_TICK\"\n | \"TIMER_SYNC\"\n | \"UPLOAD_NEW_GAME_STATE\"\n | \"HIT_CHECKPOINT\"\n | \"KEYBOARD_UPDATE_CONFIG\"\n>\n\ntype SupportedIncomingMessages = Pick<\n MessagesReceived,\n | \"READY_DATA\"\n | \"START_GAME\"\n | \"PAUSE_GAME\"\n | \"RESUME_GAME\"\n | \"SETTINGS_UPDATE\"\n | \"RETRY_PUZZLE\"\n | \"KEYBOARD_KEY_PRESS\"\n | \"KEYBOARD_CURSOR_CHANGE\"\n | \"KEYBOARD_CURSOR_END\"\n>\n\ntype MessageHandler<T extends keyof SupportedIncomingMessages> = (data: SupportedIncomingMessages[T]) => void\n\nexport type SDKEventMap = {\n start: void\n pause: void\n resume: void\n retry: void\n settingsUpdate: any\n /** A key on the on-screen keyboard was tapped. */\n keyboardKeyPress: { key: string }\n /** The drag cursor moved across the keyboard. Only fires when `supportsDragCursor` is true. */\n keyboardCursorChange: { position: [number, number] }\n /** The drag cursor was released. Only fires when `supportsDragCursor` is true. */\n keyboardCursorEnd: void\n}\n\nexport type SDKEventType = keyof SDKEventMap\n\nexport interface SDKTimer {\n /** Get elapsed time in milliseconds */\n timeMs: () => number\n /** Get elapsed time in seconds */\n timeSecs: () => number\n /** Get added/penalty time in milliseconds */\n addedTimeMs: () => number\n /** Get added/penalty time in seconds */\n addedTimeSecs: () => number\n /** Get elapsed time without penalties in seconds */\n timeWithoutPenaltySecs: () => number\n /** Get formatted display strings [elapsed, added] */\n display: () => [string, string]\n /** Add penalty time in milliseconds */\n addPenalty: (ms: number) => void\n /** Check if timer is paused */\n isPaused: () => boolean\n /** Check if timer has been started */\n isRunning: () => boolean\n}\n\nfunction formatTime(timeMs: number): string {\n const isAnHourOrMore = timeMs >= 60 * 60 * 1000\n return new Date(timeMs)\n .toISOString()\n .slice(isAnHourOrMore ? 11 : 14, -1)\n .split(\".\")[0]\n}\n\nfunction createTimer(\n initialTimeMs = 0,\n initialAddedTimeMs = 0,\n): SDKTimer & {\n _init: () => void\n _pause: () => void\n _resume: () => void\n _reset: (initialTimeMs?: number, initialAddedTimeMs?: number) => void\n _conclude: () => void\n} {\n let baseTime = initialTimeMs\n let addedTime = initialAddedTimeMs\n let pausedTime = 0\n let concludeTime: number | undefined\n\n let startDate: number | undefined = undefined\n let pausedDate: number | undefined = undefined\n\n const getTime = (): number => {\n if (startDate === undefined) return baseTime + addedTime\n if (concludeTime !== undefined) return concludeTime\n const now = pausedDate ?? performance.now()\n const elapsed = now - startDate - pausedTime\n return baseTime + addedTime + elapsed\n }\n\n return {\n _init: () => {\n if (startDate === undefined) {\n startDate = performance.now()\n pausedDate = undefined\n }\n },\n _pause: () => {\n if (pausedDate !== undefined || startDate === undefined) return\n pausedDate = performance.now()\n },\n _resume: () => {\n if (pausedDate === undefined) return\n pausedTime += performance.now() - pausedDate\n pausedDate = undefined\n },\n _reset: (newInitialTimeMs = 0, newInitialAddedTimeMs = 0) => {\n baseTime = newInitialTimeMs\n addedTime = newInitialAddedTimeMs\n pausedTime = 0\n concludeTime = undefined\n startDate = undefined\n pausedDate = undefined\n },\n _conclude: () => {\n if (pausedDate !== undefined) {\n concludeTime = getTime()\n return\n }\n if (startDate === undefined) {\n concludeTime = baseTime + addedTime\n return\n }\n concludeTime = getTime()\n },\n timeMs: () => getTime(),\n timeSecs: () => getTime() / 1000,\n addedTimeMs: () => addedTime,\n addedTimeSecs: () => addedTime / 1000,\n timeWithoutPenaltySecs: () => (getTime() - addedTime) / 1000,\n addPenalty: (ms: number) => {\n addedTime += ms\n },\n isPaused: () => pausedDate !== undefined || startDate === undefined,\n isRunning: () => startDate !== undefined && pausedDate === undefined,\n display: () => {\n const elapsed = getTime() - addedTime\n const elapsedStr = formatTime(Math.max(0, elapsed))\n const addedStr = addedTime === 0 ? \"\" : formatTime(addedTime)\n return [elapsedStr, addedStr]\n },\n }\n}\n\nfunction createHostAPI() {\n const messageHandlers = new Map<string, Set<(data: any) => void>>()\n\n const sendMessage = <T extends keyof SupportedOutgoingMessages>(type: T, json: SupportedOutgoingMessages[T]) => {\n const message = { type, json, _: \"p\", __: \"mp\", private: true }\n\n if (\"parent\" in window && window.parent !== window) window.parent.postMessage(message, \"*\")\n\n window.postMessage(message, \"*\")\n\n if (\"webkit\" in window && (window as any).webkit?.messageHandlers?.app) (window as any).webkit.messageHandlers.app.postMessage(message)\n\n if (\"puzzmoMessageString\" in window) (window as any).puzzmoMessageString(JSON.stringify(message))\n\n if (\"ReactNativeWebView\" in window && (window as any).ReactNativeWebView?.postMessage)\n (window as any).ReactNativeWebView.postMessage(JSON.stringify(message))\n\n if (type !== \"TIMER_TICK\" && type !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] sent:\", type, json)\n }\n\n const onMessage = <T extends keyof SupportedIncomingMessages>(type: T, handler: MessageHandler<T>) => {\n if (!messageHandlers.has(type)) messageHandlers.set(type, new Set())\n messageHandlers.get(type)!.add(handler)\n\n return () => {\n messageHandlers.get(type)?.delete(handler)\n }\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", (event) => {\n if (!event?.data?.type) return\n const msgType = event.data.type as string\n const handlers = messageHandlers.get(msgType)\n if (handlers) {\n const msgData = event.data.data ?? event.data.json ?? {}\n if (msgType !== \"TIMER_TICK\" && msgType !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] received:\", msgType, msgData)\n handlers.forEach((handler) => handler(msgData))\n }\n })\n }\n\n return { sendMessage, onMessage }\n}\n\nconst hostAPI = createHostAPI()\n\n/** Creates a Puzzmo SDK instance for communicating with the Puzzmo host */\nexport const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {\n let readyData: MessagesReceived[\"READY_DATA\"] | null = null\n let readyDataResolve: ((data: MessagesReceived[\"READY_DATA\"]) => void) | null = null\n\n const getGameplay = () => readyData?.startOrFindGameplay?.gamePlayed\n const getGameplayID = () => getGameplay()?.id ?? null\n const getPuzzleString = () => getGameplay()?.puzzle.puzzle ?? null\n const getBoardState = () => getGameplay()?.boardState ?? null\n const getTheme = () => readyData?.theme ?? null\n const getCompleted = () => getGameplay()?.completed ?? false\n\n const eventListeners = new Map<SDKEventType, Set<(data?: any) => void>>()\n\n const internalTimer = createTimer()\n let timerTickInterval: ReturnType<typeof setInterval> | null = null\n let timerSyncInterval: ReturnType<typeof setInterval> | null = null\n\n const startTimerIntervals = () => {\n if (timerTickInterval) return\n\n timerTickInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n const [elapsed, added] = internalTimer.display()\n hostAPI.sendMessage(\"TIMER_TICK\", { display: [elapsed, added] })\n }, 500)\n\n timerSyncInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n hostAPI.sendMessage(\"TIMER_SYNC\", Math.floor(internalTimer.timeWithoutPenaltySecs()))\n }, 10000)\n }\n\n const stopTimerIntervals = () => {\n if (timerTickInterval) {\n clearInterval(timerTickInterval)\n timerTickInterval = null\n }\n if (timerSyncInterval) {\n clearInterval(timerSyncInterval)\n timerSyncInterval = null\n }\n }\n\n const emit = <T extends SDKEventType>(event: T, data?: SDKEventMap[T]) => {\n const listeners = eventListeners.get(event)\n if (listeners) listeners.forEach((listener) => listener(data))\n }\n\n hostAPI.onMessage(\"START_GAME\", () => {\n internalTimer._init()\n startTimerIntervals()\n emit(\"start\")\n })\n\n hostAPI.onMessage(\"PAUSE_GAME\", () => {\n internalTimer._pause()\n emit(\"pause\")\n })\n\n hostAPI.onMessage(\"RESUME_GAME\", () => {\n internalTimer._resume()\n emit(\"resume\")\n })\n\n hostAPI.onMessage(\"SETTINGS_UPDATE\", (data) => emit(\"settingsUpdate\", data))\n\n hostAPI.onMessage(\"KEYBOARD_KEY_PRESS\", (data) => emit(\"keyboardKeyPress\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_CHANGE\", (data) => emit(\"keyboardCursorChange\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_END\", () => emit(\"keyboardCursorEnd\"))\n\n hostAPI.onMessage(\"RETRY_PUZZLE\", () => {\n internalTimer._reset()\n stopTimerIntervals()\n emit(\"retry\")\n })\n\n hostAPI.onMessage(\"READY_DATA\", (data) => {\n const bootstrapData = data as MessagesReceived[\"READY_DATA\"]\n readyData = bootstrapData\n\n const gamePlayed = bootstrapData.startOrFindGameplay?.gamePlayed\n if (gamePlayed) {\n const existingTime = (gamePlayed.elapsedTimeSecs ?? 0) * 1000\n const existingAddedTime = (gamePlayed.additionalTimeAddedSecs ?? 0) * 1000\n internalTimer._reset(existingTime, existingAddedTime)\n }\n\n if (readyDataResolve) {\n readyDataResolve(bootstrapData)\n readyDataResolve = null\n }\n })\n\n const timer: SDKTimer = {\n timeMs: () => internalTimer.timeMs(),\n timeSecs: () => internalTimer.timeSecs(),\n addedTimeMs: () => internalTimer.addedTimeMs(),\n addedTimeSecs: () => internalTimer.addedTimeSecs(),\n timeWithoutPenaltySecs: () => internalTimer.timeWithoutPenaltySecs(),\n display: () => internalTimer.display(),\n addPenalty: (ms: number) => internalTimer.addPenalty(ms),\n isPaused: () => internalTimer.isPaused(),\n isRunning: () => internalTimer.isRunning(),\n }\n\n return {\n timer,\n\n gameReady: async (): Promise<{\n puzzleString: string\n inputString: string | null\n theme: Theme | null\n completed: boolean\n readyData: MessagesReceived[\"READY_DATA\"] | null\n }> => {\n hostAPI.sendMessage(\"READY\", {})\n\n if (getPuzzleString()) {\n const inputString = getBoardState()\n return {\n puzzleString: getPuzzleString()!,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n }\n\n const timeout = options.timeout ?? 5000\n const readyDataPromise = new Promise<MessagesReceived[\"READY_DATA\"]>((resolve, reject) => {\n readyDataResolve = resolve\n setTimeout(() => {\n if (readyDataResolve) {\n readyDataResolve = null\n reject(new Error(`Timeout waiting for READY_DATA after ${timeout}ms`))\n }\n }, timeout)\n })\n\n await readyDataPromise\n\n const puzzleString = getPuzzleString()\n if (!puzzleString) throw new Error(\"READY_DATA received but no puzzle data found\")\n\n const inputString = getBoardState()\n return {\n puzzleString,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n },\n\n gameLoaded: (state: any = {}) => {\n hostAPI.sendMessage(\"READY_GAME_LOADED\", {\n state,\n gameRuntimeContract: \"1.0\",\n embedRuntimeContract: \"1.0\",\n })\n },\n\n on: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void): (() => void) => {\n if (!eventListeners.has(event)) eventListeners.set(event, new Set())\n eventListeners.get(event)!.add(listener)\n return () => {\n eventListeners.get(event)?.delete(listener)\n }\n },\n\n off: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void) => {\n eventListeners.get(event)?.delete(listener)\n },\n\n updateGameState: (inputString: string, play?: Partial<GamePlay>) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n hostAPI.sendMessage(\"UPLOAD_NEW_GAME_STATE\", {\n id: gameplayID,\n input: {\n boardState: inputString,\n elapsedTimeSecs: play?.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play?.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n collabUserReferences: [],\n },\n })\n },\n\n gameCompleted: (play: Partial<GamePlay>, config?: AugmentationConfig) => {\n internalTimer._conclude()\n stopTimerIntervals()\n\n const finalPlay: Partial<GamePlay> = {\n ...play,\n elapsedTimeSecs: play.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n }\n\n const deeds: Deed[] = (config?.deeds as any) ?? []\n deeds.push({ id: \"points\", value: play.pointsAwarded })\n deeds.push({\n id: \"time\",\n value: Math.round(finalPlay.elapsedTimeSecs ?? 0) + Math.round(finalPlay.additionalTimeAddedSecs ?? 0),\n })\n\n const gameplayID = getGameplayID()\n if (gameplayID) {\n hostAPI.sendMessage(\"GAME_COMPLETED\", {\n id: gameplayID,\n input: finalPlay,\n config,\n })\n }\n },\n\n showCompletionScreen: (results: any[], gameplay: GamePlay, showRetry = true) => {\n hostAPI.sendMessage(\"SHOW_GAME_COMPLETE_SCREEN\", {\n results,\n showRetry,\n gameplay,\n })\n },\n\n hitCheckpoint: (checkpointName: string, checkpointConfig: CheckpointConfig, config?: AugmentationConfig) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n const inputStr = getBoardState() ?? \"\"\n const play: Partial<GamePlay> = {\n elapsedTimeSecs: internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: internalTimer.addedTimeSecs(),\n }\n\n hostAPI.sendMessage(\"HIT_CHECKPOINT\", {\n checkpointName,\n gameplay: { inputStr, play },\n checkpointConfig,\n augConfig: config ?? {},\n })\n },\n\n keyboard: {\n /** Show the on-screen keyboard with the given config. Call again to update state (e.g. to change disabled keys). */\n show: (config: KeyboardConfig) => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", config)\n },\n /** Hide the on-screen keyboard. */\n hide: () => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", {\n layout: [],\n symbols: {},\n highlight: [],\n disabled: [],\n xl: [],\n l: [],\n supportsDragCursor: false,\n })\n },\n },\n\n _hostAPI: hostAPI,\n }\n}\n","import type { KeyboardConfig } from \"./types\"\n\n/**\n * A standard QWERTY layout with Enter and Backspace — a reasonable default for\n * any game that needs text input. Customize from here by spreading and overriding.\n *\n * @example\n * // Use as-is\n * sdk.keyboard.show(defaultKeyboardConfig)\n *\n * @example\n * // Extend with dynamic disabled letters\n * sdk.keyboard.show({ ...defaultKeyboardConfig, disabled: usedLetters })\n */\nexport const defaultKeyboardConfig: KeyboardConfig = {\n layout: [\"qwertyuiop\", \"asdfghjkl\", \"↵zxcvbnm⌫\", undefined],\n symbols: { \"↵\": \"Enter\", \"⌫\": \"bsp\" },\n highlight: [\"↵\", \"⌫\"],\n disabled: [],\n xl: [],\n l: [\"↵\", \"⌫\"],\n supportsDragCursor: false,\n}\n","/** Severity level for validation issues */\nexport type ValidationLevel = \"error\" | \"warning\" | \"info\"\n\n/** Represents a single validation issue found during puzzle validation */\nexport interface ValidationIssue {\n level: ValidationLevel\n message: string\n line?: number\n col?: number\n length?: number\n}\n\n/** Complete validation report for a puzzle */\nexport interface ValidationReport {\n success: boolean\n issues: ValidationIssue[]\n}\n\nexport type ImportErrorType = \"invalid_format\" | \"parsing_error\" | \"unknown\"\n\n/** Custom error class for workshop import failures */\nexport class EditorImportError extends Error {\n constructor(\n public type: ImportErrorType,\n message: string,\n public originalError?: unknown,\n ) {\n super(message)\n this.name = \"EditorImportError\"\n }\n}\n\n/** Result of a successful puzzle import operation */\nexport interface ImportResult {\n data: string\n warnings?: ValidationIssue[]\n title?: string\n authors?: string[]\n editors?: string[]\n}\n\n/** Settings UI descriptor returned by an editor bundle */\nexport interface EditorBundleSettings<TComponent = unknown> {\n components: TComponent[]\n defaults: Record<string, unknown>\n}\n\n/** Main interface for a Workshop bundle */\nexport interface EditorBundle<TSettingsComponent = unknown> {\n validator: {\n validate(data: string): Promise<ValidationReport> | ValidationReport\n }\n importer?: {\n onImport(filename: string, contents: string | ArrayBuffer): Promise<ImportResult> | ImportResult\n }\n /** Embed-level settings UI, populated from the bundle's declared settings */\n settings?: EditorBundleSettings<TSettingsComponent>\n /** Editor-level settings UI, populated from the bundle's declared editor settings */\n editorSettings?: EditorBundleSettings<TSettingsComponent>\n /** Custom puzzle editor, if provided by the bundle */\n editor?: {\n mount(...args: unknown[]): unknown\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/sdk.ts","../src/keyboard.ts","../src/editor.ts"],"sourcesContent":["import type {\n MessagesSentFromEmbed,\n MessagesReceived,\n GamePlay,\n AugmentationConfig,\n CheckpointConfig,\n Theme,\n Deed,\n KeyboardConfig,\n} from \"./types\"\n\nexport type SDK = ReturnType<typeof createPuzzmoSDK>\n\nexport interface PuzzmoSDKOptions {\n /** Optional timeout in ms to wait for READY_DATA (default: 5000) */\n timeout?: number\n}\n\ntype SupportedOutgoingMessages = Pick<\n MessagesSentFromEmbed,\n | \"READY\"\n | \"READY_GAME_LOADED\"\n | \"GAME_COMPLETED\"\n | \"SHOW_GAME_COMPLETE_SCREEN\"\n | \"TIMER_TICK\"\n | \"TIMER_SYNC\"\n | \"UPLOAD_NEW_GAME_STATE\"\n | \"HIT_CHECKPOINT\"\n | \"KEYBOARD_UPDATE_CONFIG\"\n>\n\ntype SupportedIncomingMessages = Pick<\n MessagesReceived,\n | \"READY_DATA\"\n | \"START_GAME\"\n | \"PAUSE_GAME\"\n | \"RESUME_GAME\"\n | \"SETTINGS_UPDATE\"\n | \"RETRY_PUZZLE\"\n | \"KEYBOARD_KEY_PRESS\"\n | \"KEYBOARD_CURSOR_CHANGE\"\n | \"KEYBOARD_CURSOR_END\"\n>\n\ntype MessageHandler<T extends keyof SupportedIncomingMessages> = (data: SupportedIncomingMessages[T]) => void\n\nexport type SDKEventMap = {\n start: void\n pause: void\n resume: void\n retry: void\n settingsUpdate: any\n /** A key on the on-screen keyboard was tapped. */\n keyboardKeyPress: { key: string }\n /** The drag cursor moved across the keyboard. Only fires when `supportsDragCursor` is true. */\n keyboardCursorChange: { position: [number, number] }\n /** The drag cursor was released. Only fires when `supportsDragCursor` is true. */\n keyboardCursorEnd: void\n}\n\nexport type SDKEventType = keyof SDKEventMap\n\nexport interface SDKTimer {\n /** Get elapsed time in milliseconds */\n timeMs: () => number\n /** Get elapsed time in seconds */\n timeSecs: () => number\n /** Get added/penalty time in milliseconds */\n addedTimeMs: () => number\n /** Get added/penalty time in seconds */\n addedTimeSecs: () => number\n /** Get elapsed time without penalties in seconds */\n timeWithoutPenaltySecs: () => number\n /** Get formatted display strings [elapsed, added] */\n display: () => [string, string]\n /** Add penalty time in milliseconds */\n addPenalty: (ms: number) => void\n /** Check if timer is paused */\n isPaused: () => boolean\n /** Check if timer has been started */\n isRunning: () => boolean\n}\n\nfunction formatTime(timeMs: number): string {\n const isAnHourOrMore = timeMs >= 60 * 60 * 1000\n return new Date(timeMs)\n .toISOString()\n .slice(isAnHourOrMore ? 11 : 14, -1)\n .split(\".\")[0]\n}\n\nfunction createTimer(\n initialTimeMs = 0,\n initialAddedTimeMs = 0,\n): SDKTimer & {\n _init: () => void\n _pause: () => void\n _resume: () => void\n _reset: (initialTimeMs?: number, initialAddedTimeMs?: number) => void\n _conclude: () => void\n} {\n let baseTime = initialTimeMs\n let addedTime = initialAddedTimeMs\n let pausedTime = 0\n let concludeTime: number | undefined\n\n let startDate: number | undefined = undefined\n let pausedDate: number | undefined = undefined\n\n const getTime = (): number => {\n if (startDate === undefined) return baseTime + addedTime\n if (concludeTime !== undefined) return concludeTime\n const now = pausedDate ?? performance.now()\n const elapsed = now - startDate - pausedTime\n return baseTime + addedTime + elapsed\n }\n\n return {\n _init: () => {\n if (startDate === undefined) {\n startDate = performance.now()\n pausedDate = undefined\n }\n },\n _pause: () => {\n if (pausedDate !== undefined || startDate === undefined) return\n pausedDate = performance.now()\n },\n _resume: () => {\n if (pausedDate === undefined) return\n pausedTime += performance.now() - pausedDate\n pausedDate = undefined\n },\n _reset: (newInitialTimeMs = 0, newInitialAddedTimeMs = 0) => {\n baseTime = newInitialTimeMs\n addedTime = newInitialAddedTimeMs\n pausedTime = 0\n concludeTime = undefined\n startDate = undefined\n pausedDate = undefined\n },\n _conclude: () => {\n if (pausedDate !== undefined) {\n concludeTime = getTime()\n return\n }\n if (startDate === undefined) {\n concludeTime = baseTime + addedTime\n return\n }\n concludeTime = getTime()\n },\n timeMs: () => getTime(),\n timeSecs: () => getTime() / 1000,\n addedTimeMs: () => addedTime,\n addedTimeSecs: () => addedTime / 1000,\n timeWithoutPenaltySecs: () => (getTime() - addedTime) / 1000,\n addPenalty: (ms: number) => {\n addedTime += ms\n },\n isPaused: () => pausedDate !== undefined || startDate === undefined,\n isRunning: () => startDate !== undefined && pausedDate === undefined,\n display: () => {\n const elapsed = getTime() - addedTime\n const elapsedStr = formatTime(Math.max(0, elapsed))\n const addedStr = addedTime === 0 ? \"\" : formatTime(addedTime)\n return [elapsedStr, addedStr]\n },\n }\n}\n\nfunction createHostAPI() {\n const messageHandlers = new Map<string, Set<(data: any) => void>>()\n\n const sendMessage = <T extends keyof SupportedOutgoingMessages>(type: T, json: SupportedOutgoingMessages[T]) => {\n const message = { type, json, _: \"p\", __: \"mp\", private: true }\n\n if (\"parent\" in window && window.parent !== window) window.parent.postMessage(message, \"*\")\n\n window.postMessage(message, \"*\")\n\n if (\"webkit\" in window && (window as any).webkit?.messageHandlers?.app) (window as any).webkit.messageHandlers.app.postMessage(message)\n\n if (\"puzzmoMessageString\" in window) (window as any).puzzmoMessageString(JSON.stringify(message))\n\n if (\"ReactNativeWebView\" in window && (window as any).ReactNativeWebView?.postMessage)\n (window as any).ReactNativeWebView.postMessage(JSON.stringify(message))\n\n if (type !== \"TIMER_TICK\" && type !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] sent:\", type, json)\n }\n\n const onMessage = <T extends keyof SupportedIncomingMessages>(type: T, handler: MessageHandler<T>) => {\n if (!messageHandlers.has(type)) messageHandlers.set(type, new Set())\n messageHandlers.get(type)!.add(handler)\n\n return () => {\n messageHandlers.get(type)?.delete(handler)\n }\n }\n\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"message\", (event) => {\n if (!event?.data?.type) return\n const msgType = event.data.type as string\n const handlers = messageHandlers.get(msgType)\n if (handlers) {\n const msgData = event.data.data ?? event.data.json ?? {}\n if (msgType !== \"TIMER_TICK\" && msgType !== \"TIMER_SYNC\") console.log(\"[Puzzmo SDK] received:\", msgType, msgData)\n handlers.forEach((handler) => handler(msgData))\n }\n })\n }\n\n return { sendMessage, onMessage }\n}\n\nconst hostAPI = createHostAPI()\n\n/** Creates a Puzzmo SDK instance for communicating with the Puzzmo host */\nexport const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {\n let readyData: MessagesReceived[\"READY_DATA\"] | null = null\n let readyDataResolve: ((data: MessagesReceived[\"READY_DATA\"]) => void) | null = null\n\n const getGameplay = () => readyData?.startOrFindGameplay?.gamePlayed\n const getGameplayID = () => getGameplay()?.id ?? null\n const getPuzzleString = () => getGameplay()?.puzzle.puzzle ?? null\n const getBoardState = () => getGameplay()?.boardState ?? null\n const getTheme = () => readyData?.theme ?? null\n const getCompleted = () => getGameplay()?.completed ?? false\n\n const eventListeners = new Map<SDKEventType, Set<(data?: any) => void>>()\n\n const internalTimer = createTimer()\n let timerTickInterval: ReturnType<typeof setInterval> | null = null\n let timerSyncInterval: ReturnType<typeof setInterval> | null = null\n\n const startTimerIntervals = () => {\n if (timerTickInterval) return\n\n timerTickInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n const [elapsed, added] = internalTimer.display()\n hostAPI.sendMessage(\"TIMER_TICK\", { display: [elapsed, added] })\n }, 500)\n\n timerSyncInterval = setInterval(() => {\n if (internalTimer.isPaused()) return\n hostAPI.sendMessage(\"TIMER_SYNC\", Math.floor(internalTimer.timeWithoutPenaltySecs()))\n }, 10000)\n }\n\n const stopTimerIntervals = () => {\n if (timerTickInterval) {\n clearInterval(timerTickInterval)\n timerTickInterval = null\n }\n if (timerSyncInterval) {\n clearInterval(timerSyncInterval)\n timerSyncInterval = null\n }\n }\n\n const emit = <T extends SDKEventType>(event: T, data?: SDKEventMap[T]) => {\n const listeners = eventListeners.get(event)\n if (listeners) listeners.forEach((listener) => listener(data))\n }\n\n hostAPI.onMessage(\"START_GAME\", () => {\n internalTimer._init()\n startTimerIntervals()\n emit(\"start\")\n })\n\n hostAPI.onMessage(\"PAUSE_GAME\", () => {\n internalTimer._pause()\n emit(\"pause\")\n })\n\n hostAPI.onMessage(\"RESUME_GAME\", () => {\n internalTimer._resume()\n emit(\"resume\")\n })\n\n hostAPI.onMessage(\"SETTINGS_UPDATE\", (data) => emit(\"settingsUpdate\", data))\n\n hostAPI.onMessage(\"KEYBOARD_KEY_PRESS\", (data) => emit(\"keyboardKeyPress\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_CHANGE\", (data) => emit(\"keyboardCursorChange\", data))\n hostAPI.onMessage(\"KEYBOARD_CURSOR_END\", () => emit(\"keyboardCursorEnd\"))\n\n hostAPI.onMessage(\"RETRY_PUZZLE\", () => {\n internalTimer._reset()\n stopTimerIntervals()\n emit(\"retry\")\n })\n\n hostAPI.onMessage(\"READY_DATA\", (data) => {\n const bootstrapData = data as MessagesReceived[\"READY_DATA\"]\n readyData = bootstrapData\n\n const gamePlayed = bootstrapData.startOrFindGameplay?.gamePlayed\n if (gamePlayed) {\n const existingTime = (gamePlayed.elapsedTimeSecs ?? 0) * 1000\n const existingAddedTime = (gamePlayed.additionalTimeAddedSecs ?? 0) * 1000\n internalTimer._reset(existingTime, existingAddedTime)\n }\n\n if (readyDataResolve) {\n readyDataResolve(bootstrapData)\n readyDataResolve = null\n }\n })\n\n const timer: SDKTimer = {\n timeMs: () => internalTimer.timeMs(),\n timeSecs: () => internalTimer.timeSecs(),\n addedTimeMs: () => internalTimer.addedTimeMs(),\n addedTimeSecs: () => internalTimer.addedTimeSecs(),\n timeWithoutPenaltySecs: () => internalTimer.timeWithoutPenaltySecs(),\n display: () => internalTimer.display(),\n addPenalty: (ms: number) => internalTimer.addPenalty(ms),\n isPaused: () => internalTimer.isPaused(),\n isRunning: () => internalTimer.isRunning(),\n }\n\n return {\n timer,\n\n gameReady: async (): Promise<{\n puzzleString: string\n inputString: string | null\n theme: Theme | null\n completed: boolean\n readyData: MessagesReceived[\"READY_DATA\"] | null\n }> => {\n hostAPI.sendMessage(\"READY\", {})\n\n if (getPuzzleString()) {\n const inputString = getBoardState()\n return {\n puzzleString: getPuzzleString()!,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n }\n\n const timeout = options.timeout ?? 5000\n const readyDataPromise = new Promise<MessagesReceived[\"READY_DATA\"]>((resolve, reject) => {\n readyDataResolve = resolve\n setTimeout(() => {\n if (readyDataResolve) {\n readyDataResolve = null\n reject(new Error(`Timeout waiting for READY_DATA after ${timeout}ms`))\n }\n }, timeout)\n })\n\n await readyDataPromise\n\n const puzzleString = getPuzzleString()\n if (!puzzleString) throw new Error(\"READY_DATA received but no puzzle data found\")\n\n const inputString = getBoardState()\n return {\n puzzleString,\n inputString,\n // @ts-expect-error - this is backwards compat\n boardState: inputString,\n theme: getTheme(),\n completed: getCompleted(),\n readyData,\n }\n },\n\n gameLoaded: (state: any = {}) => {\n hostAPI.sendMessage(\"READY_GAME_LOADED\", {\n state,\n gameRuntimeContract: \"1.0\",\n embedRuntimeContract: \"1.0\",\n })\n },\n\n on: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void): (() => void) => {\n if (!eventListeners.has(event)) eventListeners.set(event, new Set())\n eventListeners.get(event)!.add(listener)\n return () => {\n eventListeners.get(event)?.delete(listener)\n }\n },\n\n off: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void) => {\n eventListeners.get(event)?.delete(listener)\n },\n\n updateGameState: (inputString: string, play?: Partial<GamePlay>) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n hostAPI.sendMessage(\"UPLOAD_NEW_GAME_STATE\", {\n id: gameplayID,\n input: {\n boardState: inputString,\n elapsedTimeSecs: play?.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play?.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n collabUserReferences: [],\n },\n })\n },\n\n gameCompleted: (play: Partial<GamePlay>, config?: AugmentationConfig) => {\n internalTimer._conclude()\n stopTimerIntervals()\n\n const finalPlay: Partial<GamePlay> = {\n ...play,\n elapsedTimeSecs: play.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: play.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),\n }\n\n const deeds: Deed[] = (config?.deeds as any) ?? []\n deeds.push({ id: \"points\", value: play.pointsAwarded })\n deeds.push({\n id: \"time\",\n value: Math.round(finalPlay.elapsedTimeSecs ?? 0) + Math.round(finalPlay.additionalTimeAddedSecs ?? 0),\n })\n\n const gameplayID = getGameplayID()\n if (gameplayID) {\n hostAPI.sendMessage(\"GAME_COMPLETED\", {\n id: gameplayID,\n input: finalPlay,\n config,\n })\n }\n },\n\n showCompletionScreen: (results: any[], gameplay: GamePlay, showRetry = true) => {\n hostAPI.sendMessage(\"SHOW_GAME_COMPLETE_SCREEN\", {\n results,\n showRetry,\n gameplay,\n })\n },\n\n hitCheckpoint: (checkpointName: string, checkpointConfig: CheckpointConfig, config?: AugmentationConfig) => {\n const gameplayID = getGameplayID()\n if (!gameplayID) return\n\n const inputStr = getBoardState() ?? \"\"\n const play: Partial<GamePlay> = {\n elapsedTimeSecs: internalTimer.timeWithoutPenaltySecs(),\n additionalTimeAddedSecs: internalTimer.addedTimeSecs(),\n }\n\n hostAPI.sendMessage(\"HIT_CHECKPOINT\", {\n checkpointName,\n gameplay: { inputStr, play },\n checkpointConfig,\n augConfig: config ?? {},\n })\n },\n\n keyboard: {\n /** Show the on-screen keyboard with the given config. Call again to update state (e.g. to change disabled keys). */\n show: (config: KeyboardConfig) => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", config)\n },\n /** Hide the on-screen keyboard. */\n hide: () => {\n hostAPI.sendMessage(\"KEYBOARD_UPDATE_CONFIG\", {\n layout: [],\n symbols: {},\n highlight: [],\n disabled: [],\n xl: [],\n l: [],\n supportsDragCursor: false,\n })\n },\n },\n\n _hostAPI: hostAPI,\n }\n}\n","import type { KeyboardConfig } from \"./types\"\n\n/**\n * A standard QWERTY layout with Enter and Backspace — a reasonable default for\n * any game that needs text input. Customize from here by spreading and overriding.\n *\n * @example\n * // Use as-is\n * sdk.keyboard.show(defaultKeyboardConfig)\n *\n * @example\n * // Extend with dynamic disabled letters\n * sdk.keyboard.show({ ...defaultKeyboardConfig, disabled: usedLetters })\n */\nexport const defaultKeyboardConfig: KeyboardConfig = {\n layout: [\"qwertyuiop\", \"asdfghjkl\", \"↵zxcvbnm⌫\", undefined],\n symbols: { \"↵\": \"Enter\", \"⌫\": \"bsp\" },\n highlight: [\"↵\", \"⌫\"],\n disabled: [],\n xl: [],\n l: [\"↵\", \"⌫\"],\n supportsDragCursor: false,\n}\n","/** Severity level for validation issues */\nexport type ValidationLevel = \"error\" | \"warning\" | \"info\"\n\n/** Represents a single validation issue found during puzzle validation */\nexport interface ValidationIssue {\n level: ValidationLevel\n message: string\n line?: number\n col?: number\n length?: number\n}\n\n/** Complete validation report for a puzzle */\nexport interface ValidationReport {\n success: boolean\n issues: ValidationIssue[]\n}\n\nexport type ImportErrorType = \"invalid_format\" | \"parsing_error\" | \"unknown\"\n\n/** Custom error class for workshop import failures */\nexport class EditorImportError extends Error {\n constructor(\n public type: ImportErrorType,\n message: string,\n public originalError?: unknown,\n ) {\n super(message)\n this.name = \"EditorImportError\"\n }\n}\n\n/** Result of a successful puzzle import operation */\nexport interface ImportResult {\n data: string\n warnings?: ValidationIssue[]\n title?: string\n authors?: string[]\n editors?: string[]\n}\n\n/** Settings UI descriptor returned by an editor bundle */\nexport interface EditorBundleSettings<TComponent = unknown> {\n components: TComponent[]\n defaults: Record<string, unknown>\n}\n\n/** Main interface for a Workshop bundle */\nexport interface EditorBundle<TSettingsComponent = unknown> {\n validator: {\n validate(data: string): Promise<ValidationReport> | ValidationReport\n }\n importer?: {\n onImport(filename: string, contents: string | ArrayBuffer): Promise<ImportResult> | ImportResult\n }\n /** Embed-level settings UI, populated from the bundle's declared settings */\n settings?: EditorBundleSettings<TSettingsComponent>\n /** Editor-level settings UI, populated from the bundle's declared editor settings */\n editorSettings?: EditorBundleSettings<TSettingsComponent>\n /** Custom puzzle editor, if provided by the bundle */\n editor?: {\n mount(...args: unknown[]): unknown\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/inputs/encodings.ts","../../src/inputs/encoder.ts","../../src/inputs/decoder.ts","../../src/inputs/migration.ts","../../src/inputs/types.ts","../../src/inputs/index.ts"],"sourcesContent":["import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from \"lz-string\"\n\nexport function encodeBitArrayToHex(bits: boolean[]): string {\n if (bits.length === 0) return \"0\"\n\n let hex = \"\"\n for (let i = 0; i < bits.length; i += 4) {\n let nibble = 0\n for (let j = 0; j < 4 && i + j < bits.length; j++) {\n if (bits[i + j]) {\n nibble |= 1 << j\n }\n }\n hex += nibble.toString(16)\n }\n\n return hex || \"0\"\n}\n\nexport function decodeBitArrayFromHex(hex: string, length: number): boolean[] {\n const bits: boolean[] = []\n\n for (let i = 0; i < length; i++) {\n const hexIndex = Math.floor(i / 4)\n const bitIndex = i % 4\n const nibble = parseInt(hex[hexIndex] || \"0\", 16)\n bits.push((nibble & (1 << bitIndex)) !== 0)\n }\n\n return bits\n}\n\nexport function encodeIntArray(arr: number[]): string {\n if (arr.length === 0) return \"\"\n\n const nonZero = arr.map((val, idx) => ({ val, idx })).filter((item) => item.val !== 0)\n\n if (nonZero.length === 0) return \"\"\n if (nonZero.length > arr.length / 2) {\n return arr.join(\",\")\n }\n\n return nonZero.map((item) => `${item.idx}=${item.val}`).join(\",\")\n}\n\nexport function decodeIntArray(str: string, defaultLength?: number): number[] {\n if (!str) return defaultLength ? Array.from({ length: defaultLength }, () => 0) : []\n\n if (str.includes(\"=\")) {\n const result = defaultLength ? Array.from({ length: defaultLength }, () => 0) : []\n const pairs = str.split(\",\")\n for (const pair of pairs) {\n const [idxStr, valStr] = pair.split(\"=\")\n const idx = Number(idxStr)\n const val = Number(valStr)\n if (!isNaN(idx) && !isNaN(val)) {\n result[idx] = val\n }\n }\n return result\n }\n\n return str.split(\",\").map((x) => Number(x))\n}\n\nexport function encodeStringArray(arr: string[], delimiter = \",\"): string {\n return arr.join(delimiter)\n}\n\nexport function decodeStringArray(str: string, delimiter = \",\"): string[] {\n if (!str) return []\n return str.split(delimiter)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON can encode any data\nexport function encodeJson(data: any): string {\n const json = JSON.stringify(data)\n return compressToEncodedURIComponent(json)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON can decode to any data\nexport function decodeJson(str: string): any {\n if (!str) return undefined\n const json = decompressFromEncodedURIComponent(str)\n return json ? JSON.parse(json) : undefined\n}\n\nexport function encodeSparseMap<T>(map: Record<string, T>, encodeValue: (val: T) => string, orderedKeys?: string[]): string {\n const entries = Object.entries(map).filter(([_, val]) => val !== null && val !== undefined)\n\n if (orderedKeys) {\n // Use numeric indices instead of string keys\n const indexedEntries = entries\n .map(([key, val]) => {\n const idx = orderedKeys.indexOf(key)\n if (idx === -1) return null\n return `${idx}=${encodeValue(val)}`\n })\n .filter((entry): entry is string => entry !== null)\n\n return indexedEntries.join(\",\")\n } else {\n // Fall back to string keys\n return entries.map(([key, val]) => `${key}=${encodeValue(val)}`).join(\";\")\n }\n}\n\nexport function decodeSparseMap<T>(str: string, decodeValue: (val: string) => T, orderedKeys?: string[]): Record<string, T> {\n if (!str) return {}\n\n const result: Record<string, T> = {}\n\n if (orderedKeys) {\n // Decode numeric indices\n const entries = str.split(\",\")\n for (const entry of entries) {\n if (!entry) continue\n const [idxStr, val] = entry.split(\"=\")\n const idx = Number(idxStr)\n if (!isNaN(idx) && val && orderedKeys[idx]) {\n result[orderedKeys[idx]] = decodeValue(val)\n }\n }\n } else {\n // Decode string keys\n const entries = str.split(\";\")\n for (const entry of entries) {\n if (!entry) continue\n const [key, val] = entry.split(\"=\")\n if (key && val) {\n result[key] = decodeValue(val)\n }\n }\n }\n\n return result\n}\n","import type { Schema, FieldType } from \"./types\"\nimport { encodeBitArrayToHex, encodeIntArray, encodeStringArray, encodeSparseMap, encodeJson } from \"./encodings\"\n\nexport function encodeField(value: any, fieldType: FieldType, context?: any): string {\n switch (fieldType.type) {\n case \"bitArray\":\n return encodeBitArrayToHex(value)\n\n case \"intArray\":\n return encodeIntArray(value)\n\n case \"int\":\n return String(value)\n\n case \"string\":\n return String(value)\n\n case \"stringArray\":\n return encodeStringArray(value, fieldType.delimiter)\n\n case \"sparseMap\":\n return encodeSparseMap(value, fieldType.valueEncoder, context?.keys)\n\n case \"json\":\n return encodeJson(value)\n\n default:\n throw new Error(`Unknown field type: ${(fieldType as any).type}`)\n }\n}\n\nexport function encode<TData>(schema: Schema<TData>, data: TData, context?: Record<string, any>): string {\n const delimiter = schema.delimiter || \":\"\n const fieldNames = Object.keys(schema.fields) as (keyof TData)[]\n\n const encodedFields = fieldNames.map((fieldName) => {\n const fieldType = schema.fields[fieldName]\n const value = data[fieldName]\n const fieldContext = context?.[fieldName as string]\n return encodeField(value, fieldType, fieldContext)\n })\n\n return `v${schema.version}${delimiter}${encodedFields.join(delimiter)}`\n}\n","import type { Schema, FieldType } from \"./types\"\nimport { decodeBitArrayFromHex, decodeIntArray, decodeStringArray, decodeSparseMap, decodeJson } from \"./encodings\"\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context is dynamic and return type depends on fieldType\nexport function decodeField(value: string, fieldType: FieldType, context?: any): any {\n switch (fieldType.type) {\n case \"bitArray\":\n if (!context?.length) {\n throw new Error(\"bitArray requires length in context\")\n }\n return decodeBitArrayFromHex(value, context.length)\n\n case \"intArray\":\n return decodeIntArray(value, context?.length)\n\n case \"int\":\n return Number(value)\n\n case \"string\":\n return value\n\n case \"stringArray\":\n return decodeStringArray(value, fieldType.delimiter)\n\n case \"sparseMap\":\n return decodeSparseMap(value, fieldType.valueDecoder, context?.keys)\n\n case \"json\":\n return decodeJson(value)\n\n default:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- used in error message for invalid type\n throw new Error(`Unknown field type: ${(fieldType as any).type}`)\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context values are dynamic\nexport function decode<TData>(schema: Schema<TData>, inputString: string, context?: Record<string, any>): TData {\n const delimiter = schema.delimiter || \":\"\n const parts = inputString.split(delimiter)\n\n const versionPart = parts[0]\n const versionMatch = versionPart.match(/^v(\\d+)$/)\n if (!versionMatch) {\n throw new Error(`Invalid input string format: ${versionPart}`)\n }\n\n const version = Number(versionMatch[1])\n if (version !== schema.version) {\n throw new Error(`Version mismatch: expected v${schema.version}, got v${version}. ` + `Make sure to run migrations first.`)\n }\n\n const fieldNames = Object.keys(schema.fields) as (keyof TData)[]\n const data = {} as TData\n\n for (let i = 0; i < fieldNames.length; i++) {\n const fieldName = fieldNames[i]\n const fieldType = schema.fields[fieldName]\n const fieldValue = parts[i + 1] || \"\"\n\n const fieldContext = context?.[fieldName as string]\n data[fieldName] = decodeField(fieldValue, fieldType, fieldContext)\n }\n\n return data\n}\n","import type { Schema } from \"./types\"\nimport { encode } from \"./encoder\"\nimport { decode } from \"./decoder\"\n\nexport function createMigrator<TData>(schema: Schema<TData>) {\n return function migrate(inputString: string, context?: Record<string, any>): string {\n const versionMatch = inputString.match(/^v(\\d+)/)\n if (!versionMatch) {\n throw new Error(\"Invalid input string: no version found\")\n }\n\n const currentVersion = Number(versionMatch[1])\n\n if (currentVersion === schema.version) {\n return inputString\n }\n\n if (!schema.migrations) {\n throw new Error(`No migrations defined for schema. Cannot migrate from v${currentVersion} to v${schema.version}`)\n }\n\n // Data type changes at each migration step (v0 → v1 → v2, etc.)\n // TypeScript can't track these transformations statically, so we use `any`\n // Migrations are developer-written and tested to maintain type safety through the chain\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let data: any = decode(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n { ...schema, version: currentVersion } as Schema<any>,\n inputString,\n context,\n )\n\n for (let v = currentVersion; v < schema.version; v++) {\n const migration = schema.migrations[v]\n if (migration) {\n data = migration(data)\n }\n }\n\n return encode(schema, data, context)\n }\n}\n","/**\n * Encodes boolean arrays as compact hexadecimal strings.\n *\n * **When to use:**\n * - Revealed/hidden state of game tiles\n * - Feature flags or settings\n * - Any boolean array where order matters\n *\n * **Encoding:**\n * - Packs 4 bits per hex character (75% size reduction)\n * - Example: [true, false, false, false, false, true, false, false] → \"12\"\n *\n * **Decoding:**\n * - Requires length in context: `{ fieldName: { length: 100 } }`\n */\nexport type BitArrayFieldType = {\n type: \"bitArray\"\n}\n\n/**\n * Encodes integer arrays with automatic sparse optimization.\n *\n * **When to use:**\n * - Arrays of numeric values (scores, counts, experience)\n * - Sparse data (mostly zeros)\n * - Dense data (will auto-fallback to CSV)\n *\n * **Encoding:**\n * - Uses decimal (base-10) numbers separated by commas\n * - Smart sparse: If >50% zeros, uses `index=value` pairs (e.g., \"2=5,4=3\")\n * - Dense fallback: If not beneficial, uses CSV (e.g., \"1,2,3,4\")\n * - Empty arrays encode as empty string\n *\n * **Decoding:**\n * - Requires length in context for sparse format: `{ fieldName: { length: 100 } }`\n * - CSV format decodes without context\n */\nexport type IntArrayFieldType = {\n type: \"intArray\"\n}\n\n/**\n * Encodes single integer values.\n *\n * **When to use:**\n * - Single numeric values (score, level, count)\n * - Non-negative integers only\n *\n * **Encoding:**\n * - Uses decimal (base-10) representation\n * - Example: 42 → \"42\"\n */\nexport type IntFieldType = {\n type: \"int\"\n}\n\n/**\n * Encodes string values as-is.\n *\n * **When to use:**\n * - Single string values (name, color, mode)\n * - Short identifiers\n *\n * **Warning:**\n * - Strings containing the schema delimiter (default ':') will break parsing\n * - Consider using a custom delimiter or different encoding for such strings\n */\nexport type StringFieldType = {\n type: \"string\"\n}\n\n/**\n * Encodes arrays of strings.\n *\n * **When to use:**\n * - Lists of tags, categories, or identifiers\n * - Ordered collections of strings\n *\n * **Encoding:**\n * - Uses comma delimiter by default\n * - Custom delimiter can be specified to avoid conflicts\n *\n * **Warning:**\n * - Strings containing the delimiter will break parsing\n * - Use custom delimiter to avoid conflicts\n */\nexport type StringArrayFieldType = {\n type: \"stringArray\"\n delimiter?: string\n}\n\n/**\n * Encodes sparse key-value maps with custom value encoding.\n *\n * **When to use:**\n * - Tile annotations, cell metadata\n * - Any sparse mapping where most keys don't have values\n * - Custom object types that need specialized encoding\n *\n * **Encoding:**\n * - With context keys: Uses numeric indices (e.g., `0=2.1,5=3.0`)\n * - Without context: Uses string keys (e.g., `key1=val1;key2=val2`)\n * - Only stores non-empty entries\n * - Keys are always strings in the data, converted to indices when context provides ordered keys\n * - valueEncoder: Convert your object to a string\n * - valueDecoder: Parse string back to your object\n *\n * **Optimization:**\n * Provide ordered keys in context to use numeric indices instead of string keys:\n * ```typescript\n * const { encode, decode } = createEncoder(schema, {\n * annotations: { keys: tileIds } // Ordered list of possible keys\n * })\n * ```\n *\n * **Example:**\n * ```typescript\n * annotations: {\n * type: 'sparseMap',\n * valueEncoder: (ann) => `${ann.level}.${ann.color}`,\n * valueDecoder: (str) => {\n * const [level, color] = str.split('.').map(Number)\n * return { level, color }\n * }\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for value type\nexport type SparseMapFieldType<TValue = any> = {\n type: \"sparseMap\"\n valueEncoder: (value: TValue) => string\n valueDecoder: (str: string) => TValue\n}\n\n/**\n * Encodes arbitrary data as JSON with automatic compression.\n *\n * **When to use:**\n * - Complex nested structures\n * - Fallback for data that doesn't fit other types\n * - Temporary solution before implementing custom encoding\n *\n * **Encoding:**\n * - Converts data to JSON string\n * - Automatically compresses with lz-string\n * - Helps reduce size of complex objects\n *\n * **Warning:**\n * - Still larger than specialized encodings even with compression\n * - Use only when necessary\n * - Consider creating custom sparseMap encoding instead\n */\nexport type JsonFieldType = {\n type: \"json\"\n}\n\n/**\n * Union of all available field types.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for field values\nexport type FieldType<TValue = any> =\n | BitArrayFieldType\n | IntArrayFieldType\n | IntFieldType\n | StringFieldType\n | StringArrayFieldType\n | SparseMapFieldType<TValue>\n | JsonFieldType\n\n/**\n * Function that migrates data from one version to another.\n *\n * **When to use:**\n * - Add new fields with default values\n * - Transform existing field values\n * - Rename or restructure fields\n *\n * **Important:**\n * - Migrations operate on decoded data objects, not strings\n * - Each migration goes from version N to version N+1\n * - Migrations are applied sequentially\n *\n * **Example:**\n * ```typescript\n * migrations: {\n * 0: (oldData) => ({\n * ...oldData,\n * newField: 'default value'\n * }),\n * 1: (oldData) => ({\n * ...oldData,\n * name: oldData.name.toUpperCase()\n * })\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameters for migration input/output\nexport type DataMigration<TFrom = any, TTo = any> = (data: TFrom) => TTo\n\n/**\n * Map of version numbers to migration functions.\n *\n * Keys are the source version numbers (0, 1, 2, etc.)\n * Values are functions that migrate from that version to the next\n */\nexport type MigrationMap = {\n [fromVersion: number]: DataMigration\n}\n\n/**\n * Complete schema definition for encoding/decoding data.\n *\n * **Basic usage:**\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * name: { type: 'string' },\n * age: { type: 'int' }\n * }\n * })\n * ```\n *\n * **With custom delimiter:**\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * delimiter: '|', // Use | instead of :\n * fields: { ... }\n * })\n * ```\n *\n * **With migrations:**\n * ```typescript\n * const schema = defineSchema({\n * version: 2,\n * fields: {\n * name: { type: 'string' },\n * value: { type: 'int' }\n * },\n * migrations: {\n * 0: (old) => ({ ...old, value: 0 }),\n * 1: (old) => ({ ...old, name: old.name.toUpperCase() })\n * }\n * })\n * ```\n *\n * @param version - Current schema version (increment when making breaking changes)\n * @param delimiter - Character to separate fields (default: ':')\n * @param fields - Map of field names to field type definitions\n * @param migrations - Optional map of version migrations\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for schema data, and field values need 'any' for flexibility\nexport type Schema<TData = any> = {\n version: number\n delimiter?: string\n fields: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field values can be any type\n [K in keyof TData]: FieldType<any>\n }\n migrations?: MigrationMap\n}\n\n/**\n * Result of creating an encoder from a schema.\n *\n * **Usage:**\n * ```typescript\n * const { encode, decode, migrate } = createEncoder(schema, context)\n *\n * // Encode data to string\n * const inputString = encode(data)\n *\n * // Decode string to data\n * const data = decode(inputString)\n *\n * // Migrate old version to current version\n * const currentVersion = migrate(oldInputString)\n * ```\n */\nexport type EncoderResult<TData> = {\n encode: (data: TData) => string\n decode: (str: string) => TData\n migrate: (str: string) => string\n}\n\n/**\n * Define a typed schema for encoding/decoding game state.\n *\n * **Purpose:**\n * - Provides type inference for encode/decode functions\n * - Documents the structure of your input strings\n * - Enables version migrations\n *\n * **Example:**\n * ```typescript\n * type GameData = {\n * revealed: boolean[]\n * score: number\n * annotations: Record<string, Annotation>\n * }\n *\n * export const schema = defineSchema<GameData>({\n * version: 1,\n * fields: {\n * revealed: { type: 'bitArray' },\n * score: { type: 'int' },\n * annotations: {\n * type: 'sparseMap',\n * valueEncoder: (ann) => `${ann.level}.${ann.color}`,\n * valueDecoder: (str) => {\n * const [level, color] = str.split('.').map(Number)\n * return { level, color }\n * }\n * }\n * }\n * })\n * ```\n */\nexport function defineSchema<TData>(schema: Schema<TData>): Schema<TData> {\n return schema\n}\n","import type { Schema, EncoderResult } from \"./types\"\nimport { encode } from \"./encoder\"\nimport { decode } from \"./decoder\"\nimport { createMigrator } from \"./migration\"\n\nexport { defineSchema } from \"./types\"\nexport type {\n Schema,\n EncoderResult,\n FieldType,\n BitArrayFieldType,\n IntArrayFieldType,\n IntFieldType,\n StringFieldType,\n StringArrayFieldType,\n SparseMapFieldType,\n JsonFieldType,\n DataMigration,\n MigrationMap,\n} from \"./types\"\n\n/**\n * Create an encoder/decoder for a schema-defined data structure.\n *\n * **Basic usage:**\n * ```typescript\n * import { defineSchema, createEncoder } from '@puzzmo/sdk/inputs'\n *\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * name: { type: 'string' },\n * age: { type: 'int' }\n * }\n * })\n *\n * const { encode, decode } = createEncoder(schema)\n *\n * const data = { name: 'Alice', age: 30 }\n * const inputString = encode(data) // \"v1:Alice:30\"\n * const decoded = decode(inputString) // { name: 'Alice', age: 30 }\n * ```\n *\n * **With context (for length-dependent fields):**\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * revealed: { type: 'bitArray' },\n * scores: { type: 'intArray' }\n * }\n * })\n *\n * const { encode, decode } = createEncoder(schema, {\n * revealed: { length: 100 }, // Required for bitArray decoding\n * scores: { length: 10 }, // Required for sparse intArray decoding\n * annotations: { keys: tileIds } // Optional: optimize sparseMap with numeric indices\n * })\n * ```\n *\n * **With migrations:**\n * ```typescript\n * const { encode, decode, migrate } = createEncoder(schema)\n *\n * const oldString = \"v0:Alice:30\"\n * const migrated = migrate(oldString) // \"v1:Alice:30:default\"\n * const data = decode(migrated)\n * ```\n *\n * @param schema - Schema definition created with defineSchema()\n * @param context - Optional context for decoding (lengths for bitArray/intArray)\n * @returns Object with encode, decode, and migrate functions\n */\nexport function createEncoder<TData>(\n schema: Schema<TData>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Context values can be any shape depending on field types\n context?: Record<string, any>,\n): EncoderResult<TData> {\n const migrator = createMigrator(schema)\n\n return {\n encode: (data: TData) => encode(schema, data, context),\n decode: (str: string) => decode(schema, str, context),\n migrate: (str: string) => migrator(str, context),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/inputs/encodings.ts","../../src/inputs/encoder.ts","../../src/inputs/decoder.ts","../../src/inputs/migration.ts","../../src/inputs/types.ts","../../src/inputs/index.ts"],"sourcesContent":["import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from \"lz-string\"\n\nexport function encodeBitArrayToHex(bits: boolean[]): string {\n if (bits.length === 0) return \"0\"\n\n let hex = \"\"\n for (let i = 0; i < bits.length; i += 4) {\n let nibble = 0\n for (let j = 0; j < 4 && i + j < bits.length; j++) {\n if (bits[i + j]) {\n nibble |= 1 << j\n }\n }\n hex += nibble.toString(16)\n }\n\n return hex || \"0\"\n}\n\nexport function decodeBitArrayFromHex(hex: string, length: number): boolean[] {\n const bits: boolean[] = []\n\n for (let i = 0; i < length; i++) {\n const hexIndex = Math.floor(i / 4)\n const bitIndex = i % 4\n const nibble = parseInt(hex[hexIndex] || \"0\", 16)\n bits.push((nibble & (1 << bitIndex)) !== 0)\n }\n\n return bits\n}\n\nexport function encodeIntArray(arr: number[]): string {\n if (arr.length === 0) return \"\"\n\n const nonZero = arr.map((val, idx) => ({ val, idx })).filter((item) => item.val !== 0)\n\n if (nonZero.length === 0) return \"\"\n if (nonZero.length > arr.length / 2) {\n return arr.join(\",\")\n }\n\n return nonZero.map((item) => `${item.idx}=${item.val}`).join(\",\")\n}\n\nexport function decodeIntArray(str: string, defaultLength?: number): number[] {\n if (!str) return defaultLength ? Array.from({ length: defaultLength }, () => 0) : []\n\n if (str.includes(\"=\")) {\n const result = defaultLength ? Array.from({ length: defaultLength }, () => 0) : []\n const pairs = str.split(\",\")\n for (const pair of pairs) {\n const [idxStr, valStr] = pair.split(\"=\")\n const idx = Number(idxStr)\n const val = Number(valStr)\n if (!isNaN(idx) && !isNaN(val)) {\n result[idx] = val\n }\n }\n return result\n }\n\n return str.split(\",\").map((x) => Number(x))\n}\n\nexport function encodeStringArray(arr: string[], delimiter = \",\"): string {\n return arr.join(delimiter)\n}\n\nexport function decodeStringArray(str: string, delimiter = \",\"): string[] {\n if (!str) return []\n return str.split(delimiter)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON can encode any data\nexport function encodeJson(data: any): string {\n const json = JSON.stringify(data)\n return compressToEncodedURIComponent(json)\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON can decode to any data\nexport function decodeJson(str: string): any {\n if (!str) return undefined\n const json = decompressFromEncodedURIComponent(str)\n return json ? JSON.parse(json) : undefined\n}\n\nexport function encodeSparseMap<T>(map: Record<string, T>, encodeValue: (val: T) => string, orderedKeys?: string[]): string {\n const entries = Object.entries(map).filter(([_, val]) => val !== null && val !== undefined)\n\n if (orderedKeys) {\n // Use numeric indices instead of string keys\n const indexedEntries = entries\n .map(([key, val]) => {\n const idx = orderedKeys.indexOf(key)\n if (idx === -1) return null\n return `${idx}=${encodeValue(val)}`\n })\n .filter((entry): entry is string => entry !== null)\n\n return indexedEntries.join(\",\")\n } else {\n // Fall back to string keys\n return entries.map(([key, val]) => `${key}=${encodeValue(val)}`).join(\";\")\n }\n}\n\nexport function decodeSparseMap<T>(str: string, decodeValue: (val: string) => T, orderedKeys?: string[]): Record<string, T> {\n if (!str) return {}\n\n const result: Record<string, T> = {}\n\n if (orderedKeys) {\n // Decode numeric indices\n const entries = str.split(\",\")\n for (const entry of entries) {\n if (!entry) continue\n const [idxStr, val] = entry.split(\"=\")\n const idx = Number(idxStr)\n if (!isNaN(idx) && val && orderedKeys[idx]) {\n result[orderedKeys[idx]] = decodeValue(val)\n }\n }\n } else {\n // Decode string keys\n const entries = str.split(\";\")\n for (const entry of entries) {\n if (!entry) continue\n const [key, val] = entry.split(\"=\")\n if (key && val) {\n result[key] = decodeValue(val)\n }\n }\n }\n\n return result\n}\n","import type { Schema, FieldType } from \"./types\"\nimport { encodeBitArrayToHex, encodeIntArray, encodeStringArray, encodeSparseMap, encodeJson } from \"./encodings\"\n\nexport function encodeField(value: any, fieldType: FieldType, context?: any): string {\n switch (fieldType.type) {\n case \"bitArray\":\n return encodeBitArrayToHex(value)\n\n case \"intArray\":\n return encodeIntArray(value)\n\n case \"int\":\n return String(value)\n\n case \"string\":\n return String(value)\n\n case \"stringArray\":\n return encodeStringArray(value, fieldType.delimiter)\n\n case \"sparseMap\":\n return encodeSparseMap(value, fieldType.valueEncoder, context?.keys)\n\n case \"json\":\n return encodeJson(value)\n\n default:\n throw new Error(`Unknown field type: ${(fieldType as any).type}`)\n }\n}\n\nexport function encode<TData>(schema: Schema<TData>, data: TData, context?: Record<string, any>): string {\n const delimiter = schema.delimiter || \":\"\n const fieldNames = Object.keys(schema.fields) as (keyof TData)[]\n\n const encodedFields = fieldNames.map((fieldName) => {\n const fieldType = schema.fields[fieldName]\n const value = data[fieldName]\n const fieldContext = context?.[fieldName as string]\n return encodeField(value, fieldType, fieldContext)\n })\n\n return `v${schema.version}${delimiter}${encodedFields.join(delimiter)}`\n}\n","import type { Schema, FieldType } from \"./types\"\nimport { decodeBitArrayFromHex, decodeIntArray, decodeStringArray, decodeSparseMap, decodeJson } from \"./encodings\"\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context is dynamic and return type depends on fieldType\nexport function decodeField(value: string, fieldType: FieldType, context?: any): any {\n switch (fieldType.type) {\n case \"bitArray\":\n if (!context?.length) {\n throw new Error(\"bitArray requires length in context\")\n }\n return decodeBitArrayFromHex(value, context.length)\n\n case \"intArray\":\n return decodeIntArray(value, context?.length)\n\n case \"int\":\n return Number(value)\n\n case \"string\":\n return value\n\n case \"stringArray\":\n return decodeStringArray(value, fieldType.delimiter)\n\n case \"sparseMap\":\n return decodeSparseMap(value, fieldType.valueDecoder, context?.keys)\n\n case \"json\":\n return decodeJson(value)\n\n default:\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- used in error message for invalid type\n throw new Error(`Unknown field type: ${(fieldType as any).type}`)\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context values are dynamic\nexport function decode<TData>(schema: Schema<TData>, inputString: string, context?: Record<string, any>): TData {\n const delimiter = schema.delimiter || \":\"\n const parts = inputString.split(delimiter)\n\n const versionPart = parts[0]\n const versionMatch = versionPart.match(/^v(\\d+)$/)\n if (!versionMatch) {\n throw new Error(`Invalid input string format: ${versionPart}`)\n }\n\n const version = Number(versionMatch[1])\n if (version !== schema.version) {\n throw new Error(`Version mismatch: expected v${schema.version}, got v${version}. ` + `Make sure to run migrations first.`)\n }\n\n const fieldNames = Object.keys(schema.fields) as (keyof TData)[]\n const data = {} as TData\n\n for (let i = 0; i < fieldNames.length; i++) {\n const fieldName = fieldNames[i]\n const fieldType = schema.fields[fieldName]\n const fieldValue = parts[i + 1] || \"\"\n\n const fieldContext = context?.[fieldName as string]\n data[fieldName] = decodeField(fieldValue, fieldType, fieldContext)\n }\n\n return data\n}\n","import type { Schema } from \"./types\"\nimport { encode } from \"./encoder\"\nimport { decode } from \"./decoder\"\n\nexport function createMigrator<TData>(schema: Schema<TData>) {\n return function migrate(inputString: string, context?: Record<string, any>): string {\n const versionMatch = inputString.match(/^v(\\d+)/)\n if (!versionMatch) {\n throw new Error(\"Invalid input string: no version found\")\n }\n\n const currentVersion = Number(versionMatch[1])\n\n if (currentVersion === schema.version) {\n return inputString\n }\n\n if (!schema.migrations) {\n throw new Error(`No migrations defined for schema. Cannot migrate from v${currentVersion} to v${schema.version}`)\n }\n\n // Data type changes at each migration step (v0 → v1 → v2, etc.)\n // TypeScript can't track these transformations statically, so we use `any`\n // Migrations are developer-written and tested to maintain type safety through the chain\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let data: any = decode(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n { ...schema, version: currentVersion } as Schema<any>,\n inputString,\n context,\n )\n\n for (let v = currentVersion; v < schema.version; v++) {\n const migration = schema.migrations[v]\n if (migration) {\n data = migration(data)\n }\n }\n\n return encode(schema, data, context)\n }\n}\n","/**\n * Encodes boolean arrays as compact hexadecimal strings.\n *\n * **When to use:**\n *\n * - Revealed/hidden state of game tiles\n * - Feature flags or settings\n * - Any boolean array where order matters\n *\n * **Encoding:**\n *\n * - Packs 4 bits per hex character (75% size reduction)\n * - Example: [true, false, false, false, false, true, false, false] → \"12\"\n *\n * **Decoding:**\n *\n * - Requires length in context: `{ fieldName: { length: 100 } }`\n */\nexport type BitArrayFieldType = {\n type: \"bitArray\"\n}\n\n/**\n * Encodes integer arrays with automatic sparse optimization.\n *\n * **When to use:**\n *\n * - Arrays of numeric values (scores, counts, experience)\n * - Sparse data (mostly zeros)\n * - Dense data (will auto-fallback to CSV)\n *\n * **Encoding:**\n *\n * - Uses decimal (base-10) numbers separated by commas\n * - Smart sparse: If >50% zeros, uses `index=value` pairs (e.g., \"2=5,4=3\")\n * - Dense fallback: If not beneficial, uses CSV (e.g., \"1,2,3,4\")\n * - Empty arrays encode as empty string\n *\n * **Decoding:**\n *\n * - Requires length in context for sparse format: `{ fieldName: { length: 100 } }`\n * - CSV format decodes without context\n */\nexport type IntArrayFieldType = {\n type: \"intArray\"\n}\n\n/**\n * Encodes single integer values.\n *\n * **When to use:**\n *\n * - Single numeric values (score, level, count)\n * - Non-negative integers only\n *\n * **Encoding:**\n *\n * - Uses decimal (base-10) representation\n * - Example: 42 → \"42\"\n */\nexport type IntFieldType = {\n type: \"int\"\n}\n\n/**\n * Encodes string values as-is.\n *\n * **When to use:**\n *\n * - Single string values (name, color, mode)\n * - Short identifiers\n *\n * **Warning:**\n *\n * - Strings containing the schema delimiter (default ':') will break parsing\n * - Consider using a custom delimiter or different encoding for such strings\n */\nexport type StringFieldType = {\n type: \"string\"\n}\n\n/**\n * Encodes arrays of strings.\n *\n * **When to use:**\n *\n * - Lists of tags, categories, or identifiers\n * - Ordered collections of strings\n *\n * **Encoding:**\n *\n * - Uses comma delimiter by default\n * - Custom delimiter can be specified to avoid conflicts\n *\n * **Warning:**\n *\n * - Strings containing the delimiter will break parsing\n * - Use custom delimiter to avoid conflicts\n */\nexport type StringArrayFieldType = {\n type: \"stringArray\"\n delimiter?: string\n}\n\n/**\n * Encodes sparse key-value maps with custom value encoding.\n *\n * **When to use:**\n *\n * - Tile annotations, cell metadata\n * - Any sparse mapping where most keys don't have values\n * - Custom object types that need specialized encoding\n *\n * **Encoding:**\n *\n * - With context keys: Uses numeric indices (e.g., `0=2.1,5=3.0`)\n * - Without context: Uses string keys (e.g., `key1=val1;key2=val2`)\n * - Only stores non-empty entries\n * - Keys are always strings in the data, converted to indices when context provides ordered keys\n * - ValueEncoder: Convert your object to a string\n * - ValueDecoder: Parse string back to your object\n *\n * **Optimization:**\n * Provide ordered keys in context to use numeric indices instead of string keys:\n *\n * ```typescript\n * const { encode, decode } = createEncoder(schema, {\n * annotations: { keys: tileIds }, // Ordered list of possible keys\n * })\n * ```\n *\n * **Example:**\n *\n * ```typescript\n * annotations: {\n * type: 'sparseMap',\n * valueEncoder: (ann) => `${ann.level}.${ann.color}`,\n * valueDecoder: (str) => {\n * const [level, color] = str.split('.').map(Number)\n * return { level, color }\n * }\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for value type\nexport type SparseMapFieldType<TValue = any> = {\n type: \"sparseMap\"\n valueEncoder: (value: TValue) => string\n valueDecoder: (str: string) => TValue\n}\n\n/**\n * Encodes arbitrary data as JSON with automatic compression.\n *\n * **When to use:**\n *\n * - Complex nested structures\n * - Fallback for data that doesn't fit other types\n * - Temporary solution before implementing custom encoding\n *\n * **Encoding:**\n *\n * - Converts data to JSON string\n * - Automatically compresses with lz-string\n * - Helps reduce size of complex objects\n *\n * **Warning:**\n *\n * - Still larger than specialized encodings even with compression\n * - Use only when necessary\n * - Consider creating custom sparseMap encoding instead\n */\nexport type JsonFieldType = {\n type: \"json\"\n}\n\n/** Union of all available field types. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for field values\nexport type FieldType<TValue = any> =\n | BitArrayFieldType\n | IntArrayFieldType\n | IntFieldType\n | StringFieldType\n | StringArrayFieldType\n | SparseMapFieldType<TValue>\n | JsonFieldType\n\n/**\n * Function that migrates data from one version to another.\n *\n * **When to use:**\n *\n * - Add new fields with default values\n * - Transform existing field values\n * - Rename or restructure fields\n *\n * **Important:**\n *\n * - Migrations operate on decoded data objects, not strings\n * - Each migration goes from version N to version N+1\n * - Migrations are applied sequentially\n *\n * **Example:**\n *\n * ```typescript\n * migrations: {\n * 0: (oldData) => ({\n * ...oldData,\n * newField: 'default value'\n * }),\n * 1: (oldData) => ({\n * ...oldData,\n * name: oldData.name.toUpperCase()\n * })\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameters for migration input/output\nexport type DataMigration<TFrom = any, TTo = any> = (data: TFrom) => TTo\n\n/**\n * Map of version numbers to migration functions.\n *\n * Keys are the source version numbers (0, 1, 2, etc.)\n * Values are functions that migrate from that version to the next\n */\nexport type MigrationMap = {\n [fromVersion: number]: DataMigration\n}\n\n/**\n * Complete schema definition for encoding/decoding data.\n *\n * **Basic usage:**\n *\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * name: { type: \"string\" },\n * age: { type: \"int\" },\n * },\n * })\n * ```\n *\n * **With custom delimiter:**\n *\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * delimiter: '|', // Use | instead of :\n * fields: { ... }\n * })\n * ```\n *\n * **With migrations:**\n *\n * ```typescript\n * const schema = defineSchema({\n * version: 2,\n * fields: {\n * name: { type: \"string\" },\n * value: { type: \"int\" },\n * },\n * migrations: {\n * 0: (old) => ({ ...old, value: 0 }),\n * 1: (old) => ({ ...old, name: old.name.toUpperCase() }),\n * },\n * })\n * ```\n *\n * @param version - Current schema version (increment when making breaking changes)\n * @param delimiter - Character to separate fields (default: ':')\n * @param fields - Map of field names to field type definitions\n * @param migrations - Optional map of version migrations\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Generic type parameter for schema data, and field values need 'any' for flexibility\nexport type Schema<TData = any> = {\n version: number\n delimiter?: string\n fields: {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field values can be any type\n [K in keyof TData]: FieldType<any>\n }\n migrations?: MigrationMap\n}\n\n/**\n * Result of creating an encoder from a schema.\n *\n * **Usage:**\n *\n * ```typescript\n * const { encode, decode, migrate } = createEncoder(schema, context)\n *\n * // Encode data to string\n * const inputString = encode(data)\n *\n * // Decode string to data\n * const data = decode(inputString)\n *\n * // Migrate old version to current version\n * const currentVersion = migrate(oldInputString)\n * ```\n */\nexport type EncoderResult<TData> = {\n encode: (data: TData) => string\n decode: (str: string) => TData\n migrate: (str: string) => string\n}\n\n/**\n * Define a typed schema for encoding/decoding game state.\n *\n * **Purpose:**\n *\n * - Provides type inference for encode/decode functions\n * - Documents the structure of your input strings\n * - Enables version migrations\n *\n * **Example:**\n *\n * ```typescript\n * type GameData = {\n * revealed: boolean[]\n * score: number\n * annotations: Record<string, Annotation>\n * }\n *\n * export const schema = defineSchema<GameData>({\n * version: 1,\n * fields: {\n * revealed: { type: \"bitArray\" },\n * score: { type: \"int\" },\n * annotations: {\n * type: \"sparseMap\",\n * valueEncoder: (ann) => `${ann.level}.${ann.color}`,\n * valueDecoder: (str) => {\n * const [level, color] = str.split(\".\").map(Number)\n * return { level, color }\n * },\n * },\n * },\n * })\n * ```\n */\nexport function defineSchema<TData>(schema: Schema<TData>): Schema<TData> {\n return schema\n}\n","import type { Schema, EncoderResult } from \"./types\"\nimport { encode } from \"./encoder\"\nimport { decode } from \"./decoder\"\nimport { createMigrator } from \"./migration\"\n\nexport { defineSchema } from \"./types\"\nexport type {\n Schema,\n EncoderResult,\n FieldType,\n BitArrayFieldType,\n IntArrayFieldType,\n IntFieldType,\n StringFieldType,\n StringArrayFieldType,\n SparseMapFieldType,\n JsonFieldType,\n DataMigration,\n MigrationMap,\n} from \"./types\"\n\n/**\n * Create an encoder/decoder for a schema-defined data structure.\n *\n * **Basic usage:**\n *\n * ```typescript\n * import { defineSchema, createEncoder } from \"@puzzmo/sdk/inputs\"\n *\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * name: { type: \"string\" },\n * age: { type: \"int\" },\n * },\n * })\n *\n * const { encode, decode } = createEncoder(schema)\n *\n * const data = { name: \"Alice\", age: 30 }\n * const inputString = encode(data) // \"v1:Alice:30\"\n * const decoded = decode(inputString) // { name: 'Alice', age: 30 }\n * ```\n *\n * **With context (for length-dependent fields):**\n *\n * ```typescript\n * const schema = defineSchema({\n * version: 1,\n * fields: {\n * revealed: { type: \"bitArray\" },\n * scores: { type: \"intArray\" },\n * },\n * })\n *\n * const { encode, decode } = createEncoder(schema, {\n * revealed: { length: 100 }, // Required for bitArray decoding\n * scores: { length: 10 }, // Required for sparse intArray decoding\n * annotations: { keys: tileIds }, // Optional: optimize sparseMap with numeric indices\n * })\n * ```\n *\n * **With migrations:**\n *\n * ```typescript\n * const { encode, decode, migrate } = createEncoder(schema)\n *\n * const oldString = \"v0:Alice:30\"\n * const migrated = migrate(oldString) // \"v1:Alice:30:default\"\n * const data = decode(migrated)\n * ```\n *\n * @param schema - Schema definition created with defineSchema()\n * @param context - Optional context for decoding (lengths for bitArray/intArray)\n * @returns Object with encode, decode, and migrate functions\n */\nexport function createEncoder<TData>(\n schema: Schema<TData>,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Context values can be any shape depending on field types\n context?: Record<string, any>,\n): EncoderResult<TData> {\n const migrator = createMigrator(schema)\n\n return {\n encode: (data: TData) => encode(schema, data, context),\n decode: (str: string) => decode(schema, str, context),\n migrate: (str: string) => migrator(str, context),\n }\n}\n"],"mappings":"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"}
@@ -5,47 +5,50 @@ export type { Schema, EncoderResult, FieldType, BitArrayFieldType, IntArrayField
5
5
  * Create an encoder/decoder for a schema-defined data structure.
6
6
  *
7
7
  * **Basic usage:**
8
+ *
8
9
  * ```typescript
9
- * import { defineSchema, createEncoder } from '@puzzmo/sdk/inputs'
10
+ * import { defineSchema, createEncoder } from "@puzzmo/sdk/inputs"
10
11
  *
11
12
  * const schema = defineSchema({
12
13
  * version: 1,
13
14
  * fields: {
14
- * name: { type: 'string' },
15
- * age: { type: 'int' }
16
- * }
15
+ * name: { type: "string" },
16
+ * age: { type: "int" },
17
+ * },
17
18
  * })
18
19
  *
19
20
  * const { encode, decode } = createEncoder(schema)
20
21
  *
21
- * const data = { name: 'Alice', age: 30 }
22
- * const inputString = encode(data) // "v1:Alice:30"
23
- * const decoded = decode(inputString) // { name: 'Alice', age: 30 }
22
+ * const data = { name: "Alice", age: 30 }
23
+ * const inputString = encode(data) // "v1:Alice:30"
24
+ * const decoded = decode(inputString) // { name: 'Alice', age: 30 }
24
25
  * ```
25
26
  *
26
27
  * **With context (for length-dependent fields):**
28
+ *
27
29
  * ```typescript
28
30
  * const schema = defineSchema({
29
31
  * version: 1,
30
32
  * fields: {
31
- * revealed: { type: 'bitArray' },
32
- * scores: { type: 'intArray' }
33
- * }
33
+ * revealed: { type: "bitArray" },
34
+ * scores: { type: "intArray" },
35
+ * },
34
36
  * })
35
37
  *
36
38
  * const { encode, decode } = createEncoder(schema, {
37
- * revealed: { length: 100 }, // Required for bitArray decoding
38
- * scores: { length: 10 }, // Required for sparse intArray decoding
39
- * annotations: { keys: tileIds } // Optional: optimize sparseMap with numeric indices
39
+ * revealed: { length: 100 }, // Required for bitArray decoding
40
+ * scores: { length: 10 }, // Required for sparse intArray decoding
41
+ * annotations: { keys: tileIds }, // Optional: optimize sparseMap with numeric indices
40
42
  * })
41
43
  * ```
42
44
  *
43
45
  * **With migrations:**
46
+ *
44
47
  * ```typescript
45
48
  * const { encode, decode, migrate } = createEncoder(schema)
46
49
  *
47
50
  * const oldString = "v0:Alice:30"
48
- * const migrated = migrate(oldString) // "v1:Alice:30:default"
51
+ * const migrated = migrate(oldString) // "v1:Alice:30:default"
49
52
  * const data = decode(migrated)
50
53
  * ```
51
54
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/inputs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAKpD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,YAAY,EACV,MAAM,EACN,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,aAAa,CAAC,KAAK,EACjC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAErB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,aAAa,CAAC,KAAK,CAAC,CAQtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/inputs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAKpD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,YAAY,EACV,MAAM,EACN,aAAa,EACb,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,aAAa,CAAC,KAAK,EACjC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAErB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,aAAa,CAAC,KAAK,CAAC,CAQtB"}
@@ -143,11 +143,13 @@ function _(t) {
143
143
  * Define a typed schema for encoding/decoding game state.
144
144
  *
145
145
  * **Purpose:**
146
+ *
146
147
  * - Provides type inference for encode/decode functions
147
148
  * - Documents the structure of your input strings
148
149
  * - Enables version migrations
149
150
  *
150
151
  * **Example:**
152
+ *
151
153
  * ```typescript
152
154
  * type GameData = {
153
155
  * revealed: boolean[]
@@ -158,17 +160,17 @@ function _(t) {
158
160
  * export const schema = defineSchema<GameData>({
159
161
  * version: 1,
160
162
  * fields: {
161
- * revealed: { type: 'bitArray' },
162
- * score: { type: 'int' },
163
+ * revealed: { type: "bitArray" },
164
+ * score: { type: "int" },
163
165
  * annotations: {
164
- * type: 'sparseMap',
166
+ * type: "sparseMap",
165
167
  * valueEncoder: (ann) => `${ann.level}.${ann.color}`,
166
168
  * valueDecoder: (str) => {
167
- * const [level, color] = str.split('.').map(Number)
169
+ * const [level, color] = str.split(".").map(Number)
168
170
  * return { level, color }
169
- * }
170
- * }
171
- * }
171
+ * },
172
+ * },
173
+ * },
172
174
  * })
173
175
  * ```
174
176
  */
@@ -179,47 +181,50 @@ function v(e) {
179
181
  * Create an encoder/decoder for a schema-defined data structure.
180
182
  *
181
183
  * **Basic usage:**
184
+ *
182
185
  * ```typescript
183
- * import { defineSchema, createEncoder } from '@puzzmo/sdk/inputs'
186
+ * import { defineSchema, createEncoder } from "@puzzmo/sdk/inputs"
184
187
  *
185
188
  * const schema = defineSchema({
186
189
  * version: 1,
187
190
  * fields: {
188
- * name: { type: 'string' },
189
- * age: { type: 'int' }
190
- * }
191
+ * name: { type: "string" },
192
+ * age: { type: "int" },
193
+ * },
191
194
  * })
192
195
  *
193
196
  * const { encode, decode } = createEncoder(schema)
194
197
  *
195
- * const data = { name: 'Alice', age: 30 }
196
- * const inputString = encode(data) // "v1:Alice:30"
197
- * const decoded = decode(inputString) // { name: 'Alice', age: 30 }
198
+ * const data = { name: "Alice", age: 30 }
199
+ * const inputString = encode(data) // "v1:Alice:30"
200
+ * const decoded = decode(inputString) // { name: 'Alice', age: 30 }
198
201
  * ```
199
202
  *
200
203
  * **With context (for length-dependent fields):**
204
+ *
201
205
  * ```typescript
202
206
  * const schema = defineSchema({
203
207
  * version: 1,
204
208
  * fields: {
205
- * revealed: { type: 'bitArray' },
206
- * scores: { type: 'intArray' }
207
- * }
209
+ * revealed: { type: "bitArray" },
210
+ * scores: { type: "intArray" },
211
+ * },
208
212
  * })
209
213
  *
210
214
  * const { encode, decode } = createEncoder(schema, {
211
- * revealed: { length: 100 }, // Required for bitArray decoding
212
- * scores: { length: 10 }, // Required for sparse intArray decoding
213
- * annotations: { keys: tileIds } // Optional: optimize sparseMap with numeric indices
215
+ * revealed: { length: 100 }, // Required for bitArray decoding
216
+ * scores: { length: 10 }, // Required for sparse intArray decoding
217
+ * annotations: { keys: tileIds }, // Optional: optimize sparseMap with numeric indices
214
218
  * })
215
219
  * ```
216
220
  *
217
221
  * **With migrations:**
222
+ *
218
223
  * ```typescript
219
224
  * const { encode, decode, migrate } = createEncoder(schema)
220
225
  *
221
226
  * const oldString = "v0:Alice:30"
222
- * const migrated = migrate(oldString) // "v1:Alice:30:default"
227
+ * const migrated = migrate(oldString) // "v1:Alice:30:default"
223
228
  * const data = decode(migrated)
224
229
  * ```
225
230
  *