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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/dist/chunk-BSJVRXI4.js +127 -0
  4. package/dist/chunk-BSJVRXI4.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +58 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/index.d.ts +21 -0
  9. package/dist/index.js +9 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +48 -0
  12. package/templates/app-overlay/app-config.json.template +6 -0
  13. package/templates/app-overlay/lib/firebase_options.dart.template +14 -0
  14. package/templates/app-overlay/lib/game/README.md +18 -0
  15. package/templates/app-overlay/lib/game/module.dart.template +29 -0
  16. package/templates/app-overlay/lib/game/v1/rules.dart.template +79 -0
  17. package/templates/app-overlay/lib/main.dart.template +39 -0
  18. package/templates/app-overlay/test/game/twin_fixtures_test.dart.template +28 -0
  19. package/templates/app-overlay/web/firebase-config.js.template +4 -0
  20. package/templates/app-overlay/web/firebase-messaging-sw.js.template +15 -0
  21. package/templates/app-overlay/web/flutter_bootstrap.js.template +19 -0
  22. package/templates/app-overlay/web/index.html.template +17 -0
  23. package/templates/project/README.md +104 -0
  24. package/templates/project/package.json +12 -0
  25. package/templates/scaffold/project.gitignore +9 -0
  26. package/templates/scaffold/worker.gitignore +10 -0
  27. package/templates/worker/.dev.vars.example +4 -0
  28. package/templates/worker/README.md +50 -0
  29. package/templates/worker/game-contract.json +109 -0
  30. package/templates/worker/package.json +51 -0
  31. package/templates/worker/public/.gitkeep +1 -0
  32. package/templates/worker/src/index.ts +17 -0
  33. package/templates/worker/src/module/fixtures/v1/counter.json +31 -0
  34. package/templates/worker/src/module/index.ts +4 -0
  35. package/templates/worker/src/module/v1.ts +57 -0
  36. package/templates/worker/test/twin.spec.ts +4 -0
  37. package/templates/worker/tsconfig.json +15 -0
  38. package/templates/worker/worker-configuration.d.ts +14718 -0
  39. 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,9 @@
1
+ .DS_Store
2
+ server/node_modules/
3
+ server/.wrangler/
4
+ server/.dev.vars*
5
+ !server/.dev.vars.example
6
+ server/.env*
7
+ server/public/*
8
+ !server/public/.gitkeep
9
+ *.log
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .wrangler/
3
+ .dev.vars*
4
+ !.dev.vars.example
5
+ .env*
6
+ !.env.example
7
+ *.log
8
+ *.tsbuildinfo
9
+ public/*
10
+ !public/.gitkeep
@@ -0,0 +1,4 @@
1
+ # Copy to .dev.vars and fill these from the service-account JSON generated at
2
+ # Firebase Console > Project settings > Service accounts. Never commit .dev.vars.
3
+ FIREBASE_CLIENT_EMAIL=
4
+ FIREBASE_PRIVATE_KEY=
@@ -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,4 @@
1
+ import type { GameModule } from "@eigeninteractive/rules";
2
+ import { rulesV1 } from "./v1.js";
3
+
4
+ export default { versions: { 1: rulesV1 } } satisfies GameModule;
@@ -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,4 @@
1
+ import { twinFixtureTests } from "@eigeninteractive/testkit";
2
+ import gameModule from "../src/module/index.js";
3
+
4
+ twinFixtureTests(gameModule, new URL("../src/module/fixtures/", import.meta.url));
@@ -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
+ }