create-smore-game 2.2.0 → 2.3.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.
Files changed (3) hide show
  1. package/README.md +159 -0
  2. package/package.json +1 -1
  3. package/templates.js +233 -61
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # create-smore-game
2
+
3
+ Scaffold a multiplayer party game for the S'MORE platform.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx create-smore-game my-game
11
+ cd my-game
12
+ npm install
13
+ npm run dev
14
+ ```
15
+
16
+ The CLI prompts you for two choices:
17
+
18
+ ```
19
+ create-smore-game
20
+
21
+ Screen (TV) template:
22
+ React + Phaser
23
+ React only
24
+ Vanilla JS
25
+
26
+ Controller (phone) template:
27
+ React
28
+ Vanilla JS
29
+ ```
30
+
31
+ After answering, your project is ready to run.
32
+
33
+ ---
34
+
35
+ ## What You Get
36
+
37
+ ```
38
+ my-game/
39
+ ├── screen/ # TV/display — game logic lives here
40
+ │ ├── src/
41
+ │ │ ├── App.tsx # Main game component
42
+ │ │ └── __tests__/ # Game tests with vitest
43
+ │ ├── package.json
44
+ │ └── vite.config.ts
45
+ ├── controller/ # Phone/player input — stateless display + input
46
+ │ ├── src/
47
+ │ │ ├── App.tsx # Controller UI
48
+ │ │ └── __tests__/ # Controller tests
49
+ │ ├── package.json
50
+ │ └── vite.config.ts
51
+ ├── dev/
52
+ │ ├── server.js # Local dev server with Socket.IO
53
+ │ ├── harness.html # Test screen + controllers together
54
+ │ └── controller-page.html
55
+ ├── game.json # Game metadata (title, player count, etc.)
56
+ ├── .env.example
57
+ └── package.json # npm workspaces root
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Development
63
+
64
+ ### Commands
65
+
66
+ | Command | Description |
67
+ |---|---|
68
+ | `npm run dev` | Start dev server (screen + controller + harness) |
69
+ | `npm run dev:screen` | Screen only |
70
+ | `npm run dev:controller` | Controller only |
71
+ | `npm run build` | Production build |
72
+ | `npm run zip` | Build + package as `game.zip` for deployment |
73
+
74
+ ### Dev Harness
75
+
76
+ Running `npm run dev` starts a local Socket.IO server and opens a harness page with the screen and one or more controller iframes side by side. You can add and remove players directly in the browser to test the full game flow without a real device.
77
+
78
+ ---
79
+
80
+ ## Architecture: Stateless Controller Pattern
81
+
82
+ S'MORE games follow a strict separation of concerns:
83
+
84
+ - **Screen (TV)** — owns all game state and logic. It is the single source of truth.
85
+ - **Controller (Phone)** — a stateless input device. It only renders what the Screen tells it to render, and only sends raw user input back.
86
+
87
+ ```
88
+ Screen → "Show vote UI with options A, B, C" → Controller renders buttons
89
+ Controller → "Player tapped A" → Screen processes vote, updates state
90
+ Screen → "Show results: A won" → Controller renders results
91
+ ```
92
+
93
+ This pattern keeps game logic centralized and makes controllers trivially simple to implement. For full API details, see the `@smoregg/sdk` documentation.
94
+
95
+ ---
96
+
97
+ ## Templates
98
+
99
+ ### Screen Templates
100
+
101
+ | Template | Best for |
102
+ |---|---|
103
+ | **React + Phaser** | Graphics-heavy games — sprites, animations, physics |
104
+ | **React** | UI-based games — cards, voting, text prompts |
105
+ | **Vanilla JS** | Minimal projects with no framework dependency |
106
+
107
+ ### Controller Templates
108
+
109
+ | Template | Best for |
110
+ |---|---|
111
+ | **React** | Recommended for most games |
112
+ | **Vanilla JS** | Lightweight alternative |
113
+
114
+ ---
115
+
116
+ ## Game Metadata
117
+
118
+ `game.json` at the project root describes your game to the S'MORE platform:
119
+
120
+ ```json
121
+ {
122
+ "id": "my-game",
123
+ "title": "My Game",
124
+ "description": "",
125
+ "version": "0.1.0",
126
+ "minPlayers": 2,
127
+ "maxPlayers": 8,
128
+ "categories": ["party"]
129
+ }
130
+ ```
131
+
132
+ Edit this file before submitting. The `id` must be unique and contain only lowercase letters, numbers, and hyphens.
133
+
134
+ ---
135
+
136
+ ## Building and Deploying
137
+
138
+ ```bash
139
+ npm run zip
140
+ ```
141
+
142
+ This builds both the screen and controller workspaces, copies `game.json` into the output, and packages everything as `game.zip`. Submit this file to the S'MORE platform to deploy your game.
143
+
144
+ ---
145
+
146
+ ## SDK Documentation
147
+
148
+ The scaffolded project uses `@smoregg/sdk` for all platform communication. See the SDK package for the full API reference, including:
149
+
150
+ - Connecting screen and controller
151
+ - Sending and receiving typed events
152
+ - Managing player sessions
153
+ - Testing utilities (`createMockScreen`, `createMockController`)
154
+
155
+ ---
156
+
157
+ ## License
158
+
159
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smore-game",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-smore-game": "./index.js"
package/templates.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // pnpm-workspace.yaml is no longer generated.
4
4
  // npm workspaces are configured in root package.json instead.
