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,410 @@
|
|
|
1
|
+
# DefGameでゲームを作る
|
|
2
|
+
|
|
3
|
+
DefGameは、ゲームルールを純粋な状態機械として定義し、保存・通信・外部サービスにつなぐTypeScriptライブラリです。
|
|
4
|
+
あなたが実装するのは、ゲームの状態とルール、プレイヤーに見せる情報、アプリ固有の外部処理です。
|
|
5
|
+
ルームの保存、Commandを実行する共通手順、WebSocketの接続管理、Alarmはライブラリが担当します。
|
|
6
|
+
|
|
7
|
+
このガイドは、ゲームを実装する人とAIエージェントに向けて、実装する順序とその理由を説明します。
|
|
8
|
+
前半を読みながら最小のゲームを作り、後半で設計の判断基準を確認してください。
|
|
9
|
+
型・戻り値・エラー・通信形式の詳細は[runtime API](runtime-api.md)にまとめています。
|
|
10
|
+
|
|
11
|
+
## 全体像:アプリが決めること、runtimeに任せること
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart TB
|
|
15
|
+
Client["クライアント"]
|
|
16
|
+
subgraph App["初期生成するアプリ所有コード:編集・置き換え可能"]
|
|
17
|
+
Entry["Worker入口:HTTP/WS接続ルート<br/>認証・公開ルームIDの解決"]
|
|
18
|
+
API["HTTP処理・外部情報の取得"]
|
|
19
|
+
Adapter["adapter:入力parser・接続可否・Alarm変換"]
|
|
20
|
+
Handler["Effect handler"]
|
|
21
|
+
Game["Game Definition<br/>初期状態・ルール・Actor別View"]
|
|
22
|
+
end
|
|
23
|
+
subgraph Lib["DefGameライブラリ:SessionRuntimeが提供"]
|
|
24
|
+
WS["標準WS管理<br/>接続とActorの紐付け・Hibernation"]
|
|
25
|
+
Runtime["共通dispatcher<br/>状態取得・Command実行・保存"]
|
|
26
|
+
Alarm["Alarm予約・発火時の処理"]
|
|
27
|
+
end
|
|
28
|
+
External["外部サービス"]
|
|
29
|
+
Client -->|HTTP・WS接続開始| Entry
|
|
30
|
+
Entry -->|認証済みActor| API
|
|
31
|
+
Entry -->|認証済みActorで接続| WS
|
|
32
|
+
API -->|Actor Command| Runtime
|
|
33
|
+
WS -->|接続のActorでCommand| Runtime
|
|
34
|
+
Runtime -->|handleCommand・projectを呼ぶ| Game
|
|
35
|
+
Game -->|次状態・Effect・View| Runtime
|
|
36
|
+
Adapter -.->|入力・接続条件を提供| WS
|
|
37
|
+
Adapter -.->|Alarm変換を提供| Alarm
|
|
38
|
+
Runtime -->|状態と一緒に予約| Alarm
|
|
39
|
+
Alarm -->|System Command| Runtime
|
|
40
|
+
Runtime -->|保存後のActor別View| WS
|
|
41
|
+
WS -->|配信| Client
|
|
42
|
+
API <-->|取得| External
|
|
43
|
+
Runtime -->|保存後の外部Effect| Handler
|
|
44
|
+
Handler <-->|外部処理| External
|
|
45
|
+
Handler -->|結果Commandを返す・runtimeがSystemとして実行| Runtime
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
枠は**コードの所有と責務の境界**です。別々のサービスとしてデプロイするという意味ではありません。
|
|
49
|
+
Game Definitionやadapter・Effect handlerも、runtimeから呼ばれるときはDO内で動きます。
|
|
50
|
+
WS接続後のメッセージはライブラリのWS処理へ届き、毎回Worker入口で認証し直す構成ではありません。
|
|
51
|
+
アプリのコードは生成後に編集できます。runtimeの共通実装は依存パッケージとして利用します。
|
|
52
|
+
|
|
53
|
+
## 1. init-workerで開発を始める
|
|
54
|
+
|
|
55
|
+
Node.js 22以降で、次を実行します。以下は`6.1.0`の利用手順です。
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx def-game@6.1.0 init-worker --directory my-game --name my-game
|
|
59
|
+
cd my-game
|
|
60
|
+
npm install
|
|
61
|
+
npm run dev
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`http://127.0.0.1:8787`を開くと、2人がデッキを選び、1枚ずつカードを出す最小ゲームを試せます。
|
|
65
|
+
別のブラウザープロファイルで共有URLを開けば、別Actorとして参加できます。
|
|
66
|
+
未公開の開発版を使う場合の導入方法は[runtime APIの初期生成手順](runtime-api.md#初期生成とローカル開発)を参照してください。
|
|
67
|
+
|
|
68
|
+
生成先には、すでにruntimeへ接続されたアプリができます。
|
|
69
|
+
|
|
70
|
+
| ファイル | あなたが実装・変更すること |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `src/game/types.ts` | ゲームのState・Command・View・Effect・Errorの型 |
|
|
73
|
+
| `src/game/definition.ts` | 初期状態、ルール、状態遷移、Actor別View |
|
|
74
|
+
| `src/game-adapter.ts` | WS入力の変換、接続可否、Alarm・外部Effectの接続 |
|
|
75
|
+
| `src/external/` | デッキ取得などの外部ドメイン処理 |
|
|
76
|
+
| `src/index.ts`、`src/auth.ts` | HTTPルート、認証、公開ルームIDの解決 |
|
|
77
|
+
| `src/room.ts`、`wrangler.jsonc` | 共通runtimeとCloudflare DOの登録 |
|
|
78
|
+
| `public/` | Viewを描画し、ユーザー操作を送る画面 |
|
|
79
|
+
|
|
80
|
+
**生成されたコードは、すべてあなたのアプリのコードです。**
|
|
81
|
+
最小ゲームを自分のゲームへ置き換え、認証やルートも必要に応じて変更してください。
|
|
82
|
+
ライブラリを更新するときは依存パッケージを更新します。アプリを再生成して編集内容を上書きする運用にはしません。
|
|
83
|
+
|
|
84
|
+
## 2. まずゲームの状態とルールを書く
|
|
85
|
+
|
|
86
|
+
最初に編集するのは`src/game/types.ts`と`src/game/definition.ts`です。
|
|
87
|
+
画面のボタンや通信処理を考える前に、「今どの状態で、誰のどんな操作によって、次にどうなるか」を表現します。
|
|
88
|
+
|
|
89
|
+
### ゲームが扱う情報を決める
|
|
90
|
+
|
|
91
|
+
| 型 | 考えること | カードゲームの例 |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| State | 再開に必要なゲームの事実 | 参加者、手札、手番、選択待ちのID |
|
|
94
|
+
| Actor Command | 認証済み利用者が行う操作 | 参加する、デッキを選ぶ、カードを出す |
|
|
95
|
+
| System Command | 信頼されたサーバー側からの入力 | 期限切れ、外部処理の完了 |
|
|
96
|
+
| View | そのActorに見せてよい情報 | 自分の手札、公開得点、availableActions |
|
|
97
|
+
| Effect | ゲームの進行から必要になった外部処理の宣言 | 期限を予約する、結果を外部へ送る |
|
|
98
|
+
| Error | ゲームとして操作を拒否する理由 | 手番ではない、カードを持っていない |
|
|
99
|
+
|
|
100
|
+
Stateの内部構造はゲームが決めます。ライブラリが参加者や所有者のフィールドを要求することはありません。
|
|
101
|
+
最初から所有者がいるゲームも、マッチングで参加者が決まるゲームも、自分のルールとして表現してください。
|
|
102
|
+
|
|
103
|
+
### Game Definitionの3つの関数を実装する
|
|
104
|
+
|
|
105
|
+
| 関数 | 実装すること |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `createInitialState()` | 空のルームに保存できる初期状態を返す |
|
|
108
|
+
| `handleCommand(state, command, context)` | 実行者と現在状態を検証し、次状態とEffect、または拒否理由を返す |
|
|
109
|
+
| `project(state, actorId)` | そのActor向けのViewを返す |
|
|
110
|
+
|
|
111
|
+
例えば、手番中のカード選択を処理する分岐は次のようになります。これは構造を示す抜粋です。
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// handleCommand内の「カードを出す」処理
|
|
115
|
+
if (context.origin !== 'actor' || context.actorId !== state.currentActorId) {
|
|
116
|
+
return { ok: false, error: 'not-your-turn' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// カードの所有、対象の選択待ちIDなども検証する
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
state: nextState,
|
|
123
|
+
effects: [],
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`handleCommand`では入力Stateを変更せず、新しい状態を返します。
|
|
128
|
+
DB保存、fetch、WebSocket送信は行いません。時刻や乱数が必要なら、信頼できる実行側で作りCommand等に明示的に載せます。
|
|
129
|
+
同じState・Command・実行元からは、同じ結果が返るようにします。
|
|
130
|
+
|
|
131
|
+
ルーム作成は`createInitialState()`の保存で完了します。作成にActorや最初のCommandは必須ではありません。
|
|
132
|
+
Joinや設定変更は、その後のゲームCommandとして実装してください。
|
|
133
|
+
「部屋を作った人が所有者になる」というルールも、ライブラリではなくゲームが決めます。
|
|
134
|
+
|
|
135
|
+
`project`では、自分の手札などActorに公開できる情報だけを返してください。
|
|
136
|
+
HTTPで部屋の概要や再読み込み用の情報を取得したい場合は、`room.getView(actor)`を使います。
|
|
137
|
+
同じprojectが呼ばれるので、未参加者には要約、参加者には個別情報という出し分けもここで実装します。WSの接続資格は要求しません。
|
|
138
|
+
`availableActions`は画面を作るための補助です。ボタンを非表示にしても不正なCommandは送れるため、操作可否は必ず`handleCommand`でも検証します。
|
|
139
|
+
|
|
140
|
+
## 3. ゲームと外部を接続する
|
|
141
|
+
|
|
142
|
+
ゲームルールができたら、HTTPやWebSocketで受けた操作、外部サービスの情報をつなぎます。
|
|
143
|
+
ここで守る中心原則は、**外部からゲームへはCommand、ゲームから外部へはEffect**です。
|
|
144
|
+
|
|
145
|
+
### 利用者の操作に必要な情報を、アプリで取得する
|
|
146
|
+
|
|
147
|
+
例えば「自分のデッキを選ぶ」操作では、HTTPルートが認証とデッキ取得を行い、取得結果をCommandに含めます。
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// actorは認証済み、deckIdはリクエストから検証済みの値
|
|
151
|
+
const deck = await loadDeck(actor.actorId, deckId);
|
|
152
|
+
const result = await room.dispatchActor(actor, {
|
|
153
|
+
type: 'select-deck',
|
|
154
|
+
deck,
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
これは利用者の操作なので、外部取得が挟まってもActor Commandです。
|
|
159
|
+
アプリは本人確認や外部デッキへのアクセス権を確認し、ゲームは最新状態で「今デッキを変更できるか」を判断します。
|
|
160
|
+
取得を待っている間にゲームが始まったなら、ゲーム側で変更を拒否できます。
|
|
161
|
+
|
|
162
|
+
`actorId`やSystem権限を、リクエスト本文の自己申告から作らないでください。
|
|
163
|
+
また、外部カタログから取得すべき能力値を、クライアントが自由に指定できる入口を作ってはいけません。
|
|
164
|
+
WS parserはクライアントが指定できる入力だけを許し、サーバー取得用のCommandと区別します。
|
|
165
|
+
手番等のゲームルールをparserに重複実装する必要はありません。
|
|
166
|
+
|
|
167
|
+
### ゲームの進行に必要な外部処理を、Effectで依頼する
|
|
168
|
+
|
|
169
|
+
結果保存や外部処理の依頼がゲームの進行から発生するなら、`handleCommand`がEffectを返します。
|
|
170
|
+
アプリの`executeEffect`がそれを実行し、結果をゲームへ戻す場合は`{ command: ... }`を返します。
|
|
171
|
+
runtimeがそのCommandをSystem起点で再び実行します。
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
Command
|
|
175
|
+
→ ゲームが次状態とEffectを返す
|
|
176
|
+
→ runtimeが状態を保存
|
|
177
|
+
→ アプリのEffect handlerが外部処理
|
|
178
|
+
→ 必要ならSystem Commandを返す
|
|
179
|
+
→ ゲームが最新状態で結果を検証
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Effect handlerからstorageやゲーム状態を直接変更しないでください。
|
|
183
|
+
結果待ちがある場合は、処理IDや待機状態をStateに保存します。古い結果が戻っても、今のゲームに適用してよいか判断できるようにします。
|
|
184
|
+
System Commandもルールの検証を省略する入口ではありません。
|
|
185
|
+
|
|
186
|
+
webhookやマッチング処理から入力する場合は、アプリで認証と呼び出し権限の確認を済ませてから、必要に応じて`getSystemRoom(...).dispatchSystem(...)`を使います。
|
|
187
|
+
|
|
188
|
+
## 4. adapterで共通runtimeにつなぐ
|
|
189
|
+
|
|
190
|
+
`src/game-adapter.ts`は、ゲームとアプリの関数をruntimeへ渡す接続点です。
|
|
191
|
+
生成済みのadapterを、自分の型とルールに合わせて変更してください。
|
|
192
|
+
|
|
193
|
+
| adapterの項目 | 必要になる場面 | 実装内容 |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `game` | 必須 | 作成したGame Definitionを渡す |
|
|
196
|
+
| `webSocket.parseCommand` | 必須 | unknownの入力を許可したActor Commandへ変換。拒否はnull |
|
|
197
|
+
| `canConnect` | 必須 | StateとActor IDから接続・View配信の可否を判定 |
|
|
198
|
+
| `timeout.effect`/`timeout.command` | 期限処理を使う場合 | Alarm用Effectの識別と、発火時のSystem Command作成 |
|
|
199
|
+
| `executeEffect` | 外部Effectを出す場合 | 外部処理と、必要なら結果Commandの返却 |
|
|
200
|
+
| `onError` | 独自の診断が必要な場合 | 失敗した区間の記録・通知 |
|
|
201
|
+
|
|
202
|
+
`executeEffect`は型上は省略可能ですが、外部Effectを出すゲームでは対応するhandlerが必要です。
|
|
203
|
+
Alarm用Effectはruntimeが状態と一緒に保存します。外部handlerから直接setAlarmする構成にはしません。
|
|
204
|
+
|
|
205
|
+
`src/room.ts`では、そのadapterを共通runtimeへ接続しています。
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
export class RoomDurableObject extends SessionRuntime<Env, Types> {
|
|
209
|
+
protected readonly adapter = adapter;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
ゲームごとに状態保存やWSの受信処理を複製する必要はありません。
|
|
214
|
+
DOクラスのexportとWranglerの登録は生成済みです。名前を変更する場合は登録側も揃えてください。adapterの変数名とは独立しています。
|
|
215
|
+
|
|
216
|
+
参加はゲームCommand、WS接続は通信の操作です。
|
|
217
|
+
HTTPでJoinが成功した後にWSを接続できます。接続に失敗しても保存済みの参加状態は残り、再接続で最新Viewを受け取れます。
|
|
218
|
+
接続とActorの紐付けのために、別のゲーム状態更新を追加する必要はありません。
|
|
219
|
+
|
|
220
|
+
## 5. 動かしながらゲームを育てる
|
|
221
|
+
|
|
222
|
+
ルールの確認は、まず`GameSimulator`へCommandの列を渡して行います。
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { GameSimulator } from 'def-game';
|
|
226
|
+
import { game } from './src/game/definition.js';
|
|
227
|
+
|
|
228
|
+
const simulator = new GameSimulator(game);
|
|
229
|
+
const result = simulator.executeCommand(
|
|
230
|
+
{ type: 'join', name: 'Alice' },
|
|
231
|
+
{ origin: 'actor', actorId: 'alice' },
|
|
232
|
+
);
|
|
233
|
+
const view = simulator.getView('alice');
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
成功する操作に加えて、手番外の操作、古い選択待ちID、拒否後に状態が変わっていないことを確認してください。
|
|
237
|
+
Simulatorは外部サービスを実行する仕組みではありません。Effectの内容を確認し、結果を模したSystem Commandを次の入力として渡せます。
|
|
238
|
+
|
|
239
|
+
その後、生成アプリで通信込みの動作を確認します。
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
npm run typecheck
|
|
243
|
+
npm run build
|
|
244
|
+
npm run dev
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`build`はWranglerのdry-runで、公開デプロイは行いません。
|
|
248
|
+
ローカル開発の認証、公開環境での認証設定、起動手順の詳細は[runtime API](runtime-api.md#初期生成とローカル開発)を参照してください。
|
|
249
|
+
|
|
250
|
+
機能を追加するときも、この順序を繰り返します。
|
|
251
|
+
入力をCommandとして定義し、ルールとViewを実装してから、必要な外部処理と画面をつなぎます。
|
|
252
|
+
AIエージェントへ実装を依頼する場合も、変更するState・Command・Effect・Viewと、その責務の置き場所を先に明らかにしてください。
|
|
253
|
+
|
|
254
|
+
## ライブラリは何を担い、どんな課題を解決するか
|
|
255
|
+
|
|
256
|
+
### 状態変更の入口を揃え、ゲームを追えるようにする
|
|
257
|
+
|
|
258
|
+
```mermaid
|
|
259
|
+
flowchart LR
|
|
260
|
+
HTTP["HTTPの操作"] -->|Actor Command| D["共通dispatcher"]
|
|
261
|
+
WS["WSの操作"] -->|Actor Command| D
|
|
262
|
+
Alarm["期限切れ"] -->|System Command| D
|
|
263
|
+
Result["外部処理の結果"] -->|System Command| D
|
|
264
|
+
D --> G["handleCommand<br/>現在状態・実行元を検証"]
|
|
265
|
+
G -->|成功| S["次状態を保存"]
|
|
266
|
+
G -->|拒否| E["状態を変えずエラーを返す"]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
HTTP、WS、timeout、外部処理の完了がそれぞれ直接状態を書き換えると、どこにルールがあるのか追いにくくなります。
|
|
270
|
+
DefGameでは初期状態の生成後、すべてのゲーム操作をCommandとして`handleCommand`へ集めます。
|
|
271
|
+
同じ操作なら通信経路によらず同じルールを通り、変更理由をCommandとその処理から読めます。
|
|
272
|
+
これは状態遷移の構造を揃える仕組みであり、Command履歴の永続保存や自動リプレイを提供するものではありません。
|
|
273
|
+
|
|
274
|
+
### 純粋な状態機械にして、ゲームを単独で考えられるようにする
|
|
275
|
+
|
|
276
|
+
```mermaid
|
|
277
|
+
flowchart LR
|
|
278
|
+
Input["State・Command・実行元<br/>時刻等も明示的な入力"] --> Game["純粋なhandleCommand<br/>外部I/Oなし"]
|
|
279
|
+
Game --> Output["次State・Effect<br/>または拒否理由"]
|
|
280
|
+
Runtime["本番のruntime"] -.->|同じ関数を呼ぶ| Game
|
|
281
|
+
Test["Simulator・単体テスト"] -.->|同じ関数を呼ぶ| Game
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
ゲームを「現在状態と入力から、次状態を計算するもの」にすると、HTTPサーバーやDBなしでルールを実行できます。
|
|
285
|
+
時刻・乱数・外部情報も入力として明示することで、同じ条件の再現と、境界条件のテストがしやすくなります。
|
|
286
|
+
Cloudflare非依存の`GameDefinition`と`GameSimulator`は、このための契約と実行手段です。
|
|
287
|
+
|
|
288
|
+
1回のCommandでは、次の入力を待てる状態まで進めます。
|
|
289
|
+
「誰の回答待ちか」「どの処理結果待ちか」をStateに表し、休止後も再開できるようにしてください。
|
|
290
|
+
ゲーム進行を、未来の処理を積んだ永続Task Queueに隠す設計は採りません。
|
|
291
|
+
|
|
292
|
+
### 副作用を分離し、外部サービスとの接続を変更しやすくする
|
|
293
|
+
|
|
294
|
+
```mermaid
|
|
295
|
+
sequenceDiagram
|
|
296
|
+
participant R as runtime
|
|
297
|
+
participant G as Game Definition
|
|
298
|
+
participant H as アプリのEffect handler
|
|
299
|
+
participant X as 外部サービス
|
|
300
|
+
R->>G: Commandを適用
|
|
301
|
+
G-->>R: 結果待ちの状態・Effect
|
|
302
|
+
R->>R: 状態を保存
|
|
303
|
+
R->>H: 外部Effectを渡す
|
|
304
|
+
H->>X: 外部処理を依頼
|
|
305
|
+
Note over R,G: 外部処理の完了前でも別のCommandを処理できる
|
|
306
|
+
X-->>H: 処理結果
|
|
307
|
+
H-->>R: 結果のSystem Commandを返す
|
|
308
|
+
R->>G: 最新状態で結果の有効性を検証
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
図は成功時の流れです。保存から外部処理・結果の反映までの配送を一体で保証するものではありません。
|
|
312
|
+
|
|
313
|
+
ゲームがEffectを宣言し、アプリが実行することで、ルールから外部APIのURL・認証・DB実装を切り離せます。
|
|
314
|
+
外部サービスを変更しても、CommandとEffectの意味が同じならゲームルールを保てます。
|
|
315
|
+
ルール単体のテストでは外部サービスを呼ばず、実際の接続は別の統合テストで確認できます。
|
|
316
|
+
|
|
317
|
+
ただし、外部Effectはbest effortです。状態保存後、handlerが起動する前に停止すれば依頼が失われる可能性があります。
|
|
318
|
+
外部処理の完了から結果Commandの保存までにも同様の隙間があります。
|
|
319
|
+
ライブラリは永続outboxや自動再試行を提供せず、Effect失敗で保存済み状態を取り消しません。
|
|
320
|
+
結果が必須の機能では、アプリ側の復旧方法やゲーム側のtimeoutを設計してください。
|
|
321
|
+
|
|
322
|
+
### 保存・Alarm・配信の手順を共通化する
|
|
323
|
+
|
|
324
|
+
```mermaid
|
|
325
|
+
flowchart TB
|
|
326
|
+
Input["プレイヤーの操作"] --> G["handleCommand"]
|
|
327
|
+
G --> Wait["安定した入力待ち状態<br/>誰の入力・どのdecision IDを待つか"]
|
|
328
|
+
G --> Effect["Alarm用Effect<br/>decision ID・期限"]
|
|
329
|
+
subgraph TX["同じstorage transactionで確定"]
|
|
330
|
+
State["入力待ち状態を保存"]
|
|
331
|
+
Reservation["予約情報とCloudflare Alarmを設定"]
|
|
332
|
+
end
|
|
333
|
+
Wait --> State
|
|
334
|
+
Effect -->|adapterで識別| Reservation
|
|
335
|
+
TX --> Saved["保存確定"]
|
|
336
|
+
Saved --> View["View配信"]
|
|
337
|
+
Saved --> Waiting["次の入力を待つ"]
|
|
338
|
+
Waiting -->|期限前の操作| Next["Actor Command"]
|
|
339
|
+
Waiting -->|Cloudflare Alarm発火| Timeout["System Command<br/>decision IDを付ける"]
|
|
340
|
+
Next --> Check["同じhandleCommand<br/>現在の入力待ちに有効か検証"]
|
|
341
|
+
Timeout --> Check
|
|
342
|
+
Check -->|有効| Transition["次の安定状態へ<br/>必要なら予約を取消・更新"]
|
|
343
|
+
Check -->|古い入力等| Reject["現在状態を進めない"]
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
手番や選択待ちに期限を付けるとき、ゲームは「期限に達したら何をするか」を実装します。
|
|
347
|
+
予約の保存とCloudflareネイティブのAlarmへの接続はruntimeに任せられます。
|
|
348
|
+
タイムアウトは任意の機能ですが、プレイヤーの離席や切断で進行が止まるゲームに役立ちます。
|
|
349
|
+
図のtransactionは保存処理だけを囲んでいます。将来の発火・Command実行や、取り消せないView配信は含みません。
|
|
350
|
+
|
|
351
|
+
複数の入力が届いても、runtimeが状態取得から更新を直列化し、状態とAlarm予約を同じtransactionで保存します。
|
|
352
|
+
これにより、ゲームごとに同じ排他制御や保存手順を実装する負担を減らします。
|
|
353
|
+
保存が確定してからViewを配信し、外部Effectは直列化区間を出て実行します。
|
|
354
|
+
外部処理を待っている間にも次のCommandを処理し、結果が戻れば最新状態に対して検証します。
|
|
355
|
+
|
|
356
|
+
Alarmの予約が状態と一緒に確定することと、将来のAlarm発火が一度だけ処理されることは別です。
|
|
357
|
+
ゲーム側でもdecision IDと現在状態を確認し、古い入力を適用しないようにします。
|
|
358
|
+
|
|
359
|
+
### ゲームの参加状態を、通信の寿命から切り離す
|
|
360
|
+
|
|
361
|
+
```mermaid
|
|
362
|
+
sequenceDiagram
|
|
363
|
+
participant C as クライアント
|
|
364
|
+
participant W as アプリWorker
|
|
365
|
+
participant R as runtime・WS管理
|
|
366
|
+
participant S as 永続State
|
|
367
|
+
C->>W: JoinのHTTP操作
|
|
368
|
+
W->>R: 認証済みActorのJoin Command
|
|
369
|
+
R->>S: ゲームが認めた参加状態を保存
|
|
370
|
+
R-->>C: Worker経由で成功応答
|
|
371
|
+
C->>W: WS接続開始
|
|
372
|
+
W->>R: 認証済みActorで接続
|
|
373
|
+
R->>S: 現在状態を取得・接続可否を確認
|
|
374
|
+
R-->>C: 最新View
|
|
375
|
+
Note over C,R: 切断・リロード(参加状態は残る)
|
|
376
|
+
C->>W: 改めて認証して再接続
|
|
377
|
+
W->>R: 同じActorの新しい接続
|
|
378
|
+
R->>S: 現在状態を取得・接続可否を確認
|
|
379
|
+
R-->>C: 最新Viewで画面を復元
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
この図は切断後の再接続です。HibernationだけならWS接続を維持したままDOが休止・復帰するため、別の流れです。
|
|
383
|
+
|
|
384
|
+
接続が切れたことだけでゲームの参加状態を失う必要はありません。
|
|
385
|
+
runtimeはゲーム状態をstorageに保存し、WS接続とActorの対応を別に管理します。
|
|
386
|
+
標準WSはHibernation APIを使い、同じActorの複数接続に対応します。再接続時は最新Viewを送り、切断中のイベント再生には依存しません。
|
|
387
|
+
|
|
388
|
+
本人確認は接続開始時にアプリが行います。接続中は各メッセージで認証トークンを再検証せず、再接続時に改めて認証します。
|
|
389
|
+
ゲーム上の参加資格や操作可否は、それとは別に最新Stateで判断します。
|
|
390
|
+
通信方式全体の差し替えや、トークン失効に連動した自動切断は標準runtimeの提供範囲に含みません。
|
|
391
|
+
|
|
392
|
+
### ゲーム固有の選択を、アプリに残す
|
|
393
|
+
|
|
394
|
+
```mermaid
|
|
395
|
+
flowchart LR
|
|
396
|
+
subgraph App["アプリごとに選ぶ"]
|
|
397
|
+
Rules["参加条件・所有者・手番・View"]
|
|
398
|
+
Services["認証・HTTPルート・外部サービス"]
|
|
399
|
+
end
|
|
400
|
+
Rules -->|Game Definition| Contract["共通の接続契約<br/>Command・Effect・adapter"]
|
|
401
|
+
Services -->|検証済み入力・外部処理| Contract
|
|
402
|
+
Contract --> Runtime["共通runtime<br/>実行・保存・通信・Alarm"]
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
ライブラリは所有者モデル、参加条件、手番の表現、公開URL、外部ドメインの保存方式を決めません。
|
|
406
|
+
生成例では推測困難なルームIDを共有してアクセスし、Actorは別途認証する形を採っていますが、公開入口の設計はアプリで変更できます。
|
|
407
|
+
認証や外部処理を変更するときも、ゲームには検証済みの実行元とCommandを渡す契約を保ってください。
|
|
408
|
+
|
|
409
|
+
DefGameが揃えるのは、状態遷移と外部処理の境界です。
|
|
410
|
+
その境界の内側でどんなゲームを作るか、外側でどんなサービスと組み合わせるかは、あなたが決めます。
|