@littlepartytime/sdk 2.0.0 → 2.1.0

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.
@@ -0,0 +1,1142 @@
1
+ # Little Party Time SDK - Game Development Guide
2
+
3
+ This guide defines the specification for developing games on the Little Party Time platform. Follow this guide precisely to create a compatible game package.
4
+
5
+ ## Overview
6
+
7
+ A game consists of two parts:
8
+ 1. **Engine** (server-side): Pure logic, no UI. Manages game state, validates actions, determines winners.
9
+ 2. **Renderer** (client-side): React component that displays the game UI and sends player actions.
10
+
11
+ Both parts communicate through the platform via well-defined interfaces.
12
+
13
+ ## Project Structure
14
+
15
+ Each game is a standalone project:
16
+
17
+ ```
18
+ my-game/
19
+ ├── package.json
20
+ ├── lpt.config.ts # Project configuration (gameId for local dev)
21
+ ├── rules.md # Game rules (Markdown, included in upload)
22
+ ├── vite.config.ts # Build configuration
23
+ ├── vitest.config.ts # Test configuration
24
+ ├── tsconfig.json
25
+ ├── assets/ # Game images and custom assets
26
+ │ ├── icon.png # 1:1 (256x256+) - game list icon
27
+ │ ├── banner.png # 16:9 (640x360+) - lobby banner
28
+ │ ├── cover.png # 21:9 (840x360+) - store/featured cover
29
+ │ ├── splash.png # 9:21 (360x840+) - loading screen
30
+ │ ├── cards/ # Custom game assets (optional)
31
+ │ │ ├── king.png
32
+ │ │ └── queen.png
33
+ │ └── sounds/
34
+ │ └── flip.mp3
35
+ ├── src/
36
+ │ ├── config.ts # GameConfig - metadata
37
+ │ ├── engine.ts # GameEngine - server-side logic
38
+ │ ├── renderer.tsx # GameRenderer - client-side React component
39
+ │ ├── types.ts # Game-specific types (actions, state shape)
40
+ │ └── index.ts # Re-exports config, engine, renderer
41
+ ├── tests/
42
+ │ ├── engine.test.ts # Engine unit tests
43
+ │ └── e2e.e2e.test.ts # Playwright E2E tests (optional)
44
+ └── dist/ # Build output (generated)
45
+ ├── bundle.js # Client-side bundle
46
+ ├── engine.cjs # Server-side engine
47
+ └── <gameId>.zip # Upload package
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```bash
53
+ # Create a new game project
54
+ npx create-littlepartytime-game my-awesome-game
55
+ cd my-awesome-game
56
+
57
+ # Install dependencies
58
+ npm install
59
+
60
+ # Run tests
61
+ npm test
62
+
63
+ # Build and package for upload
64
+ npm run pack
65
+ ```
66
+
67
+ ## Step-by-Step Implementation
68
+
69
+ ### Step 1: Define Game Config
70
+
71
+ ```typescript
72
+ // src/config.ts
73
+ import type { GameConfig } from "@littlepartytime/sdk";
74
+
75
+ const config: GameConfig = {
76
+ name: "My Game", // Display name (Chinese preferred for CN users)
77
+ description: "...", // Brief description
78
+ assets: {
79
+ icon: "assets/icon.png", // 1:1 game list icon
80
+ banner: "assets/banner.png", // 16:9 lobby banner
81
+ cover: "assets/cover.png", // 21:9 store/featured cover
82
+ splash: "assets/splash.png", // 9:21 loading screen
83
+ },
84
+ minPlayers: 2,
85
+ maxPlayers: 6,
86
+ tags: ["strategy", "card"],
87
+ version: "1.0.0",
88
+ sdkVersion: "2.0.0",
89
+ };
90
+
91
+ export default config;
92
+ ```
93
+
94
+ ### Step 2: Define Game-Specific Types
95
+
96
+ ```typescript
97
+ // src/types.ts
98
+ import type { GameAction, GameState } from "@littlepartytime/sdk";
99
+
100
+ // Define all possible actions your game supports
101
+ export type MyGameAction =
102
+ | { type: "PLAY_CARD"; payload: { cardId: string } }
103
+ | { type: "DRAW_CARD" }
104
+ | { type: "PASS" };
105
+
106
+ // Define the shape of your game's data field in GameState
107
+ export interface MyGameData {
108
+ deck: Card[];
109
+ currentPlayerIndex: number;
110
+ direction: 1 | -1;
111
+ // ... game-specific fields
112
+ }
113
+
114
+ // GameState.data will be typed as Record<string, unknown> at the SDK level.
115
+ // Cast it in your engine: const data = state.data as MyGameData;
116
+ ```
117
+
118
+ ### Step 3: Implement Game Engine
119
+
120
+ The engine is the core of your game. It MUST be a pure, deterministic state machine.
121
+
122
+ ```typescript
123
+ // src/engine.ts
124
+ import type { GameEngine, GameState, Player, GameAction, GameResult } from "@littlepartytime/sdk";
125
+
126
+ const engine: GameEngine = {
127
+ /**
128
+ * Initialize game state for the given players.
129
+ * Called once when the host starts the game.
130
+ *
131
+ * @param players - Array of players in the game (from room's online members)
132
+ * @param options - Optional game settings (e.g., difficulty, variant rules)
133
+ * @returns Initial GameState
134
+ */
135
+ init(players: Player[], options?: Record<string, unknown>): GameState {
136
+ return {
137
+ phase: "playing", // Use meaningful phase names: "playing", "voting", "scoring", etc.
138
+ players: players.map(p => ({
139
+ id: p.id,
140
+ // Add per-player state here (hand, score, etc.)
141
+ })),
142
+ data: {
143
+ // Global game state (deck, board, current turn, etc.)
144
+ },
145
+ };
146
+ },
147
+
148
+ /**
149
+ * Process a player action and return the new state.
150
+ * This is the main game logic function.
151
+ *
152
+ * IMPORTANT:
153
+ * - MUST be pure: do not mutate the input state, return a new object.
154
+ * - MUST validate the action: check if it's the player's turn, if the action is legal, etc.
155
+ * - If the action is invalid, return the state unchanged (or throw an error).
156
+ *
157
+ * @param state - Current game state
158
+ * @param playerId - ID of the player performing the action
159
+ * @param action - The action being performed
160
+ * @returns New GameState after applying the action
161
+ */
162
+ handleAction(state: GameState, playerId: string, action: GameAction): GameState {
163
+ // 1. Validate it's this player's turn
164
+ // 2. Validate the action is legal
165
+ // 3. Apply the action and return new state
166
+ // 4. Advance to next player or next phase as needed
167
+ return { ...state };
168
+ },
169
+
170
+ /**
171
+ * Check if the game has ended.
172
+ * Called after every handleAction.
173
+ *
174
+ * @param state - Current game state
175
+ * @returns true if the game is over
176
+ */
177
+ isGameOver(state: GameState): boolean {
178
+ return state.phase === "ended";
179
+ },
180
+
181
+ /**
182
+ * Compute final results/rankings.
183
+ * Called only when isGameOver returns true.
184
+ *
185
+ * @param state - Final game state
186
+ * @returns GameResult with player rankings
187
+ */
188
+ getResult(state: GameState): GameResult {
189
+ return {
190
+ rankings: state.players.map((p, i) => ({
191
+ playerId: p.id,
192
+ rank: i + 1,
193
+ score: 0,
194
+ isWinner: i === 0,
195
+ })),
196
+ };
197
+ },
198
+
199
+ /**
200
+ * Filter the state to show only what a specific player should see.
201
+ * This is critical for games with hidden information (e.g., cards in hand).
202
+ *
203
+ * For games with no hidden info, you can return the full state.
204
+ * For games with hidden info:
205
+ * - Hide other players' hands
206
+ * - Hide the deck contents
207
+ * - Only reveal public information
208
+ *
209
+ * @param state - Full game state
210
+ * @param playerId - The player requesting their view
211
+ * @returns Filtered state visible to this player
212
+ */
213
+ getPlayerView(state: GameState, playerId: string): Partial<GameState> {
214
+ return state; // Override for hidden information games
215
+ },
216
+ };
217
+
218
+ export default engine;
219
+ ```
220
+
221
+ ### Step 4: Implement Game Renderer
222
+
223
+ The renderer is a React component that receives the platform API and the player's visible state.
224
+
225
+ ```tsx
226
+ // src/renderer.tsx
227
+ "use client";
228
+
229
+ import { useState, useEffect, useCallback } from "react";
230
+ import type { GameRendererProps, GameAction } from "@littlepartytime/sdk";
231
+
232
+ export default function GameRenderer({ platform, state }: GameRendererProps) {
233
+ // Access player info
234
+ const me = platform.getLocalPlayer();
235
+ const players = platform.getPlayers();
236
+
237
+ // Listen for state updates from the server
238
+ const [gameState, setGameState] = useState(state);
239
+
240
+ useEffect(() => {
241
+ const handleStateUpdate = (newState: typeof state) => {
242
+ setGameState(newState);
243
+ };
244
+ platform.on("stateUpdate", handleStateUpdate);
245
+ return () => platform.off("stateUpdate", handleStateUpdate);
246
+ }, [platform]);
247
+
248
+ // Send actions to the server
249
+ const sendAction = useCallback(
250
+ (action: GameAction) => {
251
+ platform.send(action);
252
+ },
253
+ [platform]
254
+ );
255
+
256
+ // Render game UI
257
+ return (
258
+ <div>
259
+ {/* Your game UI here */}
260
+ {/* Use Tailwind CSS classes - the platform provides Tailwind */}
261
+ {/* Use the design tokens from the platform: bg-bg-primary, text-accent, etc. */}
262
+ </div>
263
+ );
264
+ }
265
+ ```
266
+
267
+ ### Step 5: Create Index File
268
+
269
+ ```typescript
270
+ // src/index.ts
271
+ export { default as config } from "./config";
272
+ export { default as engine } from "./engine";
273
+ export { default as GameRenderer } from "./renderer";
274
+ ```
275
+
276
+ ## Testing Your Game
277
+
278
+ The SDK provides testing utilities to help you write tests for your game engine.
279
+
280
+ ### Using createMockPlayers
281
+
282
+ Generate test players quickly:
283
+
284
+ ```typescript
285
+ import { createMockPlayers } from '@littlepartytime/sdk/testing';
286
+
287
+ // Create 3 players with default IDs (player-1, player-2, player-3)
288
+ const players = createMockPlayers(3);
289
+
290
+ // The first player is the host by default
291
+ console.log(players[0].isHost); // true
292
+
293
+ // Override specific player properties
294
+ const customPlayers = createMockPlayers(2, [
295
+ { nickname: 'Alice' },
296
+ { nickname: 'Bob', isHost: true }
297
+ ]);
298
+ ```
299
+
300
+ ### Using GameTester for Unit Tests
301
+
302
+ `GameTester` provides a simple wrapper for testing individual engine methods:
303
+
304
+ ```typescript
305
+ import { describe, it, expect } from 'vitest';
306
+ import { GameTester, createMockPlayers } from '@littlepartytime/sdk/testing';
307
+ import engine from '../src/engine';
308
+
309
+ describe('My Game Engine', () => {
310
+ it('should initialize with correct phase', () => {
311
+ const tester = new GameTester(engine);
312
+ tester.init(createMockPlayers(3));
313
+
314
+ expect(tester.phase).toBe('playing');
315
+ expect(tester.playerStates).toHaveLength(3);
316
+ });
317
+
318
+ it('should handle valid actions', () => {
319
+ const tester = new GameTester(engine);
320
+ const players = createMockPlayers(2);
321
+ tester.init(players);
322
+
323
+ // Perform an action
324
+ tester.act(players[0].id, { type: 'PLAY_CARD', payload: { cardId: '1' } });
325
+
326
+ // Assert state changes
327
+ expect(tester.state.data.cardsPlayed).toContain('1');
328
+ });
329
+
330
+ it('should reject invalid actions', () => {
331
+ const tester = new GameTester(engine);
332
+ const players = createMockPlayers(2);
333
+ tester.init(players);
334
+
335
+ const before = tester.state;
336
+ // Wrong player tries to act
337
+ tester.act(players[1].id, { type: 'PLAY_CARD', payload: { cardId: '1' } });
338
+
339
+ // State should be unchanged
340
+ expect(tester.state).toBe(before);
341
+ });
342
+
343
+ it('should filter player views correctly', () => {
344
+ const tester = new GameTester(engine);
345
+ const players = createMockPlayers(2);
346
+ tester.init(players);
347
+
348
+ const view = tester.getPlayerView(players[0].id);
349
+ expect(view.data).not.toHaveProperty('secretInfo');
350
+ });
351
+ });
352
+ ```
353
+
354
+ ### Using GameSimulator for E2E Tests
355
+
356
+ `GameSimulator` helps you simulate complete game sessions:
357
+
358
+ ```typescript
359
+ import { describe, it, expect } from 'vitest';
360
+ import { GameSimulator } from '@littlepartytime/sdk/testing';
361
+ import engine from '../src/engine';
362
+
363
+ describe('My Game E2E', () => {
364
+ it('should play a complete game', () => {
365
+ const sim = new GameSimulator(engine, { playerCount: 3 });
366
+ sim.start();
367
+
368
+ // Simulate game actions using player indices (0, 1, 2)
369
+ sim.act(0, { type: 'PLAY_CARD', payload: { cardId: '1' } });
370
+ sim.act(1, { type: 'DRAW_CARD' });
371
+ sim.act(2, { type: 'PASS' });
372
+
373
+ // Continue until game ends
374
+ while (!sim.isGameOver()) {
375
+ const turn = sim.currentTurn;
376
+ sim.act(turn, { type: 'PASS' });
377
+ }
378
+
379
+ // Verify results
380
+ expect(sim.isGameOver()).toBe(true);
381
+ const result = sim.getResult();
382
+ expect(result.rankings).toHaveLength(3);
383
+
384
+ // Access the action log for debugging
385
+ console.log(`Game finished in ${sim.actionLog.length} actions`);
386
+ });
387
+
388
+ it('should provide correct player views', () => {
389
+ const sim = new GameSimulator(engine, { playerCount: 2 });
390
+ sim.start();
391
+
392
+ // Each player sees only their own view
393
+ const view0 = sim.getView(0);
394
+ const view1 = sim.getView(1);
395
+
396
+ expect(view0).not.toEqual(view1);
397
+ });
398
+ });
399
+ ```
400
+
401
+ ## Local Development Server
402
+
403
+ The dev-kit provides a local development server for previewing and testing your game without uploading to the platform.
404
+
405
+ ### Starting the Dev Server
406
+
407
+ ```bash
408
+ # From your game project directory
409
+ npm run dev
410
+ # or directly:
411
+ npx lpt-dev-kit dev
412
+ ```
413
+
414
+ This starts two servers and provides three pages:
415
+
416
+ ```
417
+ Preview: http://localhost:4000/preview # Single-player preview with engine
418
+ Multiplayer: http://localhost:4000/play # Multi-player via Socket.IO
419
+ Debug Panel: http://localhost:4000/debug # Real-time state inspection
420
+ ```
421
+
422
+ ### Preview Page (Single-Player)
423
+
424
+ The Preview page runs your **engine locally in the browser** — no network needed.
425
+
426
+ Features:
427
+ - **Engine integration**: `platform.send()` calls `engine.handleAction()` locally, computes `getPlayerView()`, and triggers `stateUpdate` events automatically
428
+ - **Player switching**: Switch between players (2-32) via a dropdown to see each player's filtered view
429
+ - **State editor**: View and override the full game state as JSON for debugging
430
+ - **Action log**: See all actions sent by players in real-time
431
+ - **Game over detection**: Automatically detects when `isGameOver()` returns true and displays results
432
+ - **Reset**: Re-initialize the game at any time
433
+
434
+ This enables a rapid development cycle: **edit code -> refresh browser -> test immediately**.
435
+
436
+ ### Multiplayer Page
437
+
438
+ The Play page runs your game with real Socket.IO multiplayer:
439
+ 1. Open multiple browser tabs/windows to `http://localhost:4000/play`
440
+ 2. Each tab enters a different nickname and joins the lobby
441
+ 3. All players click "Ready", then the host starts the game
442
+ 4. Actions flow through Socket.IO to the engine running on the dev server
443
+
444
+ ### Debug Page
445
+
446
+ The Debug page shows the raw room state and full (unfiltered) game state in real-time. Useful for inspecting hidden information during development.
447
+
448
+ ## Playwright E2E Testing
449
+
450
+ For automated UI testing, the dev-kit provides a `GamePreview` class that orchestrates the dev server and Playwright browser.
451
+
452
+ ### Setup
453
+
454
+ ```bash
455
+ # Install playwright as a dev dependency
456
+ npm install -D playwright
457
+ ```
458
+
459
+ ### Writing E2E Tests
460
+
461
+ Create a test file (e.g., `tests/e2e.e2e.test.ts`):
462
+
463
+ ```typescript
464
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
465
+ import { GamePreview } from '@littlepartytime/dev-kit/testing';
466
+ import path from 'path';
467
+
468
+ describe('My Game E2E', () => {
469
+ let preview: GamePreview;
470
+
471
+ beforeAll(async () => {
472
+ preview = new GamePreview({
473
+ projectDir: path.resolve(__dirname, '..'),
474
+ playerCount: 3,
475
+ headless: true,
476
+ port: 4100, // Use non-default ports to avoid conflicts
477
+ socketPort: 4101,
478
+ });
479
+ await preview.start();
480
+ }, 30000); // Allow time for server startup and browser launch
481
+
482
+ afterAll(async () => {
483
+ await preview.stop();
484
+ }, 10000);
485
+
486
+ it('should show lobby with all players', async () => {
487
+ const page = preview.getPlayerPage(0);
488
+ await expect(page.locator('text=Alice')).toBeVisible();
489
+ await expect(page.locator('text=Bob')).toBeVisible();
490
+ await expect(page.locator('text=Carol')).toBeVisible();
491
+ });
492
+
493
+ it('should play through a complete game', async () => {
494
+ // All players click "Ready"
495
+ await preview.readyAll();
496
+
497
+ // Host starts the game
498
+ await preview.startGame();
499
+
500
+ // Interact with the game UI via Playwright
501
+ const hostPage = preview.getPlayerPage(0);
502
+ const player2Page = preview.getPlayerPage(1);
503
+
504
+ // Use standard Playwright assertions
505
+ await expect(hostPage.locator('.game-board')).toBeVisible({ timeout: 5000 });
506
+
507
+ // Send actions by interacting with the UI
508
+ await hostPage.click('button:has-text("Play")');
509
+
510
+ // Assert state changes on other players' screens
511
+ await expect(player2Page.locator('text=waiting')).toBeVisible();
512
+ });
513
+ });
514
+ ```
515
+
516
+ ### GamePreview API
517
+
518
+ ```typescript
519
+ import { GamePreview } from '@littlepartytime/dev-kit/testing';
520
+
521
+ const preview = new GamePreview({
522
+ projectDir: string; // Absolute path to game project
523
+ playerCount: number; // Number of players (2-8)
524
+ port?: number; // Vite server port (default: 4100)
525
+ socketPort?: number; // Socket.IO port (default: 4101)
526
+ headless?: boolean; // Headless browser (default: true)
527
+ browserType?: 'chromium' | 'firefox' | 'webkit'; // default: 'chromium'
528
+ });
529
+
530
+ await preview.start(); // Start server + browser, join all players
531
+ const page = preview.getPlayerPage(0); // Get Playwright Page for player (0 = host)
532
+ const pages = preview.getPlayerPages(); // Get all Page objects
533
+ await preview.readyAll(); // Click "Ready" for all players
534
+ await preview.startGame(); // Host clicks "Start Game"
535
+ await preview.stop(); // Clean up browser + server
536
+ ```
537
+
538
+ ### Excluding E2E Tests from Unit Test Runs
539
+
540
+ E2E tests are slower and require playwright. Exclude them from the default `vitest run`:
541
+
542
+ ```typescript
543
+ // vitest.config.ts
544
+ export default defineConfig({
545
+ test: {
546
+ include: ["tests/**/*.test.ts"],
547
+ exclude: ["tests/**/*.e2e.test.ts"],
548
+ },
549
+ });
550
+ ```
551
+
552
+ Run E2E tests separately:
553
+
554
+ ```bash
555
+ npx vitest run tests/e2e.e2e.test.ts
556
+ ```
557
+
558
+ ## Building and Packaging
559
+
560
+ ### The pack Command
561
+
562
+ Use `lpt-dev-kit pack` (or `npm run pack`) to build and package your game:
563
+
564
+ ```bash
565
+ # From your game project directory
566
+ npm run pack
567
+ ```
568
+
569
+ This command:
570
+ 1. Runs `vite build` to compile your game
571
+ 2. Validates the build output (checks for bundle.js and engine.cjs)
572
+ 3. Reads `GameConfig` from the built engine to extract metadata
573
+ 4. Validates the required image assets (format, dimensions, aspect ratio)
574
+ 5. Validates `rules.md` exists and is non-empty
575
+ 6. Generates `manifest.json` from your config
576
+ 7. Creates a `.zip` file in the `dist/` directory containing code, manifest, rules, and images
577
+
578
+ ### Build Output
579
+
580
+ After running `pack`, your `dist/` folder will contain:
581
+
582
+ ```
583
+ dist/
584
+ ├── bundle.js # Client-side bundle (React component + dependencies)
585
+ ├── engine.cjs # Server-side engine (CommonJS for Node.js)
586
+ └── <gameId>.zip # Upload package
587
+ ```
588
+
589
+ The `.zip` upload package contains:
590
+
591
+ ```
592
+ <gameId>.zip
593
+ ├── manifest.json # Auto-generated metadata (from GameConfig)
594
+ ├── rules.md # Game rules
595
+ ├── bundle.js # Client-side bundle
596
+ ├── engine.cjs # Server-side engine
597
+ ├── icon.png # 1:1 game icon
598
+ ├── banner.png # 16:9 banner
599
+ ├── cover.png # 21:9 cover
600
+ ├── splash.png # 9:21 splash screen
601
+ └── assets/ # Custom game assets (if any)
602
+ ├── cards/
603
+ │ └── king.png
604
+ └── sounds/
605
+ └── flip.mp3
606
+ ```
607
+
608
+ ### Configuration
609
+
610
+ The `lpt.config.ts` file configures local development. The `gameId` is a local project identifier used for the zip filename and dev server — it is NOT the platform game ID (the platform assigns that upon upload):
611
+
612
+ ```typescript
613
+ // lpt.config.ts
614
+ export default {
615
+ gameId: 'my-awesome-game', // Local project identifier (zip filename, dev server)
616
+ };
617
+ ```
618
+
619
+ ### Vite Configuration (Important)
620
+
621
+ Your `vite.config.ts` **must** use a single entry point with `fileName` mapping. Do NOT use multiple entry points, as Vite will extract shared code into separate chunk files that won't be included in the upload package.
622
+
623
+ **Recommended configuration:**
624
+
625
+ ```typescript
626
+ // vite.config.ts
627
+ import { defineConfig } from "vite";
628
+ import react from "@vitejs/plugin-react";
629
+ import path from "path";
630
+
631
+ export default defineConfig({
632
+ plugins: [react()],
633
+ build: {
634
+ lib: {
635
+ entry: path.resolve(__dirname, "src/index.ts"), // Single entry point
636
+ formats: ["es", "cjs"],
637
+ fileName: (format) => {
638
+ if (format === "es") return "bundle.js";
639
+ if (format === "cjs") return "engine.cjs";
640
+ return `bundle.${format}.js`;
641
+ },
642
+ },
643
+ rollupOptions: {
644
+ external: ["react", "react-dom", "react/jsx-runtime", "@littlepartytime/sdk"],
645
+ },
646
+ outDir: "dist",
647
+ emptyOutDir: true,
648
+ },
649
+ });
650
+ ```
651
+
652
+ **Why single entry?** With multiple entry points (e.g., separate `renderer.tsx` and `engine.ts`), Vite extracts shared code into chunk files like `types-D0Vb4wB4.js`. The `pack` command only includes `bundle.js` and `engine.cjs` in the upload package — extra chunks will be missing at runtime, causing 404 errors.
653
+
654
+ **The `pack` command will reject builds with extra chunk files** to prevent this issue.
655
+
656
+ ### Required Game Images
657
+
658
+ Every game must include 4 images in the `assets/` directory. These are validated and packaged by the `pack` command.
659
+
660
+ | Image | Aspect Ratio | Min Size | Purpose |
661
+ |-------|-------------|----------|---------|
662
+ | `icon.png` | 1:1 | 256x256 | Game list icon |
663
+ | `banner.png` | 16:9 | 640x360 | Lobby banner |
664
+ | `cover.png` | 21:9 | 840x360 | Store/featured cover |
665
+ | `splash.png` | 9:21 | 360x840 | Loading/splash screen |
666
+
667
+ **Rules:**
668
+ - **Formats**: PNG or WebP only
669
+ - **Aspect ratio**: Must match exactly (1% tolerance)
670
+ - **Minimum dimensions**: Must meet or exceed the minimum size
671
+ - **File size**: Warning if any image exceeds 2MB
672
+ - **Paths**: Referenced in `GameConfig.assets` as relative paths from the project root
673
+
674
+ The `pack` command reads these images, validates them, and includes them in the zip with canonical names (`icon.png`, `banner.png`, etc.).
675
+
676
+ ### Custom Game Assets
677
+
678
+ For assets used inside your game UI (card images, sound effects, fonts, etc.), place them in subdirectories under `assets/`:
679
+
680
+ ```
681
+ assets/
682
+ ├── icon.png # Platform display images (root level, required)
683
+ ├── banner.png
684
+ ├── cover.png
685
+ ├── splash.png
686
+ ├── cards/ # Custom game assets (subdirectories)
687
+ │ ├── king.png
688
+ │ └── queen.png
689
+ └── sounds/
690
+ └── flip.mp3
691
+ ```
692
+
693
+ Access custom assets in your renderer via `platform.getAssetUrl()`:
694
+
695
+ ```tsx
696
+ export default function GameRenderer({ platform, state }: GameRendererProps) {
697
+ const cardImg = platform.getAssetUrl('cards/king.png');
698
+ const flipSound = platform.getAssetUrl('sounds/flip.mp3');
699
+
700
+ return (
701
+ <div>
702
+ <img src={cardImg} alt="King" />
703
+ <audio src={flipSound} />
704
+ </div>
705
+ );
706
+ }
707
+ ```
708
+
709
+ **How it works:**
710
+ - During `lpt-dev-kit dev`: returns a local URL (e.g., `http://localhost:4000/assets/cards/king.png`)
711
+ - In production: returns a CDN URL (the platform uploads assets to OSS automatically)
712
+
713
+ **Validation rules (enforced by `pack` command):**
714
+
715
+ | Rule | Limit |
716
+ |------|-------|
717
+ | Single file size | ≤ 10MB |
718
+ | Total assets size | ≤ 50MB |
719
+ | Allowed file types | `.png`, `.jpg`, `.jpeg`, `.webp`, `.svg`, `.gif`, `.mp3`, `.wav`, `.ogg`, `.json`, `.woff2`, `.woff` |
720
+ | Path rules | No `..`, no spaces |
721
+
722
+ **Alternative approaches for small assets:**
723
+
724
+ | Approach | When to Use | How |
725
+ |----------|-------------|-----|
726
+ | **Inline in bundle** | Tiny assets (< 4KB) | Vite automatically inlines as data URLs |
727
+ | **CSS/SVG** | UI elements, icons | Tailwind CSS utilities or inline SVG components |
728
+ | **Emoji/Unicode** | Simple visual indicators | Unicode characters directly in JSX |
729
+
730
+ > **Tip:** Use `platform.getAssetUrl()` for assets 100KB+ instead of inlining them into the bundle.
731
+
732
+ ### Submitting Your Game
733
+
734
+ 1. Run `npm run pack` to build and package
735
+ 2. Navigate to the Little Party Time admin panel
736
+ 3. Upload the generated `.zip` file from `dist/`
737
+ 4. Fill in game details and submit for review
738
+
739
+ ## SDK Interfaces Reference
740
+
741
+ ### Platform (injected by the platform into the renderer)
742
+
743
+ | Method | Description |
744
+ |--------|-------------|
745
+ | `getPlayers()` | Returns all players in the game |
746
+ | `getLocalPlayer()` | Returns the current user's Player object |
747
+ | `send(action)` | Sends a GameAction to the server |
748
+ | `on(event, handler)` | Listens for events from the server |
749
+ | `off(event, handler)` | Removes an event listener |
750
+ | `reportResult(result)` | Reports game results (called by platform, not games) |
751
+ | `getAssetUrl(path)` | Returns the runtime URL for a custom asset (e.g., `"cards/king.png"` → CDN URL) |
752
+
753
+ ### Events the renderer should listen for
754
+
755
+ | Event | Payload | Description |
756
+ |-------|---------|-------------|
757
+ | `stateUpdate` | `Partial<GameState>` | Player-specific state update after any action |
758
+
759
+ ### GameState
760
+
761
+ ```typescript
762
+ interface GameState {
763
+ phase: string; // Current game phase
764
+ players: PlayerState[]; // Per-player state
765
+ data: Record<string, unknown>; // Global game data
766
+ }
767
+
768
+ interface PlayerState {
769
+ id: string;
770
+ [key: string]: unknown; // Game-specific per-player data
771
+ }
772
+ ```
773
+
774
+ ## Rules and Constraints
775
+
776
+ ### Engine Rules
777
+ 1. **Pure functions**: `handleAction` must NOT mutate the input state. Always return a new object.
778
+ 2. **Deterministic**: Same state + same action = same result. No `Math.random()` in `handleAction`. Use random values only in `init()` to set up the initial state (e.g., shuffle deck).
779
+ 3. **Validate everything**: Never trust the action payload. Validate it's the correct player's turn, the action is legal, etc.
780
+ 4. **No side effects**: No network calls, no timers, no console.log in production.
781
+ 5. **Serializable state**: `GameState` must be JSON-serializable (no functions, no class instances, no Dates - use ISO strings).
782
+
783
+ ### Renderer Rules
784
+ 1. **React functional component**: Use hooks, not class components.
785
+ 2. **Tailwind CSS only**: Use the platform's design tokens for consistent styling.
786
+ 3. **Mobile-first**: Design for phone screens (375px width). The platform is a PWA.
787
+ 4. **No direct socket access**: Only use `platform.send()` and `platform.on()`.
788
+ 5. **Chinese UI text**: The platform targets Chinese-speaking users.
789
+ 6. **Responsive touch targets**: Buttons should be at least 44x44px for mobile.
790
+
791
+ ### Design Token Reference
792
+
793
+ Use these CSS variables / Tailwind classes for consistent styling:
794
+
795
+ | Purpose | CSS Variable | Tailwind Class |
796
+ |---------|-------------|----------------|
797
+ | Page background | `--bg-primary` | `bg-bg-primary` |
798
+ | Card background | `--bg-secondary` | `bg-bg-secondary` |
799
+ | Elevated surface | `--bg-tertiary` | `bg-bg-tertiary` |
800
+ | Primary accent | `--accent-primary` | `text-accent` / `bg-accent` |
801
+ | Primary text | `--text-primary` | `text-text-primary` |
802
+ | Secondary text | `--text-secondary` | `text-text-secondary` |
803
+ | Muted text | `--text-tertiary` | `text-text-tertiary` |
804
+ | Border | `--border-default` | `border-border-default` |
805
+ | Success | `--success` | `text-success` |
806
+ | Error | `--error` | `text-error` |
807
+ | Display font | `--font-display` | `font-display` |
808
+ | Body font | `--font-body` | `font-body` |
809
+
810
+ ## Platform Runtime Constraints
811
+
812
+ The production platform runs game engines inside a Node.js `vm` sandbox. The dev-kit (`npm run dev`) automatically enforces key constraints so issues surface during local development, not after deployment.
813
+
814
+ ### Timer APIs Are Disabled
815
+
816
+ The sandbox replaces `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` with no-ops. Calling them inside engine code will:
817
+
818
+ - **In production**: silently do nothing (the callback is never executed)
819
+ - **In dev-kit**: print a warning to the console and return `0`
820
+
821
+ ```typescript
822
+ // BAD - will not work in production
823
+ handleAction(state, playerId, action) {
824
+ setTimeout(() => {
825
+ // This callback will NEVER run in the sandbox
826
+ }, 2000);
827
+ return { ...state, phase: 'animating' };
828
+ }
829
+
830
+ // GOOD - let the client handle timing
831
+ handleAction(state, playerId, action) {
832
+ return { ...state, phase: 'animating' };
833
+ }
834
+ // In renderer: after animation completes, send a follow-up action:
835
+ // platform.send({ type: 'ANIMATION_DONE' })
836
+ // Engine handles ANIMATION_DONE to advance to the next phase.
837
+ ```
838
+
839
+ ### State Must Be JSON-Serializable
840
+
841
+ The platform stores game state in Redis via `JSON.stringify` / `JSON.parse` round-trips. Non-serializable types will silently lose data:
842
+
843
+ | Type | After JSON Round-Trip | Result |
844
+ |------|----------------------|--------|
845
+ | `Map` | `{}` | Data lost |
846
+ | `Set` | `{}` | Data lost |
847
+ | `Date` | `"2026-02-10T..."` (string) | Type changed |
848
+ | `undefined` | Removed | Field disappears |
849
+ | `RegExp` | `{}` | Data lost |
850
+ | Function | Removed | Lost |
851
+
852
+ The dev-kit checks your state after every `init()` and `handleAction()` call and prints warnings if non-serializable types are detected.
853
+
854
+ ```typescript
855
+ // BAD
856
+ data: {
857
+ players: new Map([['p1', { score: 0 }]]),
858
+ seen: new Set(['card-1']),
859
+ createdAt: new Date(),
860
+ }
861
+
862
+ // GOOD
863
+ data: {
864
+ players: { p1: { score: 0 } },
865
+ seen: ['card-1'],
866
+ createdAt: '2026-02-10T00:00:00.000Z',
867
+ }
868
+ ```
869
+
870
+ ### Engine Instance Is Shared
871
+
872
+ The platform loads your engine bundle once and reuses it across all game rooms. This means **module-level mutable variables are shared between games**:
873
+
874
+ ```typescript
875
+ // BAD - shared across all rooms!
876
+ let gameCounter = 0;
877
+
878
+ const engine: GameEngine = {
879
+ init(players) {
880
+ gameCounter++; // Will increment across different rooms
881
+ // ...
882
+ },
883
+ };
884
+
885
+ // GOOD - all state lives in GameState
886
+ const engine: GameEngine = {
887
+ init(players) {
888
+ return {
889
+ phase: 'playing',
890
+ players: players.map(p => ({ id: p.id })),
891
+ data: { roundNumber: 1 }, // State is per-room
892
+ };
893
+ },
894
+ };
895
+ ```
896
+
897
+ ### Restricted Global Variables
898
+
899
+ The following globals are `undefined` in the sandbox:
900
+
901
+ | Global | Alternative |
902
+ |--------|-------------|
903
+ | `fetch` | Not available. Engines cannot make network calls. |
904
+ | `process` | Not available. |
905
+ | `globalThis` | Not available. |
906
+ | `global` | Not available. |
907
+
908
+ Available built-ins: `Math`, `JSON`, `Date`, `Array`, `Object`, `Map`, `Set`, `Number`, `String`, `Boolean`, `Error`, `TypeError`, `RangeError`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `console` (log/error/warn).
909
+
910
+ ### `require()` Whitelist
911
+
912
+ Only the following modules can be required in engine code (all are stubs for compatibility):
913
+ `react`, `react/jsx-runtime`, `react-dom`, `react-dom/client`
914
+
915
+ Any other `require()` call will throw an error.
916
+
917
+ ## Data Flow Diagram
918
+
919
+ ```
920
+ Player taps button
921
+
922
+
923
+ Renderer calls platform.send({ type: "PLAY_CARD", payload: { cardId: "3" } })
924
+
925
+
926
+ Platform sends action via Socket.IO to server
927
+
928
+
929
+ Server:
930
+ 1. Loads engine for this game
931
+ 2. Gets current GameState
932
+ 3. Calls engine.handleAction(state, playerId, action) → newState
933
+ 4. Saves newState
934
+ 5. For each player: engine.getPlayerView(newState, playerId) → playerView
935
+ 6. Emits "stateUpdate" to each player's socket with their view
936
+ 7. If engine.isGameOver(newState): triggers handleGameEnd
937
+
938
+
939
+ Renderer receives "stateUpdate" event with filtered state
940
+
941
+
942
+ React re-renders with new state
943
+ ```
944
+
945
+ ## Complete Example: Number Guessing Game
946
+
947
+ See the [`examples/number-guess`](../../examples/number-guess) directory for a complete working example.
948
+
949
+ ### config.ts
950
+ ```typescript
951
+ import type { GameConfig } from "@littlepartytime/sdk";
952
+
953
+ const config: GameConfig = {
954
+ name: "Guess the Number",
955
+ description: "Take turns guessing a secret number between 1-100. The range narrows with each guess!",
956
+ assets: {
957
+ icon: "assets/icon.png",
958
+ banner: "assets/banner.png",
959
+ cover: "assets/cover.png",
960
+ splash: "assets/splash.png",
961
+ },
962
+ minPlayers: 2,
963
+ maxPlayers: 8,
964
+ tags: ["casual", "party"],
965
+ version: "1.0.0",
966
+ sdkVersion: "2.0.0",
967
+ };
968
+
969
+ export default config;
970
+ ```
971
+
972
+ ### types.ts
973
+ ```typescript
974
+ export interface GuessGameData {
975
+ secretNumber: number;
976
+ low: number;
977
+ high: number;
978
+ currentPlayerIndex: number;
979
+ lastGuess: { playerId: string; guess: number; hint: "high" | "low" } | null;
980
+ loserId: string | null;
981
+ }
982
+
983
+ export type GuessAction = { type: "GUESS"; payload: { number: number } };
984
+ ```
985
+
986
+ ### engine.ts
987
+ ```typescript
988
+ import type { GameEngine, GameState, Player, GameAction, GameResult } from "@littlepartytime/sdk";
989
+ import type { GuessGameData } from "./types";
990
+
991
+ const engine: GameEngine = {
992
+ init(players: Player[]): GameState {
993
+ const secretNumber = Math.floor(Math.random() * 100) + 1;
994
+ return {
995
+ phase: "playing",
996
+ players: players.map(p => ({ id: p.id, nickname: p.nickname })),
997
+ data: {
998
+ secretNumber,
999
+ low: 1,
1000
+ high: 100,
1001
+ currentPlayerIndex: 0,
1002
+ lastGuess: null,
1003
+ loserId: null,
1004
+ } satisfies GuessGameData as unknown as Record<string, unknown>,
1005
+ };
1006
+ },
1007
+
1008
+ handleAction(state: GameState, playerId: string, action: GameAction): GameState {
1009
+ if (action.type !== "GUESS") return state;
1010
+ const data = state.data as unknown as GuessGameData;
1011
+
1012
+ const currentPlayer = state.players[data.currentPlayerIndex];
1013
+ if (currentPlayer.id !== playerId) return state; // Not your turn
1014
+
1015
+ const guess = (action.payload as { number: number }).number;
1016
+ if (guess < data.low || guess > data.high) return state; // Out of range
1017
+
1018
+ if (guess === data.secretNumber) {
1019
+ return {
1020
+ ...state,
1021
+ phase: "ended",
1022
+ data: {
1023
+ ...data,
1024
+ loserId: playerId,
1025
+ lastGuess: { playerId, guess, hint: "low" },
1026
+ } as unknown as Record<string, unknown>,
1027
+ };
1028
+ }
1029
+
1030
+ const hint = guess > data.secretNumber ? "high" : "low";
1031
+ const newLow = hint === "low" ? Math.max(data.low, guess + 1) : data.low;
1032
+ const newHigh = hint === "high" ? Math.min(data.high, guess - 1) : data.high;
1033
+
1034
+ return {
1035
+ ...state,
1036
+ data: {
1037
+ ...data,
1038
+ low: newLow,
1039
+ high: newHigh,
1040
+ currentPlayerIndex: (data.currentPlayerIndex + 1) % state.players.length,
1041
+ lastGuess: { playerId, guess, hint },
1042
+ } as unknown as Record<string, unknown>,
1043
+ };
1044
+ },
1045
+
1046
+ isGameOver(state: GameState): boolean {
1047
+ return state.phase === "ended";
1048
+ },
1049
+
1050
+ getResult(state: GameState): GameResult {
1051
+ const data = state.data as unknown as GuessGameData;
1052
+ return {
1053
+ rankings: state.players.map(p => ({
1054
+ playerId: p.id,
1055
+ rank: p.id === data.loserId ? state.players.length : 1,
1056
+ score: p.id === data.loserId ? 0 : 1,
1057
+ isWinner: p.id !== data.loserId,
1058
+ })),
1059
+ };
1060
+ },
1061
+
1062
+ getPlayerView(state: GameState, playerId: string): Partial<GameState> {
1063
+ const data = state.data as unknown as GuessGameData;
1064
+ // Hide the secret number while game is in progress
1065
+ if (state.phase !== "ended") {
1066
+ return {
1067
+ ...state,
1068
+ data: {
1069
+ low: data.low,
1070
+ high: data.high,
1071
+ currentPlayerIndex: data.currentPlayerIndex,
1072
+ lastGuess: data.lastGuess,
1073
+ loserId: data.loserId,
1074
+ // secretNumber is NOT included
1075
+ } as unknown as Record<string, unknown>,
1076
+ };
1077
+ }
1078
+ // Reveal everything after game ends
1079
+ return state;
1080
+ },
1081
+ };
1082
+
1083
+ export default engine;
1084
+ ```
1085
+
1086
+ ### Test file (tests/engine.test.ts)
1087
+ ```typescript
1088
+ import { describe, it, expect } from 'vitest';
1089
+ import { GameTester, GameSimulator, createMockPlayers } from '@littlepartytime/sdk/testing';
1090
+ import engine from '../src/engine';
1091
+
1092
+ describe('Number Guess Engine', () => {
1093
+ describe('GameTester - unit tests', () => {
1094
+ it('should initialize with playing phase', () => {
1095
+ const tester = new GameTester(engine);
1096
+ tester.init(createMockPlayers(3));
1097
+ expect(tester.phase).toBe('playing');
1098
+ expect(tester.playerStates).toHaveLength(3);
1099
+ });
1100
+
1101
+ it('should hide secretNumber in player view', () => {
1102
+ const tester = new GameTester(engine);
1103
+ const players = createMockPlayers(2);
1104
+ tester.init(players);
1105
+ const view = tester.getPlayerView(players[0].id);
1106
+ expect(view.data).not.toHaveProperty('secretNumber');
1107
+ });
1108
+
1109
+ it('should reject out-of-turn actions', () => {
1110
+ const tester = new GameTester(engine);
1111
+ const players = createMockPlayers(2);
1112
+ tester.init(players);
1113
+ const before = tester.state;
1114
+ tester.act(players[1].id, { type: 'GUESS', payload: { number: 50 } });
1115
+ expect(tester.state).toBe(before);
1116
+ });
1117
+ });
1118
+
1119
+ describe('GameSimulator - E2E', () => {
1120
+ it('should play a complete game', () => {
1121
+ const sim = new GameSimulator(engine, { playerCount: 3 });
1122
+ sim.start();
1123
+
1124
+ // Binary search to end the game quickly
1125
+ let lo = 1, hi = 100;
1126
+ while (!sim.isGameOver()) {
1127
+ const mid = Math.floor((lo + hi) / 2);
1128
+ sim.act(sim.currentTurn, { type: 'GUESS', payload: { number: mid } });
1129
+ if (!sim.isGameOver()) {
1130
+ const data = sim.state.data as { low: number; high: number };
1131
+ lo = data.low;
1132
+ hi = data.high;
1133
+ }
1134
+ }
1135
+
1136
+ expect(sim.isGameOver()).toBe(true);
1137
+ const result = sim.getResult();
1138
+ expect(result.rankings).toHaveLength(3);
1139
+ });
1140
+ });
1141
+ });
1142
+ ```
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @littlepartytime/sdk
2
+
3
+ Game SDK for the [Little Party Time](https://github.com/chesterli710/littlepartytime-sdk) platform — type definitions and testing utilities for game developers.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @littlepartytime/sdk
9
+ ```
10
+
11
+ ## What's Included
12
+
13
+ - **Type definitions** — `GameConfig`, `GameEngine`, `GameState`, `GameAction`, `GameResult`, `GameRendererProps`, etc.
14
+ - **Testing utilities** — `GameTester`, `GameSimulator`, `createMockPlayers` (import from `@littlepartytime/sdk/testing`)
15
+
16
+ ## Quick Example
17
+
18
+ ```typescript
19
+ import type { GameEngine, GameState, Player } from "@littlepartytime/sdk";
20
+
21
+ const engine: GameEngine = {
22
+ init(players: Player[]): GameState { /* ... */ },
23
+ handleAction(state, playerId, action) { /* ... */ },
24
+ isGameOver(state) { /* ... */ },
25
+ getResult(state) { /* ... */ },
26
+ getPlayerView(state, playerId) { /* ... */ },
27
+ };
28
+ ```
29
+
30
+ ## Documentation
31
+
32
+ Full development guide with step-by-step instructions, testing patterns, build configuration, and asset requirements:
33
+
34
+ **[GAME_DEV_GUIDE.md](https://github.com/chesterli710/littlepartytime-sdk/blob/main/packages/sdk/GAME_DEV_GUIDE.md)**
35
+
36
+ After installing the SDK, the guide is also available locally:
37
+
38
+ ```bash
39
+ cat node_modules/@littlepartytime/sdk/GAME_DEV_GUIDE.md
40
+ ```
41
+
42
+ ## Related Packages
43
+
44
+ | Package | Description |
45
+ |---------|-------------|
46
+ | [`@littlepartytime/dev-kit`](https://www.npmjs.com/package/@littlepartytime/dev-kit) | CLI dev server, build & pack toolchain |
47
+ | [`create-littlepartytime-game`](https://www.npmjs.com/package/create-littlepartytime-game) | Project scaffolding (`npx create-littlepartytime-game`) |
48
+
49
+ ## License
50
+
51
+ MIT
package/dist/types.d.ts CHANGED
@@ -24,5 +24,11 @@ export interface Platform {
24
24
  on(event: string, handler: (...args: unknown[]) => void): void;
25
25
  off(event: string, handler: (...args: unknown[]) => void): void;
26
26
  reportResult(result: GameResult): void;
27
+ /**
28
+ * 获取游戏资产的运行时 URL。
29
+ * @param path - 相对于游戏 assets/ 目录的路径,如 "cards/king.png"、"sounds/flip.mp3"
30
+ * @returns 可直接用于 <img src> / <audio src> / fetch() 的完整 URL
31
+ */
32
+ getAssetUrl(path: string): string;
27
33
  }
28
34
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,OAAO,CAAC;KACnB,EAAE,CAAC;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,IAAI,MAAM,EAAE,CAAC;IACvB,cAAc,IAAI,MAAM,CAAC;IACzB,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAChE,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;CACxC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,OAAO,CAAC;KACnB,EAAE,CAAC;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,IAAI,MAAM,EAAE,CAAC;IACvB,cAAc,IAAI,MAAM,CAAC;IACzB,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAChE,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IACvC;;;;OAIG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlepartytime/sdk",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Game SDK for Little Party Time platform - type definitions and testing utilities",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "test": "vitest run",
23
23
  "test:watch": "vitest"
24
24
  },
25
- "files": ["dist"],
25
+ "files": ["dist", "GAME_DEV_GUIDE.md"],
26
26
  "keywords": ["boardgame", "sdk", "game-engine", "littlepartytime"],
27
27
  "license": "MIT",
28
28
  "repository": {