Skip to content

Commit c7796a4

Browse files
Sync packages from monorepo (6ae3349)
1 parent 1fcf261 commit c7796a4

13 files changed

Lines changed: 1136 additions & 14 deletions

File tree

packages/sdk/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ import { createPuzzmoSDK } from "@puzzmo/sdk"
3535
const sdk = createPuzzmoSDK()
3636

3737
// 1. Wait for puzzle data from Puzzmo
38-
const { puzzleString, boardState, theme, completed } = await sdk.gameReady()
38+
const { puzzleString, inputString, theme, completed } = await sdk.gameReady()
3939

4040
// 2. Set up your game with the puzzle data
4141
const puzzle = JSON.parse(puzzleString)
4242
initializeGame(puzzle)
43-
if (boardState) restoreState(boardState)
43+
if (inputString) restoreState(inputString)
4444

4545
// 3. Signal that you're ready
4646
sdk.gameLoaded()
@@ -179,8 +179,8 @@ import type { EditorBundle, ValidationReport } from "@puzzmo/sdk"
179179
import { puzzleToSVG } from "./src/puzzleToSVG"
180180

181181
export const AppBundle = {
182-
renderThumbnail(puzzleString, boardState, config) {
183-
return puzzleToSVG(puzzleString, boardState, config)
182+
renderThumbnail(puzzleString, inputString, config) {
183+
return puzzleToSVG(puzzleString, inputString, config)
184184
},
185185
} satisfies EditorBundle
186186
```

packages/sdk/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
"import": "./dist/index.js",
1313
"require": "./dist/index.cjs"
1414
},
15+
"./inputs": {
16+
"types": "./dist/inputs/index.d.ts",
17+
"import": "./dist/inputs/index.js",
18+
"require": "./dist/inputs/index.cjs"
19+
},
1520
"./simulator": {
1621
"types": "./dist/simulator/index.d.ts",
1722
"import": "./dist/simulator/index.js",
@@ -52,6 +57,9 @@
5257
"provenance": true,
5358
"registry": "https://registry.npmjs.org/"
5459
},
60+
"dependencies": {
61+
"lz-string": "^1.5.0"
62+
},
5563
"devDependencies": {
5664
"typescript": "catalog:",
5765
"vite": "catalog:"

packages/sdk/src/inputs/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Input String Encoder
2+
3+
A schema-driven library for encoding and decoding game state into compact, human-readable strings.
4+
5+
## Features
6+
7+
- **Compact encoding**: 40-60% size reduction vs JSON
8+
- **Human-readable**: Colon-delimited format for easy debugging
9+
- **Type-safe**: Full TypeScript inference
10+
- **Migration support**: Built-in version migration system
11+
- **Smart optimization**: Automatic sparse encoding for arrays, JSON compression with lz-string
12+
13+
## Quick Start
14+
15+
```typescript
16+
import { defineSchema, createEncoder } from '@puzzmo/sdk/inputs'
17+
18+
// Define your data type
19+
type GameData = {
20+
revealed: boolean[]
21+
score: number
22+
annotations: Record<string, { level: number; color: number }>
23+
}
24+
25+
// Create a schema
26+
const schema = defineSchema<GameData>({
27+
version: 1,
28+
fields: {
29+
revealed: { type: 'bitArray' },
30+
score: { type: 'int' },
31+
annotations: {
32+
type: 'sparseMap',
33+
valueEncoder: (ann) => `${ann.level}.${ann.color}`,
34+
valueDecoder: (str) => {
35+
const [level, color] = str.split('.').map(Number)
36+
return { level, color }
37+
}
38+
}
39+
}
40+
})
41+
42+
// Create encoder/decoder with context for length-dependent fields
43+
const { encode, decode } = createEncoder(schema, {
44+
revealed: { length: 100 } // Required for bitArray decoding
45+
})
46+
47+
// Use it
48+
const data = { revealed: [...], score: 42, annotations: { ... } }
49+
const inputString = encode(data) // "v1:a3f5...:42:0,0=2.1;1,1=3.0"
50+
const decoded = decode(inputString)
51+
```
52+
53+
## Field Types
54+
55+
| Type | Use Case | Encoding | Example |
56+
|------|----------|----------|---------|
57+
| `bitArray` | Boolean arrays (revealed tiles, flags) | Hex (4 bits/char) | `[true, false, false, false, false, true]``"12"` |
58+
| `intArray` | Number arrays (scores, experience) | Smart sparse/CSV | `[0, 0, 5, 0, 3]``"2=5,4=3"` |
59+
| `int` | Single integers | Decimal | `42``"42"` |
60+
| `string` | Text values | As-is | `"Alice"``"Alice"` |
61+
| `stringArray` | String lists | Comma-separated | `["a", "b", "c"]``"a,b,c"` |
62+
| `sparseMap` | Key-value maps with custom encoding | `idx=value,...` or `key=value;...` | `{a: {x: 1}}``"0=1.2"` (with keys) or `"a=1.2"` |
63+
| `json` | Complex objects (fallback) | Compressed JSON | Auto lz-string compression |
64+
65+
## Migrations
66+
67+
Add version migrations to your schema:
68+
69+
```typescript
70+
const schema = defineSchema({
71+
version: 2,
72+
fields: {
73+
name: { type: 'string' },
74+
value: { type: 'int' }
75+
},
76+
migrations: {
77+
0: (old) => ({ ...old, value: 0 }),
78+
1: (old) => ({ ...old, name: old.name.toUpperCase() })
79+
}
80+
})
81+
82+
const { migrate } = createEncoder(schema)
83+
const updated = migrate("v0:alice") // "v2:ALICE:1"
84+
```
85+
86+
## Context for Optimization
87+
88+
Several field types benefit from context information:
89+
90+
```typescript
91+
const { encode, decode } = createEncoder(schema, {
92+
revealed: { length: 100 }, // Required for bitArray
93+
scores: { length: 10 }, // Required for sparse intArray
94+
annotations: { keys: tileIds } // Optional: optimize sparseMap with numeric indices
95+
})
96+
```
97+
98+
**sparseMap optimization**: Providing ordered keys converts string keys to numeric indices:
99+
- Without keys: `"0,0=2.1;1,1=3.0"` (17 chars, `;` separator)
100+
- With keys: `"0=2.1,11=3.0"` (13 chars, `,` separator)
101+
102+
The keys array must match the key order used when creating the data.
103+
104+
## Documentation
105+
106+
See detailed documentation in [types.ts](./types.ts) for:
107+
- Complete field type specifications
108+
- When to use each type
109+
- Encoding/decoding details
110+
- Migration patterns
111+
- Best practices

packages/sdk/src/inputs/decoder.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Schema, FieldType } from "./types"
2+
import { decodeBitArrayFromHex, decodeIntArray, decodeStringArray, decodeSparseMap, decodeJson } from "./encodings"
3+
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context is dynamic and return type depends on fieldType
5+
export function decodeField(value: string, fieldType: FieldType, context?: any): any {
6+
switch (fieldType.type) {
7+
case "bitArray":
8+
if (!context?.length) {
9+
throw new Error("bitArray requires length in context")
10+
}
11+
return decodeBitArrayFromHex(value, context.length)
12+
13+
case "intArray":
14+
return decodeIntArray(value, context?.length)
15+
16+
case "int":
17+
return Number(value)
18+
19+
case "string":
20+
return value
21+
22+
case "stringArray":
23+
return decodeStringArray(value, fieldType.delimiter)
24+
25+
case "sparseMap":
26+
return decodeSparseMap(value, fieldType.valueDecoder, context?.keys)
27+
28+
case "json":
29+
return decodeJson(value)
30+
31+
default:
32+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- used in error message for invalid type
33+
throw new Error(`Unknown field type: ${(fieldType as any).type}`)
34+
}
35+
}
36+
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- context values are dynamic
38+
export function decode<TData>(schema: Schema<TData>, inputString: string, context?: Record<string, any>): TData {
39+
const delimiter = schema.delimiter || ":"
40+
const parts = inputString.split(delimiter)
41+
42+
const versionPart = parts[0]
43+
const versionMatch = versionPart.match(/^v(\d+)$/)
44+
if (!versionMatch) {
45+
throw new Error(`Invalid input string format: ${versionPart}`)
46+
}
47+
48+
const version = Number(versionMatch[1])
49+
if (version !== schema.version) {
50+
throw new Error(`Version mismatch: expected v${schema.version}, got v${version}. ` + `Make sure to run migrations first.`)
51+
}
52+
53+
const fieldNames = Object.keys(schema.fields) as (keyof TData)[]
54+
const data = {} as TData
55+
56+
for (let i = 0; i < fieldNames.length; i++) {
57+
const fieldName = fieldNames[i]
58+
const fieldType = schema.fields[fieldName]
59+
const fieldValue = parts[i + 1] || ""
60+
61+
const fieldContext = context?.[fieldName as string]
62+
data[fieldName] = decodeField(fieldValue, fieldType, fieldContext)
63+
}
64+
65+
return data
66+
}

packages/sdk/src/inputs/encoder.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Schema, FieldType } from "./types"
2+
import { encodeBitArrayToHex, encodeIntArray, encodeStringArray, encodeSparseMap, encodeJson } from "./encodings"
3+
4+
export function encodeField(value: any, fieldType: FieldType, context?: any): string {
5+
switch (fieldType.type) {
6+
case "bitArray":
7+
return encodeBitArrayToHex(value)
8+
9+
case "intArray":
10+
return encodeIntArray(value)
11+
12+
case "int":
13+
return String(value)
14+
15+
case "string":
16+
return String(value)
17+
18+
case "stringArray":
19+
return encodeStringArray(value, fieldType.delimiter)
20+
21+
case "sparseMap":
22+
return encodeSparseMap(value, fieldType.valueEncoder, context?.keys)
23+
24+
case "json":
25+
return encodeJson(value)
26+
27+
default:
28+
throw new Error(`Unknown field type: ${(fieldType as any).type}`)
29+
}
30+
}
31+
32+
export function encode<TData>(schema: Schema<TData>, data: TData, context?: Record<string, any>): string {
33+
const delimiter = schema.delimiter || ":"
34+
const fieldNames = Object.keys(schema.fields) as (keyof TData)[]
35+
36+
const encodedFields = fieldNames.map((fieldName) => {
37+
const fieldType = schema.fields[fieldName]
38+
const value = data[fieldName]
39+
const fieldContext = context?.[fieldName as string]
40+
return encodeField(value, fieldType, fieldContext)
41+
})
42+
43+
return `v${schema.version}${delimiter}${encodedFields.join(delimiter)}`
44+
}

0 commit comments

Comments
 (0)