Skip to content

Commit 000a3bf

Browse files
Sync packages from monorepo (40c7524)
1 parent 47c7fcd commit 000a3bf

10 files changed

Lines changed: 290 additions & 150 deletions

File tree

packages/sdk/README.md

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1-
# @puzzmo/sdk
1+
# Puzzmo Game Infra
22

3-
SDK for building games on the Puzzmo platform. Handles communication between your game and the Puzzmo host (puzzmo.com, embeds, native apps).
3+
Over the years we have a pretty refined route for making web games: prototype in HTML with no concern for code quality, share with people you like, then migrate it to a "real" codebase.
4+
5+
LLMs have changed this, and we're finding that the 'prototype in HTML' phase is getting close enough to production quality that it does not always warrant a a multi-month conversion to React/TypeScript/Redux to ensure the codebase can live forever.
6+
7+
So, after shipping two full games with this pipeline, we've knocked enough kinks out that it's ready for a more public space.
8+
9+
So, how do you make a game? Well first, you need a game - we can't help there! However, once you do, then you can start migrating it to run on Puzzmo by: `yarn create puzzmo game`.
10+
11+
## @puzzmo/sdk
12+
13+
This repo is the SDK for building games on the Puzzmo platform. Handles communication between your game and Puzzmo.
14+
15+
There are a few parts here!
16+
17+
1. The SDK, e.g. the runtime API for launching a game, completing it and other essentials
18+
2. The simulator, which provides the same message sending infrastructure Puzzmo.com will send to your game
19+
3. App integration information, e.g. how to make custom thumbnails for your puzzles (and more)
20+
4. Vite plugins which can help you get up and running faster
421

522
## Install
623

724
```bash
825
npm install @puzzmo/sdk
26+
27+
yarn add @puzzmo/sdk
928
```
1029

1130
## Quick Start
@@ -43,33 +62,33 @@ Creates an SDK instance. Options:
4362

4463
### Lifecycle
4564

46-
| Method | Description |
47-
| -------------------------- | -------------------------------------------------------------------------- |
48-
| `sdk.gameReady()` | Async. Signals readiness, returns puzzle data and theme. |
49-
| `sdk.gameLoaded(state?)` | Signals game UI is ready. Host will send `start`. |
50-
| `sdk.on(event, handler)` | Listen for events: `start`, `pause`, `resume`, `retry`, `settingsUpdate`. |
51-
| `sdk.off(event, handler)` | Remove an event listener. |
65+
| Method | Description |
66+
| ------------------------- | ------------------------------------------------------------------------- |
67+
| `sdk.gameReady()` | Async. Signals readiness, returns puzzle data and theme. |
68+
| `sdk.gameLoaded(state?)` | Signals game UI is ready. Host will send `start`. |
69+
| `sdk.on(event, handler)` | Listen for events: `start`, `pause`, `resume`, `retry`, `settingsUpdate`. |
70+
| `sdk.off(event, handler)` | Remove an event listener. |
5271

5372
### Game State
5473

55-
| Method | Description |
56-
| ----------------------------------------------------------- | ---------------------------------------------------------- |
57-
| `sdk.updateGameState(stateString, play?)` | Save current game state for persistence. |
58-
| `sdk.gameCompleted(play, config?)` | Signal game completion with metrics and deeds. |
59-
| `sdk.showCompletionScreen(results, gameplay, showRetry?)` | Show the Puzzmo completion UI. |
60-
| `sdk.hitCheckpoint(name, config, augConfig?)` | Signal a gameplay milestone (for ads, leaderboards). |
74+
| Method | Description |
75+
| --------------------------------------------------------- | ---------------------------------------------------- |
76+
| `sdk.updateGameState(stateString, play?)` | Save current game state for persistence. |
77+
| `sdk.gameCompleted(play, config?)` | Signal game completion with metrics and deeds. |
78+
| `sdk.showCompletionScreen(results, gameplay, showRetry?)` | Show the Puzzmo completion UI. |
79+
| `sdk.hitCheckpoint(name, config, augConfig?)` | Signal a gameplay milestone (for ads, leaderboards). |
6180

6281
### Timer
6382

6483
The SDK manages a timer automatically (starts on `start`, pauses on `pause`, resets on `retry`).
6584

