@puzzmo/sdk
v1.0.62
Published
Puzzmo runtime SDK for game developers
Readme
Puzzmo Game Infra
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.
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.
So, after shipping two full games with this pipeline, we've knocked enough kinks out that it's ready for a more public space.
So, how do you make a game? Well first, you need a game idea - we can't help there! However, once you do, then you can start migrating it to run on Puzzmo by: yarn create puzzmo game.
@puzzmo/sdk
This repo is the SDK for building games on the Puzzmo platform. Handles communication between your game and Puzzmo.
There are a few parts here!
- The SDK, e.g. the runtime API for launching a game, completing it and other essentials
- The simulator, which provides the same message sending infrastructure Puzzmo.com will send to your game
- App integration information, e.g. how to make custom thumbnails for your puzzles (and more)
- Vite plugins which can help you get up and running faster
Install
npm install @puzzmo/sdk
yarn add @puzzmo/sdkQuick Start
import { createPuzzmoSDK } from "@puzzmo/sdk"
const sdk = createPuzzmoSDK()
// 1. Wait for puzzle data from Puzzmo
const { puzzleString, inputString, theme, completed } = await sdk.gameReady()
// 2. Set up your game with the puzzle data
const puzzle = JSON.parse(puzzleString)
initializeGame(puzzle)
if (inputString) restoreState(inputString)
// 3. Signal that you're ready
sdk.gameLoaded()
// 4. Listen for lifecycle events
sdk.on("start", () => startGameLoop())
sdk.on("pause", () => pauseGameLoop())
sdk.on("resume", () => resumeGameLoop())
sdk.on("retry", () => resetGame())API
createPuzzmoSDK(options?)
Creates an SDK instance. Options:
timeout- Timeout in ms waiting for puzzle data (default: 5000)
Lifecycle
| Method | Description |
| ------------------------- | ------------------------------------------------------------------------- |
| sdk.gameReady() | Async. Signals readiness, returns puzzle data and theme. |
| sdk.gameLoaded(state?) | Signals game UI is ready. Host will send start. |
| sdk.on(event, handler) | Listen for events: start, pause, resume, retry, settingsUpdate. |
| sdk.off(event, handler) | Remove an event listener. |
Game State
| Method | Description |
| --------------------------------------------------------- | ---------------------------------------------------- |
| sdk.updateGameState(stateString, play?) | Save current game state for persistence. |
| sdk.gameCompleted(play, config?) | Signal game completion with metrics and deeds. |
| sdk.showCompletionScreen(results, gameplay, showRetry?) | Show the Puzzmo completion UI. |
| sdk.hitCheckpoint(name, config, augConfig?) | Signal a gameplay milestone (for ads, leaderboards). |
Timer
The SDK manages a timer automatically (starts on start, pauses on pause, resets on retry).
sdk.timer.timeMs() // Elapsed time in ms
sdk.timer.timeSecs() // Elapsed time in seconds
sdk.timer.display() // ["1:23", "0:05"] (elapsed, penalty)
sdk.timer.addPenalty(5000) // Add 5s penalty
sdk.timer.isPaused() // Check if paused
sdk.timer.isRunning() // Check if runningHaptics
sdk.haptics.play("selection") // on placing a letter
sdk.haptics.play("success", { id: "puzzle-solved" }) // `id` names the cue in host logsNames are the iOS feedback generators: selection, light, medium, heavy, soft, rigid, success, warning, error.
Fire-and-forget — it never reports back, and it is a no-op wherever the host can't deliver one, so never make gameplay depend on a haptic landing:
| Where | What happens |
| --- | --- |
| Puzzmo's iOS app | Real UIKit haptics via the native bridge |
| puzzmo.com on Android/desktop | Approximated with navigator.vibrate |
| Partner embeds | No haptic — see below |
The call goes to the host rather than vibrating from your game because Chrome has blocked navigator.vibrate in cross-origin iframes since Chrome 55, and games run in one. There is no Permissions-Policy opt-in, so calling it yourself is silently ignored. Partner embeds fire no haptic for the same reason: our frame is cross-origin to the partner's page, so the host is blocked too and there is no native bridge to fall back on. The cue is still forwarded to the partner page on the private message stream, so a partner can action it themselves.
Haptics are also off entirely when the player has turned them off in their Puzzmo settings, and the OS drops them in silent/DND or on hardware with no motor.
Theme
The theme object from gameReady() contains color tokens for the current Puzzmo theme:
const { theme } = await sdk.gameReady()
// Key colors
theme.g_bg // Game background
theme.fg // Foreground text
theme.key // Primary accent
theme.player // Player color (blue)
theme.alt1 // Accent green
theme.alt2 // Accent yellow
theme.alt3 // Accent purple
theme.type // "light" or "dark"See the Theme type export for the full list of tokens.
Deeds
Deeds are gameplay statistics sent on completion:
sdk.gameCompleted(metrics, {
deeds: [
{ id: "moves", value: 42 },
{ id: "accuracy", value: 95 },
{ id: "hit-streak", value: 8 },
],
})The SDK automatically adds points and time deeds.
On-Screen Keyboard
For games that need text input on touch devices, the SDK can show Puzzmo's on-screen keyboard. The keyboard is only visible on touch devices — calling these methods on desktop is a no-op.
import { createPuzzmoSDK, defaultKeyboardConfig } from "@puzzmo/sdk"
const sdk = createPuzzmoSDK()
// Show the keyboard when the player selects an input
sdk.keyboard.show(defaultKeyboardConfig)
// Listen for key presses
sdk.on("keyboardKeyPress", ({ key }) => {
if (key === "⌫") handleBackspace()
else if (key === "↵") handleEnter()
else handleLetter(key)
})
// Hide when input is dismissed
sdk.keyboard.hide()defaultKeyboardConfig is a standard QWERTY layout with Enter and Backspace. Customize it by spreading:
sdk.keyboard.show({ ...defaultKeyboardConfig,
// Disable letters that are no longer valid given the current game state
disabled: usedLetters })
// Override inidividual keys with their own styles for emphasis
individualKeyStyles: keyStylesFromRules([
{ keys: ["?"], background: { backgroundColor: "#ffd54a" } },
{ keys: usedLetters, text: { opacity: "0.4", transition: "opacity 200ms ease-out" } },
]),For games with a spatial input model (e.g. selecting a grid cell by dragging across the keyboard), enable drag cursor support and listen for the additional events:
sdk.keyboard.show({ ...defaultKeyboardConfig, supportsDragCursor: true })
sdk.on("keyboardCursorChange", ({ position }) => highlightCellAtPosition(position))
sdk.on("keyboardCursorEnd", () => confirmCellSelection())See the KeyboardConfig type export and the add-keyboard-support skill for full field documentation.
App Integration
For games to show a dynamic thumbnail, you will need an App Bundle
import type { EditorBundle, ValidationReport } from "@puzzmo/sdk"
import { puzzleToSVG } from "./src/puzzleToSVG"
export const AppBundle = {
renderThumbnail(puzzleString, inputString, config) {
return puzzleToSVG(puzzleString, inputString, config)
},
} satisfies EditorBundleThis and the editor bundle are separate JavaScript files from your main game.
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.
Thumbnail JSX
We have found over time that using JSX for thumbnails makes it a lot easier to ensure correct SVG output, but React/Preact are big runtimes, so we have a smaller JSX runtime built just for non-interactive SVGs based on understated.
To use it, point your tsconfig's JSX factory at h. Your .tsx files then compile to the SVG runtime instead of to React:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}A classic factory has to be in scope, so import h in every file that writes JSX even though nothing calls it by hand — and Fragment too, if you use <>…</>, which becomes a <g> since render produces a single node. Attribute names can be SVG's own (stroke-width) or React's (strokeWidth): camelCase is kebab-cased for you, bar the few SVG spells that way itself, like viewBox.
import { h, render } from "@puzzmo/sdk/svgJSX"
export function renderThumbnail(puzzleString: string, inputString?: string, config?: ThumbnailConfig): ThumbnailResult {
const puzzle = JSON.parse(puzzleString)
const size = 200
const svg = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox={`0 0 ${size} ${size}`}>
<rect width={size} height={size} fill={config?.theme?.g_bg ?? "#1a1a2e"} />
<text x={size / 2} y={size / 2} textAnchor="middle" fill={config?.theme?.fg ?? "#fff"} fontSize="24">
{puzzle.title}
</text>
</svg>
)
const el = render(svg)
// Duck-typed rather than `instanceof Element`: the server-side renderer runs bundles against a
// minimal SVG DOM that has no such global.
if (!el || !("outerHTML" in el)) throw new Error("Could not render the thumbnail")
return { svg: el.outerHTML, width: size, height: size }
}The h function is a JSX factory that creates virtual DOM nodes, and render converts them into real DOM elements. Since thumbnails can run server-side or in a DOM-shimmed environment, the SVG is serialized via outerHTML and returned alongside its width/height so the host can size the thumbnail without parsing it.
Editor Integration
For games that support puzzle editing in Puzzmo Workshop, you will need an Editor Bundle:
import type { EditorBundle, ValidationReport } from "@puzzmo/sdk"
export const validator = {
validate(data) {
return { success: true, issues: [] }
},
} satisfies EditorBundle