@uzuhq/code-sdk 0.7.4 → 0.7.5

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/README.md CHANGED
@@ -144,7 +144,7 @@ sync<GameState>({
144
144
  | キー | 型 | 必須 | 説明 |
145
145
  | -------------- | ------------------------------------------------------------ | ---- | ---------------------------- |
146
146
  | `playerCount` | `number` | Yes | プレイヤー数 |
147
- | `initialState` | `(seats: Seat[]) => S` | Yes | 初期 state を生成 |
147
+ | `initialState` | `(players: Seat[]) => S` | Yes | 初期 state を生成 |
148
148
  | `onState` | `(state: S, myPlayerId: string, serverTime: number) => void` | Yes | state 更新時のコールバック |
149
149
  | `inputs` | `(patch: PatchFn, set: SetFn) => void` | Yes | 入力ハンドラ登録 |
150
150
  | `events` | `Record<string, (data: Record<string, unknown>) => void>` | No | ゲームイベントハンドラ |
@@ -222,10 +222,9 @@ run({
222
222
  import type { GameLogic } from '@uzuhq/code-sdk';
223
223
 
224
224
  const logic: GameLogic<MyState> = {
225
- setup({ seats, ctx }) {
225
+ setup({ players, ctx }) {
226
226
  // 初期 state を生成。ctx.random は SeededRandom、ctx.now はサーバーの実時刻。
227
- // seats には観戦系の席 (kind: 'spectator' | 'admin') も含まれ得るため、
228
- // ゲームの席は kind === 'player' に絞る
227
+ // players は配役を受け取る参加者だけで、観測者は含まれない
229
228
  return { players: {}, items: [] };
230
229
  },
231
230
 
@@ -260,13 +259,13 @@ const logic: GameLogic<MyState> = {
260
259
  };
261
260
  ```
262
261
 
263
- | キー | 型 | 必須 | 説明 |
264
- | --------------- | ---------------------------------------- | ---- | ---------------------------------------------------------------- |
265
- | `setup` | `(args: SetupArgs) => S` | Yes | 初期 state を生成。seats には `kind !== 'player'` の席も含まれる |
266
- | `actions` | `Record<string, ActionHandler<S>>` | Yes | クライアント先読み + サーバーの 2 回走る。決定的であること |
267
- | `serverActions` | `Record<string, ServerActionHandler<S>>` | No | サーバーでのみ走る。async 可。`ctx` に `tick` / `random` が入る |
268
- | `update` | `(args: UpdateArgs<S>) => void` | Yes | 毎 tick 実行 (`tickRate` が 0 なら呼ばれない) |
269
- | `tickRate` | `number` | No | 秒間 tick 数 (default: 0 = tick なし) |
262
+ | キー | 型 | 必須 | 説明 |
263
+ | --------------- | ---------------------------------------- | ---- | --------------------------------------------------------------- |
264
+ | `setup` | `(args: SetupArgs) => S` | Yes | 初期 state を生成。`args.players` は配役を受け取る参加者だけ |
265
+ | `actions` | `Record<string, ActionHandler<S>>` | Yes | クライアント先読み + サーバーの 2 回走る。決定的であること |
266
+ | `serverActions` | `Record<string, ServerActionHandler<S>>` | No | サーバーでのみ走る。async 可。`ctx` に `tick` / `random` が入る |
267
+ | `update` | `(args: UpdateArgs<S>) => void` | Yes | 毎 tick 実行 (`tickRate` が 0 なら呼ばれない) |
268
+ | `tickRate` | `number` | No | 秒間 tick 数 (default: 0 = tick なし) |
270
269
 
271
270
  ハンドラの引数は 1 つのオブジェクトで、使うものだけ書けばよい。
272
271
  `state` / `payload` / `playerId` はその呼び出しの事実、`ctx` は実行環境が与えるもの。
@@ -460,22 +459,45 @@ interface BridgeMessage {
460
459
 
461
460
  ### Seat
462
461
 
463
- セッション参加者。ゲームの席を占める `player` のほか、観戦席 (`spectator`) と進行管理席 (`admin`、dev ハーネスのテストプレイ用) がある。
462
+ roster に載る席。roster は配役を受け取る参加者だけで構成される。席種別は roster エントリ
463
+ ではなく、自分の `SeatKind` として `onState` に渡る。
464
464
 
465
465
  ```ts
466
- type SeatKind = 'player' | 'spectator' | 'admin';
467
-
468
466
  interface Seat {
469
467
  id: string;
470
468
  nickname: string;
471
469
  iconUrl: string;
472
470
  /** 選択済みキャラクターの ID。未選択時は undefined */
473
471
  characterId?: string;
474
- /** 席種 */
475
- kind: SeatKind;
476
472
  }
477
473
  ```
478
474
 
475
+ ### SeatKind
476
+
477
+ 自分の席種別。ホストが iframe URL の `?seatKind=` で伝え、SDK が `onState` の第 3 引数
478
+ として渡す。
479
+
480
+ ```ts
481
+ type SeatKind = 'player' | 'spectator' | 'admin';
482
+ ```
483
+
484
+ 観戦席・進行管理席は roster に載らないまま接続してくる。`setup()` の `players` にも
485
+ 現れないので「player か観測者か」は `state.players` の空振りで分かるが、`spectator` と
486
+ `admin` の区別は state から導けない。そこをこの値で分ける。
487
+
488
+ ```ts
489
+ onState(state, myPlayerId, mySeatKind) {
490
+ const me = state.players.find((p) => p.playerId === myPlayerId);
491
+ if (me) return playerView(me);
492
+ // roster に居ない = 観測者
493
+ return mySeatKind === 'admin' ? gmView() : spectatorView();
494
+ }
495
+ ```
496
+
497
+ 自己申告なので表示の分岐にだけ使う。渡るのは自分の席種別だけで、他プレイヤーの席種別は
498
+ サーバー側 handler (`ActionArgs`) にも渡らない。seatId の命名規約 (`admin_0` 等) から
499
+ 判定すると、命名が変わった瞬間に静かに壊れるので避けること。
500
+
479
501
  ### ConnectionState
480
502
 
481
503
  ```ts
package/dist/index.js CHANGED
@@ -250,9 +250,9 @@ export function run(config) {
250
250
  const origOnState = config.onState;
251
251
  const wrappedConfig = {
252
252
  ...config,
253
- onState(state, myPlayerId) {
253
+ onState(state, myPlayerId, mySeatKind) {
254
254
  _lastStateSnap = { state, serverTime: 0, myId: myPlayerId };
255
- origOnState(state, myPlayerId);
255
+ origOnState(state, myPlayerId, mySeatKind);
256
256
  notifyDevSnapshot(state);
257
257
  },
258
258
  };
@@ -263,8 +263,9 @@ export function run(config) {
263
263
  }
264
264
  else if (_gameEndpoint) {
265
265
  // ServerAction モード: GameRoom に接続 (本番 Cloudflare Worker DO / uzu dev の Node ws)
266
- const { seatId, seats } = resolveSeatParams(params);
267
- runOnlineServerAction(wrappedConfig, _gameEndpoint, roomId, seatId, seats);
266
+ const { seatId, players } = resolveSeatParams(params);
267
+ const seatKind = resolveSeatKind(params);
268
+ runOnlineServerAction(wrappedConfig, _gameEndpoint, roomId, seatId, players, seatKind);
268
269
  }
269
270
  else {
270
271
  throw new Error('[UZU SDK] roomId is set but ?server= is missing. ' +
@@ -294,8 +295,8 @@ export function sync(config) {
294
295
  _syncHandle = syncLocal(wrappedConfig);
295
296
  }
296
297
  else if (_syncEndpoint) {
297
- const { seatId, seats } = resolveSeatParams(params);
298
- const { ws } = syncOnline(wrappedConfig, _syncEndpoint, roomId, seatId, seats);
298
+ const { seatId, players } = resolveSeatParams(params);
299
+ const { ws } = syncOnline(wrappedConfig, _syncEndpoint, roomId, seatId, players);
299
300
  _syncWs = ws;
300
301
  // online sync: dev hooks は read-only (setRawState 未提供)
301
302
  }
@@ -306,6 +307,25 @@ export function sync(config) {
306
307
  attachDevHooksIfNotHosted();
307
308
  }
308
309
  // ─── Internal ───────────────────────────────────────────────
310
+ /**
311
+ * 自分の席種別を URL から取り出す。
312
+ *
313
+ * `?seatKind=` はホスト (harness / emulator) が観測席の iframe に付ける。付いていない
314
+ * ホストからの接続は player とみなす — 本番 (mobile / uzutokyo) は player 席しか開かない。
315
+ *
316
+ * この値は iframe URL 限定で、WebSocket には出さない。play-server は roster に居ない接続を
317
+ * 通すだけで、その席が admin か spectator かを知る必要が無い。
318
+ */
319
+ function resolveSeatKind(params) {
320
+ const raw = params.get('seatKind');
321
+ return raw === 'admin' || raw === 'spectator' ? raw : 'player';
322
+ }
323
+ /**
324
+ * 自席の ID と roster を URL から取り出す。
325
+ *
326
+ * roster (`?seats=`) に自席が居ないことは異常ではない。観測者はそもそも roster に
327
+ * 載らないまま接続してくる。
328
+ */
309
329
  function resolveSeatParams(params) {
310
330
  const seatId = params.get('seatId');
311
331
  if (!seatId) {
@@ -316,19 +336,13 @@ function resolveSeatParams(params) {
316
336
  throw new Error('[UZU SDK] seats is required. Pass ?seats=[...] in the URL.');
317
337
  }
318
338
  const raw = JSON.parse(json);
319
- const seats = raw.map((p) => {
320
- if (!p.kind) {
321
- throw new Error(`[UZU SDK] seats entry ${p.id} is missing kind.`);
322
- }
323
- return {
324
- id: p.id,
325
- nickname: p.name,
326
- iconUrl: p.iconUrl,
327
- characterId: p.characterId,
328
- kind: p.kind,
329
- };
330
- });
331
- return { seatId, seats };
339
+ const players = raw.map((p) => ({
340
+ id: p.id,
341
+ nickname: p.name,
342
+ iconUrl: p.iconUrl,
343
+ characterId: p.characterId,
344
+ }));
345
+ return { seatId, players };
332
346
  }
333
347
  function requireEndpoint(endpoint, name) {
334
348
  if (!endpoint) {
@@ -437,10 +451,8 @@ function calcHudInsets(params) {
437
451
  const json = params.get('seats');
438
452
  if (json) {
439
453
  try {
440
- // spectator / admin 席はゲームの player 数に数えない
441
454
  const raw = JSON.parse(json);
442
- const playerCount = raw.filter((p) => p.kind === 'player').length;
443
- actionCount = playerCount >= 2 ? 2 : 0;
455
+ actionCount = raw.length >= 2 ? 2 : 0;
444
456
  }
445
457
  catch {
446
458
  // パース失敗時はデフォルト値を使用
@@ -3,7 +3,9 @@ import { SeededRandomImpl } from '../random.js';
3
3
  import { isServerOnlyAction } from '../server-only.js';
4
4
  import { applyJsonMergePatch, applyJsonPatch } from '../dev-state-patch.js';
5
5
  export function runLocalServerAction(config) {
6
- const { logic, onState, inputs, events } = config;
6
+ const { logic, inputs, events } = config;
7
+ // ソロモードは自分ひとりで観測者が存在しないので席種別は常に player。
8
+ const onState = (next, id) => config.onState(next, id, 'player');
7
9
  const tickRate = logic.tickRate ?? 0; // DO と同じデフォルト(0=tickなし)
8
10
  const seed = Math.floor(Math.random() * 0xffffffff);
9
11
  const random = new SeededRandomImpl(seed);
@@ -11,7 +13,6 @@ export function runLocalServerAction(config) {
11
13
  id: `local_${i}`,
12
14
  nickname: `Player ${i + 1}`,
13
15
  iconUrl: DEFAULT_ICON_URLS[i % DEFAULT_ICON_URLS.length],
14
- kind: 'player',
15
16
  }));
16
17
  const myId = players[0].id;
17
18
  // イベント収集→一括配信(DO と同じパターン)
@@ -98,8 +99,13 @@ export function runLocalServerAction(config) {
98
99
  events?.[e.name]?.handler(e.data);
99
100
  }
100
101
  };
102
+ // 移行期: publish 済みの logic.js は `setup({ seats })` で destructure したまま
103
+ // 固まっている。`players` へ寄せただけだと `seats === undefined` を受け取って
104
+ // throw するので、同じ配列を旧名でも渡す。`SetupArgs` に `seats` を宣言しないのは、
105
+ // 新規シナリオに旧名を選ばせないため。全 revision の再 publish 後に落とす。
106
+ const setupArgs = { players, seats: players, ctx: { random, now: Date.now() } };
101
107
  // setRawState で全置換できるよう let。closures は名前参照なので最新束縛を読む。
102
- let state = logic.setup({ seats: players, ctx: { random, now: Date.now() } });
108
+ let state = logic.setup(setupArgs);
103
109
  let tick = 0;
104
110
  const playerInputs = {};
105
111
  // Action 処理。`actions` は同期実行で `sendAction()` 直後の同期 onState を保証する
@@ -17,11 +17,15 @@ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
17
17
  const run = (logic) => {
18
18
  const fired = [];
19
19
  const states = [];
20
+ const seatKinds = [];
20
21
  let send = () => { };
21
22
  const config = {
22
23
  logic,
23
24
  playerCount: 1,
24
- onState: (s) => states.push(structuredClone(s)),
25
+ onState: (s, _myPlayerId, mySeatKind) => {
26
+ states.push(structuredClone(s));
27
+ seatKinds.push(mySeatKind);
28
+ },
25
29
  inputs: (sendAction) => {
26
30
  send = sendAction;
27
31
  },
@@ -41,7 +45,7 @@ const run = (logic) => {
41
45
  },
42
46
  };
43
47
  runLocalServerAction(config);
44
- return { send: (t, p) => send(t, p), fired, states };
48
+ return { send: (t, p) => send(t, p), fired, states, seatKinds };
45
49
  };
46
50
  const baseLogic = (overrides = {}) => ({
47
51
  setup: () => ({ moves: 0, charged: 0, rolled: -1 }),
@@ -216,4 +220,23 @@ describe('runLocalServerAction', () => {
216
220
  expect(h.fired).toEqual(['charged']);
217
221
  });
218
222
  });
223
+ /**
224
+ * `onState` の第 3 引数は自分の席種別。roster に自分が居ないとき (観測者) に
225
+ * GM ビューと観戦ビューを出し分けるためのもので、他プレイヤーの席種別は渡らない。
226
+ */
227
+ describe('自分の席種別', () => {
228
+ /** ソロモードは自分ひとりで観測者が存在しないので、常に 'player' が渡る。 */
229
+ it('ソロモードでは常に player が渡る', () => {
230
+ const h = run(baseLogic({
231
+ actions: {
232
+ move: ({ state }) => {
233
+ state.moves += 1;
234
+ },
235
+ },
236
+ }));
237
+ h.send('move');
238
+ expect(h.seatKinds.length).toBeGreaterThan(1);
239
+ expect(new Set(h.seatKinds)).toEqual(new Set(['player']));
240
+ });
241
+ });
219
242
  });
@@ -12,5 +12,5 @@
12
12
  * `optimistic-action-client.ts` に集約。本ファイルは WebSocket 固有の責務
13
13
  * (接続管理 / メッセージ振り分け / delta seq の連続性チェック / フル state 再要求) のみ持つ。
14
14
  */
15
- import type { GameConfig, Seat } from '../types.js';
16
- export declare function runOnlineServerAction<S>(config: GameConfig<S>, gameEndpoint: string, roomId: string, seatId: string, seats: Seat[]): void;
15
+ import type { GameConfig, Seat, SeatKind } from '../types.js';
16
+ export declare function runOnlineServerAction<S>(config: GameConfig<S>, gameEndpoint: string, roomId: string, seatId: string, players: Seat[], seatKind: SeatKind): void;
@@ -1,17 +1,19 @@
1
1
  import { ReconnectableWebSocket } from '../reconnectable-ws.js';
2
2
  import { createOptimisticActionClient } from './optimistic-action-client.js';
3
- export function runOnlineServerAction(config, gameEndpoint, roomId, seatId, seats) {
3
+ export function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, seatKind) {
4
+ // `kind` は移行期の互換措置。旧 SDK / 旧 play-server が読む wire を変えないために
5
+ // 残す。roster は player だけになったので値は常に 'player'。
4
6
  const toWire = (p) => ({
5
7
  id: p.id,
6
8
  name: p.nickname,
7
9
  iconUrl: p.iconUrl,
8
10
  characterId: p.characterId,
9
- kind: p.kind,
11
+ kind: 'player',
10
12
  });
11
13
  const query = new URLSearchParams({
12
14
  seatId,
13
15
  nickname: 'Player',
14
- seats: JSON.stringify(seats.map(toWire)),
16
+ seats: JSON.stringify(players.map(toWire)),
15
17
  });
16
18
  const wsUrl = `${gameEndpoint}/${roomId}?${query}`;
17
19
  console.log(`[SDK ServerAction] 🔗 Connecting wsUrl=${wsUrl}`);
@@ -44,7 +46,9 @@ export function runOnlineServerAction(config, gameEndpoint, roomId, seatId, seat
44
46
  const client = createOptimisticActionClient({
45
47
  logic: config.logic,
46
48
  playerId: seatId,
47
- onState: config.onState,
49
+ // seatKind は接続ごとに固定なのでここで束ねる。楽観更新クライアント側は
50
+ // 席種別を一切見ない (state の再適用にしか関心が無い)。
51
+ onState: (state, playerId) => config.onState(state, playerId, seatKind),
48
52
  events: config.events,
49
53
  sendAction: ({ action, payload, seq }) => {
50
54
  console.log(`[SDK ServerAction] ➡ send __action action=${action} seq=${seq}`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,96 @@
1
+ /**
2
+ * server-action.ts (オンライン接続) の unit test。
3
+ *
4
+ * 検証対象は「自分の席種別 (`seatKind`) の扱い」に絞る。楽観的更新まわりは
5
+ * `optimistic-action-client.test.ts` が持っている。
6
+ *
7
+ * `seatKind` は roster に載らない観測者が GM ビューと観戦ビューを出し分けるためだけの
8
+ * 値で、**client 内で完結する**。サーバーは roster に居ない接続を通すだけで、その席が
9
+ * admin か spectator かを知る必要が無い。ここではその 2 点を固定する。
10
+ */
11
+ import { describe, expect, it, vi, afterEach } from 'vitest';
12
+ import { runOnlineServerAction } from './server-action.js';
13
+ const logic = {
14
+ setup: () => ({ moves: 0 }),
15
+ actions: {},
16
+ update: () => { },
17
+ };
18
+ const players = [
19
+ { id: 'p1', nickname: 'P1', iconUrl: '' },
20
+ { id: 'p2', nickname: 'P2', iconUrl: '' },
21
+ ];
22
+ const stubWebSocket = () => {
23
+ const sockets = [];
24
+ class FakeWebSocket {
25
+ constructor(url) {
26
+ this.url = url;
27
+ this.readyState = FakeWebSocket.OPEN;
28
+ this.onopen = null;
29
+ this.onclose = null;
30
+ this.onerror = null;
31
+ this.onmessage = null;
32
+ sockets.push({
33
+ url,
34
+ push: (msg) => this.onmessage?.({ data: JSON.stringify(msg) }),
35
+ });
36
+ // ReconnectableWebSocket は onopen 登録後に発火する必要がある。
37
+ setTimeout(() => this.onopen?.(), 0);
38
+ }
39
+ send() { }
40
+ close() {
41
+ this.readyState = FakeWebSocket.CLOSED;
42
+ }
43
+ }
44
+ FakeWebSocket.OPEN = 1;
45
+ FakeWebSocket.CLOSED = 3;
46
+ vi.stubGlobal('WebSocket', FakeWebSocket);
47
+ return { sockets };
48
+ };
49
+ const connect = (seatKind, seatId = 'p1') => {
50
+ const { sockets } = stubWebSocket();
51
+ const onState = vi.fn();
52
+ const config = {
53
+ logic,
54
+ playerCount: 2,
55
+ onState,
56
+ inputs: () => { },
57
+ };
58
+ runOnlineServerAction(config, 'ws://localhost:1234/ws/games/rev1', 'room1', seatId, players, seatKind);
59
+ return { sockets, onState };
60
+ };
61
+ afterEach(() => {
62
+ vi.unstubAllGlobals();
63
+ });
64
+ describe('runOnlineServerAction の seatKind', () => {
65
+ /**
66
+ * `seatKind` は iframe URL 限定。WebSocket に載せると「サーバーが席種別を知っている」
67
+ * ように見えるが、自己申告なので何も保証しない。プロトコルを増やさない。
68
+ */
69
+ it('WebSocket URL に seatKind を載せない', () => {
70
+ const { sockets } = connect('admin', 'admin_0');
71
+ expect(sockets).toHaveLength(1);
72
+ const url = new URL(sockets[0].url);
73
+ expect(url.searchParams.get('seatKind')).toBeNull();
74
+ expect(url.searchParams.get('seatId')).toBe('admin_0');
75
+ });
76
+ /**
77
+ * roster (`?seats=`) は player 席だけ。観測者は自分が載っていない roster を送る。
78
+ */
79
+ it('roster には自分が居なくてよい', () => {
80
+ const { sockets } = connect('admin', 'admin_0');
81
+ const raw = JSON.parse(new URL(sockets[0].url).searchParams.get('seats') ?? '[]');
82
+ expect(raw.map((s) => s.id)).toEqual(['p1', 'p2']);
83
+ });
84
+ /** シナリオが GM ビューと観戦ビューを分けられるよう、自分の席種別だけを渡す。 */
85
+ it('onState の第 3 引数に自分の seatKind を渡す', () => {
86
+ const { sockets, onState } = connect('admin', 'admin_0');
87
+ sockets[0].push({ type: '__game_start', state: { moves: 0 }, seq: 0 });
88
+ expect(onState).toHaveBeenCalledWith({ moves: 0 }, 'admin_0', 'admin');
89
+ });
90
+ /** player 席も同じ経路で自分の席種別を受け取る。 */
91
+ it('player 席には player が渡る', () => {
92
+ const { sockets, onState } = connect('player');
93
+ sockets[0].push({ type: '__game_start', state: { moves: 0 }, seq: 0 });
94
+ expect(onState).toHaveBeenCalledWith({ moves: 0 }, 'p1', 'player');
95
+ });
96
+ });
@@ -15,7 +15,6 @@ export function syncLocal(config) {
15
15
  id: `local_${i}`,
16
16
  nickname: `Player ${i + 1}`,
17
17
  iconUrl: DEFAULT_ICON_URLS[i % DEFAULT_ICON_URLS.length],
18
- kind: 'player',
19
18
  });
20
19
  }
21
20
  let state = initialState(players);
package/dist/types.d.ts CHANGED
@@ -31,20 +31,29 @@ export interface BridgeMessage {
31
31
  playerId?: string;
32
32
  }
33
33
  /**
34
- * セッションの席種。
35
- * - player: ゲームの参加者 (キャラクターを担当する)
36
- * - spectator: 観戦者 (state を閲覧するだけで、ゲームの席は占めない)
37
- * - admin: 進行管理席 (観戦 + GM 操作。dev harness のテストプレイ用)
34
+ * 自分の席種別。ホストが iframe URL の `?seatKind=` で伝える。
35
+ *
36
+ * roster に載るのは `player` だけ。観測者 (`spectator` / `admin`) は roster 外の接続として
37
+ * 開くので、「player か観測者か」は `state.players` の空振りで分かる。一方 **`spectator`
38
+ * `admin` の区別は state から導けない**ため、この値で分ける。seatId の命名規約
39
+ * (`admin_0` 等) をシナリオに見せると、規約が変わった瞬間に静かに壊れる。
40
+ *
41
+ * 自己申告なので権限の根拠にはならない。表示の分岐にだけ使うこと。
38
42
  */
39
43
  export type SeatKind = 'player' | 'spectator' | 'admin';
44
+ /**
45
+ * roster に載る席。
46
+ *
47
+ * roster は配役を受け取る参加者だけで構成される。観測者 (GM 席・観戦席) は roster に
48
+ * 載らないまま接続してくるので、シナリオは「roster に居ない = 観測者」で判別する。
49
+ * 席種別は roster エントリではなく自分の `SeatKind` として渡る。
50
+ */
40
51
  export interface Seat {
41
52
  id: string;
42
53
  nickname: string;
43
54
  iconUrl: string;
44
55
  /** ホスト(mobile / emulator)から渡される、選択済みキャラクターの ID。未選択時は undefined。 */
45
56
  characterId?: string;
46
- /** 席種。 */
47
- kind: SeatKind;
48
57
  }
49
58
  /** プレイヤーごとのリアルタイム状態 */
50
59
  export interface PlayerVoiceState {
@@ -218,11 +227,8 @@ export interface SetupContext {
218
227
  }
219
228
  /** `setup()` の引数。 */
220
229
  export interface SetupArgs {
221
- /**
222
- * seats には kind !== 'player' の席 (spectator / admin) も含まれる。
223
- * ゲームの配役は kind === 'player' (または kind 省略) だけを対象にすること。
224
- */
225
- seats: Seat[];
230
+ /** 配役を受け取る参加者。観測者は含まれない。 */
231
+ players: Seat[];
226
232
  ctx: SetupContext;
227
233
  }
228
234
  /**
@@ -279,7 +285,11 @@ export interface GameLogic<S, A extends ActionMap<S> = ActionMap<S>, SA extends
279
285
  }
280
286
  export interface GameConfig<S, A extends ActionMap<S> = ActionMap<S>, SA extends ServerActionMap<S> = ServerActionMap<S>> extends ConnectionCallbacks {
281
287
  logic: GameLogic<S, A, SA>;
282
- onState: (state: S, myPlayerId: string) => void;
288
+ /**
289
+ * `mySeatKind` は自分の席種別。roster に自分が居ない (= 観測者) ときに、GM ビューと
290
+ * 観戦ビューを出し分けるために使う。他プレイヤーの席種別は渡らない。
291
+ */
292
+ onState: (state: S, myPlayerId: string, mySeatKind: SeatKind) => void;
283
293
  inputs: (sendAction: SendAction<A & SA>) => void;
284
294
  /**
285
295
  * `emit(name, data)` の購読。 キーごとに `predict` の宣言が必須。
@@ -303,7 +313,7 @@ export interface GameConfig<S, A extends ActionMap<S> = ActionMap<S>, SA extends
303
313
  export type PatchFn = (ops: Operation[]) => void;
304
314
  export type SetFn = (path: string, value: unknown) => void;
305
315
  export interface SyncConfig<S = any> extends ConnectionCallbacks {
306
- initialState: (seats: Seat[]) => S;
316
+ initialState: (players: Seat[]) => S;
307
317
  onState: (state: S, myPlayerId: string, serverTime: number) => void;
308
318
  inputs: (patch: PatchFn, set: SetFn) => void;
309
319
  events?: Record<string, (data: Record<string, unknown>) => void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-sdk",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "UZU PlayScreen SDK - Flutter ↔ JS ゲーム通信ライブラリ",
5
5
  "type": "module",
6
6
  "exports": {