6685
```ts
67-
sdk.timer.timeMs() // Elapsed time in ms
68-
sdk.timer.timeSecs() // Elapsed time in seconds
69-
sdk.timer.display() // ["1:23", "0:05"] (elapsed, penalty)
70-
sdk.timer.addPenalty(5000) // Add 5s penalty
71-
sdk.timer.isPaused() // Check if paused
72-
sdk.timer.isRunning() // Check if running
86+
sdk.timer.timeMs() // Elapsed time in ms
87+
sdk.timer.timeSecs() // Elapsed time in seconds
88+
sdk.timer.display() // ["1:23", "0:05"] (elapsed, penalty)
89+
sdk.timer.addPenalty(5000) // Add 5s penalty
90+
sdk.timer.isPaused() // Check if paused
91+
sdk.timer.isRunning() // Check if running
7392
```
7493

7594
## Theme
@@ -80,14 +99,14 @@ The `theme` object from `gameReady()` contains color tokens for the current Puzz
8099
const { theme } = await sdk.gameReady()
81100

82101
// Key colors
83-
theme.g_bg // Game background
84-
theme.fg // Foreground text
85-
theme.key // Primary accent
86-
theme.player // Player color (blue)
87-
theme.alt1 // Accent green
88-
theme.alt2 // Accent yellow
89-
theme.alt3 // Accent purple
90-
theme.type // "light" or "dark"
102+
theme.g_bg // Game background
103+
theme.fg // Foreground text
104+
theme.key // Primary accent
105+
theme.player // Player color (blue)
106+
theme.alt1 // Accent green
107+
theme.alt2 // Accent yellow
108+
theme.alt3 // Accent purple
109+
theme.type // "light" or "dark"
91110
```
92111

93112
See the `Theme` type export for the full list of tokens.
@@ -101,23 +120,42 @@ sdk.gameCompleted(metrics, {
101120
deeds: [
102121
{ id: "moves", value: 42 },
103122
{ id: "accuracy", value: 95 },
104-
{ id: "streak", value: 8 },
123+
{ id: "hit-streak", value: 8 },
105124
],
106125
})
107126
```
108127

109128
The SDK automatically adds `points` and `time` deeds.
110129

111-
## Workshop Types
130+
## App Integration
131+
132+
For games to show a dynamic thumbnail, you will need an App Bundle
133+
134+
```ts
135+
import type { EditorBundle, ValidationReport } from "@puzzmo/sdk"
136+
import { puzzleToSVG } from "./src/puzzleToSVG"
137+
138+
export const AppBundle = {
139+
renderThumbnail(puzzleString, boardState, config) {
140+
return puzzleToSVG(puzzleString, boardState, config)
141+
},
142+
} satisfies EditorBundle
143+
```
144+
145+
This and the editor bundle are separate JavaScript files from your main game.
146+
147+
There are Vite plugins to make this easy, but otherwise, they should be files in your upload named `app-bundle.js` and `editor-bundle.js` with ESM exports which match the shapes of the TypeScript types.
148+
149+
## Editor Integration
112150

113-
For games that support puzzle editing in Puzzmo Workshop:
151+
For games that support puzzle editing in Puzzmo Workshop, you will need an Editor Bundle:
114152