5
5
 
6
- const SDK_VERSION = '^2.2.0';
6
+ const SDK_VERSION = '^2.3.0';
7
7
 
8
8
  export function rootPackageJson(name) {
9
9
  return JSON.stringify(
@@ -35,6 +35,160 @@ export function envExample() {
35
35
  `;
36
36
  }
37
37
 
38
+ // ─── Test file templates ───
39
+
40
+ export function screenTestFile() {
41
+ return `import { describe, it, expect, beforeEach } from 'vitest';
42
+ import { createMockScreen } from '@smoregg/sdk/testing';
43
+ import type { GameEvents } from '../App';
44
+
45
+ describe('Game Screen', () => {
46
+ let screen: ReturnType<typeof createMockScreen<GameEvents>>;
47
+
48
+ beforeEach(() => {
49
+ screen = createMockScreen<GameEvents>({ autoReady: true });
50
+ });
51
+
52
+ it('should broadcast score-update when player taps', async () => {
53
+ await screen.ready;
54
+
55
+ // Simulate a tap from player 0
56
+ screen.simulateEvent('tap', 0, { timestamp: Date.now() });
57
+
58
+ // Verify the screen would broadcast the score
59
+ // (Your game logic goes here)
60
+ });
61
+
62
+ it('should handle player reconnection', async () => {
63
+ await screen.ready;
64
+
65
+ // Simulate player reconnect
66
+ screen.simulateControllerReconnect(0);
67
+
68
+ // In a stateless controller pattern, Screen should re-push
69
+ // the current view state to the reconnecting controller
70
+ });
71
+ });
72
+ `;
73
+ }
74
+
75
+ export function controllerTestFile() {
76
+ return `import { describe, it, expect, beforeEach } from 'vitest';
77
+ import { createMockController } from '@smoregg/sdk/testing';
78
+ import type { GameEvents } from '../App';
79
+
80
+ describe('Game Controller', () => {
81
+ let controller: ReturnType<typeof createMockController<GameEvents>>;
82
+
83
+ beforeEach(() => {
84
+ controller = createMockController<GameEvents>({ autoReady: true });
85
+ });
86
+
87
+ it('should send tap event on user input', async () => {
88
+ await controller.ready;
89
+
90
+ controller.send('tap', { timestamp: Date.now() });
91
+
92
+ const sends = controller.getSends();
93
+ expect(sends).toHaveLength(1);
94
+ expect(sends[0].event).toBe('tap');
95
+ });
96
+
97
+ it('should update display when Screen pushes score', async () => {
98
+ await controller.ready;
99
+
100
+ // Simulate Screen pushing a score update
101
+ controller.simulateEvent('score-update', { score: 42 });
102
+
103
+ // Your rendering logic would update based on this event
104
+ });
105
+ });
106
+ `;
107
+ }
108
+
109
+ export function screenTestFileVanilla() {
110
+ return `import { describe, it, beforeEach } from 'vitest';
111
+ import { createMockScreen } from '@smoregg/sdk/testing';
112
+
113
+ interface GameEvents {
114
+ // Screen → Controller (view state)
115
+ 'score-update': { score: number };
116
+ 'personal-message': { text: string };
117
+ // Controller → Screen (input)
118
+ 'tap': { timestamp: number };
119
+ }
120
+
121
+ describe('Game Screen', () => {
122
+ let screen: ReturnType<typeof createMockScreen<GameEvents>>;
123
+
124
+ beforeEach(() => {
125
+ screen = createMockScreen<GameEvents>({ autoReady: true });
126
+ });
127
+
128
+ it('should broadcast score-update when player taps', async () => {
129
+ await screen.ready;
130
+
131
+ // Simulate a tap from player 0
132
+ screen.simulateEvent('tap', 0, { timestamp: Date.now() });
133
+
134
+ // Verify the screen would broadcast the score
135
+ // (Your game logic goes here)
136
+ });
137
+
138
+ it('should handle player reconnection', async () => {
139
+ await screen.ready;
140
+
141
+ // Simulate player reconnect
142
+ screen.simulateControllerReconnect(0);
143
+
144
+ // In a stateless controller pattern, Screen should re-push
145
+ // the current view state to the reconnecting controller
146
+ });
147
+ });
148
+ `;
149
+ }
150
+
151
+ export function controllerTestFileVanilla() {
152
+ return `import { describe, it, expect, beforeEach } from 'vitest';
153
+ import { createMockController } from '@smoregg/sdk/testing';
154
+
155
+ interface GameEvents {
156
+ // Screen → Controller (view state)
157
+ 'score-update': { score: number };
158
+ 'personal-message': { text: string };
159
+ // Controller → Screen (input)
160
+ 'tap': { timestamp: number };
161
+ }
162
+
163
+ describe('Game Controller', () => {
164
+ let controller: ReturnType<typeof createMockController<GameEvents>>;
165
+
166
+ beforeEach(() => {
167
+ controller = createMockController<GameEvents>({ autoReady: true });
168
+ });
169
+
170
+ it('should send tap event on user input', async () => {
171
+ await controller.ready;
172
+
173
+ controller.send('tap', { timestamp: Date.now() });
174
+
175
+ const sends = controller.getSends();
176
+ expect(sends).toHaveLength(1);
177
+ expect(sends[0].event).toBe('tap');
178
+ });
179
+
180
+ it('should update display when Screen pushes score', async () => {
181
+ await controller.ready;
182
+
183
+ // Simulate Screen pushing a score update
184
+ controller.simulateEvent('score-update', { score: 42 });
185
+
186
+ // Your rendering logic would update based on this event
187
+ });
188
+ });
189
+ `;
190
+ }
191
+
38
192
  // ─── Screen templates ───
39
193
 
40
194
  const screenTsconfig = JSON.stringify(
@@ -130,6 +284,7 @@ export function screenReactPhaser(gameId) {
130
284
  scripts: {
131
285
  dev: "vite",
132
286
  build: "tsc && vite build",
287
+ test: "vitest run",
133
288
  },
134
289
  dependencies: {
135
290
  react: "^18.3.1",
@@ -143,6 +298,7 @@ export function screenReactPhaser(gameId) {
143
298
  "@vitejs/plugin-react": "^4.3.0",
144
299
  typescript: "^5.5.0",
145
300
  vite: "^5.4.0",
301
+ vitest: "^1.6.0",
146
302
  },
147
303
  },
148
304
  null,
@@ -163,8 +319,10 @@ import Phaser from 'phaser';
163
319
  import { GameScene } from './scenes/GameScene';
164
320
 
165
321
  interface GameEvents {
322
+ // Screen → Controller (view state)
166
323
  'score-update': { score: number };
167
324
  'personal-message': { text: string };
325
+ // Controller → Screen (input)
168
326
  'tap': { timestamp: number };
169
327
  }
170
328
 
@@ -185,6 +343,7 @@ export function App() {
185
343
  const gameRef = useRef<Phaser.Game | null>(null);
186
344
  const [roomCode, setRoomCode] = useState('');
187
345
  const [controllers, setControllers] = useState<ControllerInfo[]>([]);
346
+ const [tapCount, setTapCount] = useState(0);
188
347
 
189
348
  useEffect(() => {
190
349
  let mounted = true;
@@ -238,6 +397,12 @@ export function App() {
238
397
  console.log('Player', playerIndex, 'tapped:', data);
239
398
  // Forward input to Phaser scene
240
399
  gameRef.current?.events.emit('player-tap', { playerIndex, ...data });
400
+ // Broadcast updated score back to controllers (Screen is source of truth)
401
+ setTapCount((prev) => {
402
+ const newCount = prev + 1;
403
+ screen.broadcast('score-update', { score: newCount });
404
+ return newCount;
405
+ });
241
406
  });
242
407
 
243
408
  return () => {
@@ -305,6 +470,7 @@ export class GameScene extends Phaser.Scene {
305
470
  }
306
471
  }
307
472
  `,
473
+ "src/__tests__/game.test.ts": screenTestFile(),
308
474
  };
309
475
  }
310
476
 
@@ -321,6 +487,7 @@ export function screenReact(gameId) {
321
487
  scripts: {
322
488
  dev: "vite",
323
489
  build: "tsc && vite build",
490
+ test: "vitest run",
324
491
  },
325
492
  dependencies: {
326
493
  react: "^18.3.1",
@@ -333,6 +500,7 @@ export function screenReact(gameId) {
333
500
  "@vitejs/plugin-react": "^4.3.0",
334
501
  typescript: "^5.5.0",
335
502
  vite: "^5.4.0",
503
+ vitest: "^1.6.0",
336
504
  },
337
505
  },
338
506
  null,
@@ -351,8 +519,10 @@ import type { Screen, ControllerInfo, GameResults } from '@smoregg/sdk';
351
519
  import { useEffect, useRef, useState } from 'react';
352
520
 
353
521
  interface GameEvents {
522
+ // Screen → Controller (view state)
354
523
  'score-update': { score: number };
355
524
  'personal-message': { text: string };
525
+ // Controller → Screen (input)
356
526
  'tap': { timestamp: number };
357
527
  }
358
528
 
@@ -373,7 +543,7 @@ export function App() {
373
543
  const [roomCode, setRoomCode] = useState('');
374
544
  const [controllers, setControllers] = useState<ControllerInfo[]>([]);
375
545
  const [taps, setTaps] = useState<{ playerIndex: number; time: number }[]>([]);
376
- const [customStates, setCustomStates] = useState<Record<number, Record<string, unknown>>>({});
546
+ const [tapCount, setTapCount] = useState(0);
377
547
 
378
548
  useEffect(() => {
379
549
  let mounted = true;
@@ -413,11 +583,6 @@ export function App() {
413
583
  setControllers([...screen.controllers]);
414
584
  });
415
585
 
416
- screen.onCustomStateChange((playerIndex, state) => {
417
- if (!mounted) return;
418
- setCustomStates((prev) => ({ ...prev, [playerIndex]: state }));
419
- });
420
-
421
586
  screenRef.current = screen;
422
587
 
423
588
  // Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
@@ -425,6 +590,12 @@ export function App() {
425
590
  screen.on('tap', (playerIndex, data) => {
426
591
  if (!mounted) return;
427
592
  setTaps((prev) => [...prev.slice(-9), { playerIndex, time: Date.now() }]);
593
+ // Broadcast updated score back to controllers (Screen is source of truth)
594
+ setTapCount((prev) => {
595
+ const newCount = prev + 1;
596
+ screen.broadcast('score-update', { score: newCount });
597
+ return newCount;
598
+ });
428
599
  });
429
600
 
430
601
  return () => {
@@ -434,11 +605,6 @@ export function App() {
434
605
  };
435
606
  }, []);
436
607
 
437
- // Example: send score update to all players
438
- const handleBroadcastScore = () => {
439
- screenRef.current?.broadcast('score-update', { score: 100 });
440
- };
441
-
442
608
  // Example: send to specific player
443
609
  const handleSendToPlayer = (playerIndex: number) => {
444
610
  screenRef.current?.sendToController(playerIndex, 'personal-message', { text: 'Hello!' });
@@ -476,6 +642,7 @@ export function App() {
476
642
  );
477
643
  }
478
644
  `,
645
+ "src/__tests__/game.test.ts": screenTestFile(),
479
646
  };
480
647
  }
481
648
 
@@ -492,6 +659,7 @@ export function screenVanilla(gameId) {
492
659
  scripts: {
493
660
  dev: "vite",
494
661
  build: "tsc && vite build",
662
+ test: "vitest run",
495
663
  },
496
664
  dependencies: {
497
665
  "@smoregg/sdk": SDK_VERSION, // TODO: auto-detect or update on release
@@ -499,6 +667,7 @@ export function screenVanilla(gameId) {
499
667
  devDependencies: {
500
668
  typescript: "^5.5.0",
501
669
  vite: "^5.4.0",
670
+ vitest: "^1.6.0",
502
671
  },
503
672
  },
504
673
  null,
@@ -535,8 +704,10 @@ export function screenVanilla(gameId) {
535
704
  import type { GameResults } from '@smoregg/sdk';
536
705
 
537
706
  interface GameEvents {
707
+ // Screen → Controller (view state)
538
708
  'score-update': { score: number };
539
709
  'personal-message': { text: string };
710
+ // Controller → Screen (input)
540
711
  'tap': { timestamp: number };
541
712
  }
542
713
 
@@ -590,6 +761,8 @@ screen.onAllReady(() => {
590
761
 
591
762
  // Use screen.on(event, handler) / screen.off(event, handler) for dynamic event listeners.
592
763
  // destroy() automatically removes all listeners, so explicit off() cleanup is not needed.
764
+ let tapCount = 0;
765
+
593
766
  screen.on('tap', (playerIndex, data) => {
594
767
  const line = document.createElement('div');
595
768
  line.textContent = \`Player \${playerIndex} tapped\`;
@@ -598,6 +771,9 @@ screen.on('tap', (playerIndex, data) => {
598
771
  while (logEl.children.length > 10) {
599
772
  logEl.removeChild(logEl.firstChild!);
600
773
  }
774
+ // Broadcast updated score back to controllers (Screen is source of truth)
775
+ tapCount++;
776
+ screen.broadcast('score-update', { score: tapCount });
601
777
  });
602
778
 
603
779
  function updateStatus() {
@@ -606,8 +782,7 @@ function updateStatus() {
606
782
  }
607
783
 
608
784
  // Example functions (can be called from console for testing):
609
- // screen.broadcast('score-update', { score: 100 });
610
- // screen.sendToController(0, 'message', { text: 'Hello!' });
785
+ // screen.sendToController(0, 'personal-message', { text: 'Hello!' });
611
786
  // const results: GameResults = {
612
787
  // scores: { 0: 50, 1: 75 },
613
788
  // // winner: 0,
@@ -615,6 +790,7 @@ function updateStatus() {
615
790
  // };
616
791
  // screen.gameOver(results);
617
792
  `,
793
+ "src/__tests__/game.test.ts": screenTestFileVanilla(),
618
794
  };
619
795
  }
620
796
 
@@ -666,6 +842,7 @@ export function controllerReact(gameId) {
666
842
  scripts: {
667
843
  dev: "vite",
668
844
  build: "tsc && vite build",
845
+ test: "vitest run",
669
846
  },
670
847
  dependencies: {
671
848
  react: "^18.3.1",
@@ -678,6 +855,7 @@ export function controllerReact(gameId) {
678
855
  "@vitejs/plugin-react": "^4.3.0",
679
856
  typescript: "^5.5.0",
680
857
  vite: "^5.4.0",
858
+ vitest: "^1.6.0",
681
859
  },
682
860
  },
683
861
  null,
@@ -725,11 +903,27 @@ import type { Controller, ControllerInfo } from '@smoregg/sdk';
725
903
  import { useEffect, useRef, useState } from 'react';
726
904
 
727
905
  interface GameEvents {
906
+ // Screen → Controller (view state)
728
907
  'score-update': { score: number };
729
908
  'personal-message': { text: string };
909
+ // Controller → Screen (input)
730
910
  'tap': { timestamp: number };
731
911
  }
732
912
 
913
+ /**
914
+ * ARCHITECTURE: Stateless Controller Pattern
915
+ *
916
+ * The controller is a stateless display + input device:
917
+ * - Render ONLY what the Screen sends (via controller.on())
918
+ * - Send ONLY user input to Screen (via controller.send())
919
+ * - Do NOT store or compute game state here
920
+ *
921
+ * Data flow:
922
+ * Controller → Screen: controller.send('tap', { timestamp }) (input only)
923
+ * Screen → Controller: 'score-update' { score } (view state)
924
+ * Controller renders: score from Screen (source of truth)
925
+ */
926
+
733
927
  export function App() {
734
928
  const controllerRef = useRef<Controller | null>(null);
735
929
  const [myIndex, setMyIndex] = useState(-1);
@@ -776,7 +970,6 @@ export function App() {
776
970
 
777
971
  const handleTap = () => {
778
972
  controllerRef.current?.send('tap', { timestamp: Date.now() });
779
- setCount((c) => c + 1);
780
973
  };
781
974
 
782
975
  return (
@@ -787,7 +980,7 @@ export function App() {
787
980
  touchAction: 'manipulation', userSelect: 'none',
788
981
  }}>
789
982
  {isReady && (
790
- <div style={{ fontSize: '16px', opacity: 0.6 }}>{me?.name ?? \`Player \${myIndex}\`}</div>
983
+ <div style={{ fontSize: '16px', opacity: 0.6 }}>{me?.nickname ?? \`Player \${myIndex}\`}</div>
791
984
  )}
792
985
  <div style={{ fontSize: '48px', fontWeight: 'bold' }}>{count}</div>
793
986
  <button
@@ -805,6 +998,7 @@ export function App() {
805
998
  );
806
999
  }
807
1000
  `,
1001
+ "src/__tests__/game.test.ts": controllerTestFile(),
808
1002
  };
809
1003
  }
810
1004
 
@@ -819,6 +1013,7 @@ export function controllerVanilla(gameId) {
819
1013
  scripts: {
820
1014
  dev: "vite",
821
1015
  build: "tsc && vite build",
1016
+ test: "vitest run",
822
1017
  },
823
1018
  dependencies: {
824
1019
  "@smoregg/sdk": SDK_VERSION, // TODO: auto-detect or update on release
@@ -826,6 +1021,7 @@ export function controllerVanilla(gameId) {
826
1021
  devDependencies: {
827
1022
  typescript: "^5.5.0",
828
1023
  vite: "^5.4.0",
1024
+ vitest: "^1.6.0",
829
1025
  },
830
1026
  },
831
1027
  null,
@@ -884,12 +1080,26 @@ export default defineConfig({
884
1080
  "src/main.ts": `import { createController } from '@smoregg/sdk';
885
1081
 
886
1082
  interface GameEvents {
1083
+ // Screen → Controller (view state)
887
1084
  'score-update': { score: number };
888
1085
  'personal-message': { text: string };
1086
+ // Controller → Screen (input)
889
1087
  'tap': { timestamp: number };
890
1088
  }
891
1089
 
892
- let count = 0;
1090
+ /**
1091
+ * ARCHITECTURE: Stateless Controller Pattern
1092
+ *
1093
+ * The controller is a stateless display + input device:
1094
+ * - Render ONLY what the Screen sends (via controller.on())
1095
+ * - Send ONLY user input to Screen (via controller.send())
1096
+ * - Do NOT store or compute game state here
1097
+ *
1098
+ * Data flow:
1099
+ * Controller → Screen: controller.send('tap', { timestamp }) (input only)
1100
+ * Screen → Controller: 'score-update' { score } (view state)
1101
+ * Controller renders: score from Screen (source of truth)
1102
+ */
893
1103
 
894
1104
  const playerInfoEl = document.getElementById('player-info')!;
895
1105
  const countEl = document.getElementById('count')!;
@@ -905,12 +1115,11 @@ const controller = createController<GameEvents>({ debug: true });
905
1115
  // To control autoReady: createScreen({ autoReady: false }) or createController({ autoReady: false })
906
1116
 
907
1117
  controller.onAllReady(() => {
908
- playerInfoEl.textContent = controller.me?.name ?? \`Player \${controller.myPlayerIndex}\`;
1118
+ playerInfoEl.textContent = controller.me?.nickname ?? \`Player \${controller.myPlayerIndex}\`;
909
1119
  });
910
1120
 
911
1121
  controller.on('score-update', (data) => {
912
- count = data.score;
913
- countEl.textContent = String(count);
1122
+ countEl.textContent = String(data.score);
914
1123
  });
915
1124
 
916
1125
  controller.on('personal-message', (data) => {
@@ -919,10 +1128,9 @@ controller.on('personal-message', (data) => {
919
1128
 
920
1129
  tapBtn.addEventListener('pointerdown', () => {
921
1130
  controller.send('tap', { timestamp: Date.now() });
922
- count++;
923
- countEl.textContent = String(count);
924
1131
  });
925
1132
  `,
1133
+ "src/__tests__/game.test.ts": controllerTestFileVanilla(),
926
1134
  };
927
1135
  }
928
1136
 
@@ -1004,7 +1212,6 @@ const room = {
1004
1212
  gameId: '',
1005
1213
  status: 'waiting',
1006
1214
  readyIds: new Set(),
1007
- customStates: new Map(), // playerIndex -> state object
1008
1215
  };
1009
1216
 
1010
1217
  function toPlayerDTO(p) {
@@ -1151,20 +1358,6 @@ async function main() {
1151
1358
  io.to(room.code).emit('smore:game-over', data);
1152
1359
  });
1153
1360
 
1154
- socket.on('smore:set-custom-state', (data) => {
1155
- if (socket.role !== 'player') return;
1156
- const prev = room.customStates.get(socket.playerIndex) || {};
1157
- const next = Object.assign({}, prev, data && typeof data === 'object' ? data : {});
1158
- room.customStates.set(socket.playerIndex, next);
1159
- io.to(room.code).emit('smore:custom-state-changed', { playerIndex: socket.playerIndex, state: next });
1160
- });
1161
-
1162
- socket.on('smore:get-custom-states', () => {
1163
- const states = {};
1164
- room.customStates.forEach((state, playerIndex) => { states[playerIndex] = state; });
1165
- socket.emit('smore:custom-states', { states });
1166
- });
1167
-
1168
1361
  socket.on('smore:return-to-selection', () => {
1169
1362
  room.status = 'waiting';
1170
1363
  room.gameId = '';
@@ -1186,7 +1379,6 @@ async function main() {
1186
1379
  room.gameId = '';
1187
1380
  room.status = 'waiting';
1188
1381
  room.readyIds = new Set();
1189
- room.customStates = new Map();
1190
1382
  console.log(' [reset] Room reset by host');
1191
1383
  if (typeof callback === 'function') callback({ success: true });
1192
1384
  });
@@ -1561,7 +1753,7 @@ export function devHarness(gameId) {
1561
1753
  var screenSysEvents = [
1562
1754
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1563
1755
  'smore:player-character-updated', 'smore:rate-limited', 'smore:game-over', 'smore:all-ready',
1564
- 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
1756
+ 'smore:self-disconnected', 'smore:self-reconnected'
1565
1757
  ];
1566
1758
  screenSysEvents.forEach(function(sysEvent) {
1567
1759
  screenSocket.on(sysEvent, function(data) {
@@ -1643,7 +1835,7 @@ export function devHarness(gameId) {
1643
1835
  'smore:game-over',
1644
1836
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1645
1837
  'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
1646
- 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
1838
+ 'smore:self-disconnected', 'smore:self-reconnected'
1647
1839
  ];
1648
1840
  ctrlSysEvents.forEach(function(sysEvent) {
1649
1841
  controllerSocket.on(sysEvent, function(data) {
@@ -1759,16 +1951,6 @@ export function devHarness(gameId) {
1759
1951
  return;
1760
1952
  }
1761
1953
 
1762
- // Relay custom state events to server
1763
- if (event === 'smore:set-custom-state') {
1764
- player.socket.emit('smore:set-custom-state', data || {});
1765
- return;
1766
- }
1767
- if (event === 'smore:get-custom-states') {
1768
- player.socket.emit('smore:get-custom-states');
1769
- return;
1770
- }
1771
-
1772
1954
  // Block smore:* events
1773
1955
  if (event.startsWith('smore:')) return;
1774
1956
 
@@ -1982,7 +2164,7 @@ export function devControllerPage(gameId) {
1982
2164
  'smore:game-over',
1983
2165
  'smore:player-joined', 'smore:player-left', 'smore:player-disconnected', 'smore:player-reconnected',
1984
2166
  'smore:player-character-updated', 'smore:rate-limited', 'smore:all-ready',
1985
- 'smore:self-disconnected', 'smore:self-reconnected', 'smore:custom-state-changed', 'smore:custom-states'
2167
+ 'smore:self-disconnected', 'smore:self-reconnected'
1986
2168
  ];
1987
2169
  sysEvents.forEach(function(sysEvent) {
1988
2170
  socket.on(sysEvent, function(data) {
@@ -2044,16 +2226,6 @@ export function devControllerPage(gameId) {
2044
2226
  return;
2045
2227
  }
2046
2228
 
2047
- // Relay custom state events to server
2048
- if (event === 'smore:set-custom-state') {
2049
- socket.emit('smore:set-custom-state', data || {});
2050
- return;
2051
- }
2052
- if (event === 'smore:get-custom-states') {
2053
- socket.emit('smore:get-custom-states');
2054
- return;
2055
- }
2056
-
2057
2229
  // Block smore:* events
2058
2230
  if (event.startsWith('smore:')) return;
2059
2231