create-eigen-game 0.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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/chunk-BSJVRXI4.js +127 -0
- package/dist/chunk-BSJVRXI4.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/templates/app-overlay/app-config.json.template +6 -0
- package/templates/app-overlay/lib/firebase_options.dart.template +14 -0
- package/templates/app-overlay/lib/game/README.md +18 -0
- package/templates/app-overlay/lib/game/module.dart.template +29 -0
- package/templates/app-overlay/lib/game/v1/rules.dart.template +79 -0
- package/templates/app-overlay/lib/main.dart.template +39 -0
- package/templates/app-overlay/test/game/twin_fixtures_test.dart.template +28 -0
- package/templates/app-overlay/web/firebase-config.js.template +4 -0
- package/templates/app-overlay/web/firebase-messaging-sw.js.template +15 -0
- package/templates/app-overlay/web/flutter_bootstrap.js.template +19 -0
- package/templates/app-overlay/web/index.html.template +17 -0
- package/templates/project/README.md +104 -0
- package/templates/project/package.json +12 -0
- package/templates/scaffold/project.gitignore +9 -0
- package/templates/scaffold/worker.gitignore +10 -0
- package/templates/worker/.dev.vars.example +4 -0
- package/templates/worker/README.md +50 -0
- package/templates/worker/game-contract.json +109 -0
- package/templates/worker/package.json +51 -0
- package/templates/worker/public/.gitkeep +1 -0
- package/templates/worker/src/index.ts +17 -0
- package/templates/worker/src/module/fixtures/v1/counter.json +31 -0
- package/templates/worker/src/module/index.ts +4 -0
- package/templates/worker/src/module/v1.ts +57 -0
- package/templates/worker/test/twin.spec.ts +4 -0
- package/templates/worker/tsconfig.json +15 -0
- package/templates/worker/worker-configuration.d.ts +14718 -0
- package/templates/worker/wrangler.jsonc +40 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{{flutter_js}}
|
|
2
|
+
{{flutter_build_config}}
|
|
3
|
+
|
|
4
|
+
(async () => {
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
await navigator.serviceWorker
|
|
7
|
+
.register("firebase-messaging-sw.js", {
|
|
8
|
+
scope: "/firebase-cloud-messaging-push-scope",
|
|
9
|
+
})
|
|
10
|
+
.catch((error) => {
|
|
11
|
+
console.warn(
|
|
12
|
+
"Firebase Messaging service worker registration failed",
|
|
13
|
+
error,
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_flutter.loader.load();
|
|
19
|
+
})();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<base href="$FLUTTER_BASE_HREF">
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
|
7
|
+
<meta name="description" content="Example Game">
|
|
8
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
9
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
|
10
|
+
<meta name="apple-mobile-web-app-title" content="Example Game">
|
|
11
|
+
<title>Example Game</title>
|
|
12
|
+
<link rel="manifest" href="manifest.json">
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<script src="flutter_bootstrap.js" async></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Example Game
|
|
2
|
+
|
|
3
|
+
This repository contains the two application-owned halves of an Eigen game.
|
|
4
|
+
The engine itself is consumed from npm and pub.dev; its repositories are not
|
|
5
|
+
part of this project.
|
|
6
|
+
|
|
7
|
+
## Worker
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
cd server
|
|
11
|
+
{{PACKAGE_MANAGER}} install
|
|
12
|
+
{{PACKAGE_MANAGER}} run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The game module is the default export of `server/src/module/index.ts`. Whenever
|
|
16
|
+
a payload schema or shared fixture changes, regenerate the contract and Dart
|
|
17
|
+
payloads together from the repository root:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
{{PACKAGE_MANAGER}} run contract
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The scaffold includes a starter fixture and both language runners. Keep the
|
|
24
|
+
Worker tests watching while editing rules:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
cd server
|
|
28
|
+
{{PACKAGE_MANAGER}} run test:watch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
After changing a schema or fixture, run `{{PACKAGE_MANAGER}} run contract` from
|
|
32
|
+
the repository root, then `flutter test` from `app/`. Use
|
|
33
|
+
`{{PACKAGE_MANAGER}} run contract:check` from the root in CI. The server and
|
|
34
|
+
app commands remain independently usable if the two halves later move to
|
|
35
|
+
separate repositories.
|
|
36
|
+
|
|
37
|
+
## Flutter app
|
|
38
|
+
|
|
39
|
+
The initial scaffold already generates Dart payloads from the Worker contract.
|
|
40
|
+
Implement the client-side legality, preview and presentation rules under
|
|
41
|
+
`app/lib/game/`, then configure Firebase and fill the public build-time values
|
|
42
|
+
in `app/app-config.json`. They are Dart compilation environment declarations,
|
|
43
|
+
not secrets; private credentials belong only in the Worker.
|
|
44
|
+
|
|
45
|
+
See `app/lib/game/README.md` for the regeneration command.
|
|
46
|
+
|
|
47
|
+
The app targets Android and web. Run the browser on the origin already allowed
|
|
48
|
+
by the Worker template:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
cd app
|
|
52
|
+
flutter run -d chrome --web-hostname localhost --web-port 7357 \
|
|
53
|
+
--dart-define-from-file=app-config.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For production, give the game one canonical custom domain, edit the public
|
|
57
|
+
values in `app/app-config.json`, then deploy both halves together:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
{{PACKAGE_MANAGER}} run deploy
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
That builds Flutter into `server/public/` and deploys one Worker containing the
|
|
64
|
+
SPA and API. The app is `/`; invite and game URLs open the installed native app
|
|
65
|
+
or the browser SPA; the native install landing page is `/download`.
|
|
66
|
+
|
|
67
|
+
Install and authenticate the Firebase and FlutterFire CLIs, then configure
|
|
68
|
+
Android, Flutter Web, and the messaging service worker together:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
{{PACKAGE_MANAGER}} run firebase:configure
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The command uses FlutterFire's selected Web app to generate both
|
|
75
|
+
`lib/firebase_options.dart` and `web/firebase-config.js`; do not copy Firebase
|
|
76
|
+
identifiers by hand. Put the public VAPID key in `app-config.json` separately —
|
|
77
|
+
Firebase's app SDK configuration does not include that Web Push certificate.
|
|
78
|
+
`eigen_flutter` bundles the pinned Cropper.js assets and loads them only when
|
|
79
|
+
avatar editing starts, so the app needs no Cropper.js setup or runtime CDN. The
|
|
80
|
+
complete production checklist is at
|
|
81
|
+
https://eigeninteractive.com/docs/ship-it/deploy-the-web-app.
|
|
82
|
+
|
|
83
|
+
Android releases use the same configuration file:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
{{PACKAGE_MANAGER}} run build:android
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Required values are intentionally empty in a fresh scaffold. The app reports
|
|
90
|
+
all missing or malformed declarations at startup instead of continuing with
|
|
91
|
+
placeholder endpoints.
|
|
92
|
+
|
|
93
|
+
The `eigen_flutter` Android plugin enables Firebase's current FID-based
|
|
94
|
+
messaging mode and supplies a Firebase SDK new enough to support it. The
|
|
95
|
+
scaffolder leaves Flutter's generated manifest and `gradle.properties`
|
|
96
|
+
untouched; it adds only the application-level core-library desugaring block
|
|
97
|
+
required by `flutter_local_notifications`. Push registration is engine-owned;
|
|
98
|
+
game code does not request or store an FCM registration token.
|
|
99
|
+
|
|
100
|
+
The Worker uses that same Firebase project for push and complete account
|
|
101
|
+
deletion. Copy `server/.dev.vars.example` to `server/.dev.vars`, then fill its
|
|
102
|
+
service-account email and private key. The Firebase project ID and web origin
|
|
103
|
+
remain in `server/wrangler.jsonc`. For deployment, store the two credentials
|
|
104
|
+
with `wrangler secret put`; never commit the downloaded JSON or `.dev.vars`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "example-game",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"build:android": "cd app && flutter build appbundle --release --dart-define-from-file=app-config.json",
|
|
6
|
+
"build:web": "cd app && flutter build web --release --output ../server/public --dart-define-from-file=app-config.json",
|
|
7
|
+
"contract": "cd server && {{PACKAGE_MANAGER}} run contract && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures",
|
|
8
|
+
"contract:check": "cd server && {{PACKAGE_MANAGER}} run contract:check && cd ../app && dart run eigen_flutter:generate_payloads --contract ../server/game-contract.json --output lib/game/generated/payloads.dart --fixtures-output test/fixtures --check",
|
|
9
|
+
"deploy": "{{PACKAGE_MANAGER}} run build:web && cd server && {{PACKAGE_MANAGER}} run deploy",
|
|
10
|
+
"firebase:configure": "cd app && dart run eigen_flutter:configure_firebase"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Example Game Worker
|
|
2
|
+
|
|
3
|
+
The authoritative game rules and Cloudflare Worker for an Eigen game.
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
Install dependencies with `npm install` or `pnpm install`, then:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run cf-typegen
|
|
11
|
+
npm run contract
|
|
12
|
+
npm test
|
|
13
|
+
npm run dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The `src/module/index.ts` default export is the game contract. Add fixtures
|
|
17
|
+
under `src/module/fixtures/`; `npm run contract` emits the language-neutral
|
|
18
|
+
`game-contract.json` consumed by the Flutter payload generator.
|
|
19
|
+
|
|
20
|
+
During rules development, keep `npm run test:watch` running. After changing a
|
|
21
|
+
schema or fixture, run `npm run contract`; CI can use
|
|
22
|
+
`npm run contract:check` to reject a stale committed artifact.
|
|
23
|
+
|
|
24
|
+
## Configure and deploy
|
|
25
|
+
|
|
26
|
+
`FIREBASE_PROJECT_ID` and `WEB_APP_ORIGIN` under `wrangler.jsonc` → `vars` are
|
|
27
|
+
the Worker's non-secret environment variables in local development and
|
|
28
|
+
production. Copy `.dev.vars.example` to `.dev.vars`, then fill
|
|
29
|
+
`FIREBASE_CLIENT_EMAIL` and `FIREBASE_PRIVATE_KEY` from the same Firebase
|
|
30
|
+
project's service-account JSON. These credentials are required for FCM and
|
|
31
|
+
complete Firebase account deletion; `.dev.vars` is git-ignored.
|
|
32
|
+
|
|
33
|
+
For deployment, store both values as Wrangler secrets:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npx wrangler secret put FIREBASE_CLIENT_EMAIL
|
|
37
|
+
npx wrangler secret put FIREBASE_PRIVATE_KEY
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`WEB_APP_ORIGIN` is the exact Flutter web origin the engine automatically
|
|
41
|
+
allows to make browser API and WebSocket requests, and the trusted base for
|
|
42
|
+
absolute notification links. The scaffolded `http://localhost:7357` value
|
|
43
|
+
matches the documented Flutter command; change it to the canonical HTTPS
|
|
44
|
+
origin before production. Configure `clientOrigins` in `createEngine` only
|
|
45
|
+
when you need multiple or non-standard browser origins.
|
|
46
|
+
|
|
47
|
+
The combined scaffold builds Flutter directly into `public/`. Static assets
|
|
48
|
+
bypass Worker execution; `/api/*`, `/join/*`, `/game/*`, `/download`, legal,
|
|
49
|
+
and verification paths run Worker-first. `npm run deploy` applies the engine
|
|
50
|
+
migrations before deploying the Worker and whatever is already in `public/`.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"formatVersion": 1,
|
|
3
|
+
"game": "Example Game",
|
|
4
|
+
"versions": {
|
|
5
|
+
"1": {
|
|
6
|
+
"schemas": {
|
|
7
|
+
"state": {
|
|
8
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"count": {
|
|
12
|
+
"maximum": 9007199254740991,
|
|
13
|
+
"minimum": 0,
|
|
14
|
+
"type": "integer"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": [
|
|
18
|
+
"count"
|
|
19
|
+
],
|
|
20
|
+
"type": "object"
|
|
21
|
+
},
|
|
22
|
+
"observation": {
|
|
23
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"count": {
|
|
27
|
+
"maximum": 9007199254740991,
|
|
28
|
+
"minimum": 0,
|
|
29
|
+
"type": "integer"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"count"
|
|
34
|
+
],
|
|
35
|
+
"type": "object"
|
|
36
|
+
},
|
|
37
|
+
"action": {
|
|
38
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
39
|
+
"properties": {
|
|
40
|
+
"amount": {
|
|
41
|
+
"maximum": 9007199254740991,
|
|
42
|
+
"minimum": 1,
|
|
43
|
+
"type": "integer"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": [
|
|
47
|
+
"amount"
|
|
48
|
+
],
|
|
49
|
+
"type": "object"
|
|
50
|
+
},
|
|
51
|
+
"config": {
|
|
52
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"properties": {
|
|
55
|
+
"target": {
|
|
56
|
+
"maximum": 9007199254740991,
|
|
57
|
+
"minimum": 1,
|
|
58
|
+
"type": "integer"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": [
|
|
62
|
+
"target"
|
|
63
|
+
],
|
|
64
|
+
"type": "object"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"fixtures": [
|
|
70
|
+
{
|
|
71
|
+
"path": "v1/counter.json",
|
|
72
|
+
"document": {
|
|
73
|
+
"cases": [
|
|
74
|
+
{
|
|
75
|
+
"action": {
|
|
76
|
+
"amount": 1
|
|
77
|
+
},
|
|
78
|
+
"config": {
|
|
79
|
+
"target": 10
|
|
80
|
+
},
|
|
81
|
+
"expected": {
|
|
82
|
+
"observation": {
|
|
83
|
+
"count": 1
|
|
84
|
+
},
|
|
85
|
+
"outcome": null,
|
|
86
|
+
"pending": [
|
|
87
|
+
1
|
|
88
|
+
],
|
|
89
|
+
"state": {
|
|
90
|
+
"count": 1
|
|
91
|
+
},
|
|
92
|
+
"valid": true
|
|
93
|
+
},
|
|
94
|
+
"kind": "action",
|
|
95
|
+
"name": "increments the counter and passes the turn",
|
|
96
|
+
"pending": [
|
|
97
|
+
0
|
|
98
|
+
],
|
|
99
|
+
"playerIndex": 0,
|
|
100
|
+
"state": {
|
|
101
|
+
"count": 0
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"schemaVersion": 1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@game/example-game-server",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The authoritative Example Game server on Cloudflare Workers.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"eigen": {
|
|
8
|
+
"game": "Example Game"
|
|
9
|
+
},
|
|
10
|
+
"cloudflare": {
|
|
11
|
+
"label": "Example Game Worker",
|
|
12
|
+
"products": [
|
|
13
|
+
"D1",
|
|
14
|
+
"Durable Objects"
|
|
15
|
+
],
|
|
16
|
+
"categories": [
|
|
17
|
+
"starter"
|
|
18
|
+
],
|
|
19
|
+
"bindings": {
|
|
20
|
+
"GAME_DB": {
|
|
21
|
+
"description": "The engine-owned D1 database. Wrangler provisions it automatically on first deployment."
|
|
22
|
+
},
|
|
23
|
+
"FIREBASE_PROJECT_ID": {
|
|
24
|
+
"description": "The Firebase project ID used to verify player identity tokens."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"cf-typegen": "wrangler types",
|
|
30
|
+
"contract": "eigen-contract",
|
|
31
|
+
"contract:check": "eigen-contract --check",
|
|
32
|
+
"deploy": "wrangler d1 migrations apply GAME_DB --remote && wrangler deploy",
|
|
33
|
+
"dev": "wrangler dev",
|
|
34
|
+
"start": "wrangler dev",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"typecheck": "wrangler types && tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@eigeninteractive/rules": "^0.1.0",
|
|
41
|
+
"@eigeninteractive/server": "^0.1.0",
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eigeninteractive/testkit": "^0.1.0",
|
|
46
|
+
"@types/node": "^26.1.1",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vitest": "^4.1.10",
|
|
49
|
+
"wrangler": "^4.110.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Flutter's release build is written into this directory by `pnpm build:web`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseGameDO, createEngine } from "@eigeninteractive/server";
|
|
2
|
+
import gameModule from "./module/index.js";
|
|
3
|
+
|
|
4
|
+
export class GameDO extends BaseGameDO<Env> {
|
|
5
|
+
protected readonly gameModule = gameModule;
|
|
6
|
+
|
|
7
|
+
protected d1(env: Env): D1Database {
|
|
8
|
+
return env.GAME_DB;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default createEngine({
|
|
13
|
+
gameModule,
|
|
14
|
+
appName: "Example Game",
|
|
15
|
+
d1: (env: Env) => env.GAME_DB,
|
|
16
|
+
gameDO: (env: Env) => env.GAME_DO,
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "action",
|
|
6
|
+
"name": "increments the counter and passes the turn",
|
|
7
|
+
"config": {
|
|
8
|
+
"target": 10
|
|
9
|
+
},
|
|
10
|
+
"state": {
|
|
11
|
+
"count": 0
|
|
12
|
+
},
|
|
13
|
+
"pending": [0],
|
|
14
|
+
"playerIndex": 0,
|
|
15
|
+
"action": {
|
|
16
|
+
"amount": 1
|
|
17
|
+
},
|
|
18
|
+
"expected": {
|
|
19
|
+
"valid": true,
|
|
20
|
+
"state": {
|
|
21
|
+
"count": 1
|
|
22
|
+
},
|
|
23
|
+
"pending": [1],
|
|
24
|
+
"outcome": null,
|
|
25
|
+
"observation": {
|
|
26
|
+
"count": 1
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { GameRules } from "@eigeninteractive/rules";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const stateSchema = z.object({ count: z.int().min(0) }).meta({ id: "ExampleGameV1State" });
|
|
5
|
+
const observationSchema = z.object({ count: z.int().min(0) }).meta({ id: "ExampleGameV1Observation" });
|
|
6
|
+
const actionSchema = z.object({ amount: z.int().min(1) }).meta({ id: "ExampleGameV1Action" });
|
|
7
|
+
const configSchema = z.object({ target: z.int().min(1) }).meta({ id: "ExampleGameV1Config" });
|
|
8
|
+
|
|
9
|
+
type State = z.infer<typeof stateSchema>;
|
|
10
|
+
type Observation = z.infer<typeof observationSchema>;
|
|
11
|
+
type Action = z.infer<typeof actionSchema>;
|
|
12
|
+
type Config = z.infer<typeof configSchema>;
|
|
13
|
+
|
|
14
|
+
const outcome = (winner: number) => [
|
|
15
|
+
{ playerIndex: winner, result: "win" as const, placement: 1, teamIndex: winner },
|
|
16
|
+
{ playerIndex: 1 - winner, result: "loss" as const, placement: 2, teamIndex: 1 - winner },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const rulesV1: GameRules<State, Observation, Action, Config> = {
|
|
20
|
+
schemas: {
|
|
21
|
+
state: stateSchema,
|
|
22
|
+
observation: observationSchema,
|
|
23
|
+
action: actionSchema,
|
|
24
|
+
config: configSchema,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
initialState: () => ({
|
|
28
|
+
state: { count: 0 },
|
|
29
|
+
pendingPlayers: [0],
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
applyAction: ({ state, data, playerIndex, config }) => {
|
|
33
|
+
const count = state.count + data.amount;
|
|
34
|
+
return {
|
|
35
|
+
state: { count },
|
|
36
|
+
pendingPlayers: count >= config.target ? [] : [1 - playerIndex],
|
|
37
|
+
...(count >= config.target ? { outcome: outcome(playerIndex) } : {}),
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
applyLifecycle: ({ state, pending, data }) => {
|
|
42
|
+
const loser = data.type === "timeout" ? (pending[0] ?? 0) : data.playerIndex;
|
|
43
|
+
return {
|
|
44
|
+
state,
|
|
45
|
+
pendingPlayers: [],
|
|
46
|
+
outcome: outcome(1 - loser),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
computeObservation: ({ state, pending }) => ({
|
|
51
|
+
data: state,
|
|
52
|
+
pendingPlayers: pending,
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
ratingPool: () => null,
|
|
56
|
+
botSeatable: () => true,
|
|
57
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024"],
|
|
5
|
+
"module": "ES2022",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"types": ["./worker-configuration.d.ts", "node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src", "test", "worker-configuration.d.ts"]
|
|
15
|
+
}
|