def-game 5.1.0-timeout.0 → 6.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.
- package/README.md +11 -114
- package/bin/def-game.cjs +8 -124
- package/bin/init-worker.cjs +48 -0
- package/dist/worker/cloudflare/index.d.ts +5 -0
- package/dist/worker/cloudflare/index.d.ts.map +1 -0
- package/dist/worker/cloudflare/index.js +2 -0
- package/dist/worker/cloudflare/protocol.d.ts +11 -0
- package/dist/worker/cloudflare/protocol.d.ts.map +1 -0
- package/dist/worker/cloudflare/protocol.js +24 -0
- package/dist/worker/cloudflare/room-client.d.ts +28 -0
- package/dist/worker/cloudflare/room-client.d.ts.map +1 -0
- package/dist/worker/cloudflare/room-client.js +21 -0
- package/dist/worker/cloudflare/session-runtime.d.ts +27 -0
- package/dist/worker/cloudflare/session-runtime.d.ts.map +1 -0
- package/dist/worker/cloudflare/session-runtime.js +279 -0
- package/dist/worker/cloudflare/types.d.ts +99 -0
- package/dist/worker/cloudflare/types.d.ts.map +1 -0
- package/dist/worker/cloudflare/types.js +1 -0
- package/dist/worker/game-definition.d.ts +44 -0
- package/dist/worker/game-definition.d.ts.map +1 -0
- package/dist/worker/game-definition.js +1 -0
- package/dist/worker/package.json +1 -0
- package/docs/design-philosophy.md +410 -0
- package/docs/runtime-api.md +490 -0
- package/package.json +33 -7
- package/templates/starter/AGENTS.md +15 -0
- package/templates/starter/README.md +18 -0
- package/templates/starter/gitignore +5 -0
- package/templates/starter/package.json +15 -0
- package/templates/starter/public/app.js +93 -0
- package/templates/starter/public/index.html +37 -0
- package/templates/starter/scripts/dev-auth.mjs +40 -0
- package/templates/starter/scripts/dev.mjs +16 -0
- package/templates/starter/src/env.ts +10 -0
- package/templates/starter/src/external/decks.ts +15 -0
- package/templates/starter/src/game/definition.ts +79 -0
- package/templates/starter/src/game/types.ts +44 -0
- package/templates/starter/src/game-adapter.ts +29 -0
- package/templates/starter/src/index.ts +88 -0
- package/templates/starter/src/room.ts +9 -0
- package/templates/starter/tsconfig.json +8 -0
- package/templates/starter/wrangler.jsonc +14 -0
- package/templates/worker/env.ts +0 -9
- package/templates/worker/index.ts +0 -84
- package/templates/worker/runtime/game-adapter.ts +0 -15
- package/templates/worker/runtime/parse.ts +0 -14
- package/templates/worker/session.ts +0 -100
- package/templates/worker/timeout/runtime/game-adapter.ts +0 -18
- package/templates/worker/timeout/session.ts +0 -152
- /package/templates/{worker → starter/src}/auth.ts +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# GameAdapter・GameDefinitionとruntime API
|
|
2
|
+
|
|
3
|
+
[設計思想](design-philosophy.md)が実装の進め方を説明するのに対し、この文書は**あなたが実装する関数の型と契約**を説明します。
|
|
4
|
+
中心は、ゲームそのものを定義する`GameDefinition`と、それをCloudflare runtimeへ接続する`GameAdapter`です。
|
|
5
|
+
|
|
6
|
+
- [GameTypes:ゲーム固有の型](#gametypes)
|
|
7
|
+
- [GameAdapter:接続する関数の一覧](#gameadapter)
|
|
8
|
+
- [GameDefinition:初期状態・ルール・View](#gamedefinition)
|
|
9
|
+
- [adapterの各プロパティ](#adapter-properties)
|
|
10
|
+
- [呼び出し側のAPI・戻り値・エラー](#room-api)
|
|
11
|
+
- [WebSocketの通信形式](#websocket-protocol)
|
|
12
|
+
- [初期生成とローカル開発](#初期生成とローカル開発)
|
|
13
|
+
|
|
14
|
+
`GameDefinition`・`CommandContext`・`TransitionResult`は`def-game`から、
|
|
15
|
+
`GameAdapter`・`GameTypes`・`SessionRuntime`と接続用の型は`def-game/cloudflare`からimportします。
|
|
16
|
+
後者はCloudflare専用のES moduleです。
|
|
17
|
+
|
|
18
|
+
<a id="gametypes"></a>
|
|
19
|
+
## GameTypes:ゲーム固有の型をまとめる
|
|
20
|
+
|
|
21
|
+
`GameTypes`は、adapter全体で使う型を一か所にまとめるためのinterfaceです。
|
|
22
|
+
以下の`My…`はアプリで定義する型です。
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import type { GameTypes } from 'def-game/cloudflare';
|
|
26
|
+
|
|
27
|
+
interface Types extends GameTypes {
|
|
28
|
+
state: MyState;
|
|
29
|
+
actorCommand: MyActorCommand;
|
|
30
|
+
systemCommand: MySystemCommand;
|
|
31
|
+
view: MyView;
|
|
32
|
+
effect: MyEffect;
|
|
33
|
+
error: MyError;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| プロパティ | 定義するもの | 使わない場合 |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `state` | 永続化するゲーム状態。参加者・手番・結果待ち等 | 必須 |
|
|
40
|
+
| `actorCommand` | 認証済みActorの操作。サーバー取得済み情報を含めてもよい | 操作がなければnever |
|
|
41
|
+
| `systemCommand` | Alarmや外部処理の結果等、サーバー起点の操作 | never |
|
|
42
|
+
| `view` | Actorに公開する情報とavailableActions | 必須 |
|
|
43
|
+
| `effect` | ゲームが宣言するAlarm・外部処理の種類と引数 | never |
|
|
44
|
+
| `error` | ゲーム上の拒否理由。文字列unionやオブジェクト等 | 拒否がなければnever |
|
|
45
|
+
|
|
46
|
+
runtimeはStateの内部構造を解釈しません。`undefined`は未作成判定に使うためStateとして返せません。
|
|
47
|
+
Stateはstorageへ保存可能なデータにし、Viewとクライアントへ返すErrorはJSONで送れる形にしてください。
|
|
48
|
+
型に`readonly`を付けても自動で深く凍結されるわけではありません。入力を変更しない契約は実装側で守ります。
|
|
49
|
+
|
|
50
|
+
<a id="gameadapter"></a>
|
|
51
|
+
## GameAdapter:ゲームとアプリを接続する契約
|
|
52
|
+
|
|
53
|
+
`Env`はWorkerのbinding・環境変数の型、`T`は上記のゲーム固有型です。
|
|
54
|
+
`T["state"]`は、例えば`Types`に定義した`state`の型を取り出すTypeScriptの記法です。
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export interface GameAdapter<Env, T extends GameTypes> {
|
|
58
|
+
readonly game: GameDefinition<T["state"], T["actorCommand"] | T["systemCommand"], string,
|
|
59
|
+
T["view"], T["effect"], T["error"]>;
|
|
60
|
+
readonly webSocket: {
|
|
61
|
+
/** クライアントが指定してよい入力だけCommandにする。手番等の検証はゲームに残す。 */
|
|
62
|
+
readonly parseCommand: (input: unknown) => T["actorCommand"] | null;
|
|
63
|
+
};
|
|
64
|
+
readonly canConnect: (state: T["state"], actorId: string) => boolean;
|
|
65
|
+
readonly timeout?: {
|
|
66
|
+
readonly effect: (effect: T["effect"]) => TimeoutEffect | null;
|
|
67
|
+
readonly command: (decisionId: string) => T["systemCommand"];
|
|
68
|
+
};
|
|
69
|
+
/** 保存後のbest-effort処理。結果はruntimeがSystem Commandとして再度dispatchする。 */
|
|
70
|
+
readonly executeEffect?: (effect: T["effect"], context: { readonly env: Env; readonly roomId: string })
|
|
71
|
+
=> Promise<void | { readonly command: T["systemCommand"] }>;
|
|
72
|
+
/** 同期の診断フック。例外はruntimeが隔離する。配送・再試行の仕組みではない。 */
|
|
73
|
+
readonly onError?: (failure: RuntimeFailure) => void;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| 項目 | 目的 | 必須か |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `game` | 純粋な初期状態生成・ルール・View生成を提供 | 必須 |
|
|
80
|
+
| `webSocket.parseCommand` | クライアントが指定してよい入力をCommandへ変換 | 必須 |
|
|
81
|
+
| `canConnect` | Actorの接続とView配信を許可するか判断 | 必須 |
|
|
82
|
+
| `timeout` | ゲームの期限処理をCloudflare Alarmにつなぐ | 期限処理を使う場合 |
|
|
83
|
+
| `executeEffect` | Alarm以外のEffectを実行し、必要なら結果を返す | 外部Effectを出す場合 |
|
|
84
|
+
| `onError` | runtimeの診断情報を受け取る | 任意 |
|
|
85
|
+
|
|
86
|
+
接続先のDOクラスでは、adapterをプロパティとして渡します。
|
|
87
|
+
`game`等の関数はruntimeから呼ばれます。保存・WS配信の共通手順を各関数へ再実装する必要はありません。
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export class RoomDurableObject extends SessionRuntime<Env, Types> {
|
|
91
|
+
protected readonly adapter = adapter;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
<a id="gamedefinition"></a>
|
|
96
|
+
## GameDefinition:adapter.gameに実装するもの
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
export interface GameDefinition<State, Command, ActorId, View, Effect, Error> {
|
|
100
|
+
/** Session や接続の情報に依存しない、ゲームの初期状態を生成する。 */
|
|
101
|
+
createInitialState(): State;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 実行元とコマンドを再検証し、次の外部入力を待てる安定状態まで遷移する。
|
|
105
|
+
* 入力 state を変更せず、拒否時は error のみを返す。外部 I/O は行わない。
|
|
106
|
+
* system 起点でも、そのコマンドが現在の状態で有効かを検証する。
|
|
107
|
+
*/
|
|
108
|
+
handleCommand(
|
|
109
|
+
state: State,
|
|
110
|
+
command: Command,
|
|
111
|
+
context: CommandContext<ActorId>
|
|
112
|
+
): TransitionResult<State, Effect, Error>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* state を変更せず、Actor に公開できる情報と availableActions を持つ View を生成する。
|
|
116
|
+
* availableActions は表示補助であり、コマンド実行時の検証を省略する根拠にはしない。
|
|
117
|
+
*/
|
|
118
|
+
project(state: State, actorId: ActorId): View;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
coreの`GameDefinition`はActor IDの型も選べます。Cloudflareの`GameAdapter`へ接続する場合は`string`です。
|
|
123
|
+
`Command`には`T["actorCommand"] | T["systemCommand"]`を渡します。ActorとSystemの処理を別の状態更新経路に分けません。
|
|
124
|
+
3つの関数はすべて**同期・純粋な関数**です。Promiseを返さず、fetch・storage・WS送信は行いません。
|
|
125
|
+
|
|
126
|
+
### createInitialState(): State
|
|
127
|
+
|
|
128
|
+
**目的:** 新しく作った空のルームに保存する土台を返します。
|
|
129
|
+
|
|
130
|
+
参加前の状態、未開始のフェーズ、空の結果等を定義します。Actor・Env・外部設定は引数に渡されません。
|
|
131
|
+
参加者や所有者を必須にせず、Joinや設定変更は後続Commandで扱ってください。
|
|
132
|
+
既存ルームの二重作成はruntimeが拒否するため、この関数でstorageの存在確認はしません。
|
|
133
|
+
例外や`undefined`を返す初期化は作成失敗になります。
|
|
134
|
+
|
|
135
|
+
### handleCommand(state, command, context): TransitionResult
|
|
136
|
+
|
|
137
|
+
**目的:** 現在の状態で入力を受け入れてよいか判断し、次の入力を待てる状態まで進めます。
|
|
138
|
+
|
|
139
|
+
| 引数 | 内容 | 書く処理 |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `state` | runtimeが取得した現在の保存状態 | 手番・参加資格・結果待ち等を調べる。直接変更しない |
|
|
142
|
+
| `command` | ActorまたはSystemのゲーム入力 | 操作の種類・値・対象IDを検証する |
|
|
143
|
+
| `context` | 実行側が確定した操作の起点 | Actorの認可、System専用操作の判定に使う |
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
export type CommandContext<ActorId> =
|
|
147
|
+
| { readonly origin: 'actor'; readonly actorId: ActorId }
|
|
148
|
+
| { readonly origin: 'system' };
|
|
149
|
+
|
|
150
|
+
export type TransitionResult<State, Effect, Error> =
|
|
151
|
+
| {
|
|
152
|
+
readonly ok: true;
|
|
153
|
+
readonly state: State;
|
|
154
|
+
readonly effects: readonly Effect[];
|
|
155
|
+
}
|
|
156
|
+
| { readonly ok: false; readonly error: Error };
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
成功時は、新しいStateとEffect配列を返します。Effectがなくても`effects: []`を返します。
|
|
160
|
+
拒否時は`{ ok: false, error }`だけを返し、runtimeは状態保存・View配信・Effect実行を行いません。
|
|
161
|
+
予定されたゲーム上の拒否に例外を使わず、このError型で表現してください。
|
|
162
|
+
|
|
163
|
+
認証済みでも「今その操作をしてよい」とは限りません。System入力も古い結果や期限を対象にしていないか検証します。
|
|
164
|
+
時刻や乱数が必要なら実行側で入力に載せ、関数内で取得しないようにします。
|
|
165
|
+
成功結果の保存はruntimeの責務です。関数が返しただけでは保存の成功は保証されません。
|
|
166
|
+
|
|
167
|
+
### project(state, actorId): View
|
|
168
|
+
|
|
169
|
+
**目的:** 現在状態から、そのActorへ公開してよい情報を作ります。
|
|
170
|
+
|
|
171
|
+
自分の手札・公開得点・選択可能な操作等を返します。全Stateをそのまま返さず、非公開情報を除いてください。
|
|
172
|
+
`availableActions`はViewに含める設計上の契約ですが、型の制約で自動的に必須化されてはいません。
|
|
173
|
+
その表示は認可の代わりにならないため、Command実行時にも検証します。
|
|
174
|
+
|
|
175
|
+
runtimeはHTTP等からの`getView`、接続時、Command保存後の配信で呼びます。同じActorの複数接続などで何度呼ばれてもStateを変更してはいけません。
|
|
176
|
+
配信中に例外が出ても保存済みCommandは取り消しません。該当接続は閉じられ、診断対象になります。
|
|
177
|
+
|
|
178
|
+
<a id="adapter-properties"></a>
|
|
179
|
+
## adapterの各プロパティに何を書くか
|
|
180
|
+
|
|
181
|
+
### webSocket.parseCommand
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
(input: unknown) => T['actorCommand'] | null
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**目的:** WSのゲーム固有入力を検証し、クライアントに許可したCommandだけを作ります。
|
|
188
|
+
共通envelope内の`command`が渡されます。envelope自体の検証はruntimeが担当します。
|
|
189
|
+
|
|
190
|
+
型や必須フィールドを調べ、許可した値だけを新しいオブジェクトへコピーしてください。
|
|
191
|
+
時刻等を信頼できる実行側で追加する場所にもなります。外部取得は同期parser内では行わず、アプリのHTTP処理等で行います。
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
parseCommand(input) {
|
|
195
|
+
if (typeof input !== 'object' || input === null) return null;
|
|
196
|
+
if (!('type' in input) || input.type !== 'play') return null;
|
|
197
|
+
if (!('cardId' in input) || typeof input.cardId !== 'string') return null;
|
|
198
|
+
if (!('decisionId' in input) || typeof input.decisionId !== 'string') return null;
|
|
199
|
+
return {
|
|
200
|
+
type: 'play', cardId: input.cardId,
|
|
201
|
+
decisionId: input.decisionId, now: Date.now(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
この例ではカード能力値やActor IDはクライアントから受け取りません。
|
|
207
|
+
手番等のStateに依存するゲームルールは`game.handleCommand`に置きます。
|
|
208
|
+
`null`を返す、または例外を投げると`ProtocolErrorEvent`で拒否します。
|
|
209
|
+
HTTPから`dispatchActor`へ渡したCommandには、このWS parserは適用されません。HTTP入力の検証はアプリで行ってください。
|
|
210
|
+
|
|
211
|
+
### canConnect
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
(state: T['state'], actorId: string) => boolean
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**目的:** このActorにWS接続とView配信を許可するか判断します。
|
|
218
|
+
例えば`state.players.some(player => player.actorId === actorId)`を返します。観戦者を許すなら、その条件もアプリが定義します。
|
|
219
|
+
同期で判定し、状態変更や外部I/Oはしません。
|
|
220
|
+
|
|
221
|
+
接続開始時、WS Command実行前、View配信時に呼ばれます。
|
|
222
|
+
falseなら接続開始は403、WS Commandは`NotRoomMember`として拒否し、配信対象なら接続を閉じます。
|
|
223
|
+
HTTP/SystemのCommand認可には使われないため、`handleCommand`の検証は必須です。
|
|
224
|
+
|
|
225
|
+
### timeout.effect / timeout.command
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
timeout?: {
|
|
229
|
+
effect: (effect: T['effect']) => TimeoutEffect | null;
|
|
230
|
+
command: (decisionId: string) => T['systemCommand'];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
type TimeoutEffect =
|
|
234
|
+
| { readonly type: 'schedule'; readonly decisionId: string; readonly deadline: number }
|
|
235
|
+
| { readonly type: 'cancel'; readonly decisionId: string };
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**目的:** ゲームが返すEffectを、状態と一緒に保存するAlarm予約へ変換します。
|
|
239
|
+
`effect`は成功した遷移の各Effectに対して呼ばれます。Alarm用なら上記の形に変換し、外部Effectなら`null`を返します。
|
|
240
|
+
変換したAlarm用Effectは`executeEffect`へは渡されません。
|
|
241
|
+
|
|
242
|
+
| 戻り値 | runtimeの動作 |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| schedule | 現在予約を置き換え、State・予約・Alarmを同じtransactionで保存 |
|
|
245
|
+
| cancel | 現在のdecisionIdと一致するときだけ予約・Alarmを削除 |
|
|
246
|
+
| null | 保存後に外部Effect handlerへ渡す |
|
|
247
|
+
|
|
248
|
+
1ルームの予約は1つです。`decisionId`は空でない文字列、`deadline`は有限のUnix時刻ミリ秒を指定します。
|
|
249
|
+
複数のAlarm用Effectは配列順に適用します。
|
|
250
|
+
|
|
251
|
+
`command`は期限に達した予約からSystem Commandを作ります。必要なら`now: Date.now()`等を追加してください。
|
|
252
|
+
実行時にゲーム側でも現在のdecision IDと状態を検証します。
|
|
253
|
+
早すぎる発火は再予約し、ゲームによる拒否・保存失敗では予約を消費せず例外を返します。
|
|
254
|
+
Alarmの予約と、将来の発火・再試行は別の実行です。一度だけ届く前提にしないでください。
|
|
255
|
+
|
|
256
|
+
### executeEffect
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
(effect: T['effect'], context: { readonly env: Env; readonly roomId: string })
|
|
260
|
+
=> Promise<void | { readonly command: T['systemCommand'] }>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**目的:** 保存後、ゲームが依頼した外部処理をアプリとして実行します。
|
|
264
|
+
`env`はWorkerのbinding・環境変数、`roomId`はDO IDの文字列であり、アプリの公開ルームIDとは限りません。
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
async executeEffect(effect, { env, roomId }) {
|
|
268
|
+
const result = await callExternalService(env, roomId, effect);
|
|
269
|
+
return { command: makeSystemCommand(result) };
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
結果を戻さなければ何も返さず完了します。戻す場合は**Commandそのものではなく`{ command }`**を返します。
|
|
274
|
+
runtimeがSystem起点でdispatchするので、handler自身による再dispatchは不要です。
|
|
275
|
+
ゲームのstorageを直接変更せず、結果の有効性は`handleCommand`で判断します。
|
|
276
|
+
|
|
277
|
+
外部処理中も別Commandは実行できます。同じ遷移のEffectは順に処理しますが、他のCommandが出すEffectとの全体順序は保証しません。
|
|
278
|
+
例外は診断対象となり、元の保存済み成功を取り消さず、残りのEffectを処理します。handler未設定も配送失敗になります。
|
|
279
|
+
配送はbest effortで、永続キュー・自動再試行・exactly-onceはありません。保存後の停止で依頼や結果が失われる場合の復旧はアプリで設計します。
|
|
280
|
+
|
|
281
|
+
### onError
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
(failure: RuntimeFailure) => void
|
|
285
|
+
|
|
286
|
+
interface RuntimeFailure {
|
|
287
|
+
readonly roomId: string;
|
|
288
|
+
readonly phase: 'create' | 'command' | 'effect' | 'effect-feedback' | 'view' | 'connection';
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**目的:** runtime内部で失敗した区間を同期的に観測します。ログ等の診断処理を書きます。
|
|
293
|
+
|
|
294
|
+
| phase | 主な対象 |
|
|
295
|
+
| --- | --- |
|
|
296
|
+
| create | 初期状態生成・作成保存の例外 |
|
|
297
|
+
| command | Command適用・Alarm変換・保存等の例外 |
|
|
298
|
+
| effect | 外部Effect handlerの例外・未設定 |
|
|
299
|
+
| effect-feedback | 結果System Commandの拒否・実行失敗 |
|
|
300
|
+
| view | View生成・送信・接続列挙の失敗 |
|
|
301
|
+
| connection | WS接続処理の例外 |
|
|
302
|
+
|
|
303
|
+
通常のゲーム上の拒否すべてを通知するフックではありません。
|
|
304
|
+
State・Command・Effect・元の例外本文は引数に含めません。未設定ならメタデータだけをconsoleへ記録します。
|
|
305
|
+
フック内の例外もruntimeが隔離します。Promiseは待機されず、復旧・配送の仕組みではありません。
|
|
306
|
+
|
|
307
|
+
<a id="room-api"></a>
|
|
308
|
+
## 呼び出し側のAPI・戻り値・エラー
|
|
309
|
+
|
|
310
|
+
アプリの利用例は生成された`src/index.ts`を参照してください。以下は共通契約です。
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
interface VerifiedActor { readonly actorId: string }
|
|
314
|
+
|
|
315
|
+
// getRoom(namespace, durableObjectId)が返す参照
|
|
316
|
+
interface RoomClient<ActorCommand, Error, View = unknown> {
|
|
317
|
+
create(): Promise<CreateRoomResult>;
|
|
318
|
+
getView(actor: VerifiedActor): Promise<GetViewResult<View>>;
|
|
319
|
+
dispatchActor(actor: VerifiedActor, command: ActorCommand): Promise<CommandResult<Error>>;
|
|
320
|
+
connect(actor: VerifiedActor): Promise<Response>;
|
|
321
|
+
}
|
|
322
|
+
// getSystemRoom(namespace, durableObjectId)が返す参照
|
|
323
|
+
// { dispatchSystem(command: SystemCommand): Promise<CommandResult<Error>> }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### getView(actor):接続せずにViewを取得する
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
const room = getRoom(env.ROOMS, id);
|
|
330
|
+
const result = await room.getView(verifiedActor);
|
|
331
|
+
// 公開HTTPのレスポンスやステータスはアプリで決める
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
type GetViewResult<View> =
|
|
336
|
+
| { readonly ok: true; readonly view: View }
|
|
337
|
+
| { readonly ok: false; readonly error: RuntimeError };
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
runtimeが保存済みStateを読み、既存の`game.project(state, actor.actorId)`を呼んで返します。
|
|
341
|
+
ゲーム側に新しい関数の実装は必要ありません。View型はDOのadapterから推論されます。
|
|
342
|
+
状態変更・Command実行・Effect・WS接続・他の接続への配信は行いません。
|
|
343
|
+
|
|
344
|
+
`canConnect`は呼びません。認証済みの未参加者にも、projectが部屋の概要等を返せます。
|
|
345
|
+
参加者・観戦者・未参加者で何を見せるかはprojectで判断し、非公開Stateを返さないでください。
|
|
346
|
+
未認証の公開閲覧を提供するAPIではなく、アプリは呼び出し前にActorを認証します。
|
|
347
|
+
|
|
348
|
+
未作成はRoomNotFound、不正ActorはInvalidRequest、読み取り・projectの例外はInternalErrorです。
|
|
349
|
+
内部例外はonErrorのview区間へ通知し、保存済み状態を変更しません。
|
|
350
|
+
返るのは読み取り時点のViewであり、その後の操作が同じ状態で受理される保証はありません。
|
|
351
|
+
|
|
352
|
+
### 作成・操作・接続
|
|
353
|
+
|
|
354
|
+
`create()`は初期状態のみを保存し、二重作成を拒否します。JoinはゲームCommandです。
|
|
355
|
+
`connect()`成功時は101のWS応答を返します。未作成404、接続資格なし403、内部例外500等になります。
|
|
356
|
+
Actorが不正な場合は呼び出し側で例外になることもあります。
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
type CommandResult<Error> =
|
|
360
|
+
| { readonly ok: true }
|
|
361
|
+
| { readonly ok: false; readonly error:
|
|
362
|
+
RuntimeError | { readonly kind: 'game'; readonly detail: Error } };
|
|
363
|
+
|
|
364
|
+
type CreateRoomResult =
|
|
365
|
+
| { readonly ok: true; readonly roomId: string }
|
|
366
|
+
| { readonly ok: false; readonly error: RuntimeError };
|
|
367
|
+
|
|
368
|
+
// RuntimeErrorは下表のcodeを持つ
|
|
369
|
+
// { readonly kind: 'runtime'; readonly code: ... }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
| runtime code | 意味 |
|
|
373
|
+
| --- | --- |
|
|
374
|
+
| RoomNotFound | 初期状態が保存されていない |
|
|
375
|
+
| RoomAlreadyExists | 既存ルームを再作成しようとした |
|
|
376
|
+
| InvalidRequest | Actor情報・WS入力等が不正 |
|
|
377
|
+
| NotRoomMember | WS操作の接続資格を満たさない |
|
|
378
|
+
| InternalError | 初期化・Command処理・保存等の内部失敗 |
|
|
379
|
+
|
|
380
|
+
`TransitionResult`はゲームがruntimeへ返す次状態・Effect、`CommandResult`はruntimeが呼び出し元へ返す成否です。
|
|
381
|
+
後者の成功は保存完了を表し、StateやEffectを返しません。View受信や外部Effect完了も保証しません。
|
|
382
|
+
DO RPCの通信失敗は例外として伝わる場合があります。公開HTTPのレスポンス形式はアプリが決めます。
|
|
383
|
+
|
|
384
|
+
作成結果のroomIdはDO IDの文字列です。公開IDをidFromNameで解決するアプリは、公開IDを別に管理します。
|
|
385
|
+
`VerifiedActor`の型や`getSystemRoom`自体に認証能力はありません。namespace bindingを持つアプリを信頼する契約です。
|
|
386
|
+
通常参照はSystem入口を隠しますが、生のDO stubにはSystem RPCがあります。
|
|
387
|
+
外部のactorId・originをそのまま渡したり、汎用RPC転送を公開したりしないでください。
|
|
388
|
+
WS接続には`connect`を使い、外部RequestをDOの内部fetchへそのまま転送しません。
|
|
389
|
+
|
|
390
|
+
<a id="websocket-protocol"></a>
|
|
391
|
+
## WebSocketの通信形式
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
interface GameCommandRequest<Command> {
|
|
395
|
+
readonly type: 'GameCommandRequest';
|
|
396
|
+
readonly requestId: string;
|
|
397
|
+
readonly command: Command;
|
|
398
|
+
}
|
|
399
|
+
type GameCommandResponse<Error> = CommandResult<Error> & {
|
|
400
|
+
readonly type: 'GameCommandResponse'; readonly requestId: string;
|
|
401
|
+
};
|
|
402
|
+
interface ViewStateEvent<View> {
|
|
403
|
+
readonly type: 'ViewStateEvent'; readonly viewState: View;
|
|
404
|
+
}
|
|
405
|
+
interface ProtocolErrorEvent {
|
|
406
|
+
readonly type: 'ProtocolErrorEvent'; readonly error: RuntimeError;
|
|
407
|
+
}
|
|
408
|
+
type ServerMessage<View, Error> =
|
|
409
|
+
GameCommandResponse<Error> | ViewStateEvent<View> | ProtocolErrorEvent;
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
入力はJSONのテキストフレーム、UTF-8で64KiBまで、requestIdは1〜128文字です。バイナリは拒否します。
|
|
413
|
+
`command`はparserへ渡すクライアント入力です。サーバーで追加する値までクライアントが指定できるという意味ではありません。
|
|
414
|
+
Actorとoriginは接続の情報からruntimeが決めます。
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{ "type": "GameCommandRequest", "requestId": "r1", "command": { "type": "start" } }
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```json
|
|
421
|
+
{ "type": "GameCommandResponse", "requestId": "r1", "ok": false, "error": { "kind": "game", "detail": "not-ready" } }
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
{ "type": "ViewStateEvent", "viewState": { "availableActions": [] } }
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
```json
|
|
429
|
+
{ "type": "ProtocolErrorEvent", "error": { "kind": "runtime", "code": "InvalidRequest" } }
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
ProtocolErrorEventにはrequestIdがありません。ViewとCommand応答の到着順には依存しないでください。
|
|
433
|
+
requestIdは応答の対応付け用で、永続的な重複排除には使いません。再送時の扱いはゲームで定義します。
|
|
434
|
+
同一Actorの複数接続を許可し、HTTP経由の更新も接続中WSへ配信します。
|
|
435
|
+
再接続時は最新Viewを送り、切断中のイベント再生は提供しません。
|
|
436
|
+
認証は接続時のみです。トークン更新はアプリ側で行い、接続中の自動再検証・失効連動の切断は行いません。
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## 初期生成とローカル開発
|
|
441
|
+
|
|
442
|
+
生成物はすべてアプリ所有です。ライブラリ更新時に再生成せず、アプリのコードとして変更します。
|
|
443
|
+
|
|
444
|
+
### 始め方
|
|
445
|
+
|
|
446
|
+
Node.js 22以降を使用します。`6.1.0`を指定して生成します。
|
|
447
|
+
|
|
448
|
+
```sh
|
|
449
|
+
npx def-game@6.1.0 init-worker --directory my-game --name my-game
|
|
450
|
+
cd my-game
|
|
451
|
+
npm install
|
|
452
|
+
npm run dev
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
開発版をこのリポジトリから試す場合は、リポジトリで`npm pack`を実行します。
|
|
456
|
+
`node bin/def-game.cjs init-worker --directory /path/to/my-game --name my-game`で生成し、生成先で
|
|
457
|
+
`npm install /absolute/path/to/def-game-6.1.0.tgz`を実行してください。
|
|
458
|
+
その後は`npm run typecheck`、`npm run build`、`npm run dev`を利用できます。buildはWranglerのdry-runで、公開しません。
|
|
459
|
+
|
|
460
|
+
生成先は新しいディレクトリに限定します。既存ディレクトリは空でも拒否します。
|
|
461
|
+
Worker名は先頭が英小文字または数字、全体が英小文字・数字・ハイフンの1〜63文字です。
|
|
462
|
+
名前は`package.json`と`wrangler.jsonc`に構造的に反映し、Def Game依存バージョンは生成に使ったCLIに揃えます。
|
|
463
|
+
ソースコード内の置換変数はありません。認証先、binding、ゲーム名の表示などは生成後に編集します。
|
|
464
|
+
|
|
465
|
+
### 動く最小例
|
|
466
|
+
|
|
467
|
+
2人が部屋に参加し、サーバーのカタログからデッキを選び、各自1枚プレイして終了します。
|
|
468
|
+
手番には30秒の期限があり、AlarmもSystem Commandとして通常と同じゲームルールを通ります。
|
|
469
|
+
|
|
470
|
+
- HTTP: 認証、空の部屋作成、Join Command、外部取得済みデッキを含むCommand。
|
|
471
|
+
- WebSocket: start/playだけをparserで許可。時刻はサーバーが追加します。
|
|
472
|
+
- View: 自分のカードだけを配信。再接続時は最新Viewを受け取ります。
|
|
473
|
+
- Effect: Alarm予約・取消と、終了時の外部処理例を分けます。
|
|
474
|
+
|
|
475
|
+
デッキ取得はサーバー側の固定データです。実際の外部サービスへ接続するときは、この取得処理を置き換えます。
|
|
476
|
+
所有権チェックやデータ取得は`src/external/decks.ts`、状態遷移は`src/game/definition.ts`に実装します。
|
|
477
|
+
Worker・認証・公開ルームID解決は`src/index.ts`等、ライブラリへの接続は`src/game-adapter.ts`と`src/room.ts`です。
|
|
478
|
+
|
|
479
|
+
### ローカル認証と公開設定
|
|
480
|
+
|
|
481
|
+
`npm run dev`は127.0.0.1の8787番でWorker、8790番で開発専用認証サーバーを起動します。
|
|
482
|
+
ブラウザーで`http://127.0.0.1:8787`を開きます。別ブラウザープロファイルで2人として試せます。
|
|
483
|
+
開発認証は署名済みJWTを発行し、Workerは本番と同じ検証を通ります。認証の省略は行いません。
|
|
484
|
+
開発認証のActorは自己発行で、本人確認用途には使えません。認証サーバーはWorkerからimportせず、デプロイ成果物に含めません。
|
|
485
|
+
ローカル認証先はdevコマンドだけで上書きし、Wranglerの公開設定を書き換えません。
|
|
486
|
+
|
|
487
|
+
初期設定の公開認証先は`auth.waki.work`です。共有Cookieが届くHTTPSの`*.waki.work`で利用する想定です。
|
|
488
|
+
`workers.dev`や別ドメインでは、認証先とCookieの設計をアプリに合わせて変更してください。
|
|
489
|
+
静的な画面は公開し、状態を扱うHTTPとWS入口で認証します。
|
|
490
|
+
|
package/package.json
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "def-game",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.1.0",
|
|
4
|
+
"description": "Pure game definitions, a simulator, and a Cloudflare Durable Object runtime for turn-based games",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "node
|
|
9
|
-
"typecheck": "tsc --noEmit",
|
|
10
|
-
"test": "npm run build && node --test test/*.test.cjs",
|
|
8
|
+
"build": "node scripts/build.cjs",
|
|
9
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.cloudflare.json --noEmit && tsc -p tsconfig.runtime-test.json",
|
|
10
|
+
"test": "npm run build && node --test --test-timeout=30000 test/*.test.cjs",
|
|
11
11
|
"prepack": "npm run typecheck && npm test"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [],
|
|
14
14
|
"author": "",
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"
|
|
17
|
+
"@cloudflare/workers-types": "^5.20260911.1",
|
|
18
|
+
"esbuild": "^0.28.2",
|
|
19
|
+
"hono": "^4.13.7",
|
|
20
|
+
"jose": "^6.2.12",
|
|
21
|
+
"miniflare": "5.20260911.0-alpha",
|
|
22
|
+
"typescript": "^5.8.2",
|
|
23
|
+
"wrangler": "^4.131.1"
|
|
18
24
|
},
|
|
19
25
|
"files": [
|
|
20
26
|
"dist",
|
|
21
27
|
"bin",
|
|
22
|
-
"templates"
|
|
28
|
+
"templates",
|
|
29
|
+
"docs/design-philosophy.md",
|
|
30
|
+
"docs/runtime-api.md"
|
|
23
31
|
],
|
|
24
32
|
"repository": {
|
|
25
33
|
"type": "git",
|
|
@@ -27,5 +35,23 @@
|
|
|
27
35
|
},
|
|
28
36
|
"bin": {
|
|
29
37
|
"def-game": "bin/def-game.cjs"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"default": "./dist/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./cloudflare": {
|
|
45
|
+
"types": "./dist/worker/cloudflare/index.d.ts",
|
|
46
|
+
"import": "./dist/worker/cloudflare/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"typesVersions": {
|
|
51
|
+
"*": {
|
|
52
|
+
"cloudflare": [
|
|
53
|
+
"dist/worker/cloudflare/index.d.ts"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
30
56
|
}
|
|
31
57
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# このゲームを実装するAIエージェントへ
|
|
2
|
+
|
|
3
|
+
最初に `node_modules/def-game/docs/design-philosophy.md` と `node_modules/def-game/docs/runtime-api.md` を読むこと。
|
|
4
|
+
インストール前はDefGameリポジトリの同名ドキュメントを参照する。
|
|
5
|
+
|
|
6
|
+
- このディレクトリは初期生成後、アプリ所有。自由に編集する。生成コマンドで更新・上書きしない。
|
|
7
|
+
- ゲームの状態変更はCommandを通す。Game Definitionの`handleCommand`を純粋に保つ。
|
|
8
|
+
- runtimeの保存・WS・Alarmをアプリ側へコピーしない。`RoomDurableObject`はadapterを接続する薄いクラスに保つ。
|
|
9
|
+
- 認証・公開ルート・room ID解決・外部取得はアプリ側。本人確認とゲーム上の操作権限を分ける。
|
|
10
|
+
- 外部から取得したデッキ等はHTTP側で検証してActor Commandへ含める。WS parserから自己申告できないようにする。
|
|
11
|
+
- ゲームが要求する外部処理はEffect、結果を返す場合はSystem Command。外部Effectはbest-effort。
|
|
12
|
+
- 参加・所有者・設定変更のルールはゲーム固有。例の2人制やカード得点をライブラリの制約だと思わない。
|
|
13
|
+
- 認証の更新は再接続時にアプリ側で行う。接続中に定期的な認証確認を足さない。
|
|
14
|
+
- `scripts/dev-auth.mjs`はループバック専用の開発用認証サーバー。公開Workerからimportせず、デプロイへ含めない。
|
|
15
|
+
- 変更後は `npm run typecheck` と `npm run build` を実行し、Commandの検証と主要な接続動作を確認する。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# DefGame Worker Starter
|
|
2
|
+
|
|
3
|
+
Commandでゲーム状態を変更し、Effectで外部処理につなぐ最小ゲームです。
|
|
4
|
+
生成後のコードはアプリ所有で、自由に編集できます。
|
|
5
|
+
|
|
6
|
+
## Getting Started
|
|
7
|
+
|
|
8
|
+
Node.js 22以降を使用します。
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
ローカルの開発版では、`npm install`の代わりに`npm install /absolute/path/to/def-game-6.1.0.tgz`を実行してください。
|
|
16
|
+
`http://127.0.0.1:8787`を開きます。別ブラウザープロファイルで共有URLを開くと、2人で試せます。
|
|
17
|
+
|
|
18
|
+
実装前に[設計思想](node_modules/def-game/docs/design-philosophy.md)を読み、詳細は[runtime API・初期生成の使い方](node_modules/def-game/docs/runtime-api.md)を参照してください。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "def-game-starter",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": { "node": ">=22" },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "node scripts/dev.mjs",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"build": "wrangler deploy --dry-run --outdir dist",
|
|
11
|
+
"deploy": "wrangler deploy"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": { "def-game": "6.1.0", "hono": "^4.13.7", "jose": "^6.2.12" },
|
|
14
|
+
"devDependencies": { "@cloudflare/workers-types": "^5.20260911.1", "typescript": "^5.8.2", "wrangler": "^4.131.1" }
|
|
15
|
+
}
|