@flayerlabs/gamemode-cli 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 +9 -0
- package/README.md +50 -0
- package/dist/AGENTS.md +213 -0
- package/dist/build-game-mode/SKILL.md +57 -0
- package/dist/build-game-mode/agents/openai.yaml +4 -0
- package/dist/lint.d.ts +20 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +70 -0
- package/dist/lint.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +65 -0
- package/dist/main.js.map +1 -0
- package/dist/scaffold.d.ts +13 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +553 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +37 -0
- package/src/lint.ts +85 -0
- package/src/main.ts +70 -0
- package/src/scaffold.ts +587 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flayer Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @flayerlabs/gamemode-cli
|
|
2
|
+
|
|
3
|
+
Create a playable Flaunch Game Mode and check that its rules use deterministic inputs.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
You need Node.js 20 or later and pnpm.
|
|
8
|
+
|
|
9
|
+
## Create a game
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm dlx @flayerlabs/gamemode-cli new my-game
|
|
15
|
+
cd my-game
|
|
16
|
+
pnpm install
|
|
17
|
+
pnpm dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The command creates a working number game. It includes:
|
|
21
|
+
|
|
22
|
+
- server-authoritative rules in `src/game/rules.ts`
|
|
23
|
+
- a browser game that uses the offline room
|
|
24
|
+
- rule tests and a deterministic rules check
|
|
25
|
+
- an `AGENTS.md` file with the rules an agent must follow
|
|
26
|
+
- a `$build-game-mode` repository skill for supported agents
|
|
27
|
+
|
|
28
|
+
The offline room runs the real rules without a gate, chain or wallet. The scaffold does not create a
|
|
29
|
+
live gate or trusted wallet integration.
|
|
30
|
+
|
|
31
|
+
## Check an existing rules file
|
|
32
|
+
|
|
33
|
+
Install the CLI in the game project, then run the check:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm add --save-dev @flayerlabs/gamemode-cli
|
|
37
|
+
pnpm exec gamemode check src/game/rules.ts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The check rejects known uses of clocks, random values, network access and system access. It also
|
|
41
|
+
rejects top-level `let` and `var` declarations. Run the project's tests and type check as well.
|
|
42
|
+
|
|
43
|
+
## Build a secure game
|
|
44
|
+
|
|
45
|
+
Use the repository guidance to continue:
|
|
46
|
+
|
|
47
|
+
- [build a Game Mode](https://github.com/flayerlabs/gamemode-sdk/blob/main/docs/guides/build-a-game.md)
|
|
48
|
+
- [read the generated-game agent rules](https://github.com/flayerlabs/gamemode-sdk/blob/main/packages/cli/templates/game/AGENTS.template.md)
|
|
49
|
+
- [compare the quiz and arrow examples](https://github.com/flayerlabs/gamemode-sdk/blob/main/examples/README.md)
|
|
50
|
+
- [review the Game Mode SDK repository](https://github.com/flayerlabs/gamemode-sdk)
|
package/dist/AGENTS.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Build a Flaunch Game Mode
|
|
2
|
+
|
|
3
|
+
This repository contains a game that awards points during a Flaunch token launch. The game can use
|
|
4
|
+
any presentation or control scheme. The server rules decide which actions earn points.
|
|
5
|
+
|
|
6
|
+
Use `$build-game-mode` when the skill is available. It gives you the safest order for changing the
|
|
7
|
+
rules, tests and browser game.
|
|
8
|
+
|
|
9
|
+
## Know what runs where
|
|
10
|
+
|
|
11
|
+
The project starts with these files:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
src/game/rules.ts pure server rules and scoring
|
|
15
|
+
src/play.ts browser game and room subscriptions
|
|
16
|
+
test/rules.test.ts replay, scoring and secrecy checks
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The browser proposes actions. It never awards points.
|
|
20
|
+
|
|
21
|
+
The gate runs `src/game/rules.ts` and stores every accepted event. A replay of the same commands
|
|
22
|
+
must always produce the same state and score.
|
|
23
|
+
|
|
24
|
+
## Use the development loop
|
|
25
|
+
|
|
26
|
+
Run these commands:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm dev
|
|
30
|
+
pnpm test
|
|
31
|
+
pnpm typecheck
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`pnpm dev` runs the real rules in a local mock room. It needs no chain, server or wallet.
|
|
35
|
+
|
|
36
|
+
`pnpm test` runs the rules check and game tests. `pnpm typecheck` checks the whole project. Both
|
|
37
|
+
must pass before you finish.
|
|
38
|
+
|
|
39
|
+
The scaffold does not include a live gate. Add `dev:live` only when you have a real gate harness to
|
|
40
|
+
run. Follow the [live gate guide](https://github.com/flayerlabs/gamemode-sdk/blob/main/docs/guides/run-a-gate.md)
|
|
41
|
+
instead of inventing a second protocol.
|
|
42
|
+
|
|
43
|
+
## Keep the rules deterministic
|
|
44
|
+
|
|
45
|
+
Keep `decide()` and `evolve()` pure.
|
|
46
|
+
|
|
47
|
+
Do not use these values or operations in the rules module:
|
|
48
|
+
|
|
49
|
+
- a process or device clock
|
|
50
|
+
- random values that do not come from the supplied seed
|
|
51
|
+
- network, file or database access
|
|
52
|
+
- environment variables
|
|
53
|
+
- mutable module-level state
|
|
54
|
+
|
|
55
|
+
Time arrives as `command.at`. Player randomness arrives as a join seed. Derive any game variation
|
|
56
|
+
from those values.
|
|
57
|
+
|
|
58
|
+
`gamemode check` catches common nondeterministic calls, Node imports and module-level `let` or
|
|
59
|
+
`var`. It is a guard, not a proof. Keep replay tests for every scoring path.
|
|
60
|
+
|
|
61
|
+
## Treat every action as untrusted
|
|
62
|
+
|
|
63
|
+
`room.send(action)` proposes an action. `decide()` accepts or refuses it.
|
|
64
|
+
|
|
65
|
+
Do not send a score, hit, kill or trusted timestamp from the browser. Send the smallest input the
|
|
66
|
+
server needs to verify the result.
|
|
67
|
+
|
|
68
|
+
For a shooter, send bounded movement, aim and fire input. The rules should enforce sequence,
|
|
69
|
+
cooldown, ammunition and hit logic. The browser may predict and animate at its own frame rate.
|
|
70
|
+
|
|
71
|
+
Use `parseAction()` to reject malformed and oversized values before `decide()` sees them. Use game
|
|
72
|
+
state in `decide()` to enforce limits such as one answer, one shot or one batch at a time.
|
|
73
|
+
|
|
74
|
+
## Keep private state out of public types
|
|
75
|
+
|
|
76
|
+
`publicView()` goes to every player and spectator. `playerView()` goes to one player.
|
|
77
|
+
|
|
78
|
+
If a value must stay private, leave it out of the `publicView()` return type. Do not return it and
|
|
79
|
+
filter it later.
|
|
80
|
+
|
|
81
|
+
For example, keep a quiz answer out of the public type until the reveal. Add a test that serialises
|
|
82
|
+
the public view and proves the secret is absent.
|
|
83
|
+
|
|
84
|
+
## Choose when to award points
|
|
85
|
+
|
|
86
|
+
Award points when the result should become known.
|
|
87
|
+
|
|
88
|
+
An arrow score can be public as soon as the arrow lands. Award it with the accepted action.
|
|
89
|
+
|
|
90
|
+
A quiz answer should stay private until the reveal. Store the answer event first. Award points from
|
|
91
|
+
the later wake that reveals the result. Otherwise the player's allowance exposes the answer early.
|
|
92
|
+
|
|
93
|
+
## Keep wallets outside the game
|
|
94
|
+
|
|
95
|
+
Creator code must not access a wallet, private key, RPC endpoint or transaction calldata.
|
|
96
|
+
|
|
97
|
+
Use the room economy:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
await room.economy.buy(maxSpendWei)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The trusted parent validates the gate authorisation, builds the transaction and asks the player to
|
|
104
|
+
approve it. Game code never handles `hookData`.
|
|
105
|
+
|
|
106
|
+
## Declare the maximum score
|
|
107
|
+
|
|
108
|
+
`rewardBounds(config)` must return the highest score one player can earn.
|
|
109
|
+
|
|
110
|
+
Derive the number from the rules. For example, multiply the shot budget by the best shot, or add
|
|
111
|
+
the points for every question.
|
|
112
|
+
|
|
113
|
+
Do not use an estimate. The platform uses this bound to check the launch wallet cap. Points above
|
|
114
|
+
an understated bound cannot become spending allowance.
|
|
115
|
+
|
|
116
|
+
## Build fixed timelines once
|
|
117
|
+
|
|
118
|
+
Use `scheduleWithin()` for a fixed series of phases:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { scheduleWithin } from '@flayerlabs/gamemode-spec/schedule'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It checks that the full schedule fits between `opensAt` and `closesAt`. It also gives each phase an
|
|
125
|
+
absolute time, so a delayed server wake does not move every later phase.
|
|
126
|
+
|
|
127
|
+
Every `nextWakeAt()` value must be later than the wake being served. Return `null` when the game has
|
|
128
|
+
no more timed work.
|
|
129
|
+
|
|
130
|
+
## Model readiness as a game rule
|
|
131
|
+
|
|
132
|
+
Presence tells you how many players are connected. It does not start the game.
|
|
133
|
+
|
|
134
|
+
If players must ready up, define a ready action and store it in game state:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
{ kind: 'ready', ready: true }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This lets `decide()` apply the rule. It also makes readiness replayable after a restart.
|
|
141
|
+
|
|
142
|
+
## Use the module contract
|
|
143
|
+
|
|
144
|
+
Implement the complete rules module:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
export const rules = defineGame<Config, State, Event, Action, PublicView, PlayerView>({
|
|
148
|
+
id: 'my-game',
|
|
149
|
+
parseAction,
|
|
150
|
+
initRound,
|
|
151
|
+
decide,
|
|
152
|
+
evolve,
|
|
153
|
+
publicView,
|
|
154
|
+
playerView,
|
|
155
|
+
nextWakeAt,
|
|
156
|
+
rewardBounds,
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Commands reaching `decide()` are `join`, `leave`, `action` and `wake`. Each command includes the
|
|
161
|
+
authoritative `at` time.
|
|
162
|
+
|
|
163
|
+
A decision returns events and may return awards. `evolve()` applies events in order. A refusal
|
|
164
|
+
returns a stable code:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
return { refuse: 'answer.window_closed' }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Do not rename a refusal code after use. Add a new code. Keep player-facing copy in the browser.
|
|
171
|
+
|
|
172
|
+
## Use the room capabilities
|
|
173
|
+
|
|
174
|
+
The browser uses this surface:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
room.subscribe(render)
|
|
178
|
+
await room.send(action)
|
|
179
|
+
room.now()
|
|
180
|
+
|
|
181
|
+
room.launch.current()
|
|
182
|
+
room.connection.subscribe(renderConnection)
|
|
183
|
+
room.economy.subscribe(renderEconomy)
|
|
184
|
+
room.economy.buy(maxSpendWei)
|
|
185
|
+
room.market.subscribe(renderMarket)
|
|
186
|
+
await room.identity.resolve(playerIds)
|
|
187
|
+
room.presence.subscribe(renderPresence)
|
|
188
|
+
room.social.react(id)
|
|
189
|
+
room.social.onReaction(renderReaction)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Use `room.now()` for every visible timer. Do not use `Date.now()` in a countdown.
|
|
193
|
+
|
|
194
|
+
The local mock implements the same room surface. Build against it before adding live
|
|
195
|
+
infrastructure.
|
|
196
|
+
|
|
197
|
+
## Finish with evidence
|
|
198
|
+
|
|
199
|
+
Before you finish, check that:
|
|
200
|
+
|
|
201
|
+
- correct play earns the expected points
|
|
202
|
+
- invalid and repeated actions are refused
|
|
203
|
+
- action sizes and rates are bounded
|
|
204
|
+
- public views contain no secrets
|
|
205
|
+
- private views contain only that player's data
|
|
206
|
+
- delayed wakes keep the authored schedule
|
|
207
|
+
- replaying the same commands gives the same result
|
|
208
|
+
- `rewardBounds()` matches the best possible score
|
|
209
|
+
- `pnpm test` passes
|
|
210
|
+
- `pnpm typecheck` passes
|
|
211
|
+
|
|
212
|
+
If the SDK contract does not fit the game, stop and explain the missing capability. Do not bypass a
|
|
213
|
+
trust boundary in browser code.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-game-mode
|
|
3
|
+
description: Builds or changes a creator Game Mode with @flayerlabs/gamemode-spec and @flayerlabs/gamemode-client. Use when working on rules, actions, views, scoring, room capabilities, browser play, mock rooms or conformance tests in this SDK or a generated game. Do not use for trusted parent, registry writer or platform-owned gate changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a Game Mode
|
|
7
|
+
|
|
8
|
+
Follow the local `AGENTS.md` as the contract. Keep this skill focused on the workflow and do not replace those rules with guesses.
|
|
9
|
+
|
|
10
|
+
## Find the working context
|
|
11
|
+
|
|
12
|
+
1. Read the nearest `AGENTS.md` in full before editing anything.
|
|
13
|
+
2. Inspect `package.json`, the game rules and their tests.
|
|
14
|
+
3. If this is the SDK, read the closest reference game. Use `examples/arrow` for continuous public scoring. Use `examples/quiz` for hidden answers, phases and deferred awards.
|
|
15
|
+
4. If this is a generated game, keep the shipped rules and tests as the working starting point.
|
|
16
|
+
|
|
17
|
+
## Define the game before editing
|
|
18
|
+
|
|
19
|
+
State assumptions and set verifiable success criteria for:
|
|
20
|
+
|
|
21
|
+
- the authoritative state and accepted commands
|
|
22
|
+
- the source of time and deterministic variation
|
|
23
|
+
- hidden values and their reveal point
|
|
24
|
+
- action shape, size, cooldown and round limits
|
|
25
|
+
- acceptance, scoring and award timing
|
|
26
|
+
- the maximum possible reward
|
|
27
|
+
- refusal codes and observable outcomes
|
|
28
|
+
|
|
29
|
+
If the SDK interface cannot represent the game safely, stop and describe the missing seam. Do not build a parallel protocol.
|
|
30
|
+
|
|
31
|
+
## Build the smallest complete loop
|
|
32
|
+
|
|
33
|
+
1. Write or update tests for the intended rule change.
|
|
34
|
+
2. Define every shared shape once. Import SDK contract types instead of copying them.
|
|
35
|
+
3. Bound malformed and oversized input in `parseAction`.
|
|
36
|
+
4. Implement `decide` and `evolve` as pure functions. Derive time and variation only from supplied inputs.
|
|
37
|
+
5. Make hidden data absent from public view types. Reveal it only when the rules allow.
|
|
38
|
+
6. Keep action acceptance separate from awards when an early balance change would leak a result.
|
|
39
|
+
7. Calculate `rewardBounds` from the rules, including the best possible play.
|
|
40
|
+
8. Build the browser around `Room` capabilities. Keep scoring, wallets, keys and calldata out of creator client code.
|
|
41
|
+
9. Prove the loop with the mock room before adding a project-specific live harness.
|
|
42
|
+
|
|
43
|
+
## Verify the result
|
|
44
|
+
|
|
45
|
+
In a generated game:
|
|
46
|
+
|
|
47
|
+
1. Run `pnpm test`.
|
|
48
|
+
2. Run `pnpm typecheck`.
|
|
49
|
+
3. Run `pnpm dev` and check the changed play loop when presentation changed.
|
|
50
|
+
|
|
51
|
+
In the SDK:
|
|
52
|
+
|
|
53
|
+
1. Run the affected example or package tests and typecheck.
|
|
54
|
+
2. Run the root typecheck and required repository tests.
|
|
55
|
+
3. Run `pnpm release:check` when a published contract or package surface changed.
|
|
56
|
+
|
|
57
|
+
Check replay, reconnect, hidden-data leaks, late actions, duplicate actions, oversized input, wake timing and the reward ceiling where they apply. If infrastructure blocks a check, report the exact check you could not run. Do not weaken or skip it.
|
package/dist/lint.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking that a game's rules are pure.
|
|
3
|
+
*
|
|
4
|
+
* `decide` and `evolve` must give the same answer for the same input, forever. Replay, reconnect,
|
|
5
|
+
* audit and anti-cheat all rest on it, and every one of them fails quietly rather than loudly when
|
|
6
|
+
* it stops being true — a round that scores differently on replay does not announce itself.
|
|
7
|
+
*
|
|
8
|
+
* Text rather than a full parse, deliberately. An agent writing `Date.now()` in a reducer is the
|
|
9
|
+
* failure this catches, and it catches that perfectly. A syntax tree would catch cleverer evasions
|
|
10
|
+
* that nobody is attempting, at the cost of a dependency and a lot of code.
|
|
11
|
+
*/
|
|
12
|
+
export interface Finding {
|
|
13
|
+
line: number;
|
|
14
|
+
found: string;
|
|
15
|
+
why: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function lintRules(source: string): Finding[];
|
|
18
|
+
/** What a person reads. No codes, no rule names — the line, what is there, and what to do. */
|
|
19
|
+
export declare function report(file: string, findings: Finding[]): string;
|
|
20
|
+
//# sourceMappingURL=lint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAmCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,CAoBnD;AAED,8FAA8F;AAC9F,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAUhE"}
|
package/dist/lint.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checking that a game's rules are pure.
|
|
3
|
+
*
|
|
4
|
+
* `decide` and `evolve` must give the same answer for the same input, forever. Replay, reconnect,
|
|
5
|
+
* audit and anti-cheat all rest on it, and every one of them fails quietly rather than loudly when
|
|
6
|
+
* it stops being true — a round that scores differently on replay does not announce itself.
|
|
7
|
+
*
|
|
8
|
+
* Text rather than a full parse, deliberately. An agent writing `Date.now()` in a reducer is the
|
|
9
|
+
* failure this catches, and it catches that perfectly. A syntax tree would catch cleverer evasions
|
|
10
|
+
* that nobody is attempting, at the cost of a dependency and a lot of code.
|
|
11
|
+
*/
|
|
12
|
+
const RULES = [
|
|
13
|
+
{ pattern: /\bDate\s*\.\s*now\s*\(/, why: 'time must come from the command, as command.at' },
|
|
14
|
+
{ pattern: /\bnew\s+Date\s*\(/, why: 'time must come from the command, as command.at' },
|
|
15
|
+
{ pattern: /\bperformance\s*\.\s*now\s*\(/, why: 'time must come from the command, as command.at' },
|
|
16
|
+
{ pattern: /\bMath\s*\.\s*random\s*\(/, why: 'randomness must be derived from the seed on the join command' },
|
|
17
|
+
{ pattern: /\bcrypto\s*\.\s*(getRandomValues|randomUUID)\s*\(/, why: 'randomness must be derived from the seed on the join command' },
|
|
18
|
+
{ pattern: /\bfetch\s*\(/, why: 'rules cannot reach the network' },
|
|
19
|
+
{ pattern: /\brequire\s*\(\s*['"]node:/, why: 'rules cannot reach the system' },
|
|
20
|
+
{ pattern: /\bfrom\s+['"]node:/, why: 'rules cannot reach the system' },
|
|
21
|
+
{ pattern: /\bprocess\s*\.\s*env\b/, why: 'rules cannot read configuration; it belongs in the round config' },
|
|
22
|
+
{ pattern: /\bglobalThis\b/, why: 'rules cannot reach outside themselves' },
|
|
23
|
+
];
|
|
24
|
+
/** Lines that are only a comment. A rule named in prose is documentation, not a violation. */
|
|
25
|
+
function isComment(line) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
return trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Module-level mutable state: a `let` or `var` at column zero.
|
|
31
|
+
*
|
|
32
|
+
* Two calls with the same arguments must not differ because of something one of them left behind.
|
|
33
|
+
*/
|
|
34
|
+
function moduleState(line) {
|
|
35
|
+
return /^(let|var)\s+\w/.test(line);
|
|
36
|
+
}
|
|
37
|
+
export function lintRules(source) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
source.split('\n').forEach((line, index) => {
|
|
40
|
+
if (isComment(line))
|
|
41
|
+
return;
|
|
42
|
+
for (const rule of RULES) {
|
|
43
|
+
const match = rule.pattern.exec(line);
|
|
44
|
+
if (match)
|
|
45
|
+
findings.push({ line: index + 1, found: match[0].replace(/\s+/g, ''), why: rule.why });
|
|
46
|
+
}
|
|
47
|
+
if (moduleState(line)) {
|
|
48
|
+
findings.push({
|
|
49
|
+
line: index + 1,
|
|
50
|
+
found: line.trim().split(/\s+/).slice(0, 2).join(' '),
|
|
51
|
+
why: 'rules cannot keep state between calls; it belongs in the game state',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return findings;
|
|
56
|
+
}
|
|
57
|
+
/** What a person reads. No codes, no rule names — the line, what is there, and what to do. */
|
|
58
|
+
export function report(file, findings) {
|
|
59
|
+
if (findings.length === 0)
|
|
60
|
+
return `${file}: rules look pure.`;
|
|
61
|
+
return [
|
|
62
|
+
`${file}: ${findings.length} thing${findings.length === 1 ? '' : 's'} to fix.`,
|
|
63
|
+
'',
|
|
64
|
+
...findings.map((f) => ` line ${f.line}: ${f.found} — ${f.why}`),
|
|
65
|
+
'',
|
|
66
|
+
'These have to go. Same input, same answer, every time — replay, reconnect and anti-cheat',
|
|
67
|
+
'all depend on it, and each of them fails quietly rather than loudly when it stops being true.',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=lint.js.map
|
package/dist/lint.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lint.js","sourceRoot":"","sources":["../src/lint.ts"],"names":[],"mappings":"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"}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { lintRules, report } from './lint.js';
|
|
4
|
+
import { scaffold } from './scaffold.js';
|
|
5
|
+
/**
|
|
6
|
+
* The whole command line.
|
|
7
|
+
*
|
|
8
|
+
* Four things, because four is what it takes to go from nothing to a game somebody can play, and a
|
|
9
|
+
* fifth would be something to learn rather than something to use.
|
|
10
|
+
*/
|
|
11
|
+
const USAGE = `
|
|
12
|
+
gamemode — build a game for a Flaunch launch
|
|
13
|
+
|
|
14
|
+
gamemode new <name> start a new game you can play straight away
|
|
15
|
+
gamemode check [file] check your rules are pure (default: src/game/rules.ts)
|
|
16
|
+
|
|
17
|
+
Once you have a game:
|
|
18
|
+
|
|
19
|
+
pnpm dev play it, with no server and no wallet
|
|
20
|
+
pnpm test check it still works
|
|
21
|
+
`;
|
|
22
|
+
async function main(argv) {
|
|
23
|
+
const [command, argument] = argv;
|
|
24
|
+
if (!command || command === 'help' || command === '--help') {
|
|
25
|
+
console.log(USAGE.trim());
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
if (command === 'new') {
|
|
29
|
+
if (!argument) {
|
|
30
|
+
console.error('What should it be called? gamemode new my-game');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const created = await scaffold(argument);
|
|
34
|
+
console.log(`\n Made ${created}\n`);
|
|
35
|
+
console.log(' Next:');
|
|
36
|
+
console.log(` cd ${argument}`);
|
|
37
|
+
console.log(' pnpm install');
|
|
38
|
+
console.log(' pnpm dev\n');
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
if (command === 'check') {
|
|
42
|
+
const file = argument ?? 'src/game/rules.ts';
|
|
43
|
+
let source;
|
|
44
|
+
try {
|
|
45
|
+
source = readFileSync(file, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
console.error(`Could not read ${file}.`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
const findings = lintRules(source);
|
|
52
|
+
console.log(report(file, findings));
|
|
53
|
+
return findings.length === 0 ? 0 : 1;
|
|
54
|
+
}
|
|
55
|
+
console.error(`Not a command: ${command}`);
|
|
56
|
+
console.error(USAGE.trim());
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
main(process.argv.slice(2))
|
|
60
|
+
.then((code) => process.exit(code))
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
console.error(error instanceof Error ? error.message : error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,KAAK,GAAG;;;;;;;;;;CAUb,CAAC;AAEF,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC;IAEjC,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC3D,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACjE,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzC,OAAO,CAAC,GAAG,CAAC,YAAY,OAAO,IAAI,CAAC,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC9B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,IAAI,mBAAmB,CAAC;QAC7C,IAAI,MAAc,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,KAAK,CAAC,kBAAkB,IAAI,GAAG,CAAC,CAAC;YACzC,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QACnC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QACpC,OAAO,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC;IAC3C,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5B,OAAO,CAAC,CAAC;AACX,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACxB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;KAClC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACxB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A new game, playable before it does anything.
|
|
3
|
+
*
|
|
4
|
+
* The first command has to produce something that runs. A scaffold that needs a database, a chain
|
|
5
|
+
* and a wallet before it shows anything is a scaffold most people abandon, and an agent given one
|
|
6
|
+
* has nothing to check its work against.
|
|
7
|
+
*
|
|
8
|
+
* So what comes out is a complete, working guessing game: rules, a page, and tests that pass. It is
|
|
9
|
+
* meant to be edited into something else, not read and replaced.
|
|
10
|
+
*/
|
|
11
|
+
export declare function scaffold(name: string): Promise<string>;
|
|
12
|
+
export declare function dependencyVersionFor(version: unknown): string;
|
|
13
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +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;AA+BD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAW7D"}
|