@littlepartytime/sdk 2.2.0 → 2.2.2

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.
package/GAME_DEV_GUIDE.md CHANGED
@@ -253,12 +253,12 @@ export default function GameRenderer({ platform, state }: GameRendererProps) {
253
253
  [platform]
254
254
  );
255
255
 
256
- // Render game UI
256
+ // Render game UI — use inline styles + CSS variables for consistent styling
257
257
  return (
258
- <div>
258
+ <div style={{ color: "var(--text-primary)" }}>
259
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. */}
260
+ {/* Use inline styles with CSS variables from the platform design tokens */}
261
+ {/* See "Renderer Styling Guide" section below for details */}
262
262
  </div>
263
263
  );
264
264
  }
@@ -724,7 +724,7 @@ export default function GameRenderer({ platform, state }: GameRendererProps) {
724
724
  | Approach | When to Use | How |
725
725
  |----------|-------------|-----|
726
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 |
727
+ | **CSS/SVG** | UI elements, icons | Inline styles, `<style>` injection, or inline SVG components |
728
728
  | **Emoji/Unicode** | Simple visual indicators | Unicode characters directly in JSX |
729
729
 
730
730
  > **Tip:** Use `platform.getAssetUrl()` for assets 100KB+ instead of inlining them into the bundle.
@@ -822,30 +822,83 @@ interface PlayerState {
822
822
 
823
823
  ### Renderer Rules
824
824
  1. **React functional component**: Use hooks, not class components.
825
- 2. **Tailwind CSS only**: Use the platform's design tokens for consistent styling.
825
+ 2. **Inline styles + `<style>` injection**: Do NOT use Tailwind CSS (see "Renderer Styling Guide" below). Use inline styles with CSS variables for layout and theming. Use `<style>` injection for animations and pseudo-classes.
826
826
  3. **Mobile-first**: Design for phone screens (375px width). The platform is a PWA.
827
827
  4. **No direct socket access**: Only use `platform.send()` and `platform.on()`.
828
828
  5. **Chinese UI text**: The platform targets Chinese-speaking users.
829
829
  6. **Responsive touch targets**: Buttons should be at least 44x44px for mobile.
830
830
 
831
+ ### Renderer Styling Guide
832
+
833
+ **Do NOT use Tailwind CSS in game renderers.** The production platform pre-compiles Tailwind at build time and only scans the platform's own source code — game bundles are loaded dynamically at runtime, so Tailwind utility classes (especially arbitrary values like `w-[280px]`, `text-[18px]`) will silently fail in production even though they work in the dev-kit.
834
+
835
+ Use **inline styles** as the primary styling method, with **`<style>` injection** for features that inline styles can't express (animations, pseudo-classes, hover states).
836
+
837
+ #### Inline styles (primary)
838
+
839
+ ```tsx
840
+ // ✅ Use inline styles with CSS variables
841
+ <div style={{ width: 280, height: 60, fontSize: 18, color: "var(--text-primary)" }}>
842
+
843
+ // ❌ Do NOT use Tailwind classes
844
+ <div className="w-[280px] h-[60px] text-[18px] text-text-primary">
845
+ ```
846
+
847
+ #### `<style>` injection (for animations and pseudo-classes)
848
+
849
+ ```tsx
850
+ const GameStyles = () => (
851
+ <style>{`
852
+ @keyframes card-flip {
853
+ 0% { transform: rotateY(0deg); }
854
+ 50% { transform: rotateY(90deg); scale: 1.1; }
855
+ 100% { transform: rotateY(180deg); }
856
+ }
857
+ .my-game-card-flip { animation: card-flip 0.6s ease-in-out; }
858
+ .my-game-btn:disabled { opacity: 0.4; }
859
+ .my-game-input:focus { outline: none; border-color: var(--accent-primary); }
860
+ `}</style>
861
+ );
862
+
863
+ export default function GameRenderer({ platform, state }: GameRendererProps) {
864
+ return (
865
+ <div>
866
+ <GameStyles />
867
+ <div className="my-game-card-flip" style={{ width: 120, height: 180 }}>
868
+ {/* card content */}
869
+ </div>
870
+ </div>
871
+ );
872
+ }
873
+ ```
874
+
875
+ > **Tip:** Prefix your CSS class names with your game name (e.g., `my-game-`) to avoid conflicts when multiple games coexist.
876
+
877
+ #### Why this approach
878
+
879
+ - **Zero dependencies**: No CSS framework needed
880
+ - **Consistent across environments**: Inline styles work identically in dev-kit and production
881
+ - **No conflicts**: Inline styles are naturally isolated; multiple games won't interfere
882
+ - **More flexible animations**: Native CSS keyframes support multi-stage animations, cubic-bezier curves, and staggered delays — more powerful than Tailwind's preset `animate-*` classes
883
+
831
884
  ### Design Token Reference
832
885
 
833
- Use these CSS variables / Tailwind classes for consistent styling:
834
-
835
- | Purpose | CSS Variable | Tailwind Class |
836
- |---------|-------------|----------------|
837
- | Page background | `--bg-primary` | `bg-bg-primary` |
838
- | Card background | `--bg-secondary` | `bg-bg-secondary` |
839
- | Elevated surface | `--bg-tertiary` | `bg-bg-tertiary` |
840
- | Primary accent | `--accent-primary` | `text-accent` / `bg-accent` |
841
- | Primary text | `--text-primary` | `text-text-primary` |
842
- | Secondary text | `--text-secondary` | `text-text-secondary` |
843
- | Muted text | `--text-tertiary` | `text-text-tertiary` |
844
- | Border | `--border-default` | `border-border-default` |
845
- | Success | `--success` | `text-success` |
846
- | Error | `--error` | `text-error` |
847
- | Display font | `--font-display` | `font-display` |
848
- | Body font | `--font-body` | `font-body` |
886
+ Use these CSS variables in your inline styles for consistent theming:
887
+
888
+ | Purpose | CSS Variable | Inline Style Example |
889
+ |---------|-------------|---------------------|
890
+ | Page background | `--bg-primary` | `background: "var(--bg-primary)"` |
891
+ | Card background | `--bg-secondary` | `background: "var(--bg-secondary)"` |
892
+ | Elevated surface | `--bg-tertiary` | `background: "var(--bg-tertiary)"` |
893
+ | Primary accent | `--accent-primary` | `color: "var(--accent-primary)"` |
894
+ | Primary text | `--text-primary` | `color: "var(--text-primary)"` |
895
+ | Secondary text | `--text-secondary` | `color: "var(--text-secondary)"` |
896
+ | Muted text | `--text-tertiary` | `color: "var(--text-tertiary)"` |
897
+ | Border | `--border-default` | `border: "1px solid var(--border-default)"` |
898
+ | Success | `--success` | `color: "var(--success)"` |
899
+ | Error | `--error` | `color: "var(--error)"` |
900
+ | Display font | `--font-display` | `fontFamily: "var(--font-display)"` |
901
+ | Body font | `--font-body` | `fontFamily: "var(--font-body)"` |
849
902
 
850
903
  ## Platform Runtime Constraints
851
904
 
@@ -982,6 +1035,101 @@ Renderer receives "stateUpdate" event with filtered state
982
1035
  React re-renders with new state
983
1036
  ```
984
1037
 
1038
+ ## Game Settlement Lifecycle
1039
+
1040
+ When `isGameOver()` returns `true`, the platform **immediately** takes over: it sends `game:result` to all clients, transitions to the settlement screen, and **unloads the game renderer**. This happens in the same tick as the final `game:state` broadcast:
1041
+
1042
+ ```
1043
+ engine.handleAction(state, playerId, action) → newState
1044
+ broadcastPlayerViews(newState) ← last game frame sent
1045
+ engine.isGameOver(newState) → true
1046
+ handleGameEnd() ← platform takes over, renderer unloaded
1047
+ ```
1048
+
1049
+ If your game needs an in-game settlement screen (animations, rankings, replay, etc.), you **must not** let `isGameOver()` return `true` until that screen is done.
1050
+
1051
+ ### Two-Phase Settlement Pattern
1052
+
1053
+ Split game ending into two phases:
1054
+
1055
+ ```
1056
+ playing → (winner decided) → settlement → (player confirms or timer expires) → finished
1057
+ ↑ ↑
1058
+ isGameOver = false isGameOver = true
1059
+ game renderer shows platform takes over
1060
+ its own result screen
1061
+ ```
1062
+
1063
+ #### Engine Implementation
1064
+
1065
+ ```typescript
1066
+ handleAction(state: GameState, playerId: string, action: GameAction): GameState {
1067
+ const data = state.data as MyGameData;
1068
+
1069
+ // 1. Normal gameplay — when winner is decided, enter settlement
1070
+ if (state.phase === "playing" && winnerDecided(data)) {
1071
+ return {
1072
+ ...state,
1073
+ phase: "settlement",
1074
+ data: { ...data, rankings: computeRankings(data) },
1075
+ };
1076
+ }
1077
+
1078
+ // 2. Settlement — wait for confirm action, then finish
1079
+ if (state.phase === "settlement" && action.type === "CONFIRM_RESULT") {
1080
+ return { ...state, phase: "finished" };
1081
+ }
1082
+
1083
+ return state;
1084
+ },
1085
+
1086
+ isGameOver(state: GameState): boolean {
1087
+ return state.phase === "finished"; // NOT "settlement"!
1088
+ },
1089
+
1090
+ getPlayerView(state: GameState, playerId: string): Partial<GameState> {
1091
+ if (state.phase === "settlement") {
1092
+ // Return rankings / stats for the in-game result screen
1093
+ return {
1094
+ ...state,
1095
+ data: { rankings: (state.data as MyGameData).rankings },
1096
+ };
1097
+ }
1098
+ // ...
1099
+ },
1100
+ ```
1101
+
1102
+ #### Renderer Implementation
1103
+
1104
+ ```tsx
1105
+ export default function GameRenderer({ platform, state }: GameRendererProps) {
1106
+ if (state.phase === "settlement") {
1107
+ return (
1108
+ <div>
1109
+ {/* Show your in-game result screen here */}
1110
+ <Rankings data={state.data.rankings} />
1111
+ <button onClick={() => platform.send({ type: "CONFIRM_RESULT" })}>
1112
+ 确认
1113
+ </button>
1114
+ </div>
1115
+ );
1116
+ }
1117
+ // ... normal gameplay UI
1118
+ }
1119
+ ```
1120
+
1121
+ ### Common Mistakes
1122
+
1123
+ | Mistake | Consequence |
1124
+ |---------|------------|
1125
+ | `isGameOver()` returns `true` in `settlement` phase | In-game result screen is skipped — platform unloads the renderer immediately |
1126
+ | No `settlement` phase; jumping straight from `playing` to `finished` | Players never see the in-game result animation / ranking |
1127
+ | Calling `platform.reportResult()` from the renderer | No-op — settlement is driven entirely by the server via `isGameOver` + `getResult` |
1128
+
1129
+ ### One-Liner Rule
1130
+
1131
+ > **`isGameOver()` is the platform takeover switch. Do not return `true` until your in-game result screen is done.**
1132
+
985
1133
  ## Complete Example: Number Guessing Game
986
1134
 
987
1135
  See the [`examples/number-guess`](../../examples/number-guess) directory for a complete working example.
@@ -32,6 +32,16 @@ export interface GameConfig {
32
32
  export interface GameEngine {
33
33
  init(players: Player[], options?: Record<string, unknown>): GameState;
34
34
  handleAction(state: GameState, playerId: string, action: GameAction): GameState;
35
+ /**
36
+ * Called after every handleAction. When this returns true, the platform
37
+ * immediately sends game:result, unloads the renderer, and shows the
38
+ * platform settlement screen.
39
+ *
40
+ * If your game has an in-game result screen (animations, rankings, etc.),
41
+ * keep returning false during that phase and only return true when the
42
+ * in-game result screen is done. See "Game Settlement Lifecycle" in
43
+ * GAME_DEV_GUIDE.md.
44
+ */
35
45
  isGameOver(state: GameState): boolean;
36
46
  getResult(state: GameState): GameResult;
37
47
  getPlayerView(state: GameState, playerId: string): Partial<GameState>;
@@ -1 +1 @@
1
- {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtE,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAChF,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;IACtC,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACxC,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CAC3B"}
1
+ {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExE,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtE,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;IAChF;;;;;;;;;OASG;IACH,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;IACtC,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACxC,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACvE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;CAC3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlepartytime/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",