@uzuhq/code-sdk 0.3.8
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/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/dev-globals.d.ts +28 -0
- package/dist/dev-globals.js +16 -0
- package/dist/dev-hooks.d.ts +233 -0
- package/dist/dev-hooks.js +130 -0
- package/dist/dev-hooks.test.d.ts +1 -0
- package/dist/dev-hooks.test.js +294 -0
- package/dist/dev-state-patch.d.ts +81 -0
- package/dist/dev-state-patch.js +295 -0
- package/dist/dev-state-patch.test.d.ts +1 -0
- package/dist/dev-state-patch.test.js +333 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +440 -0
- package/dist/json-patch.d.ts +7 -0
- package/dist/json-patch.js +78 -0
- package/dist/random.d.ts +11 -0
- package/dist/random.js +34 -0
- package/dist/reconnectable-ws.d.ts +60 -0
- package/dist/reconnectable-ws.js +229 -0
- package/dist/room.d.ts +23 -0
- package/dist/room.js +36 -0
- package/dist/run/local-server-action.d.ts +15 -0
- package/dist/run/local-server-action.js +97 -0
- package/dist/run/optimistic-action-client.d.ts +57 -0
- package/dist/run/optimistic-action-client.js +119 -0
- package/dist/run/server-action.d.ts +16 -0
- package/dist/run/server-action.js +170 -0
- package/dist/server-only.d.ts +26 -0
- package/dist/server-only.js +20 -0
- package/dist/sync/local.d.ts +8 -0
- package/dist/sync/local.js +51 -0
- package/dist/sync/online.d.ts +5 -0
- package/dist/sync/online.js +165 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +8 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sally, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# @uzuhq/code-sdk
|
|
2
|
+
|
|
3
|
+
[UZU](https://uzu-app.com) 上で動くゲーム・シナリオを作るための通信 SDK。マルチプレイ状態同期、サウンド再生、デバイス連携を提供します。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @uzuhq/code-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
ゲームのビルド・配信には [@uzuhq/code-cli](https://www.npmjs.com/package/@uzuhq/code-cli) を併用してください。
|
|
10
|
+
|
|
11
|
+
> **Note:** API を変更した場合、このドキュメントも必ず同期してください。
|
|
12
|
+
|
|
13
|
+
## メッセージプロトコル
|
|
14
|
+
|
|
15
|
+
すべてのブリッジメッセージは `{ channel, type, payload }` の共通構造を持つ。
|
|
16
|
+
|
|
17
|
+
| チャネル | 用途 | 説明 |
|
|
18
|
+
| -------- | ---------------------------- | ------------------------------------------------------------- |
|
|
19
|
+
| `sdk` | SDK/プラットフォームコマンド | サウンド再生、マイク制御、ルーム移動など SDK 内部のメッセージ |
|
|
20
|
+
| `game` | ゲーム独自メッセージ | `send()` / `on()` で送受信するカスタムメッセージ |
|
|
21
|
+
|
|
22
|
+
`send()` は `channel: 'game'` のメッセージのみを送信する。`on()` は `channel: 'game'` のメッセージのみを受信し、handler には `payload` が直接渡される。
|
|
23
|
+
|
|
24
|
+
SDK チャネルのメッセージは `playSound()`, `setMicEnabled()` などの専用関数から自動送信される。
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 3 つのパラダイム
|
|
29
|
+
|
|
30
|
+
SDK は用途に応じて 3 つの API パターンを提供する。
|
|
31
|
+
|
|
32
|
+
| パラダイム | API | state 管理 | 向いているゲーム |
|
|
33
|
+
| ---------- | --------------------- | ---------------------------------- | ---------------------------- |
|
|
34
|
+
| **Relay** | `init()` + `onRoom()` | ゲーム側の責務 | 独自プロトコルが必要なゲーム |
|
|
35
|
+
| **sync()** | `sync()` | SDK + サーバーが JSON Patch で同期 | ターン制・ボードゲーム |
|
|
36
|
+
| **run()** | `run()` | SDK + reducer がサーバーで逐次実行 | リアルタイム・アクション全般 |
|
|
37
|
+
|
|
38
|
+
### 選択基準
|
|
39
|
+
|
|
40
|
+
- 同時操作で同じリソースを奪い合う → **`run()`**(reducer が最新 state に対して逐次実行、条件保証あり)
|
|
41
|
+
- 各プレイヤーが自分の領域だけ変更 → **`sync()`** で十分
|
|
42
|
+
- 独自のメッセージプロトコルが必要 → **Relay**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 初期化
|
|
47
|
+
|
|
48
|
+
### `init(): void`
|
|
49
|
+
|
|
50
|
+
SDK を初期化し、Flutter ホスト / 親ウィンドウに準備完了を通知する。
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { init } from '@uzuhq/code-sdk';
|
|
54
|
+
init();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `run()` / `sync()` を使う場合は内部で `init()` が呼ばれるため不要
|
|
58
|
+
- Relay パターン (`onRoom()`) のみ、最初に呼び出す
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## パターン 1: Relay
|
|
63
|
+
|
|
64
|
+
### `onRoom(callback: (room: RoomLike) => void): void`
|
|
65
|
+
|
|
66
|
+
Room に接続されたときのコールバックを登録する。
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { init, onRoom } from '@uzuhq/code-sdk';
|
|
70
|
+
|
|
71
|
+
init();
|
|
72
|
+
|
|
73
|
+
onRoom((room) => {
|
|
74
|
+
console.log('My ID:', room.myId);
|
|
75
|
+
|
|
76
|
+
room.on('attack', (msg) => {
|
|
77
|
+
console.log(`${msg.__from} sent ${msg.lines} lines`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
room.broadcast({ type: 'attack', lines: 2 });
|
|
81
|
+
room.send(targetId, { type: 'whisper', text: 'hello' });
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Room API
|
|
86
|
+
|
|
87
|
+
| メソッド/プロパティ | 型 | 説明 |
|
|
88
|
+
| ------------------------ | ------------------------------------------------------ | -------------------------------------------------------- |
|
|
89
|
+
| `room.myId` | `string` (readonly) | 自分の playerId |
|
|
90
|
+
| `room.broadcast(msg)` | `(msg: Record<string, unknown>) => void` | 自分以外の全員に送信 |
|
|
91
|
+
| `room.send(id, msg)` | `(id: string, msg: Record<string, unknown>) => void` | 特定プレイヤーに送信 |
|
|
92
|
+
| `room.on(type, handler)` | `(type: string, handler: (data: any) => void) => void` | メッセージハンドラ登録。受信データに `__from` が含まれる |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## パターン 2: sync()
|
|
97
|
+
|
|
98
|
+
### `sync<S>(config: SyncConfig<S>): void`
|
|
99
|
+
|
|
100
|
+
JSON Patch ベースの状態同期を開始する。
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { sync, SERVER_TIME } from '@uzuhq/code-sdk';
|
|
104
|
+
import type { Seat } from '@uzuhq/code-sdk';
|
|
105
|
+
|
|
106
|
+
sync<GameState>({
|
|
107
|
+
playerCount: 2,
|
|
108
|
+
|
|
109
|
+
initialState(players: Seat[]) {
|
|
110
|
+
return { board: createBoard(8), currentPlayer: players[0].id };
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
onState(state, myPlayerId, serverTime) {
|
|
114
|
+
currentState = state;
|
|
115
|
+
render();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
inputs(patch, set) {
|
|
119
|
+
canvas.addEventListener('click', (e) => {
|
|
120
|
+
const { row, col } = getCellFromClick(e);
|
|
121
|
+
// 複数操作をまとめて送信
|
|
122
|
+
patch([
|
|
123
|
+
{ op: 'replace', path: `/board/${row}/${col}`, value: myPlayerId },
|
|
124
|
+
{ op: 'replace', path: '/lastMoveAt', value: SERVER_TIME },
|
|
125
|
+
]);
|
|
126
|
+
// 単一値のショートカット
|
|
127
|
+
set('/currentPlayer', getNextPlayer());
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
connection: {
|
|
132
|
+
onConnectionStateChange(state) {
|
|
133
|
+
console.log('Connection:', state);
|
|
134
|
+
},
|
|
135
|
+
onPatchFailed(error) {
|
|
136
|
+
console.error('Patch failed:', error);
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### SyncConfig
|
|
143
|
+
|
|
144
|
+
| キー | 型 | 必須 | 説明 |
|
|
145
|
+
| -------------- | ------------------------------------------------------------ | ---- | ---------------------------- |
|
|
146
|
+
| `playerCount` | `number` | Yes | プレイヤー数 |
|
|
147
|
+
| `initialState` | `(seats: Seat[]) => S` | Yes | 初期 state を生成 |
|
|
148
|
+
| `onState` | `(state: S, myPlayerId: string, serverTime: number) => void` | Yes | state 更新時のコールバック |
|
|
149
|
+
| `inputs` | `(patch: PatchFn, set: SetFn) => void` | Yes | 入力ハンドラ登録 |
|
|
150
|
+
| `events` | `Record<string, (data: Record<string, unknown>) => void>` | No | ゲームイベントハンドラ |
|
|
151
|
+
| `connection` | `ConnectionCallbacks` | No | 接続状態・エラーコールバック |
|
|
152
|
+
|
|
153
|
+
### PatchFn / SetFn
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
type PatchFn = (ops: Operation[]) => void; // 複数操作をまとめて送信
|
|
157
|
+
type SetFn = (path: string, value: unknown) => void; // 単一パスの replace ショートカット
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Operation (JSON Patch)
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
interface Operation {
|
|
164
|
+
op: 'replace' | 'add' | 'remove';
|
|
165
|
+
path: string; // JSON Pointer パス (例: '/players/alice/score')
|
|
166
|
+
value?: unknown; // 値。SERVER_TIME sentinel 使用可
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 楽観的更新
|
|
171
|
+
|
|
172
|
+
`sync()` は送信した patch をローカルに即座に適用する。サーバーからの権威的な state を受信すると上書きする。
|
|
173
|
+
|
|
174
|
+
**注意**: `sync()` は patch の条件を検証しない。同時に同じパスを変更すると後勝ちになる。条件付きの状態遷移が必要なら `run()` を使う。
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## パターン 3: run()
|
|
179
|
+
|
|
180
|
+
### `run<S>(config: GameConfig<S>): void`
|
|
181
|
+
|
|
182
|
+
Host-authoritative ゲームループを実行する。サーバー (GameRoom DO) が reducer を逐次実行するため、条件保証あり。
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { run } from '@uzuhq/code-sdk';
|
|
186
|
+
|
|
187
|
+
run({
|
|
188
|
+
logic,
|
|
189
|
+
onState(state, myPlayerId) {
|
|
190
|
+
currentState = state;
|
|
191
|
+
myId = myPlayerId;
|
|
192
|
+
},
|
|
193
|
+
inputs(sendAction) {
|
|
194
|
+
document.addEventListener('keydown', (e) => {
|
|
195
|
+
if (e.key === 'ArrowUp') sendAction('move', { dx: 0, dy: -1 });
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
events: {
|
|
199
|
+
sound(data) {
|
|
200
|
+
new Audio(`/sounds/${data.sound}.mp3`).play().catch(() => {});
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
playerCount: 2,
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### GameConfig
|
|
208
|
+
|
|
209
|
+
| キー | 型 | 必須 | 説明 |
|
|
210
|
+
| ------------------------- | -------------------------------------------------------------- | ---- | ----------------------------------- |
|
|
211
|
+
| `logic` | `GameLogic<S>` | Yes | ゲームロジック定義 |
|
|
212
|
+
| `onState` | `(state: S, myPlayerId: string) => void` | Yes | state 更新時のコールバック |
|
|
213
|
+
| `inputs` | `(sendAction: (action: string, payload: any) => void) => void` | Yes | 入力ハンドラ登録 |
|
|
214
|
+
| `events` | `Record<string, (data: any) => void>` | No | ゲームイベントハンドラ |
|
|
215
|
+
| `playerCount` | `number` | Yes | プレイヤー数 |
|
|
216
|
+
| `onConnectionStateChange` | `(state: ConnectionState) => void` | No | 接続状態変化コールバック |
|
|
217
|
+
| `onPatchFailed` | `(reason: string) => void` | No | サーバー patch 適用失敗コールバック |
|
|
218
|
+
|
|
219
|
+
### GameLogic
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { serverOnly, type GameLogic } from '@uzuhq/code-sdk';
|
|
223
|
+
|
|
224
|
+
const logic: GameLogic<MyState> = {
|
|
225
|
+
setup(seats, random) {
|
|
226
|
+
// 初期 state を生成。random は SeededRandom。
|
|
227
|
+
// seats には観戦系の席 (kind: 'spectator' | 'admin') も含まれ得るため、
|
|
228
|
+
// ゲームの席は kind === 'player' に絞る
|
|
229
|
+
return { players: {}, items: [] };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
actions: {
|
|
233
|
+
move(state, payload, playerId, emit, ctx) {
|
|
234
|
+
// state を直接変更する(Immer 的な mutable 操作)
|
|
235
|
+
state.players[playerId].x += payload.dx;
|
|
236
|
+
// emit でイベント発火
|
|
237
|
+
emit('sound', { sound: 'step' });
|
|
238
|
+
// ctx.tick でサーバーの現在 tick を参照可能
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// serverOnly(): client 先行実行をスキップしてサーバーだけで実行 (async OK)
|
|
242
|
+
notifyExternal: serverOnly(async (state, payload, playerId) => {
|
|
243
|
+
await fetch('https://example.com/notify', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
body: JSON.stringify({ playerId, ...payload }),
|
|
246
|
+
});
|
|
247
|
+
state.notifiedAt = Date.now();
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
update(state, ctx) {
|
|
252
|
+
// 毎 tick 実行。ctx.tick, ctx.random, ctx.emit, ctx.playerInputs が使える
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
tickRate: 10, // 秒間 tick 数 (default: 0 = tick なし, 0 でターン制)
|
|
256
|
+
};
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| キー | 型 | 必須 | 説明 |
|
|
260
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------------------------------- |
|
|
261
|
+
| `setup` | `(seats: Seat[], random: SeededRandom) => S` | Yes | 初期 state を生成。seats には `kind !== 'player'` の席も含まれる |
|
|
262
|
+
| `actions` | `Record<string, ActionHandler<S> \| ServerOnlyAction<S>>` | Yes | アクションハンドラ。`serverOnly()` で wrap すると client 先行実行をスキップ |
|
|
263
|
+
| `update` | `(state: S, ctx: { random: SeededRandom, tick: number, emit: EmitFn, playerInputs: Record<string, Record<string, any>> }) => void` | Yes | 毎 tick 実行 |
|
|
264
|
+
| `tickRate` | `number` | No | 秒間 tick 数 (default: 0 = tick なし) |
|
|
265
|
+
|
|
266
|
+
#### `serverOnly(handler)`
|
|
267
|
+
|
|
268
|
+
`actions` に登録する handler を「サーバーでだけ実行される」ものに変換する wrapper。
|
|
269
|
+
|
|
270
|
+
- クライアント側ではローカル先行実行 (楽観的更新) をスキップし、サーバーからの state delta を待つ
|
|
271
|
+
- サーバー側で `await` で実行され、`fetch` などの副作用付き処理を安全に書ける
|
|
272
|
+
- handler のシグネチャ: `(state, payload, playerId, emit, ctx) => Promise<void> | void`
|
|
273
|
+
- 例外を投げると送信元クライアントに `__action_error` が返る (state は変更されない)
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { serverOnly } from '@uzuhq/code-sdk';
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### manifest.json(run() を使う場合)
|
|
280
|
+
|
|
281
|
+
`run()` を使う場合、`manifest.json` に `serverActionLogicPath` を指定する:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"id": "my-game",
|
|
286
|
+
"serverActionLogicPath": "./src/logic.ts",
|
|
287
|
+
"playerCount": 2,
|
|
288
|
+
"build": "npm run build",
|
|
289
|
+
"output": "dist"
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## SeededRandom
|
|
296
|
+
|
|
297
|
+
`GameLogic` の `setup` / `update` で提供される seed 付き乱数生成器。
|
|
298
|
+
|
|
299
|
+
| メソッド | 説明 |
|
|
300
|
+
| ---------------- | ----------------------------------- |
|
|
301
|
+
| `float()` | 0.0〜1.0 の浮動小数点 |
|
|
302
|
+
| `int(max)` | 0〜max-1 の整数 |
|
|
303
|
+
| `pick(array)` | 配列からランダムに 1 つ選択 |
|
|
304
|
+
| `shuffle(array)` | 配列をシャッフル (新しい配列を返す) |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## サウンド
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { playSound, playBgm, stopBgm } from '@uzuhq/code-sdk';
|
|
312
|
+
|
|
313
|
+
playSound('sounds/clear.mp3'); // 効果音
|
|
314
|
+
playBgm('bgm/main.mp3'); // BGM ループ再生
|
|
315
|
+
stopBgm(); // BGM 停止
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
サウンドファイルはゲームの `public/` ディレクトリに配置する。
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## デバイス連携
|
|
323
|
+
|
|
324
|
+
### `setMicEnabled(enabled: boolean): void`
|
|
325
|
+
|
|
326
|
+
マイクのオンオフを切り替える。
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { setMicEnabled } from '@uzuhq/code-sdk';
|
|
330
|
+
|
|
331
|
+
setMicEnabled(false); // マイクをミュート
|
|
332
|
+
setMicEnabled(true); // マイクをオン
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### `changeRoom(roomId: string | null): void`
|
|
336
|
+
|
|
337
|
+
ボイスチャットルームを移動する。`null` でデフォルトルーム(全体ルーム)に戻る。
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { changeRoom } from '@uzuhq/code-sdk';
|
|
341
|
+
|
|
342
|
+
changeRoom('room_a'); // room_a に移動(同じルームのプレイヤーとのみ音声通話可能)
|
|
343
|
+
changeRoom(null); // デフォルトルーム(全員が同じ音声チャンネル)に戻る
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
- 初期状態ではすべてのプレイヤーがデフォルトルーム(`null`)に所属する
|
|
347
|
+
- `roomId` に文字列を指定すると、そのプレイヤーは指定ルームへ移動する
|
|
348
|
+
|
|
349
|
+
### `onPlayersChanged(handler: (players: Record<string, PlayerVoiceState>) => void): void`
|
|
350
|
+
|
|
351
|
+
プレイヤーのリアルタイム状態(音声状態)が変化したときのハンドラを登録する。
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { onPlayersChanged } from '@uzuhq/code-sdk';
|
|
355
|
+
|
|
356
|
+
onPlayersChanged((players) => {
|
|
357
|
+
for (const [id, state] of Object.entries(players)) {
|
|
358
|
+
// state.audioStatus: 'speaking' | 'listening' | 'muted' | 'unstable' | null
|
|
359
|
+
updatePlayerUI(id, state.audioStatus);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## メッセージング
|
|
367
|
+
|
|
368
|
+
### `send(type: string, payload?: object): void`
|
|
369
|
+
|
|
370
|
+
Flutter ホスト / 親ウィンドウへゲームチャネル (`channel: 'game'`) のメッセージを送信する。
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { send } from '@uzuhq/code-sdk';
|
|
374
|
+
send('attack', { lines: 2 });
|
|
375
|
+
// → { channel: 'game', type: 'attack', payload: { lines: 2 } }
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### `on(type: string, handler: (payload: any) => void): void`
|
|
379
|
+
|
|
380
|
+
ゲームチャネル (`channel: 'game'`) のメッセージを受信する。handler には `payload` が直接渡される。
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
import { on } from '@uzuhq/code-sdk';
|
|
384
|
+
|
|
385
|
+
on('attack', (payload) => {
|
|
386
|
+
console.log(`Received attack with ${payload.lines} lines`);
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
:::caution
|
|
391
|
+
`on()` は `channel: 'game'` のメッセージのみを受信する。SDK チャネルのメッセージ(`playersChanged` 等)は `onPlayersChanged()` などの専用関数を使用すること。
|
|
392
|
+
:::
|
|
393
|
+
|
|
394
|
+
### `isHosted`
|
|
395
|
+
|
|
396
|
+
`boolean` (読み取り専用)。Flutter WebView または iframe 内で動作しているか。
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## SERVER_TIME
|
|
401
|
+
|
|
402
|
+
サーバー時刻 sentinel 定数。Patch の `value` に指定すると、サーバー側で `Date.now()` に自動置換される。
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { SERVER_TIME } from '@uzuhq/code-sdk';
|
|
406
|
+
set('/meta/phaseStartedAt', SERVER_TIME);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## 型定義
|
|
412
|
+
|
|
413
|
+
### BridgeMessage
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
interface BridgeMessage {
|
|
417
|
+
channel: 'sdk' | 'game';
|
|
418
|
+
type: string;
|
|
419
|
+
payload: Record<string, unknown>;
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Seat
|
|
424
|
+
|
|
425
|
+
セッション参加者。ゲームの席を占める `player` のほか、観戦席 (`spectator`) と進行管理席 (`admin`、dev ハーネスのテストプレイ用) がある。
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
type SeatKind = 'player' | 'spectator' | 'admin';
|
|
429
|
+
|
|
430
|
+
interface Seat {
|
|
431
|
+
id: string;
|
|
432
|
+
nickname: string;
|
|
433
|
+
iconUrl: string;
|
|
434
|
+
/** 選択済みキャラクターの ID。未選択時は undefined */
|
|
435
|
+
characterId?: string;
|
|
436
|
+
/** 席種 */
|
|
437
|
+
kind: SeatKind;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### ConnectionState
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### ConnectionCallbacks
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
interface ConnectionCallbacks {
|
|
451
|
+
onConnectionStateChange?: (state: ConnectionState) => void;
|
|
452
|
+
onPatchFailed?: (error: string) => void;
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### PlayerVoiceState
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
interface PlayerVoiceState {
|
|
460
|
+
audioStatus: 'speaking' | 'listening' | 'muted' | 'unstable' | null;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
| audioStatus | 説明 |
|
|
465
|
+
| ----------- | ------------------------ |
|
|
466
|
+
| `speaking` | 発話中 |
|
|
467
|
+
| `listening` | 音声接続済み・聞いている |
|
|
468
|
+
| `muted` | ミュート中 |
|
|
469
|
+
| `unstable` | 接続に問題あり |
|
|
470
|
+
| `null` | 音声通話に未接続 |
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## URL パラメータ
|
|
475
|
+
|
|
476
|
+
`init()` / `run()` / `sync()` は以下の URL パラメータを読み取る。
|
|
477
|
+
|
|
478
|
+
| パラメータ | 説明 |
|
|
479
|
+
| ----------- | --------------------------------------------- |
|
|
480
|
+
| `?server=` | WebSocket サーバーの URL |
|
|
481
|
+
| `?roomId=` | ルーム ID。指定するとオンラインモードになる |
|
|
482
|
+
| `?seatId=` | 自分の席 ID |
|
|
483
|
+
| `?players=` | JSON エンコードされたプレイヤーリスト |
|
|
484
|
+
| `?__dev=N` | Dev ハーネスモード (N 画面の iframe 並列表示) |
|
|
485
|
+
|
|
486
|
+
### モードの自動判定
|
|
487
|
+
|
|
488
|
+
- URL に `?roomId=xxx` がない → **ローカルモード** (サーバー不要)
|
|
489
|
+
- URL に `?roomId=xxx&server=xxx` がある → **オンラインモード**
|
|
490
|
+
- URL に `?__dev=N` がある → **Dev ハーネスモード**
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Dev Hooks (`window.__uzu_dev`)
|
|
495
|
+
|
|
496
|
+
外部 automation (Playwright / AI agent / E2E test) が iframe 内 state を決定論的に読み書きするための API。SDK は state shape に依存しない generic primitive だけを提供する。
|
|
497
|
+
|
|
498
|
+
**Flutter native ホスト (`window.FlutterHost` あり) では一切 attach されない**。authoritative state を持つ frame に 1 個だけ生える:
|
|
499
|
+
|
|
500
|
+
- `run()` の devHarness → **親 frame だけ** (`page.mainFrame()`)
|
|
501
|
+
- `runLocalServerAction` (ソロモード) 単独 frame → その frame
|
|
502
|
+
- sync 系単独 frame → その frame
|
|
503
|
+
- devHarness の **子 iframe には attach しない**
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
window.__uzu_dev.getSnapshot(); // 現在 state (run devHarness 親 = authoritative raw)
|
|
507
|
+
window.__uzu_dev.getRawState(); // 生 server-side state (dev/local mode のみ)
|
|
508
|
+
window.__uzu_dev.playerId(); // 現在の player ID (run devHarness 親は null)
|
|
509
|
+
|
|
510
|
+
// action 送信 (run devHarness 親 frame のみ attach、それ以外は undefined)
|
|
511
|
+
// `as` は必須 — server-side dispatch の `from` を指定する
|
|
512
|
+
// Promise<void> を返す。await すると handler 完了 + broadcast 投函まで待つ
|
|
513
|
+
await window.__uzu_dev.send?.({ as: 'dev_0', type: 'host.phase.jump', payload: { phaseId: 'p1' } });
|
|
514
|
+
await window.__uzu_dev.send?.({
|
|
515
|
+
as: 'dev_1',
|
|
516
|
+
type: 'move.piece',
|
|
517
|
+
payload: { from: 'a1', to: 'a2' },
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// illegal move は Promise rejection になる
|
|
521
|
+
await expect(
|
|
522
|
+
window.__uzu_dev.send({
|
|
523
|
+
as: 'dev_0',
|
|
524
|
+
type: 'move.piece',
|
|
525
|
+
payload: {/* 相手の手番 */},
|
|
526
|
+
}),
|
|
527
|
+
).rejects.toThrow('Not your turn');
|
|
528
|
+
|
|
529
|
+
// 生 state 書換は 3 API。用途別に使い分ける:
|
|
530
|
+
|
|
531
|
+
// 1. 全置換 (dump した state を流し込み、bug 再現など)
|
|
532
|
+
await window.__uzu_dev.setRawState(fullState);
|
|
533
|
+
|
|
534
|
+
// 2. RFC 7396 風 Merge Patch (object 階層の部分更新、array は atomic replace のみ)
|
|
535
|
+
await window.__uzu_dev.mergeRawState({ game: { timerEndsAt: null } });
|
|
536
|
+
|
|
537
|
+
// 3. RFC 6902 JSON Patch (path-based ops、array index 単体書換 OK)
|
|
538
|
+
await window.__uzu_dev.patchRawState([
|
|
539
|
+
{ op: 'replace', path: '/board/1/4', value: 99 },
|
|
540
|
+
{ op: 'replace', path: '/currentTurn', value: 'dev_1' },
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
// snapshot が条件を満たすまで待つ (default 10s timeout)
|
|
544
|
+
await window.__uzu_dev.waitForSnapshot((s) => s.self.isReady === true);
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
- **state 書換 3 API**: `setRawState` (全置換) / `mergeRawState` (RFC 7396 風 Merge Patch) / `patchRawState` (RFC 6902 JSON Patch)
|
|
548
|
+
- 部分更新は `mergeRawState`、array 要素単体の書換は `patchRawState` を使う
|
|
549
|
+
- `mergeRawState` で array field に non-array object patch を当てると **throw** する (array が pure object に化けるのを構造的に防止)
|
|
550
|
+
- online ServerAction では 3 API すべて `undefined`
|
|
551
|
+
- `send` は run devHarness 親 frame のみ。非 parent モード (runLocal / emulator / sync) では scenario 側の `window.__uzu.sendAction` を使う
|
|
552
|
+
- `await d.send(...)` / `await d.*RawState(...)` は **(a) handler 完了 (b) parent state broadcast 投函** までを待つ。**子 iframe canvas の paint 完了は含まない** ので、screenshot / visual e2e test では `await new Promise(r => requestAnimationFrame(r))` を別途挟むこと
|
|
553
|
+
- phase 遷移時の field reset や `markReady` 等の **state shape を仮定する helper は SDK には含めない**。scenario 側で `window.__<scene>_dev` を生やして上記 primitive を組み合わせる
|
|
554
|
+
|
|
555
|
+
### scenario test/tools から型補完を効かせる
|
|
556
|
+
|
|
557
|
+
scenario の `test/*.ts` / `tools/*.ts` で SDK を import せず `window.__uzu_dev` だけ叩く場合、`tsconfig.json` の `types` に `@uzuhq/code-sdk/dev-globals` を足すと ambient で `UzuDevHooks` が効く:
|
|
558
|
+
|
|
559
|
+
```jsonc
|
|
560
|
+
{
|
|
561
|
+
"compilerOptions": {
|
|
562
|
+
"types": ["@uzuhq/code-sdk/dev-globals"],
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
これで `(window as any).__uzu_dev` で殴らずに `window.__uzu_dev?.send({ as: 'dev_0', type: '...', payload: ... })` が補完 + 型チェックされる。SDK を直接 import するモジュールでは `dev-hooks.ts` 側の `declare global` で既に型が見えているため、本 entry を `types` に足す必要はない。
|
|
568
|
+
|
|
569
|
+
詳細は `docs/play_screen_v3/sdk-guide/dev-hooks.md` を参照。
|
|
570
|
+
|
|
571
|
+
## License
|
|
572
|
+
|
|
573
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scenario 側 (`test/*.ts` / `tools/*.ts`) から `window.__uzu_dev` を
|
|
3
|
+
* 補完 + 型チェック付きで呼べるようにする ambient module。
|
|
4
|
+
*
|
|
5
|
+
* 使い方: scenario の `tsconfig.json` で
|
|
6
|
+
*
|
|
7
|
+
* ```jsonc
|
|
8
|
+
* { "compilerOptions": { "types": ["@uzuhq/code-sdk/dev-globals"] } }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* SDK 自身を import するコード (= `@uzuhq/code-sdk` の named import を持つ
|
|
12
|
+
* モジュール) では `dev-hooks.ts` 側の `declare global` で同じ型が既に効く
|
|
13
|
+
* ため、本 module を `types` に足す必要はない。target は import しない
|
|
14
|
+
* scenario test/tools のみ。
|
|
15
|
+
*/
|
|
16
|
+
import type { UzuDevHooks } from './dev-hooks.js';
|
|
17
|
+
declare global {
|
|
18
|
+
interface Window {
|
|
19
|
+
/**
|
|
20
|
+
* dev harness / Playwright / 単独 page で attach される dev hooks。
|
|
21
|
+
* 本番 (Flutter native ホスト) では undefined。
|
|
22
|
+
* - run() devHarness: 親 frame に attach (authoritative state)
|
|
23
|
+
* - sync() / online: 子 frame で undefined
|
|
24
|
+
*/
|
|
25
|
+
__uzu_dev?: UzuDevHooks<unknown>;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scenario 側 (`test/*.ts` / `tools/*.ts`) から `window.__uzu_dev` を
|
|
3
|
+
* 補完 + 型チェック付きで呼べるようにする ambient module。
|
|
4
|
+
*
|
|
5
|
+
* 使い方: scenario の `tsconfig.json` で
|
|
6
|
+
*
|
|
7
|
+
* ```jsonc
|
|
8
|
+
* { "compilerOptions": { "types": ["@uzuhq/code-sdk/dev-globals"] } }
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* SDK 自身を import するコード (= `@uzuhq/code-sdk` の named import を持つ
|
|
12
|
+
* モジュール) では `dev-hooks.ts` 側の `declare global` で同じ型が既に効く
|
|
13
|
+
* ため、本 module を `types` に足す必要はない。target は import しない
|
|
14
|
+
* scenario test/tools のみ。
|
|
15
|
+
*/
|
|
16
|
+
export {};
|