@flayerlabs/gamemode-cli 0.3.1 → 0.4.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.
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-a-game-server
|
|
3
|
+
description: Connects a game with its own authoritative multiplayer server to Flaunch Game Mode with @flayerlabs/gamemode-gate. Use when a game runs its own realtime server or region fleet (custom netcode, Colyseus, Socket.IO, raw WebSockets) and needs the gate, /config, join tickets, awards and the platform submission to line up. Do not use for rules-based games with no server — $build-game-mode covers those.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run a game server behind a Game Mode gate
|
|
7
|
+
|
|
8
|
+
You have a game whose gameplay lives on your own server. The platform never hosts or relays your
|
|
9
|
+
realtime traffic: you run the server and the gate, and the platform gives your hosted game a strict
|
|
10
|
+
policy that only reaches what you declared. This skill exists because that declaration lives in
|
|
11
|
+
three places that must agree, and because the launch form refuses a gate that does not announce
|
|
12
|
+
itself. Work through the sections in order; each ends with a check you can run.
|
|
13
|
+
|
|
14
|
+
## Know the three services and who hosts them
|
|
15
|
+
|
|
16
|
+
1. Your game bundle: static files you zip and submit. The platform hosts these on Moongate and
|
|
17
|
+
serves them from `https://<deploy-id>.games.moongate.com`. The deploy id is derived from the
|
|
18
|
+
bundle's content, so every re-upload changes the hostname. Never hardcode it; never expect it
|
|
19
|
+
to be stable.
|
|
20
|
+
2. Your gate: one process you deploy (`createGameServerGate()` from `@flayerlabs/gamemode-gate`).
|
|
21
|
+
It owns wallet sessions, the points ledger, spend authorisations and settlement. One process
|
|
22
|
+
serves one chain.
|
|
23
|
+
3. Your game server(s): your realtime processes, up to four public HTTPS origins. Gameplay,
|
|
24
|
+
scoring decisions and anti-abuse are yours; points move only through the gate's award route.
|
|
25
|
+
|
|
26
|
+
## Declare the same origins in all three places
|
|
27
|
+
|
|
28
|
+
The reviewed game-server origins must match exactly — same scheme, same host, no path — in:
|
|
29
|
+
|
|
30
|
+
1. The platform submission form's game server addresses field (one per line). This becomes your
|
|
31
|
+
hosted game's `connect-src`: the browser can only reach origins listed here or your gate.
|
|
32
|
+
2. `gameServerOrigins` in your `createGameServerGate()` options. This puts each origin into the
|
|
33
|
+
set of valid join-ticket audiences.
|
|
34
|
+
3. The `joinTicket(origin)` call in your game client and the `audience` your server verifies.
|
|
35
|
+
|
|
36
|
+
A mismatch fails at a different layer each time: missing from (1) and the fetch dies on CSP with
|
|
37
|
+
"Refused to connect"; missing from (2) and the fetch succeeds but the ticket does not verify;
|
|
38
|
+
wrong in (3) and verification rejects every player. At most four origins, exact HTTPS only —
|
|
39
|
+
wildcards, paths and plain HTTP are refused. Rotating regions? Put them behind stable hostnames;
|
|
40
|
+
the list is not meant to churn.
|
|
41
|
+
|
|
42
|
+
## Serve /config, or the launch form refuses your gate
|
|
43
|
+
|
|
44
|
+
The launch page reads `GET /config` from your gate before it lets anyone launch a coin through
|
|
45
|
+
your game. `startGate()` serves it automatically; `createGameServerGate()` serves it only when
|
|
46
|
+
you pass `announce`. Without it the form reports your server "isn't answering — it may be offline,
|
|
47
|
+
or built before the current Game Mode SDK". Pass it:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const app = createGameServerGate({
|
|
51
|
+
// ...pool, sessions, claims, discovery, settlement, gameId, awardToken...
|
|
52
|
+
gameServerOrigins: ['https://us.game.example.com', 'https://eu.game.example.com'],
|
|
53
|
+
announce: () => ({
|
|
54
|
+
chainId: 84532,
|
|
55
|
+
contracts: {
|
|
56
|
+
positionManager: '0x4E7cB1e6800a7B297B38BddcecAF9Ca5b6616FDC',
|
|
57
|
+
spendGatedCalculator: '0x8cbbE6b4cFA5Ccf68399Dbf1429d91A21097ebA5',
|
|
58
|
+
},
|
|
59
|
+
signer: SIGNER_ADDRESS, // the address of your gate's signing key
|
|
60
|
+
settler: SIGNER_ADDRESS,
|
|
61
|
+
walletCapWei: '25000000000000000',
|
|
62
|
+
roundDurationMs: 90_000,
|
|
63
|
+
minLobbyLeadMs: 60_000,
|
|
64
|
+
gateEndsAtGraceS: 10,
|
|
65
|
+
flaunchVariant: 'legacy11',
|
|
66
|
+
requiresEoa: false,
|
|
67
|
+
accepting: true,
|
|
68
|
+
publicLaunchesOpen: true,
|
|
69
|
+
privateLaunches: false,
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The addresses above are Base Sepolia (chain 84532), read back from the chain. `signer` is what a
|
|
75
|
+
launch writes into its pool as the trusted signer — announce an address you do not hold and every
|
|
76
|
+
coin launched through your game is unplayable. `walletCapWei` must cover a flawless round
|
|
77
|
+
(`maxPointsPerPlayer` times the wei value of a point) or boot refuses.
|
|
78
|
+
|
|
79
|
+
Check: `curl https://<your-gate>/config` returns JSON with your `chainId` and `signer`, and
|
|
80
|
+
`curl https://<your-gate>/health` returns `{"ok":true}`.
|
|
81
|
+
|
|
82
|
+
## Point the gate's environment at the right origins
|
|
83
|
+
|
|
84
|
+
The two origin settings developers most often get backwards:
|
|
85
|
+
|
|
86
|
+
- Allowed origins (browser CORS and the WebSocket `Origin` check) name where your GAME is served
|
|
87
|
+
from — the Moongate origin, not the flaunch page. Because the Moongate hostname changes per
|
|
88
|
+
upload, use the single-label wildcard: `https://*.games.moongate.com`.
|
|
89
|
+
- The sign-in domain names the page the player is looking at — the flaunch site embedding your
|
|
90
|
+
game — not your game and not your gate.
|
|
91
|
+
|
|
92
|
+
Generate the signing key, session secret (32+ characters) and award token (32+ characters,
|
|
93
|
+
`openssl rand -hex 32`) fresh per environment. The award token lives on the gate and your game
|
|
94
|
+
server only; it never reaches a browser or a zip.
|
|
95
|
+
|
|
96
|
+
## Wire the ticket handshake
|
|
97
|
+
|
|
98
|
+
Browser, after `joinEconomy()`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const gameServerOrigin = 'https://us.game.example.com' // one reviewed origin, chosen by you
|
|
102
|
+
const { ticket } = await gameMode.joinTicket(gameServerOrigin)
|
|
103
|
+
socket.send(JSON.stringify({ type: 'join', ticket })) // first message; never in the URL
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Your server, before accepting any gameplay frame:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const playerJoin = verifyPlayerJoinTicket(message.ticket, {
|
|
110
|
+
awardToken,
|
|
111
|
+
gameId,
|
|
112
|
+
roundId,
|
|
113
|
+
audience: gameServerOrigin, // the exact origin the browser connected to
|
|
114
|
+
})
|
|
115
|
+
if (!playerJoin || usedTicketIds.has(playerJoin.jti)) return socket.close(4401, 'invalid join ticket')
|
|
116
|
+
usedTicketIds.add(playerJoin.jti)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Also check the WebSocket `Origin` header against the Moongate wildcard, keep one live connection
|
|
120
|
+
per wallet, and treat every later frame as hostile input. Award points only through
|
|
121
|
+
`POST /internal/rounds/:id/awards` with the award token — see the gate README for the routes.
|
|
122
|
+
|
|
123
|
+
## Strip development fallbacks from the production bundle
|
|
124
|
+
|
|
125
|
+
A production zip that pings `localhost` ports, probes a region list on boot, or falls back to a
|
|
126
|
+
dev wallet endpoint will emit CSP errors on every load and can hang the game on a spinner when the
|
|
127
|
+
fallback wins a race. Gate development-only network paths behind a build flag so the submitted
|
|
128
|
+
bundle contains none of them.
|
|
129
|
+
|
|
130
|
+
## Submit, then verify against the real policy
|
|
131
|
+
|
|
132
|
+
Submit the zip with the gate option on (your gate origin) and the game server addresses filled in.
|
|
133
|
+
The form live-probes your gate's `/health` at submission, so deploy the gate first. Localhost
|
|
134
|
+
cannot rehearse the deployed CSP: after hosting, open the deployed game and confirm every declared
|
|
135
|
+
origin connects and an undeclared one is blocked. Then launch a fresh test coin through the room
|
|
136
|
+
link — a coin launched before a registration change stays bound to what it was launched with.
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5D;
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5D;AA0CD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW7D"}
|
package/dist/scaffold.js
CHANGED
|
@@ -37,6 +37,12 @@ async function readCreatorSkill() {
|
|
|
37
37
|
async function readCreatorSkillMetadata() {
|
|
38
38
|
return readAsset('./build-game-mode/agents/openai.yaml', '../../../.agents/skills/build-game-mode/agents/openai.yaml');
|
|
39
39
|
}
|
|
40
|
+
async function readServerSkill() {
|
|
41
|
+
return readAsset('./run-a-game-server/SKILL.md', '../../../.agents/skills/run-a-game-server/SKILL.md');
|
|
42
|
+
}
|
|
43
|
+
async function readServerSkillMetadata() {
|
|
44
|
+
return readAsset('./run-a-game-server/agents/openai.yaml', '../../../.agents/skills/run-a-game-server/agents/openai.yaml');
|
|
45
|
+
}
|
|
40
46
|
async function readAsset(packaged, source) {
|
|
41
47
|
try {
|
|
42
48
|
return await readFile(new URL(packaged, import.meta.url), 'utf8');
|
|
@@ -67,16 +73,19 @@ const files = async (name) => {
|
|
|
67
73
|
type: 'module',
|
|
68
74
|
scripts: {
|
|
69
75
|
dev: 'vite',
|
|
76
|
+
gate: 'tsx src/gate.ts',
|
|
70
77
|
test: 'gamemode check src/game/rules.ts && vitest run',
|
|
71
78
|
check: 'gamemode check src/game/rules.ts',
|
|
72
79
|
typecheck: 'tsc --noEmit',
|
|
73
80
|
},
|
|
74
81
|
dependencies: {
|
|
75
82
|
'@flayerlabs/gamemode-client': sdk,
|
|
83
|
+
'@flayerlabs/gamemode-gate': sdk,
|
|
76
84
|
'@flayerlabs/gamemode-spec': sdk,
|
|
77
85
|
},
|
|
78
86
|
devDependencies: {
|
|
79
87
|
'@flayerlabs/gamemode-cli': sdk,
|
|
88
|
+
tsx: '^4.23.12',
|
|
80
89
|
typescript: '^5.7.2',
|
|
81
90
|
vite: '^5.4.21',
|
|
82
91
|
vitest: '^2.1.8',
|
|
@@ -106,6 +115,28 @@ pnpm test
|
|
|
106
115
|
pnpm typecheck
|
|
107
116
|
\`\`\`
|
|
108
117
|
|
|
118
|
+
## Deploy your gate
|
|
119
|
+
|
|
120
|
+
Game Mode needs your game to bring its own round server — a gate — and its public https origin is
|
|
121
|
+
exactly the **Gate URL** you enter when you submit the game at flaunch.gg/game-mode/create. Flaunch
|
|
122
|
+
never runs a gate for you and never holds your signing key.
|
|
123
|
+
|
|
124
|
+
\`src/gate.ts\` is the whole entrypoint; \`.env.example\` is the whole contract. On Railway:
|
|
125
|
+
|
|
126
|
+
1. Create a project from this repo (\`railpack.json\` names the build and start commands) and
|
|
127
|
+
attach a Postgres whose URL lands in \`DATABASE_URL\`.
|
|
128
|
+
2. Set the variables from \`.env.example\`, generating the key and secret fresh.
|
|
129
|
+
3. Give the service a public domain, set it as \`GATE_ORIGIN\`, and smoke-test it:
|
|
130
|
+
|
|
131
|
+
\`\`\`bash
|
|
132
|
+
curl https://<your-gate>/health # { "ok": true }
|
|
133
|
+
curl https://<your-gate>/config # chainId must be the chain you meant; note "signer"
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
A gate that boots is a gate whose signatures a swap can verify — boot refuses loudly on a
|
|
137
|
+
wrong-chain RPC, a demo key, or an economy your rewardBounds cannot honour. The full environment
|
|
138
|
+
table lives in \`@flayerlabs/gamemode-gate\`'s DEPLOY.md.
|
|
139
|
+
|
|
109
140
|
## Work with an agent
|
|
110
141
|
|
|
111
142
|
Ask the agent to read \`AGENTS.md\` before it edits the game. Use \`$build-game-mode\` when the
|
|
@@ -131,6 +162,8 @@ For the rules and client workflow, read the
|
|
|
131
162
|
`,
|
|
132
163
|
'.agents/skills/build-game-mode/SKILL.md': await readCreatorSkill(),
|
|
133
164
|
'.agents/skills/build-game-mode/agents/openai.yaml': await readCreatorSkillMetadata(),
|
|
165
|
+
'.agents/skills/run-a-game-server/SKILL.md': await readServerSkill(),
|
|
166
|
+
'.agents/skills/run-a-game-server/agents/openai.yaml': await readServerSkillMetadata(),
|
|
134
167
|
'src/game/rules.ts': `import { defineGame, type Decision, type PlayerId, type Refusal } from '@flayerlabs/gamemode-spec';
|
|
135
168
|
import { scheduleWithin } from '@flayerlabs/gamemode-spec/schedule';
|
|
136
169
|
|
|
@@ -321,11 +354,72 @@ export interface PlayerView {
|
|
|
321
354
|
wasRight: boolean | null;
|
|
322
355
|
}
|
|
323
356
|
`,
|
|
324
|
-
'src/
|
|
357
|
+
'src/gate.ts': `import { startGate } from '@flayerlabs/gamemode-gate';
|
|
358
|
+
import { rules, type Config } from './game/rules.js';
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Your game's own server, as a deployable process.
|
|
362
|
+
*
|
|
363
|
+
* A gate holds YOUR signing key and runs YOUR rules: a creator who launches a coin through your
|
|
364
|
+
* game writes your signer's address into their pool, and the chain trusts it for that pool alone.
|
|
365
|
+
* Flaunch never holds the key. \`startGate\` does the wiring — database migrations, the chain
|
|
366
|
+
* address book, boot refusals for anything misconfigured, /config for launch tooling — and this
|
|
367
|
+
* file stays what it should be: your round's economy, stated once.
|
|
368
|
+
*
|
|
369
|
+
* Everything environment-shaped comes from the environment; see .env.example.
|
|
370
|
+
*/
|
|
371
|
+
const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
|
|
372
|
+
|
|
373
|
+
const { port, signer } = await startGate(rules, config);
|
|
374
|
+
|
|
375
|
+
// The signer address is what a launch must name for this gate to adopt it — it belongs in the
|
|
376
|
+
// deploy log, where an operator can read it back without a debugger.
|
|
377
|
+
console.log(\`[gate] listening on \${port} — signer \${signer}\`);
|
|
378
|
+
`,
|
|
379
|
+
'.env.example': `# The environment contract for \`pnpm gate\` (full table: @flayerlabs/gamemode-gate DEPLOY.md).
|
|
380
|
+
# Generate SIGNER_PRIVATE_KEY and SESSION_SECRET fresh for every environment — a key shared
|
|
381
|
+
# between two environments lets one gate's bug mint authorisations against the other's pools.
|
|
382
|
+
|
|
383
|
+
# The one chain this process serves. 84532 (Base Sepolia) is in the address book.
|
|
384
|
+
CHAIN_ID=84532
|
|
385
|
+
# An RPC endpoint for that chain; boot checks its eth_chainId matches.
|
|
386
|
+
RPC_URL=https://sepolia.base.org
|
|
387
|
+
# Your key, generated by you, never shared. openssl rand -hex 32 (prefix with 0x).
|
|
388
|
+
SIGNER_PRIVATE_KEY=
|
|
389
|
+
# Postgres. Migrations run at boot.
|
|
390
|
+
DATABASE_URL=
|
|
391
|
+
# At least 32 characters. openssl rand -hex 32
|
|
392
|
+
SESSION_SECRET=
|
|
393
|
+
# The page the player is looking at when they sign in — the embedding site, not this gate.
|
|
394
|
+
SIGN_IN_DOMAIN=flaunch.gg
|
|
395
|
+
# This gate's own public https origin — the exact value you submit as the Gate URL.
|
|
396
|
+
GATE_ORIGIN=
|
|
397
|
+
# Comma-separated origins allowed to call this gate: your game's hosted origin(s) and the pages
|
|
398
|
+
# that embed it. One * may span a single DNS label.
|
|
399
|
+
ALLOWED_ORIGINS=https://*.games.moongate.com,https://flaunch.gg
|
|
400
|
+
`,
|
|
401
|
+
'railpack.json': `${JSON.stringify({
|
|
402
|
+
$schema: 'https://schema.railpack.com',
|
|
403
|
+
build: { builder: 'RAILPACK', buildCommand: 'pnpm install' },
|
|
404
|
+
deploy: { startCommand: 'pnpm gate' },
|
|
405
|
+
}, null, 2)}\n`,
|
|
406
|
+
'src/play.ts': `import { connectHost, createMockRoom, joinRoom, type Snapshot } from '@flayerlabs/gamemode-client';
|
|
325
407
|
import { rules, type Action, type Config, type PlayerView, type PublicView } from './game/rules.js';
|
|
326
408
|
|
|
327
409
|
const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
|
|
328
|
-
|
|
410
|
+
|
|
411
|
+
// Embedded on flaunch.gg, the page tells this game which gate and round to join and services its
|
|
412
|
+
// wallet calls over the frame bridge. Standing alone (pnpm dev), there is no page to ask — null is
|
|
413
|
+
// that answer, and the mock room runs the same rules with the chain and wallet faked.
|
|
414
|
+
const embedded = await connectHost();
|
|
415
|
+
const room = embedded
|
|
416
|
+
? await joinRoom<PublicView, PlayerView, Action>({
|
|
417
|
+
gateUrl: embedded.context.gateUrl,
|
|
418
|
+
roundId: embedded.context.roundId,
|
|
419
|
+
host: embedded.host,
|
|
420
|
+
...(embedded.context.launch ? { launch: embedded.context.launch } : {}),
|
|
421
|
+
})
|
|
422
|
+
: createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
|
|
329
423
|
const app = document.querySelector<HTMLElement>('#app');
|
|
330
424
|
if (!app) throw new Error('the page needs an #app element');
|
|
331
425
|
const gameRoot = app;
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/scaffold.ts
CHANGED
|
@@ -47,6 +47,17 @@ async function readCreatorSkillMetadata(): Promise<string> {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
async function readServerSkill(): Promise<string> {
|
|
51
|
+
return readAsset('./run-a-game-server/SKILL.md', '../../../.agents/skills/run-a-game-server/SKILL.md');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readServerSkillMetadata(): Promise<string> {
|
|
55
|
+
return readAsset(
|
|
56
|
+
'./run-a-game-server/agents/openai.yaml',
|
|
57
|
+
'../../../.agents/skills/run-a-game-server/agents/openai.yaml',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
async function readAsset(packaged: string, source: string): Promise<string> {
|
|
51
62
|
try {
|
|
52
63
|
return await readFile(new URL(packaged, import.meta.url), 'utf8');
|
|
@@ -86,16 +97,19 @@ const files = async (name: string): Promise<Record<string, string>> => {
|
|
|
86
97
|
type: 'module',
|
|
87
98
|
scripts: {
|
|
88
99
|
dev: 'vite',
|
|
100
|
+
gate: 'tsx src/gate.ts',
|
|
89
101
|
test: 'gamemode check src/game/rules.ts && vitest run',
|
|
90
102
|
check: 'gamemode check src/game/rules.ts',
|
|
91
103
|
typecheck: 'tsc --noEmit',
|
|
92
104
|
},
|
|
93
105
|
dependencies: {
|
|
94
106
|
'@flayerlabs/gamemode-client': sdk,
|
|
107
|
+
'@flayerlabs/gamemode-gate': sdk,
|
|
95
108
|
'@flayerlabs/gamemode-spec': sdk,
|
|
96
109
|
},
|
|
97
110
|
devDependencies: {
|
|
98
111
|
'@flayerlabs/gamemode-cli': sdk,
|
|
112
|
+
tsx: '^4.23.12',
|
|
99
113
|
typescript: '^5.7.2',
|
|
100
114
|
vite: '^5.4.21',
|
|
101
115
|
vitest: '^2.1.8',
|
|
@@ -130,6 +144,28 @@ pnpm test
|
|
|
130
144
|
pnpm typecheck
|
|
131
145
|
\`\`\`
|
|
132
146
|
|
|
147
|
+
## Deploy your gate
|
|
148
|
+
|
|
149
|
+
Game Mode needs your game to bring its own round server — a gate — and its public https origin is
|
|
150
|
+
exactly the **Gate URL** you enter when you submit the game at flaunch.gg/game-mode/create. Flaunch
|
|
151
|
+
never runs a gate for you and never holds your signing key.
|
|
152
|
+
|
|
153
|
+
\`src/gate.ts\` is the whole entrypoint; \`.env.example\` is the whole contract. On Railway:
|
|
154
|
+
|
|
155
|
+
1. Create a project from this repo (\`railpack.json\` names the build and start commands) and
|
|
156
|
+
attach a Postgres whose URL lands in \`DATABASE_URL\`.
|
|
157
|
+
2. Set the variables from \`.env.example\`, generating the key and secret fresh.
|
|
158
|
+
3. Give the service a public domain, set it as \`GATE_ORIGIN\`, and smoke-test it:
|
|
159
|
+
|
|
160
|
+
\`\`\`bash
|
|
161
|
+
curl https://<your-gate>/health # { "ok": true }
|
|
162
|
+
curl https://<your-gate>/config # chainId must be the chain you meant; note "signer"
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
A gate that boots is a gate whose signatures a swap can verify — boot refuses loudly on a
|
|
166
|
+
wrong-chain RPC, a demo key, or an economy your rewardBounds cannot honour. The full environment
|
|
167
|
+
table lives in \`@flayerlabs/gamemode-gate\`'s DEPLOY.md.
|
|
168
|
+
|
|
133
169
|
## Work with an agent
|
|
134
170
|
|
|
135
171
|
Ask the agent to read \`AGENTS.md\` before it edits the game. Use \`$build-game-mode\` when the
|
|
@@ -156,6 +192,8 @@ For the rules and client workflow, read the
|
|
|
156
192
|
|
|
157
193
|
'.agents/skills/build-game-mode/SKILL.md': await readCreatorSkill(),
|
|
158
194
|
'.agents/skills/build-game-mode/agents/openai.yaml': await readCreatorSkillMetadata(),
|
|
195
|
+
'.agents/skills/run-a-game-server/SKILL.md': await readServerSkill(),
|
|
196
|
+
'.agents/skills/run-a-game-server/agents/openai.yaml': await readServerSkillMetadata(),
|
|
159
197
|
|
|
160
198
|
'src/game/rules.ts': `import { defineGame, type Decision, type PlayerId, type Refusal } from '@flayerlabs/gamemode-spec';
|
|
161
199
|
import { scheduleWithin } from '@flayerlabs/gamemode-spec/schedule';
|
|
@@ -348,11 +386,79 @@ export interface PlayerView {
|
|
|
348
386
|
}
|
|
349
387
|
`,
|
|
350
388
|
|
|
351
|
-
'src/
|
|
389
|
+
'src/gate.ts': `import { startGate } from '@flayerlabs/gamemode-gate';
|
|
390
|
+
import { rules, type Config } from './game/rules.js';
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Your game's own server, as a deployable process.
|
|
394
|
+
*
|
|
395
|
+
* A gate holds YOUR signing key and runs YOUR rules: a creator who launches a coin through your
|
|
396
|
+
* game writes your signer's address into their pool, and the chain trusts it for that pool alone.
|
|
397
|
+
* Flaunch never holds the key. \`startGate\` does the wiring — database migrations, the chain
|
|
398
|
+
* address book, boot refusals for anything misconfigured, /config for launch tooling — and this
|
|
399
|
+
* file stays what it should be: your round's economy, stated once.
|
|
400
|
+
*
|
|
401
|
+
* Everything environment-shaped comes from the environment; see .env.example.
|
|
402
|
+
*/
|
|
403
|
+
const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
|
|
404
|
+
|
|
405
|
+
const { port, signer } = await startGate(rules, config);
|
|
406
|
+
|
|
407
|
+
// The signer address is what a launch must name for this gate to adopt it — it belongs in the
|
|
408
|
+
// deploy log, where an operator can read it back without a debugger.
|
|
409
|
+
console.log(\`[gate] listening on \${port} — signer \${signer}\`);
|
|
410
|
+
`,
|
|
411
|
+
|
|
412
|
+
'.env.example': `# The environment contract for \`pnpm gate\` (full table: @flayerlabs/gamemode-gate DEPLOY.md).
|
|
413
|
+
# Generate SIGNER_PRIVATE_KEY and SESSION_SECRET fresh for every environment — a key shared
|
|
414
|
+
# between two environments lets one gate's bug mint authorisations against the other's pools.
|
|
415
|
+
|
|
416
|
+
# The one chain this process serves. 84532 (Base Sepolia) is in the address book.
|
|
417
|
+
CHAIN_ID=84532
|
|
418
|
+
# An RPC endpoint for that chain; boot checks its eth_chainId matches.
|
|
419
|
+
RPC_URL=https://sepolia.base.org
|
|
420
|
+
# Your key, generated by you, never shared. openssl rand -hex 32 (prefix with 0x).
|
|
421
|
+
SIGNER_PRIVATE_KEY=
|
|
422
|
+
# Postgres. Migrations run at boot.
|
|
423
|
+
DATABASE_URL=
|
|
424
|
+
# At least 32 characters. openssl rand -hex 32
|
|
425
|
+
SESSION_SECRET=
|
|
426
|
+
# The page the player is looking at when they sign in — the embedding site, not this gate.
|
|
427
|
+
SIGN_IN_DOMAIN=flaunch.gg
|
|
428
|
+
# This gate's own public https origin — the exact value you submit as the Gate URL.
|
|
429
|
+
GATE_ORIGIN=
|
|
430
|
+
# Comma-separated origins allowed to call this gate: your game's hosted origin(s) and the pages
|
|
431
|
+
# that embed it. One * may span a single DNS label.
|
|
432
|
+
ALLOWED_ORIGINS=https://*.games.moongate.com,https://flaunch.gg
|
|
433
|
+
`,
|
|
434
|
+
|
|
435
|
+
'railpack.json': `${JSON.stringify(
|
|
436
|
+
{
|
|
437
|
+
$schema: 'https://schema.railpack.com',
|
|
438
|
+
build: { builder: 'RAILPACK', buildCommand: 'pnpm install' },
|
|
439
|
+
deploy: { startCommand: 'pnpm gate' },
|
|
440
|
+
},
|
|
441
|
+
null,
|
|
442
|
+
2,
|
|
443
|
+
)}\n`,
|
|
444
|
+
|
|
445
|
+
'src/play.ts': `import { connectHost, createMockRoom, joinRoom, type Snapshot } from '@flayerlabs/gamemode-client';
|
|
352
446
|
import { rules, type Action, type Config, type PlayerView, type PublicView } from './game/rules.js';
|
|
353
447
|
|
|
354
448
|
const config: Config = { rounds: 3, ceiling: 10, points: 500, roundMs: 10_000 };
|
|
355
|
-
|
|
449
|
+
|
|
450
|
+
// Embedded on flaunch.gg, the page tells this game which gate and round to join and services its
|
|
451
|
+
// wallet calls over the frame bridge. Standing alone (pnpm dev), there is no page to ask — null is
|
|
452
|
+
// that answer, and the mock room runs the same rules with the chain and wallet faked.
|
|
453
|
+
const embedded = await connectHost();
|
|
454
|
+
const room = embedded
|
|
455
|
+
? await joinRoom<PublicView, PlayerView, Action>({
|
|
456
|
+
gateUrl: embedded.context.gateUrl,
|
|
457
|
+
roundId: embedded.context.roundId,
|
|
458
|
+
host: embedded.host,
|
|
459
|
+
...(embedded.context.launch ? { launch: embedded.context.launch } : {}),
|
|
460
|
+
})
|
|
461
|
+
: createMockRoom(rules, { config, lobbyMs: 1_000, roundMs: 60_000 });
|
|
356
462
|
const app = document.querySelector<HTMLElement>('#app');
|
|
357
463
|
if (!app) throw new Error('the page needs an #app element');
|
|
358
464
|
const gameRoot = app;
|