115153
```ts
116-
import type { WorkshopBundle, ValidationReport } from "@puzzmo/sdk"
154+
import type { EditorBundle, ValidationReport } from "@puzzmo/sdk"
117155

118156
export const validator = {
119-
validate(data: string): ValidationReport {
157+
validate(data) {
120158
return { success: true, issues: [] }
121159
},
122-
}
160+
} satisfies EditorBundle
123161
```
Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ export interface ValidationReport {
1919
export type ImportErrorType = "invalid_format" | "parsing_error" | "unknown"
2020

2121
/** Custom error class for workshop import failures */
22-
export class WorkshopImportError extends Error {
22+
export class EditorImportError extends Error {
2323
constructor(
2424
public type: ImportErrorType,
2525
message: string,
2626
public originalError?: unknown,
2727
) {
2828
super(message)
29-
this.name = "WorkshopImportError"
29+
this.name = "EditorImportError"
3030
}
3131
}
3232

@@ -39,12 +39,26 @@ export interface ImportResult {
3939
editors?: string[]
4040
}
4141

42+
/** Settings UI descriptor returned by an editor bundle */
43+
export interface EditorBundleSettings<TComponent = unknown> {
44+
components: TComponent[]
45+
defaults: Record<string, unknown>
46+
}
47+
4248
/** Main interface for a Workshop bundle */
43-
export interface WorkshopBundle {
49+
export interface EditorBundle<TSettingsComponent = unknown> {
4450
validator: {
4551
validate(data: string): Promise<ValidationReport> | ValidationReport
4652
}
4753
importer?: {
4854
onImport(filename: string, contents: string | ArrayBuffer): Promise<ImportResult> | ImportResult
4955
}
56+
/** Embed-level settings UI, populated from the bundle's declared settings */
57+
settings?: EditorBundleSettings<TSettingsComponent>
58+
/** Editor-level settings UI, populated from the bundle's declared editor settings */
59+
editorSettings?: EditorBundleSettings<TSettingsComponent>
60+
/** Custom puzzle editor, if provided by the bundle */
61+
editor?: {
62+
mount(...args: unknown[]): unknown
63+
}
5064
}

packages/sdk/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { createPuzzmoSDK } from "./sdk"
2+
23
export type { SDK as PuzzmoSDK, PuzzmoSDKOptions, SDKEventMap, SDKEventType, SDKTimer } from "./sdk"
4+
35
export type {
46
Theme,
57
GamePlay,
@@ -11,6 +13,8 @@ export type {
1113
MessagesReceived,
1214
MessagesSentFromEmbed,
1315
ThumbnailConfig,
14-
ThumbnailFunction,
1516
} from "./types"
16-
export * from "./workshop"
17+
18+
export type { ValidationIssue, ValidationReport, ImportErrorType, ImportResult, EditorBundle } from "./editor"
19+
20+
export { EditorImportError } from "./editor"

packages/sdk/src/sdk.ts

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import type {
2-
MessagesSentFromEmbed,
3-
MessagesReceived,
4-
GamePlay,
5-
AugmentationConfig,
6-
CheckpointConfig,
7-
Theme,
8-
Deed,
9-
} from "./types"
1+
import type { MessagesSentFromEmbed, MessagesReceived, GamePlay, AugmentationConfig, CheckpointConfig, Theme, Deed } from "./types"
102

113
export type SDK = ReturnType<typeof createPuzzmoSDK>
124

@@ -159,27 +151,22 @@ function createHostAPI() {
159151
const sendMessage = <T extends keyof SupportedOutgoingMessages>(type: T, json: SupportedOutgoingMessages[T]) => {
160152
const message = { type, json, _: "p", __: "mp", private: true }
161153

162-
if ("parent" in window && window.parent !== window)
163-
window.parent.postMessage(message, "*")
154+
if ("parent" in window && window.parent !== window) window.parent.postMessage(message, "*")
164155

165156
window.postMessage(message, "*")
166157

167-
if ("webkit" in window && (window as any).webkit?.messageHandlers?.app)
168-
(window as any).webkit.messageHandlers.app.postMessage(message)
158+
if ("webkit" in window && (window as any).webkit?.messageHandlers?.app) (window as any).webkit.messageHandlers.app.postMessage(message)
169159

170-
if ("puzzmoMessageString" in window)
171-
(window as any).puzzmoMessageString(JSON.stringify(message))
160+
if ("puzzmoMessageString" in window) (window as any).puzzmoMessageString(JSON.stringify(message))
172161

173162
if ("ReactNativeWebView" in window && (window as any).ReactNativeWebView?.postMessage)
174163
(window as any).ReactNativeWebView.postMessage(JSON.stringify(message))
175164

176-
if (type !== "TIMER_TICK" && type !== "TIMER_SYNC")
177-
console.log("[Puzzmo SDK] sent:", type, json)
165+
if (type !== "TIMER_TICK" && type !== "TIMER_SYNC") console.log("[Puzzmo SDK] sent:", type, json)
178166
}
179167

180168
const onMessage = <T extends keyof SupportedIncomingMessages>(type: T, handler: MessageHandler<T>) => {
181-
if (!messageHandlers.has(type))
182-
messageHandlers.set(type, new Set())
169+
if (!messageHandlers.has(type)) messageHandlers.set(type, new Set())
183170
messageHandlers.get(type)!.add(handler)
184171

185172
return () => {
@@ -194,8 +181,7 @@ function createHostAPI() {
194181
const handlers = messageHandlers.get(msgType)
195182
if (handlers) {
196183
const msgData = event.data.data ?? event.data.json ?? {}
197-
if (msgType !== "TIMER_TICK" && msgType !== "TIMER_SYNC")
198-
console.log("[Puzzmo SDK] received:", msgType, msgData)
184+
if (msgType !== "TIMER_TICK" && msgType !== "TIMER_SYNC") console.log("[Puzzmo SDK] received:", msgType, msgData)
199185
handlers.forEach((handler) => handler(msgData))
200186
}
201187
})
@@ -364,8 +350,7 @@ export const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {
364350
},
365351

366352
on: <T extends SDKEventType>(event: T, listener: (data?: SDKEventMap[T]) => void): (() => void) => {
367-
if (!eventListeners.has(event))
368-
eventListeners.set(event, new Set())
353+
if (!eventListeners.has(event)) eventListeners.set(event, new Set())
369354
eventListeners.get(event)!.add(listener)
370355
return () => {
371356
eventListeners.get(event)?.delete(listener)
@@ -386,13 +371,6 @@ export const createPuzzmoSDK = (options: PuzzmoSDKOptions = {}) => {
386371
boardState: inputString,
387372
elapsedTimeSecs: play?.elapsedTimeSecs ?? internalTimer.timeWithoutPenaltySecs(),
388373
additionalTimeAddedSecs: play?.additionalTimeAddedSecs ?? internalTimer.addedTimeSecs(),
389-
hintsUsed: play?.hintsUsed,
390-
resetsUsed: play?.resetsUsed,
391-
metric1: 0,
392-
metric2: 0,
393-
metric3: 0,
394-
metric4: 0,
395-
metricStrings: [],
396374
collabUserReferences: [],
397375
},
398376
})

packages/sdk/src/simulator/createSimulator.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* - Providing UI controls for START_GAME, PAUSE_GAME, RESUME_GAME, RETRY_PUZZLE
88
*
99
* Usage with Vite plugin (recommended):
10+
*
1011
* ```ts
1112
* // vite.config.ts
1213
* import { puzzmoSimulator } from "@puzzmo/sdk/vite"
@@ -15,6 +16,8 @@
1516
* })
1617
* ```
1718
*
19+
* The plugin handles making sure it is removed on vite builds.
20+
*
1821
* Usage with manual imports:
1922
* ```html
2023
* <script type="module">
@@ -414,11 +417,6 @@ function createSimulator(config: SimulatorConfig = {}): SimulatorInstance {
414417
elapsedTimeSecs: 0,
415418
hintsUsed: 0,
416419
id: `simulator-gameplay-${Date.now()}`,
417-
metric1: 0,
418-
metric2: 0,
419-
metric3: 0,
420-
metric4: 0,
421-
metricStrings: [],
422420
ownerID: "simulator-owner",
423421
pointsAwarded: 0,
424422
resetsUsed: 0,

packages/sdk/src/simulator/views/DataView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ThumbnailConfig, ThumbnailFunction } from "../../types"
1+
import type { AppBundle, ThumbnailConfig } from "../../types"
22
import type { SimulatorContext, SimulatorView } from "../types"
33

44
// Storage key for saved states (global across all puzzles)
@@ -7,11 +7,11 @@ const SAVED_STATES_KEY = "simulator-saved-states"
77
/**
88
* Find thumbnail function on globalThis (looks for functions ending in "Thumbnail")
99
*/
10-
function findThumbnailFn(): { name: string; fn: ThumbnailFunction } | null {
10+
function findThumbnailFn(): { name: string; fn: AppBundle["renderThumbnail"] } | null {
1111
const globalObj = globalThis as Record<string, unknown>
1212
for (const key of Object.keys(globalObj)) {
1313
if (key.endsWith("Thumbnail") && typeof globalObj[key] === "function") {
14-
return { name: key, fn: globalObj[key] as ThumbnailFunction }
14+
return { name: key, fn: globalObj[key] as AppBundle["renderThumbnail"] }
1515
}
1616
}
1717
return null

packages/sdk/src/simulator/views/ThumbView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import type { ThumbnailConfig, ThumbnailFunction } from "../../types"
1+
import type { AppBundle, ThumbnailConfig } from "../../types"
22
import type { SimulatorContext, SimulatorView } from "../types"
33
import { persistRenderContext, persistRenderHost } from "../state"
44

55
/**
66
* Find thumbnail function on globalThis (looks for functions ending in "Thumbnail")
77
*/
8-
function findThumbnailFn(): { name: string; fn: ThumbnailFunction } | null {
8+
function findThumbnailFn(): { name: string; fn: AppBundle["renderThumbnail"] } | null {
99
const globalObj = globalThis as Record<string, unknown>
1010
for (const key of Object.keys(globalObj)) {
1111
if (key.endsWith("Thumbnail") && typeof globalObj[key] === "function") {
12-
return { name: key, fn: globalObj[key] as ThumbnailFunction }
12+
return { name: key, fn: globalObj[key] as AppBundle["renderThumbnail"] }
1313
}
1414
}
1515
return null

0 commit comments

Comments
 (0)