def-game 4.0.0 → 5.0.1
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 +60 -200
- package/bin/def-game.cjs +131 -0
- package/dist/game-definition.d.ts +43 -0
- package/dist/game-definition.d.ts.map +1 -0
- package/dist/{types.js → game-definition.js} +0 -1
- package/dist/game-simulator.d.ts +19 -0
- package/dist/game-simulator.d.ts.map +1 -0
- package/dist/game-simulator.js +32 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/package.json +19 -5
- package/templates/worker/auth.ts +23 -0
- package/templates/worker/env.ts +9 -0
- package/templates/worker/index.ts +84 -0
- package/templates/worker/runtime/game-adapter.ts +15 -0
- package/templates/worker/runtime/parse.ts +14 -0
- package/templates/worker/session.ts +100 -0
- package/dist/example.d.ts +0 -2
- package/dist/example.d.ts.map +0 -1
- package/dist/example.js +0 -104
- package/dist/helper.d.ts +0 -8
- package/dist/helper.d.ts.map +0 -1
- package/dist/helper.js +0 -49
- package/dist/simulator.d.ts +0 -13
- package/dist/simulator.d.ts.map +0 -1
- package/dist/simulator.js +0 -63
- package/dist/types.d.ts +0 -123
- package/dist/types.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,236 +1,96 @@
|
|
|
1
|
-
# def-game
|
|
2
|
-
- a framework for creating online board games
|
|
1
|
+
# def-game V5
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
ターン制ゲームのルールを通信・永続化から独立させる、TypeScript の最小 contract とメモリ上の simulator です。
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
このフレームワークを使って、ゲームルールを定義した共通モジュールを作成する。
|
|
10
|
-
その共通モジュールはサーバーサイドプロジェクトとクライアントサイドプロジェクトからimportすることを前提とする。
|
|
11
|
-
開発者は、サーバーサイド側のコードを修正する必要がない。
|
|
12
|
-
共通モジュールで定義したゲームロジックがそのまま動くようになっているからである。
|
|
13
|
-
また、サーバークライアント間での型共有が容易になるのも利点の一つである。
|
|
14
|
-
|
|
15
|
-
ここからは、このフレームワークが要求するデータ型や関数についての説明を行う。
|
|
16
|
-
`固有の用語`と`データ型`については、インラインコードブロックで示す。
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# 設計 | design
|
|
20
|
-
|
|
21
|
-
## アウトライン
|
|
22
|
-
このフレームワークが要求する関数のすべては、`GameRule`に定義されている。
|
|
23
|
-
```ts
|
|
24
|
-
export interface GameRule<T extends Map, S extends Map, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> {
|
|
25
|
-
initialGameLogicState: (staticValues: any) => L;
|
|
26
|
-
createRoom: (state: GameState<T, S, C, L, P>, param: GameParameters) => GameState<T, S, C, L, P>;
|
|
27
|
-
onStartGame: (state: GameState<T, S, C, L, P>, joiner: Player) => GameState<T, S, C, L, P>;
|
|
28
|
-
generateTasks(state: GameState<T, S, C, L, P>, event: ToServer<C>): Task<C, T>[];
|
|
29
|
-
prioritizeTasks(newTasks: Task<C, T>[], gameState: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
|
|
30
|
-
doTasks(state: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### initialGameLogicState
|
|
35
|
-
`initialGameLogicState: (staticValues: any) => L;`
|
|
36
|
-
`ゲームロジック状態`のデフォルトを作成する関数である。
|
|
37
|
-
これはプレイヤーが参加する前に仮に作成される`ゲーム状態`である。
|
|
38
|
-
引数の`staticValues`は、静的な設定値のオブジェクトである。これはプログラム上にハードコードされたものである。
|
|
39
|
-
|
|
40
|
-
ここで重要なのは、できる限り関数(計算)の中で`staticValues`から他の値を算出することである。
|
|
41
|
-
これは、ランダムマップの生成や、ゲーム内の計算で使用される値などを算出することを念頭においている。
|
|
42
|
-
逆に`initialGameLogicState`の中でのハードコードは避け、`staticValues`にすべてを集約することも大切である。
|
|
43
|
-
|
|
44
|
-
このようにして`ゲームロジック状態`のデフォルト`L`が作成される。これは`ルーム`が作成される前の段階である。
|
|
45
|
-
|
|
46
|
-
### createRoom
|
|
47
|
-
`createRoom: (state: GameState<T, S, C, L, P>, param: GameParameters) => GameState<T, S, C, L, P>;`
|
|
48
|
-
この関数は、`ルーム`が作成されるタイミングで実行される関数である。
|
|
49
|
-
開発者は、`GameParameters`を設定することができる。
|
|
50
|
-
これは`ルーム`を作成する段階で、入力可能な設定値のことである。画面から入力されることを想定している。
|
|
51
|
-
|
|
52
|
-
例えば、プレイヤーの人数や制限時間、ゲームに固有なパラメータなどなんでも`ゲームロジック状態`に設定できる。
|
|
53
|
-
そして、この関数は、`GameState`が持つ`GameParameters`を更新することを行う。
|
|
54
|
-
これは、プレイヤーの待ち合わせ処理などの`ゲームロジック状態`とは分離して処理を行う場合を想定しており、`GameParameters`はそこで使用される。
|
|
55
|
-
|
|
56
|
-
それに加えて、必要であれば、`ゲームロジック状態`に含まれる設定値などもこのタイミングで更新を行う。
|
|
57
|
-
それらを全ての更新を行なった`GameState<T, S, C, L, P>`を返す。
|
|
58
|
-
|
|
59
|
-
この段階で、`ルーム`に参加しているプレイヤーに対しては、UIを提供する必要があり、
|
|
60
|
-
UIに関する情報は、`ゲームロジック状態`の中に保持することを推奨している。
|
|
61
|
-
|
|
62
|
-
### onStartGame
|
|
63
|
-
`onStartGame: (state: GameState<T, S, C, L, P>, joiner: Player) => GameState<T, S, C, L, P>;`
|
|
64
|
-
これは、プレイヤーの参加ごとに呼び出される関数である。
|
|
65
|
-
プレイヤーの待ち合わせ処理を実装する。
|
|
66
|
-
参加したプレイヤーに対しては適切なUI情報を返す。
|
|
67
|
-
この関数が実行されている段階ですでにWebSocket接続が確立されている。
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
### generateTasks | prioritizeTasks | doTasks
|
|
71
|
-
この3つの関数は、ゲームの進行を定義するのに重要な関数となる。
|
|
72
|
-
サーバーサイドプロジェクトにデフォルトで実装されている処理フローは、`src/simulatir.ts`で確認することができる。
|
|
73
|
-
以下の部分である。
|
|
74
|
-
```ts
|
|
75
|
-
// ------ Game Logic ------
|
|
76
|
-
const tasks = this.rules.generateTasks(state, action);
|
|
77
|
-
const updatedQueueState = this.rules.prioritizeTasks(tasks, state);
|
|
78
|
-
const newState = this.rules.doTasks(updatedQueueState);
|
|
5
|
+
```sh
|
|
6
|
+
npm install def-game@^5
|
|
79
7
|
```
|
|
80
8
|
|
|
81
|
-
|
|
82
|
-
`ゲーム状態`は`タスクキュー`をもつ。
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
1. `generateTasks(state: GameState<T, S, C, L, P>, event: ToServer<C>): Task<C, T>[];`
|
|
86
|
-
|
|
87
|
-
プレイヤーからの`イベント`を受け取った時に、実行されるのがこの関数である。
|
|
88
|
-
この関数では`ゲーム状態`を更新しない。新しく発生する`タスク`の配列を返す。
|
|
89
|
-
|
|
90
|
-
この関数では、`イベント`を受け取った際のバリデーションを実装することができる。
|
|
91
|
-
|
|
92
|
-
2. `prioritizeTasks(newTasks: Task<C, T>[], gameState: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;`
|
|
93
|
-
|
|
94
|
-
この関数では、`ゲーム状態`が持つ`タスクキュー`と`generateTasks`により生成された新しい`タスク`の配列をマージする処理を実装する。
|
|
95
|
-
`タスク`の優先度に応じて並び替える処理を実装できる。
|
|
96
|
-
|
|
97
|
-
3. `doTasks(state: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;`
|
|
98
|
-
|
|
99
|
-
この関数は、`タスクキュー`をループして、一つ一つのタスクを実行する処理を記述する。
|
|
100
|
-
ここで重要なのは、一つのタスクが`タスクリザルト`を返すことである。
|
|
9
|
+
## 公開 API
|
|
101
10
|
|
|
102
|
-
|
|
103
|
-
|
|
11
|
+
- `GameDefinition`: 初期状態の生成、コマンドによる状態遷移、Actor 別の projection。
|
|
12
|
+
- `TransitionResult`: 成功時の次状態と Effect、または失敗理由。
|
|
13
|
+
- `CommandContext`: Actor と system の実行元の区別。
|
|
14
|
+
- `GameSimulator`: 同じゲーム定義へコマンドを順に流し、状態・View・Effect を検証する実行器。
|
|
104
15
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
- task1はユーザーBからの応答を要請する
|
|
108
|
-
- タスクの実行を一時中断: (task2, task3, task4)
|
|
109
|
-
- ユーザーB: request
|
|
110
|
-
- タスク生成: (task1B)
|
|
111
|
-
- 優先度による並び替え: (task1B, task2, task3, task4)
|
|
112
|
-
- タスクキューの実行再開
|
|
16
|
+
型の詳細と契約は [game-definition.ts](src/game-definition.ts)、実行器は [game-simulator.ts](src/game-simulator.ts) を参照してください。
|
|
17
|
+
[シナリオテスト](test/game-simulator.test.cjs) に、2人の入力待ちと system timeout を扱う実行例があります。
|
|
113
18
|
|
|
114
|
-
|
|
115
|
-
この例でいえば、"task1はユーザーBからの応答を要請する"際に、`タスクリザルト`は、タスクの実行を一時中断するだけでなく、次に期待されるタスクを"task1B"に設定する。
|
|
116
|
-
これにより、再開時に、"task1B"以外の実行をキャンセルする。
|
|
19
|
+
## 責務
|
|
117
20
|
|
|
21
|
+
各ゲームは GameState と Command を定義し、1コマンドで次の外部入力を待てる安定状態まで遷移させます。入力状態は変更せず、失敗時はエラーだけを返します。現在の入力待ちはゲーム自身の状態として表現します。
|
|
118
22
|
|
|
119
|
-
|
|
120
|
-
`privaten(destinationId: PlayerId, l: L): L`
|
|
23
|
+
接続、認証、永続化、時刻の取得、Alarm、外部 I/O は実行側の責務です。実行側が検証済みの Actor または system を context に指定します。リクエストに書かれた実行元をそのまま信用してはいけません。ゲーム側でも実行元と現在の状態に対してコマンドを検証します。
|
|
121
24
|
|
|
122
|
-
|
|
123
|
-
基本的には、`ゲームロジック状態`が更新されるたびに、`ゲームロジック状態`をシリアライズしたものをクライアント側に通知する。
|
|
124
|
-
クライアントから`ゲームロジック状態`の一部を隠さなければならない状況に対処するため、この関数を実装する。
|
|
25
|
+
Effect は副作用の宣言です。本番の実行側は次状態を保存してから Effect を実行します。simulator は Effect を実行せず、遷移結果に含めて返します。
|
|
125
26
|
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
async broadcast<T>(result: Game1State) {
|
|
129
|
-
Object.keys(result.gameLogicState.forClient).forEach(async to => {
|
|
130
|
-
await this.sendTo(to, JSON.stringify(gameRule.privaten(to, result.gameLogicState)));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
```
|
|
27
|
+
projection は秘密情報を除いた Actor 別の View と availableActions を生成します。availableActions は表示補助であり、コマンド実行時の検証を置き換えません。
|
|
134
28
|
|
|
29
|
+
simulator の getState は検証用の全状態への参照です。呼び出し側も状態を直接変更しないでください。deep clone、freeze、失敗時の rollback は行いません。クライアントへ公開する情報には getView を使用します。
|
|
135
30
|
|
|
136
|
-
##
|
|
31
|
+
## V4 からの移行
|
|
137
32
|
|
|
138
|
-
|
|
33
|
+
V5 は破壊的変更です。GameRule、GameEngine、TaskQueue に関する型、旧通信型と helper は削除しています。V4 を利用する既存ゲームは移植が完了するまで V4 を使用してください。
|
|
139
34
|
|
|
140
|
-
|
|
141
|
-
|
|
35
|
+
- 旧ライフサイクルを GameDefinition の初期化・コマンド処理・projection に移します。
|
|
36
|
+
- task queue に未来の処理を保存する代わりに、ゲーム固有の状態で現在の入力待ちを表現します。
|
|
37
|
+
- 参加・開始などもゲームの Command として扱います。
|
|
38
|
+
- 旧 GameEngine の代わりに GameSimulator でコマンド列を実行します。
|
|
142
39
|
|
|
143
|
-
|
|
144
|
-
これは、`タスク`を定義するための`Map`である。
|
|
40
|
+
## Worker generator(5.0.1)
|
|
145
41
|
|
|
146
|
-
|
|
147
|
-
interface Tasks {
|
|
148
|
-
"sample": SampleTask
|
|
149
|
-
"update": UpdateTask
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
と言った具体である。
|
|
153
|
-
タスクハンドラーは、`ゲームロジック状態`とこのタスク(例えば`SampleTask`型のデータ)を受け取り、`ゲームロジック状態`を更新する処理を行う。
|
|
42
|
+
Selfish で検証した Hono Worker、SQLite-backed Durable Object、waki.work JWT 認証、Hibernation WebSocket、保存・配信処理を生成します。GameDefinition の契約変更はありません。現在は timeout / Effect 実行なしの構成が対象です。
|
|
154
43
|
|
|
155
|
-
|
|
156
|
-
これは、サーバーサイドから返却されるUI状態を定義するための`Map`である。
|
|
44
|
+
server package に `def-game.worker.json` を作ります。全パスはこの設定ファイルのディレクトリ基準です。
|
|
157
45
|
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"outputDir": "src/worker",
|
|
49
|
+
"adapter": "src/worker/game-adapter.ts",
|
|
50
|
+
"entry": "src/index.ts",
|
|
51
|
+
"wranglerConfig": "wrangler.jsonc",
|
|
52
|
+
"name": "my-game",
|
|
53
|
+
"assets": "../client/public",
|
|
54
|
+
"compatibilityDate": "2026-09-12"
|
|
162
55
|
}
|
|
163
56
|
```
|
|
164
57
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
これは、クライアントサイドからイベントは発生させるためのリクエストを定義するための`Map`である。
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
interface Req {
|
|
174
|
-
"propose": Custom<ProposeReq>
|
|
175
|
-
"accept": Simple<AcceptReq>
|
|
176
|
-
}
|
|
58
|
+
```sh
|
|
59
|
+
npm install --save-dev def-game@5.0.1 wrangler @cloudflare/workers-types
|
|
60
|
+
npm install hono jose
|
|
61
|
+
npx def-game generate-worker --config def-game.worker.json
|
|
62
|
+
npx def-game generate-worker --config def-game.worker.json --check
|
|
177
63
|
```
|
|
178
|
-
`ReqMap`は、値の型として、`Custom`か`Simple`のいずれかでラップすることを要請する。
|
|
179
|
-
これはサーバーサイド側での実装を簡略化するために導入された型である。
|
|
180
|
-
|
|
181
|
-
なんらかの`Req`を受け取ってから`Task[]`に変換する処理はしばしば冗長である。
|
|
182
|
-
そこでリクエストペイロードをそのまま`タスク`として定義することができるようになっている。
|
|
183
|
-
この例では、`Simple<AcceptReq>`のように`Simple`によってラップされたペイロード(`AcceptReq`)は、そのままタスクとしてキューに追加される。
|
|
184
|
-
|
|
185
|
-
この仕組みは、以下の型定義により、型的にもサポートされている。
|
|
186
|
-
```ts
|
|
187
|
-
export type TaskMapType<A extends ReqMap, B extends Map> = Merge<SimpleOnlyPayload<A>, CustomTaskMap<B>>;
|
|
188
|
-
export type Task<A extends ReqMap, B extends Map> = {
|
|
189
|
-
[K in keyof TaskMapType<A, B>]: {
|
|
190
|
-
type: K;
|
|
191
|
-
payload: TaskMapType<A, B>[K];
|
|
192
|
-
}
|
|
193
|
-
}[keyof TaskMapType<A, B>];
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
64
|
|
|
65
|
+
生成対象は `outputDir` 内の `index.ts`、`session.ts`、`auth.ts`、`env.ts`、`runtime/parse.ts`、`runtime/game-adapter.ts` と、`entry`、`wranglerConfig` の計8ファイルです。Hono / jose は利用側の runtime dependency で、generator 自体に実行時依存はありません。Selfish は Hono 4.13.7、jose 6.2.12、Wrangler 4.131.1 で検証しています。
|
|
199
66
|
|
|
67
|
+
ゲーム側は既存ファイルとして `adapter` を用意し、次を export します。このファイル、domain、shared は生成・上書きしません。
|
|
200
68
|
|
|
201
|
-
|
|
69
|
+
- `gameAdapter`: 生成される `GameAdapter<State, Command, View, Error>` に適合するオブジェクト。
|
|
70
|
+
- `gameAdapter.game`: ゲーム自身の GameDefinition(Effect は `never`)。
|
|
71
|
+
- `parseCreate(input)` / `parseJoin(input)` / `parseCommand(input)`: 未検証入力から command を返す関数。形式不正なら null。WS の関数へ渡るのは envelope 内の command だけです。
|
|
72
|
+
- `canConnect(state, actorId)`: 接続・配信を許可するかの判定。command の認可は GameDefinition が再検証します。
|
|
73
|
+
- 型の export: `State`、`ActorId`(string)、`ServerMessage`、`ProtocolError`、`PublicError`、`CreateSessionResponse`、`JoinSessionResponse`。
|
|
202
74
|
|
|
203
|
-
|
|
75
|
+
公開通信は `GameCommandRequest { requestId, command }`、`GameCommandResponse { requestId, ok, error? }`、`ViewStateEvent { viewState }`、`ProtocolErrorEvent { error }` の type で区別します。HTTP 応答は `CreateSessionResponse` / `JoinSessionResponse` の type と、成功時 `ok: true, sessionId`、失敗時 `ok: false, error` を持ちます。ProtocolError の code は `AuthenticationRequired` / `SessionNotFound` / `NotSessionMember` / `InvalidRequest` / `InternalError`。PublicError はそれとゲームエラーの union です。
|
|
204
76
|
|
|
205
|
-
|
|
206
|
-
- このタイミングでroomKeyと呼ばれるJWTを返す。
|
|
207
|
-
`{ playerId: playerId, roomId: body.roomId }`
|
|
208
|
-
- クライアントは、そのJWTをWebSocket接続時のパラメータに加える。
|
|
209
|
-
- これにより、接続とplayerIdが関連づけられる。
|
|
210
|
-
- WebSocket接続を通じて送られる全てのJSONデータはplayerIdプロパティを持っている。
|
|
211
|
-
- サーバーサイドは、そのplayerIdプロパティがWebSocket接続に紐づいたplayerIdと一致することを確認する。
|
|
77
|
+
認証は Cookie `__token` を JWKS / ES256 / issuer / audience / exp で検証し、UUID の sub を ActorId として使います。Static Assets も認証必須。ログイン誘導・refresh は client の責務です。
|
|
212
78
|
|
|
79
|
+
出力が同一なら変更しません。既存ファイルと差分がある場合は書き込み前に停止します。確認後に `--force` を付けると生成対象8ファイルだけを更新します。`--check` は欠落・差分を非ゼロ終了で報告し、ファイルを変更しません。生成ファイルは Git 管理し、修正は generator / 設定側へ戻してください。Wrangler 設定も全体が生成対象なので、独自 bindings や migrations の追加には generator の対応が必要です。
|
|
213
80
|
|
|
214
|
-
|
|
81
|
+
CLI は dependencies、package scripts、tsconfig を書き換えません。利用側で Worker の型と Hono / jose を設定し、生成された entry を Wrangler から実行します。CLI には Node.js 20 以降が必要です。
|
|
215
82
|
|
|
216
|
-
|
|
217
|
-
これは、要求されたアクションの時間制限に関する確認処理をトリガーする。
|
|
83
|
+
## 開発と公開
|
|
218
84
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
defaultTask: {
|
|
225
|
-
playerId: "samplePlayer",
|
|
226
|
-
proposerId: "proposer",
|
|
227
|
-
isAccept: false,
|
|
228
|
-
}
|
|
229
|
-
}
|
|
85
|
+
```sh
|
|
86
|
+
npm ci
|
|
87
|
+
npm run typecheck
|
|
88
|
+
npm test
|
|
89
|
+
npm pack
|
|
230
90
|
```
|
|
231
|
-
deadlineを過ぎていれば、`defaultTask`が`タスクキュー`の先頭に配置され、実行が再開するという具体である。
|
|
232
|
-
|
|
233
|
-
|
|
234
91
|
|
|
92
|
+
開発時のテストには Node.js 20 以降を使用してください。npm test はビルドと Node 標準のシナリオテストを実行します。
|
|
235
93
|
|
|
94
|
+
npm pack / npm publish の前に prepack で型チェック・ビルド・テストが実行されます。build は dist を作り直すため、削除した旧 API の生成物が混入しません。公開対象は dist、bin、templates、package.json、README、LICENSE です。
|
|
236
95
|
|
|
96
|
+
main にレビュー済みの変更を反映した後、公開する version と認証アカウントを確認し、検証済みのパッケージを npm publish で公開します。
|
package/bin/def-game.cjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { version } = require('../package.json');
|
|
7
|
+
|
|
8
|
+
const help = `def-game ${version}
|
|
9
|
+
Usage: def-game generate-worker --config <file.json> [--force | --check]
|
|
10
|
+
|
|
11
|
+
Generate a Hono Worker, SQLite Durable Object, waki.work authentication and Wrangler config.
|
|
12
|
+
--force Replace differing output files (never edits the game adapter).
|
|
13
|
+
--check Verify generated files match without writing anything.
|
|
14
|
+
Node.js 20+ is required. Timeout and Effect execution are not included in this version.`;
|
|
15
|
+
|
|
16
|
+
function relativeImport(from, target) {
|
|
17
|
+
let value = path.relative(path.dirname(from), target).split(path.sep).join('/').replace(/\.ts$/, '.js');
|
|
18
|
+
if (!value.startsWith('.')) value = './' + value;
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function main(args) {
|
|
23
|
+
if (args.length === 0 || args[0] === '--help') return console.log(help);
|
|
24
|
+
if (args[0] === '--version') return console.log(version);
|
|
25
|
+
if (Number(process.versions.node.split('.')[0]) < 20) throw new Error('Node.js 20+ is required');
|
|
26
|
+
if (args.shift() !== 'generate-worker') throw new Error('Unknown command. Use --help.');
|
|
27
|
+
let configPath;
|
|
28
|
+
let force = false;
|
|
29
|
+
let check = false;
|
|
30
|
+
while (args.length) {
|
|
31
|
+
const arg = args.shift();
|
|
32
|
+
if (arg === '--config' && !configPath && args[0] && !args[0].startsWith('--')) configPath = args.shift();
|
|
33
|
+
else if (arg === '--force' && !force) force = true;
|
|
34
|
+
else if (arg === '--check' && !check) check = true;
|
|
35
|
+
else throw new Error(`Unknown or repeated option: ${arg}`);
|
|
36
|
+
}
|
|
37
|
+
if (!configPath || (force && check)) throw new Error('Provide --config and at most one of --force / --check');
|
|
38
|
+
configPath = path.resolve(configPath);
|
|
39
|
+
const root = path.dirname(configPath);
|
|
40
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
41
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) throw new Error('Config must be an object');
|
|
42
|
+
const allowed = new Set(['outputDir', 'adapter', 'entry', 'wranglerConfig', 'name', 'assets', 'compatibilityDate']);
|
|
43
|
+
for (const key of Object.keys(config)) if (!allowed.has(key)) throw new Error(`Unknown config key: ${key}`);
|
|
44
|
+
const required = (key) => {
|
|
45
|
+
if (typeof config[key] !== 'string' || !config[key].trim() || config[key].includes('\0')) throw new Error(`Invalid config: ${key}`);
|
|
46
|
+
return config[key];
|
|
47
|
+
};
|
|
48
|
+
const inside = (value) => {
|
|
49
|
+
if (path.isAbsolute(value)) throw new Error(`Path must be relative: ${value}`);
|
|
50
|
+
const resolved = path.resolve(root, value);
|
|
51
|
+
const relative = path.relative(root, resolved);
|
|
52
|
+
if (!relative || relative === '..' || relative.startsWith('..' + path.sep)) throw new Error(`Path must stay inside config directory: ${value}`);
|
|
53
|
+
// Do not follow existing symlinks when overwriting output or locating the adapter.
|
|
54
|
+
let current = root;
|
|
55
|
+
for (const part of relative.split(path.sep)) {
|
|
56
|
+
current = path.join(current, part);
|
|
57
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error(`Symlink is not supported: ${current}`);
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
};
|
|
61
|
+
const outputDir = inside(required('outputDir'));
|
|
62
|
+
const adapter = inside(required('adapter'));
|
|
63
|
+
if (!/\.(ts|js)$/.test(adapter) || !fs.statSync(adapter).isFile()) throw new Error('adapter must be an existing .ts or .js file');
|
|
64
|
+
const entry = inside(required('entry'));
|
|
65
|
+
if (!entry.endsWith('.ts')) throw new Error('entry must end with .ts');
|
|
66
|
+
const wrangler = inside(required('wranglerConfig'));
|
|
67
|
+
if (!wrangler.endsWith('.jsonc')) throw new Error('wranglerConfig must end with .jsonc');
|
|
68
|
+
const name = required('name');
|
|
69
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) throw new Error('Invalid Worker name');
|
|
70
|
+
const assets = required('assets');
|
|
71
|
+
if (path.isAbsolute(assets)) throw new Error('assets must be relative to the config directory');
|
|
72
|
+
const date = required('compatibilityDate');
|
|
73
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(date)) || new Date(date).toISOString().slice(0, 10) !== date) {
|
|
74
|
+
throw new Error('Invalid compatibilityDate');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const banner = `// Generated by def-game ${version}. Do not edit; change the generator or its config.\n`;
|
|
78
|
+
const files = new Map();
|
|
79
|
+
const add = (file, text) => {
|
|
80
|
+
if (files.has(file) || file === adapter || file === configPath) throw new Error(`Output path collision: ${file}`);
|
|
81
|
+
inside(path.relative(root, file));
|
|
82
|
+
files.set(file, banner + text);
|
|
83
|
+
};
|
|
84
|
+
for (const file of ['index.ts', 'session.ts', 'auth.ts', 'env.ts', 'runtime/parse.ts', 'runtime/game-adapter.ts']) {
|
|
85
|
+
const target = path.join(outputDir, file);
|
|
86
|
+
const source = fs.readFileSync(path.join(__dirname, '../templates/worker', file), 'utf8');
|
|
87
|
+
add(target, source.replaceAll('__ADAPTER_IMPORT__', relativeImport(target, adapter)));
|
|
88
|
+
}
|
|
89
|
+
add(entry, `export { default, SessionDurableObject } from ${relativeImport(entry, path.join(outputDir, 'index.ts'))};\n`);
|
|
90
|
+
const fromWrangler = (target) => path.relative(path.dirname(wrangler), target).split(path.sep).join('/');
|
|
91
|
+
add(wrangler, JSON.stringify({
|
|
92
|
+
name,
|
|
93
|
+
main: fromWrangler(entry),
|
|
94
|
+
compatibility_date: date,
|
|
95
|
+
assets: { directory: fromWrangler(path.resolve(root, assets)), binding: 'ASSETS', run_worker_first: true },
|
|
96
|
+
durable_objects: { bindings: [{ name: 'SESSIONS', class_name: 'SessionDurableObject' }] },
|
|
97
|
+
migrations: [{ tag: 'v1', new_sqlite_classes: ['SessionDurableObject'] }],
|
|
98
|
+
vars: {
|
|
99
|
+
AUTH_ISSUER: 'https://auth.waki.work', AUTH_AUDIENCE: 'waki.work',
|
|
100
|
+
AUTH_JWKS_URL: 'https://auth.waki.work/.well-known/jwks.json',
|
|
101
|
+
},
|
|
102
|
+
}, null, 2) + '\n');
|
|
103
|
+
|
|
104
|
+
// Preflight every output before the first write, so conflicts cannot leave half-generated code.
|
|
105
|
+
const changed = [];
|
|
106
|
+
for (const [file, contents] of files) {
|
|
107
|
+
const exists = fs.existsSync(file);
|
|
108
|
+
if (exists && !fs.statSync(file).isFile()) throw new Error(`Output is not a file: ${file}`);
|
|
109
|
+
if (exists && fs.readFileSync(file, 'utf8') === contents) continue;
|
|
110
|
+
if (exists && !force && !check) throw new Error(`Refusing to overwrite ${file}; review the changes and use --force`);
|
|
111
|
+
changed.push([file, contents]);
|
|
112
|
+
}
|
|
113
|
+
if (check) {
|
|
114
|
+
if (changed.length) throw new Error(`Generated files differ or are missing:\n${changed.map(([file]) => path.relative(root, file)).join('\n')}`);
|
|
115
|
+
return console.log(`Verified ${files.size} generated files (def-game ${version})`);
|
|
116
|
+
}
|
|
117
|
+
for (const [file, contents] of changed) {
|
|
118
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
119
|
+
const temp = file + `.def-game-${process.pid}.tmp`;
|
|
120
|
+
try {
|
|
121
|
+
fs.writeFileSync(temp, contents, { flag: 'wx' });
|
|
122
|
+
fs.renameSync(temp, file);
|
|
123
|
+
} finally {
|
|
124
|
+
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
console.log(`Generated ${files.size} files; changed ${changed.length} (def-game ${version})`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try { main(process.argv.slice(2)); }
|
|
131
|
+
catch (error) { console.error(`def-game: ${error.message}`); process.exitCode = 1; }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker や simulator が確定したコマンドの実行元。
|
|
3
|
+
* actorId は認証済みの主体を表し、接続 ID ではない。
|
|
4
|
+
* 外部リクエストの自己申告をそのまま使わず、system は信頼できる実行側だけが指定する。
|
|
5
|
+
*/
|
|
6
|
+
export type CommandContext<ActorId> = {
|
|
7
|
+
readonly origin: "actor";
|
|
8
|
+
readonly actorId: ActorId;
|
|
9
|
+
} | {
|
|
10
|
+
readonly origin: "system";
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 成功時は次の安定状態と副作用の宣言、失敗時はゲーム上の拒否理由を返す。
|
|
14
|
+
* 実行側は成功した状態を保存してから Effect を処理する。
|
|
15
|
+
*/
|
|
16
|
+
export type TransitionResult<State, Effect, Error> = {
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
readonly state: State;
|
|
19
|
+
readonly effects: readonly Effect[];
|
|
20
|
+
} | {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly error: Error;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 通信・永続化から独立したゲームそのものの契約。
|
|
26
|
+
* State の構造や入力待ちの表現は各ゲームが定義し、kernel は task queue を要求しない。
|
|
27
|
+
*/
|
|
28
|
+
export interface GameDefinition<State, Command, ActorId, View, Effect, Error> {
|
|
29
|
+
/** Session や接続の情報に依存しない、ゲームの初期状態を生成する。 */
|
|
30
|
+
createInitialState(): State;
|
|
31
|
+
/**
|
|
32
|
+
* 実行元とコマンドを再検証し、次の外部入力を待てる安定状態まで遷移する。
|
|
33
|
+
* 入力 state を変更せず、拒否時は error のみを返す。外部 I/O は行わない。
|
|
34
|
+
* system 起点でも、そのコマンドが現在の状態で有効かを検証する。
|
|
35
|
+
*/
|
|
36
|
+
handleCommand(state: State, command: Command, context: CommandContext<ActorId>): TransitionResult<State, Effect, Error>;
|
|
37
|
+
/**
|
|
38
|
+
* state を変更せず、Actor に公開できる情報と availableActions を持つ View を生成する。
|
|
39
|
+
* availableActions は表示補助であり、コマンド実行時の検証を省略する根拠にはしない。
|
|
40
|
+
*/
|
|
41
|
+
project(state: State, actorId: ActorId): View;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=game-definition.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-definition.d.ts","sourceRoot":"","sources":["../src/game-definition.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,OAAO,IAC5B;IAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEpC;;;GAGG;AACH,MAAM,MAAM,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAC3C;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC,GACC;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpD;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;IACxE,0CAA0C;IAC1C,kBAAkB,IAAI,KAAK,CAAC;IAE5B;;;;OAIG;IACH,aAAa,CACT,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GACjC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAE1C;;;OAGG;IACH,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CACjD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CommandContext, GameDefinition, TransitionResult } from "./game-definition";
|
|
2
|
+
/**
|
|
3
|
+
* Worker なしで同じゲーム定義にコマンドを順番に流す実行器。
|
|
4
|
+
* 状態はメモリだけに保存し、Effect は実行せず検証用に返す。
|
|
5
|
+
* ゲーム定義と呼び出し側は、受け取った状態を直接変更しない契約に従う。
|
|
6
|
+
*/
|
|
7
|
+
export declare class GameSimulator<State, Command, ActorId, View, Effect, Error> {
|
|
8
|
+
private readonly definition;
|
|
9
|
+
private state;
|
|
10
|
+
/** ゲーム定義が生成した初期状態から、独立した試行を開始する。 */
|
|
11
|
+
constructor(definition: GameDefinition<State, Command, ActorId, View, Effect, Error>);
|
|
12
|
+
/** コマンドが成功した場合だけ次の状態を採用し、結果をそのまま返す。 */
|
|
13
|
+
executeCommand(command: Command, context: CommandContext<ActorId>): TransitionResult<State, Effect, Error>;
|
|
14
|
+
/** シナリオの検証用に全状態を参照する。クライアント配信用ではない。 */
|
|
15
|
+
getState(): State;
|
|
16
|
+
/** 現在の状態を指定 Actor 向けに投影する。 */
|
|
17
|
+
getView(actorId: ActorId): View;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=game-simulator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-simulator.d.ts","sourceRoot":"","sources":["../src/game-simulator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1F;;;;GAIG;AACH,qBAAa,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK;IAK/D,OAAO,CAAC,QAAQ,CAAC,UAAU;IAJ/B,OAAO,CAAC,KAAK,CAAQ;IAErB,oCAAoC;gBAEf,UAAU,EAAE,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;IAK7F,uCAAuC;IACvC,cAAc,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,GACjC,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC;IAQzC,uCAAuC;IACvC,QAAQ,IAAI,KAAK;IAIjB,8BAA8B;IAC9B,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;CAGlC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GameSimulator = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Worker なしで同じゲーム定義にコマンドを順番に流す実行器。
|
|
6
|
+
* 状態はメモリだけに保存し、Effect は実行せず検証用に返す。
|
|
7
|
+
* ゲーム定義と呼び出し側は、受け取った状態を直接変更しない契約に従う。
|
|
8
|
+
*/
|
|
9
|
+
class GameSimulator {
|
|
10
|
+
/** ゲーム定義が生成した初期状態から、独立した試行を開始する。 */
|
|
11
|
+
constructor(definition) {
|
|
12
|
+
this.definition = definition;
|
|
13
|
+
this.state = definition.createInitialState();
|
|
14
|
+
}
|
|
15
|
+
/** コマンドが成功した場合だけ次の状態を採用し、結果をそのまま返す。 */
|
|
16
|
+
executeCommand(command, context) {
|
|
17
|
+
const result = this.definition.handleCommand(this.state, command, context);
|
|
18
|
+
if (result.ok) {
|
|
19
|
+
this.state = result.state;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
/** シナリオの検証用に全状態を参照する。クライアント配信用ではない。 */
|
|
24
|
+
getState() {
|
|
25
|
+
return this.state;
|
|
26
|
+
}
|
|
27
|
+
/** 現在の状態を指定 Actor 向けに投影する。 */
|
|
28
|
+
getView(actorId) {
|
|
29
|
+
return this.definition.project(this.state, actorId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.GameSimulator = GameSimulator;
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./
|
|
18
|
-
__exportStar(require("./simulator"), exports);
|
|
19
|
-
__exportStar(require("./helper"), exports);
|
|
17
|
+
__exportStar(require("./game-definition"), exports);
|
|
18
|
+
__exportStar(require("./game-simulator"), exports);
|
package/package.json
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "def-game",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "5.0.1",
|
|
4
|
+
"description": "Game definition contracts and an in-memory simulator for turn-based games",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"
|
|
8
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"test": "npm run build && node --test test/*.test.cjs",
|
|
11
|
+
"prepack": "npm run typecheck && npm test"
|
|
10
12
|
},
|
|
11
13
|
"keywords": [],
|
|
12
14
|
"author": "",
|
|
13
15
|
"license": "ISC",
|
|
14
16
|
"devDependencies": {
|
|
15
17
|
"typescript": "^5.8.2"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"bin",
|
|
22
|
+
"templates"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/MFQWKMR4/def-game.git"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"def-game": "bin/def-game.cjs"
|
|
16
30
|
}
|
|
17
|
-
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
import type { Env } from "./env.js";
|
|
3
|
+
|
|
4
|
+
let cachedKeys: { url: string; keys: ReturnType<typeof createRemoteJWKSet> } | undefined;
|
|
5
|
+
|
|
6
|
+
/** JWT は Worker 境界だけで扱う。署名・必須 claims を検証した sub だけを DO へ渡す。 */
|
|
7
|
+
export async function authenticate(token: string | undefined, env: Env): Promise<string | null> {
|
|
8
|
+
if (!token) return null;
|
|
9
|
+
try {
|
|
10
|
+
if (cachedKeys?.url !== env.AUTH_JWKS_URL) {
|
|
11
|
+
// jose がキャッシュと未知 kid に対する再取得(cooldown 付き)を扱う。
|
|
12
|
+
cachedKeys = { url: env.AUTH_JWKS_URL, keys: createRemoteJWKSet(new URL(env.AUTH_JWKS_URL)) };
|
|
13
|
+
}
|
|
14
|
+
const { payload } = await jwtVerify(token, cachedKeys.keys, {
|
|
15
|
+
issuer: env.AUTH_ISSUER, audience: env.AUTH_AUDIENCE,
|
|
16
|
+
algorithms: ["ES256"], requiredClaims: ["sub", "exp", "iat"],
|
|
17
|
+
});
|
|
18
|
+
return typeof payload.sub === "string" && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(payload.sub)
|
|
19
|
+
? payload.sub : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Hono, type Context } from "hono";
|
|
2
|
+
import { getCookie } from "hono/cookie";
|
|
3
|
+
import type { ActorId, CreateSessionResponse, JoinSessionResponse, ProtocolError, PublicError } from __ADAPTER_IMPORT__;
|
|
4
|
+
import type { Env } from "./env.js";
|
|
5
|
+
import { authenticate } from "./auth.js";
|
|
6
|
+
export { SessionDurableObject } from "./session.js";
|
|
7
|
+
|
|
8
|
+
type AppEnv = { Bindings: Env; Variables: { actorId: ActorId } };
|
|
9
|
+
const app = new Hono<AppEnv>();
|
|
10
|
+
|
|
11
|
+
// Static Assets を含む全ルートで認証し、以後は検証済み ActorId を context から取得する。
|
|
12
|
+
app.use("*", async (c, next) => {
|
|
13
|
+
const actorId = await authenticate(getCookie(c, "__token"), c.env);
|
|
14
|
+
if (!actorId) return errorResponse(c, "AuthenticationRequired", 401);
|
|
15
|
+
c.set("actorId", actorId);
|
|
16
|
+
await next();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.use("*", async (c, next) => {
|
|
20
|
+
const origin = c.req.header("origin");
|
|
21
|
+
const isWebSocket = c.req.path.startsWith("/ws/");
|
|
22
|
+
if (origin && origin !== new URL(c.req.url).origin && (c.req.method !== "GET" || isWebSocket)) {
|
|
23
|
+
return errorResponse(c, "InvalidRequest", 403);
|
|
24
|
+
}
|
|
25
|
+
await next();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.post("/api/sessions", (c) => joinSession(c, c.env.SESSIONS.newUniqueId(), "create"));
|
|
29
|
+
|
|
30
|
+
app.post("/api/sessions/:sessionId/join", (c) => {
|
|
31
|
+
const sessionId = c.req.param("sessionId");
|
|
32
|
+
if (!/^[0-9a-f]{64}$/.test(sessionId)) return errorResponse(c, "InvalidRequest", 404);
|
|
33
|
+
return joinSession(c, c.env.SESSIONS.idFromString(sessionId), "join");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
app.get("/ws/:sessionId", (c) => {
|
|
37
|
+
const sessionId = c.req.param("sessionId");
|
|
38
|
+
if (!/^[0-9a-f]{64}$/.test(sessionId)) return errorResponse(c, "InvalidRequest", 404);
|
|
39
|
+
if (c.req.header("upgrade")?.toLowerCase() !== "websocket") return errorResponse(c, "InvalidRequest", 426);
|
|
40
|
+
const id = c.env.SESSIONS.idFromString(sessionId);
|
|
41
|
+
// DO の Hibernation 接続をそのまま返す。token や外部のヘッダーは転送しない。
|
|
42
|
+
return c.env.SESSIONS.get(id).fetch(new Request("https://session/connect", {
|
|
43
|
+
headers: { upgrade: "websocket", "x-actor-id": c.get("actorId") },
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.all("/api/*", (c) => errorResponse(c, "InvalidRequest", 404));
|
|
48
|
+
app.all("/ws/*", (c) => errorResponse(c, "InvalidRequest", 404));
|
|
49
|
+
app.all("*", (c) => c.env.ASSETS.fetch(c.req.raw));
|
|
50
|
+
|
|
51
|
+
app.onError((_error, c) => {
|
|
52
|
+
console.error("Worker request failed");
|
|
53
|
+
return errorResponse(c, "InternalError", 500);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export default app;
|
|
57
|
+
|
|
58
|
+
/** create と join に共通する本文検証と DO 呼び出し。本文のゲーム固有形式と参加可否は DO 内の adapter / domain が判断する。 */
|
|
59
|
+
async function joinSession(c: Context<AppEnv>, id: DurableObjectId, operation: "create" | "join"): Promise<Response> {
|
|
60
|
+
if (!c.req.header("content-type")?.startsWith("application/json")) return errorResponse(c, "InvalidRequest", 400);
|
|
61
|
+
const raw = await c.req.text();
|
|
62
|
+
if (raw.length > 64 * 1024) return errorResponse(c, "InvalidRequest", 400);
|
|
63
|
+
let body: unknown;
|
|
64
|
+
try { body = JSON.parse(raw); } catch { return errorResponse(c, "InvalidRequest", 400); }
|
|
65
|
+
|
|
66
|
+
const result = await c.env.SESSIONS.get(id).fetch(new Request(`https://session/${operation}`, {
|
|
67
|
+
method: "POST", headers: { "x-actor-id": c.get("actorId"), "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify(body),
|
|
69
|
+
}));
|
|
70
|
+
const data = await result.json<{ ok: true } | { ok: false; error: PublicError }>();
|
|
71
|
+
const type = operation === "create" ? "CreateSessionResponse" : "JoinSessionResponse";
|
|
72
|
+
const response: CreateSessionResponse | JoinSessionResponse = data.ok
|
|
73
|
+
? { type, ok: true, sessionId: id.toString() }
|
|
74
|
+
: { type, ok: false, error: data.error };
|
|
75
|
+
return Response.json(response, { status: result.status });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** middleware で拒否する場合も、既存の公開レスポンス形式を維持する。 */
|
|
79
|
+
function errorResponse(c: Context<AppEnv>, code: ProtocolError["code"], status: 400 | 401 | 403 | 404 | 426 | 500): Response {
|
|
80
|
+
const type = c.req.method === "POST" && c.req.path === "/api/sessions"
|
|
81
|
+
? "CreateSessionResponse"
|
|
82
|
+
: /^\/api\/sessions\/[0-9a-f]{64}\/join$/.test(c.req.path) ? "JoinSessionResponse" : undefined;
|
|
83
|
+
return c.json({ ...(type ? { type } : {}), ok: false, error: { code } }, status);
|
|
84
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GameDefinition } from "def-game";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 生成対象の runtime とゲーム固有コードの接続契約。GameDefinition 自体は変更しない。
|
|
5
|
+
* decoder は未検証の本文からゲーム command を作り、形式不正なら null を返す。
|
|
6
|
+
* 参加・復帰の意味や認可は game.handleCommand が判断する。
|
|
7
|
+
* 現段階は Effect なし。timeout / Effect の生成オプションは実装時に別途追加する。
|
|
8
|
+
*/
|
|
9
|
+
export interface GameAdapter<State, Command, View, Error> {
|
|
10
|
+
readonly game: GameDefinition<State, Command, string, View, never, Error>;
|
|
11
|
+
readonly parseCreate: (input: unknown) => Command | null;
|
|
12
|
+
readonly parseJoin: (input: unknown) => Command | null;
|
|
13
|
+
readonly parseCommand: (input: unknown) => Command | null;
|
|
14
|
+
readonly canConnect: (state: State, actorId: string) => boolean;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** 通信 envelope だけを検証する。command の名前・payload の意味は adapter が判断する。 */
|
|
6
|
+
export function parseCommandRequest(raw: string | ArrayBuffer): { requestId: string; command: unknown } | null {
|
|
7
|
+
if (typeof raw !== "string" || raw.length > 64 * 1024) return null;
|
|
8
|
+
try {
|
|
9
|
+
const value: unknown = JSON.parse(raw);
|
|
10
|
+
if (!isRecord(value) || value.type !== "GameCommandRequest" || typeof value.requestId !== "string"
|
|
11
|
+
|| value.requestId.length === 0 || value.requestId.length > 128 || !Object.hasOwn(value, "command")) return null;
|
|
12
|
+
return { requestId: value.requestId, command: value.command };
|
|
13
|
+
} catch { return null; }
|
|
14
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import { gameAdapter, type State, type ServerMessage, type ProtocolError } from __ADAPTER_IMPORT__;
|
|
3
|
+
import type { Env } from "./env.js";
|
|
4
|
+
import { isRecord, parseCommandRequest } from "./runtime/parse.js";
|
|
5
|
+
|
|
6
|
+
const STATE_KEY = "game-state";
|
|
7
|
+
|
|
8
|
+
/** 生成対象の Session runtime。ゲーム固有の判断は gameAdapter だけを通して呼ぶ。 */
|
|
9
|
+
export class SessionDurableObject extends DurableObject<Env> {
|
|
10
|
+
// State の独自キャッシュを持たない。再起動時にも Storage と attachment だけで復元する。
|
|
11
|
+
async fetch(request: Request): Promise<Response> {
|
|
12
|
+
return this.ctx.blockConcurrencyWhile(async () => {
|
|
13
|
+
const actorId = request.headers.get("x-actor-id");
|
|
14
|
+
const fail = (code: ProtocolError["code"], status: number) => Response.json({ ok: false, error: { code } }, { status });
|
|
15
|
+
if (!actorId) return fail("AuthenticationRequired", 401);
|
|
16
|
+
const path = new URL(request.url).pathname;
|
|
17
|
+
const stored = await this.ctx.storage.get<State>(STATE_KEY);
|
|
18
|
+
if (path === "/connect" && request.method === "GET") {
|
|
19
|
+
if (stored === undefined) return fail("SessionNotFound", 404);
|
|
20
|
+
if (!gameAdapter.canConnect(stored, actorId)) return fail("NotSessionMember", 403);
|
|
21
|
+
const viewState = gameAdapter.game.project(stored, actorId);
|
|
22
|
+
const pair = new WebSocketPair();
|
|
23
|
+
pair[1].serializeAttachment({ actorId });
|
|
24
|
+
this.ctx.acceptWebSocket(pair[1]);
|
|
25
|
+
this.send(pair[1], { type: "ViewStateEvent", viewState });
|
|
26
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
27
|
+
}
|
|
28
|
+
if (request.method !== "POST" || (path !== "/create" && path !== "/join")) return fail("InvalidRequest", 400);
|
|
29
|
+
if (path === "/create" && stored !== undefined) return fail("InvalidRequest", 409);
|
|
30
|
+
if (path === "/join" && stored === undefined) return fail("SessionNotFound", 404);
|
|
31
|
+
let body: unknown;
|
|
32
|
+
try { body = await request.json(); } catch { return fail("InvalidRequest", 400); }
|
|
33
|
+
const command = path === "/create" ? gameAdapter.parseCreate(body) : gameAdapter.parseJoin(body);
|
|
34
|
+
if (command === null) return fail("InvalidRequest", 400);
|
|
35
|
+
const result = gameAdapter.game.handleCommand(stored === undefined ? gameAdapter.game.createInitialState() : stored, command, { origin: "actor", actorId });
|
|
36
|
+
if (!result.ok) return Response.json(result, { status: 409 });
|
|
37
|
+
// 作成と作成者の参加は、この1回の保存で確定する。
|
|
38
|
+
if (result.state !== stored) {
|
|
39
|
+
await this.ctx.storage.put(STATE_KEY, result.state);
|
|
40
|
+
this.broadcast(result.state);
|
|
41
|
+
}
|
|
42
|
+
return Response.json({ ok: true });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async webSocketMessage(socket: WebSocket, raw: string | ArrayBuffer): Promise<void> {
|
|
47
|
+
await this.ctx.blockConcurrencyWhile(async () => {
|
|
48
|
+
const parsed = parseCommandRequest(raw);
|
|
49
|
+
const command = parsed ? gameAdapter.parseCommand(parsed.command) : null;
|
|
50
|
+
if (!parsed || command === null) {
|
|
51
|
+
this.send(socket, { type: "ProtocolErrorEvent", error: { code: "InvalidRequest" } });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const attachment: unknown = socket.deserializeAttachment();
|
|
56
|
+
const state = await this.ctx.storage.get<State>(STATE_KEY);
|
|
57
|
+
if (state === undefined || !isRecord(attachment) || typeof attachment.actorId !== "string" || !gameAdapter.canConnect(state, attachment.actorId)) {
|
|
58
|
+
this.send(socket, { type: "GameCommandResponse", requestId: parsed.requestId, ok: false, error: { code: "NotSessionMember" } });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const result = gameAdapter.game.handleCommand(state, command, { origin: "actor", actorId: attachment.actorId });
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
this.send(socket, { type: "GameCommandResponse", requestId: parsed.requestId, ok: false, error: result.error });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await this.ctx.storage.put(STATE_KEY, result.state);
|
|
67
|
+
// Effect は現在 never。実装時もこの保存の後に実行し、配送失敗で rollback しない。
|
|
68
|
+
this.send(socket, { type: "GameCommandResponse", requestId: parsed.requestId, ok: true });
|
|
69
|
+
this.broadcast(result.state);
|
|
70
|
+
} catch {
|
|
71
|
+
console.error("Session command failed");
|
|
72
|
+
this.send(socket, { type: "GameCommandResponse", requestId: parsed.requestId, ok: false, error: { code: "InternalError" } });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private broadcast(state: State): void {
|
|
78
|
+
for (const socket of this.ctx.getWebSockets()) {
|
|
79
|
+
try {
|
|
80
|
+
const attachment: unknown = socket.deserializeAttachment();
|
|
81
|
+
if (!isRecord(attachment) || typeof attachment.actorId !== "string" || !gameAdapter.canConnect(state, attachment.actorId)) continue;
|
|
82
|
+
this.send(socket, { type: "ViewStateEvent", viewState: gameAdapter.game.project(state, attachment.actorId) });
|
|
83
|
+
} catch {
|
|
84
|
+
console.error("Session view delivery failed");
|
|
85
|
+
this.close(socket);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private send(socket: WebSocket, message: ServerMessage): void {
|
|
91
|
+
try { socket.send(JSON.stringify(message)); } catch { this.close(socket); }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private close(socket: WebSocket): void {
|
|
95
|
+
try { socket.close(1011, "Delivery failed"); } catch { /* 切断済み。確定済みの保存結果へ影響させない。 */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
webSocketClose(socket: WebSocket, code: number, reason: string): void { socket.close(code, reason); }
|
|
99
|
+
webSocketError(socket: WebSocket): void { socket.close(1011, "Connection error"); }
|
|
100
|
+
}
|
package/dist/example.d.ts
DELETED
package/dist/example.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../src/example.ts"],"names":[],"mappings":""}
|
package/dist/example.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const helper_1 = require("./helper");
|
|
4
|
-
// A equals to SimpleOnly<Req>
|
|
5
|
-
const test = {
|
|
6
|
-
type: "login",
|
|
7
|
-
payload: (0, helper_1.custom)({
|
|
8
|
-
playerId: "a",
|
|
9
|
-
name: "b",
|
|
10
|
-
}),
|
|
11
|
-
};
|
|
12
|
-
const customEventHander = (r) => {
|
|
13
|
-
switch (r.type) {
|
|
14
|
-
case "login":
|
|
15
|
-
return [
|
|
16
|
-
{
|
|
17
|
-
type: "authorize",
|
|
18
|
-
payload: {
|
|
19
|
-
name: "a",
|
|
20
|
-
password: "b",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
type: "authenticate",
|
|
25
|
-
payload: {
|
|
26
|
-
name: "a",
|
|
27
|
-
level: 1,
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
];
|
|
31
|
-
case "complicatedReq":
|
|
32
|
-
return [
|
|
33
|
-
{
|
|
34
|
-
type: "task1",
|
|
35
|
-
payload: {
|
|
36
|
-
name: "a",
|
|
37
|
-
password: "b",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
type: "task2",
|
|
42
|
-
payload: {
|
|
43
|
-
name: "a",
|
|
44
|
-
level: 1,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: "task3",
|
|
49
|
-
payload: {
|
|
50
|
-
name: "a",
|
|
51
|
-
level: 1,
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
];
|
|
55
|
-
default:
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
// no need to write process that handles simple requests
|
|
60
|
-
const _sampleGenerateTasks = (0, helper_1.simplify)(customEventHander);
|
|
61
|
-
// ------------------------------------------------------------
|
|
62
|
-
// this is how you handle tasks
|
|
63
|
-
// task cases are composed
|
|
64
|
-
// - keyof SimpleOnlyPayload<Req>
|
|
65
|
-
// - keyof Tasks
|
|
66
|
-
const _switchTask = (task) => {
|
|
67
|
-
switch (task.type) {
|
|
68
|
-
case "move":
|
|
69
|
-
return moveTaskHandler(task.payload);
|
|
70
|
-
// login is a custom request, so we need to handle it differently
|
|
71
|
-
case "authorize":
|
|
72
|
-
return authorizeTaskHandler(task.payload);
|
|
73
|
-
case "authenticate":
|
|
74
|
-
return authenticateTaskHandler(task.payload);
|
|
75
|
-
case "task1":
|
|
76
|
-
return task1Handler(task.payload);
|
|
77
|
-
case "task2":
|
|
78
|
-
return task2Handler(task.payload);
|
|
79
|
-
case "task3":
|
|
80
|
-
return task3Handler(task.payload);
|
|
81
|
-
default:
|
|
82
|
-
const _exhaustiveCheck = task;
|
|
83
|
-
console.log("unknown event", task);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
const moveTaskHandler = (payload) => {
|
|
87
|
-
console.log(payload);
|
|
88
|
-
};
|
|
89
|
-
const authorizeTaskHandler = (payload) => {
|
|
90
|
-
console.log(payload);
|
|
91
|
-
};
|
|
92
|
-
const authenticateTaskHandler = (payload) => {
|
|
93
|
-
console.log(payload);
|
|
94
|
-
};
|
|
95
|
-
const task1Handler = (payload) => {
|
|
96
|
-
console.log(payload);
|
|
97
|
-
};
|
|
98
|
-
const task2Handler = (payload) => {
|
|
99
|
-
console.log(payload);
|
|
100
|
-
};
|
|
101
|
-
const task3Handler = (payload) => {
|
|
102
|
-
console.log(payload);
|
|
103
|
-
};
|
|
104
|
-
// ------------------------------------------------------------
|
package/dist/helper.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Custom, CustomOnly, Notify, ReqMap, ReqPayload, Require, RequireSetting, Simple, Task, ToServer } from "./types";
|
|
2
|
-
export declare const simple: <T extends ReqPayload>(payload: T) => Simple<T>;
|
|
3
|
-
export declare const custom: <T extends ReqPayload>(payload: T) => Custom<T>;
|
|
4
|
-
export declare const requiring: <T>(payload: T, setting: RequireSetting) => Require<T>;
|
|
5
|
-
export declare const notifying: <T>(payload: T) => Notify<T>;
|
|
6
|
-
export type CustomEventHander<A extends ReqMap, B extends Record<string, any>> = (r: ToServer<CustomOnly<A>>) => Task<A, B>[];
|
|
7
|
-
export declare const simplify: <A extends ReqMap, B extends Record<string, any>>(h: CustomEventHander<A, B>) => (a: ToServer<A>) => Task<A, B>[];
|
|
8
|
-
//# sourceMappingURL=helper.d.ts.map
|
package/dist/helper.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAqB,IAAI,EAAe,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEzJ,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,UAAU,EAAE,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,CAKjE,CAAA;AAED,eAAO,MAAM,MAAM,GAAI,CAAC,SAAS,UAAU,EAAE,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,CAKjE,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,SAAS,CAAC,EAAE,SAAS,cAAc,KAAG,OAAO,CAAC,CAAC,CAM3E,CAAA;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,CAKjD,CAAA;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IACvE,CAAC,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAA;AAGlD,eAAO,MAAM,QAAQ,GAChB,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,MACvE,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,EAe3B,CAAA"}
|
package/dist/helper.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.simplify = exports.notifying = exports.requiring = exports.custom = exports.simple = void 0;
|
|
4
|
-
const simple = (payload) => {
|
|
5
|
-
return {
|
|
6
|
-
kind: "simple",
|
|
7
|
-
value: payload
|
|
8
|
-
};
|
|
9
|
-
};
|
|
10
|
-
exports.simple = simple;
|
|
11
|
-
const custom = (payload) => {
|
|
12
|
-
return {
|
|
13
|
-
kind: "custom",
|
|
14
|
-
value: payload
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
exports.custom = custom;
|
|
18
|
-
const requiring = (payload, setting) => {
|
|
19
|
-
return {
|
|
20
|
-
kind: "require",
|
|
21
|
-
value: payload,
|
|
22
|
-
setting
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
exports.requiring = requiring;
|
|
26
|
-
const notifying = (payload) => {
|
|
27
|
-
return {
|
|
28
|
-
kind: "notify",
|
|
29
|
-
value: payload
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
exports.notifying = notifying;
|
|
33
|
-
const simplify = (h) => (a) => {
|
|
34
|
-
switch (a.payload.kind) {
|
|
35
|
-
case "simple":
|
|
36
|
-
const key = a.type;
|
|
37
|
-
const payload = a.payload.value;
|
|
38
|
-
return [
|
|
39
|
-
{
|
|
40
|
-
type: key,
|
|
41
|
-
payload,
|
|
42
|
-
}
|
|
43
|
-
];
|
|
44
|
-
case "custom":
|
|
45
|
-
const b = a;
|
|
46
|
-
return h(b);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
exports.simplify = simplify;
|
package/dist/simulator.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { DefaultGameLogicState, GameParameters, GameRule, GameState, Player, ReqMap, ToServer } from "./types";
|
|
2
|
-
export declare class GameEngine<T extends Record<string, any>, S extends Record<string, any>, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> {
|
|
3
|
-
private rules;
|
|
4
|
-
private storage;
|
|
5
|
-
constructor(rules: GameRule<T, S, C, L, P>);
|
|
6
|
-
createRoom(p: P): void;
|
|
7
|
-
join(player: Player): void;
|
|
8
|
-
startGame(): void;
|
|
9
|
-
executeAction(action: ToServer<C>): GameState<T, S, C, L, P>;
|
|
10
|
-
getState(): GameState<T, S, C, L, P>;
|
|
11
|
-
setState(state: GameState<T, S, C, L, P>): void;
|
|
12
|
-
}
|
|
13
|
-
//# sourceMappingURL=simulator.d.ts.map
|
package/dist/simulator.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"simulator.d.ts","sourceRoot":"","sources":["../src/simulator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAU,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGvH,qBAAa,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,cAAc;IAChK,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,OAAO,CAAkC;gBAErC,KAAK,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAa1C,UAAU,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI;IAMtB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM1B,SAAS,IAAI,IAAI;IAMjB,aAAa,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAc5D,QAAQ,IAAI,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAIpC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI;CAGlD"}
|
package/dist/simulator.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GameEngine = void 0;
|
|
4
|
-
// This is a simulator
|
|
5
|
-
class GameEngine {
|
|
6
|
-
constructor(rules) {
|
|
7
|
-
this.rules = rules;
|
|
8
|
-
this.storage = new InMemoryGameStateStorage();
|
|
9
|
-
// Initialize the game state
|
|
10
|
-
const gameState = {
|
|
11
|
-
gameParameters: null,
|
|
12
|
-
gameLogicState: this.rules.initialGameLogicState(),
|
|
13
|
-
taskQueue: []
|
|
14
|
-
};
|
|
15
|
-
this.storage.saveGameState(gameState);
|
|
16
|
-
}
|
|
17
|
-
createRoom(p) {
|
|
18
|
-
const state = this.storage.loadGameState();
|
|
19
|
-
const configured = this.rules.createRoom(state, p);
|
|
20
|
-
this.storage.saveGameState(configured);
|
|
21
|
-
}
|
|
22
|
-
join(player) {
|
|
23
|
-
const state = this.storage.loadGameState();
|
|
24
|
-
const joined = this.rules.onJoin(state, player);
|
|
25
|
-
this.storage.saveGameState(joined);
|
|
26
|
-
}
|
|
27
|
-
startGame() {
|
|
28
|
-
const state = this.storage.loadGameState();
|
|
29
|
-
const started = this.rules.onStartGame(state);
|
|
30
|
-
this.storage.saveGameState(started);
|
|
31
|
-
}
|
|
32
|
-
executeAction(action) {
|
|
33
|
-
const state = this.storage.loadGameState();
|
|
34
|
-
// ------ Game Logic ------
|
|
35
|
-
const tasks = this.rules.generateTasks(state, action);
|
|
36
|
-
const updatedQueueState = this.rules.prioritizeTasks(tasks, state);
|
|
37
|
-
const newState = this.rules.doTasks(updatedQueueState);
|
|
38
|
-
// ------------------------
|
|
39
|
-
this.storage.saveGameState(newState);
|
|
40
|
-
return newState; // this means that this game state is broadcasted to all players
|
|
41
|
-
}
|
|
42
|
-
getState() {
|
|
43
|
-
return this.storage.loadGameState();
|
|
44
|
-
}
|
|
45
|
-
setState(state) {
|
|
46
|
-
this.storage.saveGameState(state);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
exports.GameEngine = GameEngine;
|
|
50
|
-
class InMemoryGameStateStorage {
|
|
51
|
-
constructor() {
|
|
52
|
-
this.state = null;
|
|
53
|
-
}
|
|
54
|
-
loadGameState() {
|
|
55
|
-
if (this.state === null) {
|
|
56
|
-
throw new Error('Game state is not initialized');
|
|
57
|
-
}
|
|
58
|
-
return this.state;
|
|
59
|
-
}
|
|
60
|
-
saveGameState(state) {
|
|
61
|
-
this.state = state;
|
|
62
|
-
}
|
|
63
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
type Map = Record<string, any>;
|
|
2
|
-
type Merge<A, B> = {
|
|
3
|
-
[K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
|
|
4
|
-
};
|
|
5
|
-
export interface ReqPayload {
|
|
6
|
-
playerId: PlayerId;
|
|
7
|
-
}
|
|
8
|
-
export type Second = number;
|
|
9
|
-
export type ReqMap = Record<string, Simple<ReqPayload> | Custom<ReqPayload>>;
|
|
10
|
-
export interface RequireSetting {
|
|
11
|
-
limitedTime: Second;
|
|
12
|
-
}
|
|
13
|
-
export type ResMap = Record<string, Require<any> | Notify<any>>;
|
|
14
|
-
export type Simple<T extends ReqPayload> = {
|
|
15
|
-
kind: "simple";
|
|
16
|
-
value: T;
|
|
17
|
-
};
|
|
18
|
-
export type Custom<T extends ReqPayload> = {
|
|
19
|
-
kind: "custom";
|
|
20
|
-
value: T;
|
|
21
|
-
};
|
|
22
|
-
export type Require<T> = {
|
|
23
|
-
kind: "require";
|
|
24
|
-
value: T;
|
|
25
|
-
setting: RequireSetting;
|
|
26
|
-
};
|
|
27
|
-
export type Notify<T> = {
|
|
28
|
-
kind: "notify";
|
|
29
|
-
value: T;
|
|
30
|
-
};
|
|
31
|
-
export type FromServerMap<T extends ResMap> = T;
|
|
32
|
-
export type ToServerMap<T extends ReqMap> = T;
|
|
33
|
-
export type CustomTaskMap<T extends Map> = T;
|
|
34
|
-
export type ExtractSimplePayload<T> = T extends Simple<infer U> ? U : never;
|
|
35
|
-
export type ExtractCustomPayload<T> = T extends Custom<infer U> ? U : never;
|
|
36
|
-
export type SimpleOnlyPayload<T extends ReqMap> = {
|
|
37
|
-
[K in keyof T as T[K] extends Simple<any> ? K : never]: ExtractSimplePayload<T[K]>;
|
|
38
|
-
};
|
|
39
|
-
export type CustomOnlyPayload<T extends ReqMap> = {
|
|
40
|
-
[K in keyof T as T[K] extends Custom<any> ? K : never]: ExtractCustomPayload<T[K]>;
|
|
41
|
-
};
|
|
42
|
-
export type CustomOnly<T extends ReqMap> = {
|
|
43
|
-
[K in keyof T as T[K] extends Custom<any> ? K : never]: T[K];
|
|
44
|
-
};
|
|
45
|
-
export type RequireOnly<T extends ReqMap> = {
|
|
46
|
-
[K in keyof T as T[K] extends Require<any> ? K : never]: T[K];
|
|
47
|
-
};
|
|
48
|
-
export type NotifyOnly<T extends ReqMap> = {
|
|
49
|
-
[K in keyof T as T[K] extends Notify<any> ? K : never]: T[K];
|
|
50
|
-
};
|
|
51
|
-
export type TaskMapType<A extends ReqMap, B extends Map> = Merge<SimpleOnlyPayload<A>, CustomTaskMap<B>>;
|
|
52
|
-
export type Player = {
|
|
53
|
-
id: PlayerId;
|
|
54
|
-
name: string;
|
|
55
|
-
};
|
|
56
|
-
export type Joiner = {
|
|
57
|
-
numberOfPlayers: number;
|
|
58
|
-
enteredPlayers: Player[];
|
|
59
|
-
};
|
|
60
|
-
export interface GameParameters {
|
|
61
|
-
owner: Player;
|
|
62
|
-
joiner: Joiner;
|
|
63
|
-
}
|
|
64
|
-
export interface DefaultGameLogicState<S extends Map> {
|
|
65
|
-
forClient: ForClient<S>;
|
|
66
|
-
}
|
|
67
|
-
export type FromServerDataType<T extends Map> = keyof FromServerMap<T>;
|
|
68
|
-
export type ToServerDataType<T extends Map> = keyof ToServerMap<T>;
|
|
69
|
-
export type FromServerData<T extends ResMap> = {
|
|
70
|
-
[K in keyof T]: {
|
|
71
|
-
type: K;
|
|
72
|
-
payload: T[K];
|
|
73
|
-
};
|
|
74
|
-
}[keyof T];
|
|
75
|
-
export type ToServerData<T extends ReqMap> = {
|
|
76
|
-
[K in keyof T]: {
|
|
77
|
-
type: K;
|
|
78
|
-
payload: T[K];
|
|
79
|
-
};
|
|
80
|
-
}[keyof T];
|
|
81
|
-
export type FromServer<S extends ResMap> = FromServerData<S>;
|
|
82
|
-
export type ToServer<C extends ReqMap> = ToServerData<C>;
|
|
83
|
-
export type ToServerWithNonStringKey<C extends ReqMap> = {
|
|
84
|
-
[K in keyof C]: {
|
|
85
|
-
type: K;
|
|
86
|
-
payload: C[K];
|
|
87
|
-
};
|
|
88
|
-
}[keyof C];
|
|
89
|
-
export type PlayerId = string;
|
|
90
|
-
export type ForUI<S extends ResMap> = {
|
|
91
|
-
requiredAction: FromServer<S> | null;
|
|
92
|
-
notifications: FromServer<S>[];
|
|
93
|
-
};
|
|
94
|
-
export type ForClient<S extends Map> = {
|
|
95
|
-
[key: PlayerId]: ForUI<S>;
|
|
96
|
-
};
|
|
97
|
-
export type Task<A extends ReqMap, B extends Map> = {
|
|
98
|
-
[K in keyof TaskMapType<A, B>]: {
|
|
99
|
-
type: K;
|
|
100
|
-
payload: TaskMapType<A, B>[K];
|
|
101
|
-
};
|
|
102
|
-
}[keyof TaskMapType<A, B>];
|
|
103
|
-
export interface TaskResult<S extends Map, L extends DefaultGameLogicState<S>> {
|
|
104
|
-
type: string;
|
|
105
|
-
state: L;
|
|
106
|
-
}
|
|
107
|
-
export type GameState<T extends Map, S extends Map, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> = {
|
|
108
|
-
gameLogicState: L;
|
|
109
|
-
taskQueue: Task<C, T>[];
|
|
110
|
-
gameParameters: P | null;
|
|
111
|
-
};
|
|
112
|
-
export interface GameRule<T extends Map, S extends Map, C extends ReqMap, L extends DefaultGameLogicState<S>, P extends GameParameters> {
|
|
113
|
-
initialGameLogicState: () => L;
|
|
114
|
-
createRoom: (state: GameState<T, S, C, L, P>, param: GameParameters) => GameState<T, S, C, L, P>;
|
|
115
|
-
onJoin: (state: GameState<T, S, C, L, P>, joiner: Player) => GameState<T, S, C, L, P>;
|
|
116
|
-
onStartGame: (state: GameState<T, S, C, L, P>) => GameState<T, S, C, L, P>;
|
|
117
|
-
generateTasks(state: GameState<T, S, C, L, P>, event: ToServer<C>): Task<C, T>[];
|
|
118
|
-
prioritizeTasks(newTasks: Task<C, T>[], gameState: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
|
|
119
|
-
doTasks(state: GameState<T, S, C, L, P>): GameState<T, S, C, L, P>;
|
|
120
|
-
privaten(destinationId: PlayerId, l: L): L;
|
|
121
|
-
}
|
|
122
|
-
export {};
|
|
123
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|