@looop-games/cli 0.1.2 → 0.1.4

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,69 @@
1
+ ---
2
+ name: update-handbook
3
+ description: Record something durable this game just taught us — convert a playtest catch into an automated check, lock in a blessed feel value, or write down a design pillar. Use right after a creator playtest catches a defect the automated checks missed, when a feel value gets approved ("that jump is perfect — keep it"), or when a design principle emerges.
4
+ ---
5
+
6
+ # /update-handbook — keep what the game taught us
7
+
8
+ `handbook/` is this game's durable truth — the layer that AUGMENTS the
9
+ engine's read-only practices with what THIS game has learned. This skill is
10
+ how it grows.
11
+
12
+ **Every handbook write needs the creator's approval first.** The handbook is
13
+ *their* game's truth — propose the exact entry ("I'd like to record: …"),
14
+ and write it only after they say yes. Never slip an entry in as a side effect
15
+ of other work. (Smoke/test FILES don't need this gate — they're regression
16
+ tests, not blessed truth; only `handbook/` writes do.)
17
+
18
+ Three kinds of lesson, three destinations:
19
+
20
+ ## 1. A playtest caught a defect → an automated check
21
+
22
+ The premise (from the engine's `shared/practices/qa.md`): **a human catching a
23
+ defect means an automated check was missing.** Convert the *class* of defect,
24
+ not the instance:
25
+
26
+ 1. Name the miss precisely — not "the door was broken" but "doors can lose
27
+ their collision when the room resets, and nothing checks collision after a
28
+ reset."
29
+ 2. Prefer an **executable check**: write a `<aspect>.smoke.mjs` (or
30
+ `*.test.mjs`) that reproduces the defect — confirm it fails RED on the
31
+ broken state, then goes green on the fix. `npx looop test` discovers it
32
+ forever after; a guard you never saw fail is a guard you can't trust.
33
+ 3. **If the check needs to SEE the game's internals** (collision boxes, depth
34
+ order, hit areas) and the game has no debug overlay yet, **build one as
35
+ part of the conversion** — a keyboard-toggled draw of the real boxes/order.
36
+ It's a small one-time cost, and every later screenshot-verify reuses it
37
+ (master list row R2).
38
+ 4. Only if it truly can't be executed (needs human perception), add it as a
39
+ procedural step in **`handbook/qa.md`** — `/qa` runs those by hand each
40
+ time.
41
+
42
+ ## 2. A feel value got blessed → `handbook/feel.md`
43
+
44
+ When the creator locks a feel call ("that speed is exactly right"), record the
45
+ value, where it lives, and WHY it's right — so no future session "improves" it
46
+ away. If it's worth defending, pin it with a smoke too.
47
+
48
+ ## 3. A design principle emerged → `handbook/design.md`
49
+
50
+ When a decision reveals what this game IS ("never text tutorials — the world
51
+ teaches", "death must always be the player's fault"), write the pillar down.
52
+ Future builds check new ideas against these.
53
+
54
+ ## The upstream half
55
+
56
+ Before writing, ask: **is this lesson specific to this game, or would every
57
+ Looop game want it?** A generic hole (an engine component that breaks a
58
+ universal expectation, a check every game should run) belongs in the engine's
59
+ master list, not just this repo — offer `/feedback` so it lands upstream for
60
+ everyone. Do both when in doubt: the handbook entry protects this game now;
61
+ the feedback fixes it everywhere later.
62
+
63
+ ## Rules
64
+
65
+ - One lesson per invocation, converted fully — an entry someone can act on
66
+ cold, not a vague reminder.
67
+ - Handbook entries are durable truth: date them, keep them short, and when a
68
+ later decision supersedes one, update it in place rather than stacking
69
+ contradictions.
@@ -0,0 +1,86 @@
1
+ # {{name}} — a Looop game
2
+
3
+ <!-- looop:managed:start -->
4
+ <!-- Everything between the looop:managed markers belongs to the Looop
5
+ platform and is REWRITTEN when the engine updates. Add your own
6
+ instructions BELOW the end marker — updates never touch them. -->
7
+
8
+ This folder is a standalone Looop game. You (the agent) build it for a person
9
+ who is probably **not a developer** — the "creator". They describe the game in
10
+ plain language and make the judgment calls; you own everything else: process,
11
+ code, and verification. They should never need to learn git, npm, or servers.
12
+
13
+ ## Where things go
14
+
15
+ The workflows live as **skills** in `.claude/skills/` (aliased at
16
+ `.agents/skills/` — same files); each skill's `description` says when it
17
+ applies. This table routes only what has no skill:
18
+
19
+ | The creator wants… | Do this |
20
+ |---|---|
21
+ | To "save my work" | `git add -A && git commit` (and push, if the repo has a remote). Saving is local; it is **not** publishing. |
22
+ | To update Looop / the engine | `npx looop update` — moves the game to the latest engine release and re-pins `looop.engine` in `package.json`. The live game changes only on the next publish. |
23
+ | Better game feel (tuning speeds, jumps, timings) | The engine's **tweaks** library (`node_modules/@looop-games/engine/shared/ui/tweaks/`) + `shared/practices/feel.md`. |
24
+ | To change how an engine file behaves | Copy it to `overrides/shared/<same path>` and edit the copy — dev and publish serve your override instead of the official module. Imports stay `/shared/...`; never rewrite them. An override is a fork: that file stops receiving engine updates until you re-port it. |
25
+
26
+ ## Commands (run from this folder)
27
+
28
+ - `npx looop dev` — the full local stack: the game at
29
+ http://localhost:8000/games/{{name}}/index.html, multiplayer on :1999,
30
+ platform services (LLM etc.) on :8788. Leave it running; edits hot-reload.
31
+ **On a new machine the first `dev` signs the creator in: a browser window
32
+ opens, they log in and click Approve (~30 seconds, once per machine). Tell
33
+ them it's coming BEFORE you run it** — an unexplained browser popup mid-build
34
+ reads as something going wrong.
35
+ - `npx looop test` — the per-change gate: runs every `*.test.mjs` (unit, under
36
+ node) and every `*.smoke.mjs` (against a real dev stack it boots itself on
37
+ free ports) in this folder.
38
+ - `npx looop login` — connect this machine to the creator's Looop account
39
+ (once per machine; the first `looop dev` runs it automatically).
40
+ - `npx looop publish` — ship to https://play.looop.games/g/{{name}}.
41
+ **The first publish permanently claims the name `{{name}}` for the creator's
42
+ account** — from then on only they can update it. `--slug <alt>` publishes an
43
+ A/B copy without touching the live game.
44
+
45
+ ## The rules that are physics (not preference)
46
+
47
+ 1. **Multiplayer-first.** Any state other players can see (positions, physics,
48
+ scores, world objects) rides the room's authority — `room.update(...)`,
49
+ `room.send('input', ...)` — never local mutation only one client sees.
50
+ Verify multiplayer behaviour with TWO browser contexts, not one.
51
+ 2. **The engine is read-only.** `/shared/...` imports come from the installed
52
+ engine at `node_modules/@looop-games/engine` (not an npm dependency —
53
+ `looop dev` downloads the version pinned in `package.json`'s `looop.engine`
54
+ and reinstalls it if an `npm install` prunes it). Never edit files in
55
+ `node_modules` — use `overrides/shared/` (see the table above).
56
+ 3. **Never hand-roll a server.** Only `looop dev` (or `looop test`) serves
57
+ this game: they alias `/shared/...` and inject the platform layer. A plain
58
+ static server 404s every engine import and the game silently never boots.
59
+ 4. **Never handle tokens or keys.** Login is a browser device flow; platform
60
+ services (LLM etc.) are already wired through the local shim. If something
61
+ seems to need an API key, you're on the wrong path.
62
+ 5. **Read what's already known before writing game code.** First this game's
63
+ own **`handbook/`** (its locked truths — qa.md, feel.md, design.md), then
64
+ the engine craft docs at
65
+ `node_modules/@looop-games/engine/shared/practices/` — `feel.md`,
66
+ `architecture.md`, `multiplayer.md`, `qa.md` (and `eval.md` when anything
67
+ is LLM-generated), which apply to every game.
68
+ The handbook augments the generic docs; where they touch the same topic,
69
+ the handbook is this game's answer.
70
+ 6. **Anything with an assertion is a regression test.** Playwright checks live
71
+ in `<aspect>.smoke.mjs` files (excluded from publish, discovered by
72
+ `looop test`). Keep them; delete only assertion-free scratch.
73
+
74
+ ## This repo's own knowledge
75
+
76
+ - **`handbook/`** — durable truth about THIS game: `qa.md` (its checks),
77
+ `feel.md` (locked feel values), `design.md` (its pillars). Consult it before
78
+ working; grow it with `/update-handbook`.
79
+ - **`notes/`** — work tracking: `notes/plans/` (what's being built — `/build`
80
+ runs from these), `notes/todos/` (captured bugs/ideas), and
81
+ `notes/feedback/` (reports for the Looop team, written by `/feedback`).
82
+ Nothing under `notes/` is ever published.
83
+
84
+ <!-- looop:managed:end -->
85
+
86
+ <!-- Your own instructions go below this line. -->
@@ -0,0 +1,4 @@
1
+ @AGENTS.md
2
+
3
+ (If your tool didn't inline the line above: read AGENTS.md — it is this
4
+ project's agent instructions.)
@@ -0,0 +1,4 @@
1
+ @AGENTS.md
2
+
3
+ (If your tool didn't inline the line above: read AGENTS.md — it is this
4
+ project's agent instructions.)
@@ -0,0 +1,21 @@
1
+ // Boot smoke — the game's first regression test, shipped with the scaffold.
2
+ // Proves the dev stack serves the game page AND the engine alias (a plain
3
+ // static server would 404 every /shared/... import and the game would
4
+ // silently never boot). Runs under `npx looop test`, which boots a real dev
5
+ // stack and exports its URL. Grow your own checks as `<aspect>.smoke.mjs`
6
+ // files next to this one.
7
+ const url = process.env.LOOOP_TEST_GAME_URL || process.env.URL;
8
+ if (!url) {
9
+ console.error('boot smoke: no game URL — run this via `npx looop test`.');
10
+ process.exit(1);
11
+ }
12
+
13
+ const page = await fetch(url);
14
+ if (!page.ok) throw new Error(`game page: ${page.status} for ${url}`);
15
+ if (!(await page.text()).includes('<canvas')) throw new Error('game page served, but no canvas in it');
16
+
17
+ const engineModule = new URL('/shared/ui/room/client.js', url);
18
+ const engine = await fetch(engineModule);
19
+ if (!engine.ok) throw new Error(`engine alias broken: ${engineModule.pathname} → ${engine.status}`);
20
+
21
+ console.log('boot smoke: game page + engine alias OK');
@@ -0,0 +1,53 @@
1
+ // A tiny multiplayer plaza — walk around, see everyone else.
2
+ // This is a REAL Looop game: it already syncs across browsers and works
3
+ // published. Reshape it into your game; the room + identity plumbing stays.
4
+ import { createRoom, getIdentity } from '/shared/ui/room/client.js';
5
+
6
+ const me = getIdentity(); // injected by the platform (fails closed if absent)
7
+ const canvas = document.getElementById('game');
8
+ const ctx = canvas.getContext('2d');
9
+ const size = () => { canvas.width = innerWidth; canvas.height = innerHeight; };
10
+ addEventListener('resize', size); size();
11
+
12
+ const self = { name: me.name, color: me.color, x: Math.random() * 400 - 200, y: Math.random() * 400 - 200 };
13
+ // One room per game: the slug (injected by the serve path) is the room id.
14
+ const room = createRoom({ room: window.GAME_SLUG ?? 'dev', self });
15
+
16
+ let others = new Map();
17
+ room.onPlayers = (players) => { others = new Map(players); };
18
+ // Handy for smokes: how many players does this client see (self included)?
19
+ window.__looopPlayers = () => others.size + 1;
20
+
21
+ const keys = new Set();
22
+ addEventListener('keydown', (e) => keys.add(e.key.toLowerCase()));
23
+ addEventListener('keyup', (e) => keys.delete(e.key.toLowerCase()));
24
+
25
+ const SPEED = 220; // px/s — tune the feel!
26
+ let last = performance.now();
27
+ function tick(now) {
28
+ const dt = Math.min((now - last) / 1000, 0.05); last = now;
29
+ const dx = (keys.has('d') || keys.has('arrowright')) - (keys.has('a') || keys.has('arrowleft'));
30
+ const dy = (keys.has('s') || keys.has('arrowdown')) - (keys.has('w') || keys.has('arrowup'));
31
+ if (dx || dy) {
32
+ const n = Math.hypot(dx, dy);
33
+ self.x += (dx / n) * SPEED * dt;
34
+ self.y += (dy / n) * SPEED * dt;
35
+ room.update({ x: self.x, y: self.y });
36
+ }
37
+
38
+ ctx.fillStyle = '#0f1220';
39
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
40
+ const cx = canvas.width / 2, cy = canvas.height / 2;
41
+ const drawAvatar = (p) => {
42
+ ctx.fillStyle = p.color ?? '#888';
43
+ ctx.fillRect(cx + (p.x ?? 0) - self.x - 14, cy + (p.y ?? 0) - self.y - 14, 28, 28);
44
+ ctx.fillStyle = '#e7e9f4';
45
+ ctx.font = '12px system-ui';
46
+ ctx.textAlign = 'center';
47
+ ctx.fillText(p.name ?? '?', cx + (p.x ?? 0) - self.x, cy + (p.y ?? 0) - self.y - 22);
48
+ };
49
+ for (const p of others.values()) drawAvatar(p);
50
+ drawAvatar(self);
51
+ requestAnimationFrame(tick);
52
+ }
53
+ requestAnimationFrame(tick);
@@ -0,0 +1,2 @@
1
+ node_modules/
2
+ .looop/
@@ -0,0 +1,7 @@
1
+ # Design — the pillars
2
+
3
+ What this game IS: the principles new ideas get checked against. Written as
4
+ they emerge from real decisions (via `/update-handbook`), not invented up
5
+ front — an empty file is honest; a speculative pillar is a trap.
6
+
7
+ _No pillars yet._
@@ -0,0 +1,8 @@
1
+ # Feel — locked values
2
+
3
+ Feel calls the creator has blessed: the value, where it lives, and why it's
4
+ right — so no future session "improves" them away. General feel craft lives in
5
+ the engine's `shared/practices/feel.md`; this file is only what THIS game has
6
+ locked. `/update-handbook` adds entries.
7
+
8
+ _Nothing locked yet._
@@ -0,0 +1,9 @@
1
+ # QA — this game's own checks
2
+
3
+ Checks THIS game has earned, on top of the engine's master list
4
+ (`node_modules/@looop-games/engine/shared/practices/qa.md`) and the executable
5
+ tests `npx looop test` discovers. `/qa` runs all three sources; `/update-handbook`
6
+ adds entries here when a playtest catches something no automated check saw —
7
+ but prefer writing a `*.smoke.mjs` when the check can be executed.
8
+
9
+ _No game-specific checks yet. The first playtest catch starts this list._
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Mobile baseline (engine practices, architecture.md): locked viewport,
6
+ no double-tap/pinch zoom, no text selection on game UI. Required on
7
+ every Looop game — keep all four pieces when reshaping this file. -->
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
9
+ <title>{{name}}</title>
10
+ <style>
11
+ html, body { margin: 0; height: 100%; background: #0f1220; overflow: hidden; touch-action: manipulation; }
12
+ * { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; }
13
+ input, textarea, [contenteditable] { -webkit-user-select: text; user-select: text; }
14
+ canvas { display: block; width: 100vw; height: 100vh; }
15
+ </style>
16
+ <script>
17
+ // Safari ignores user-scalable=no — block iOS gesture-zoom too.
18
+ for (const ev of ['gesturestart', 'gesturechange', 'gestureend'])
19
+ document.addEventListener(ev, (e) => e.preventDefault());
20
+ </script>
21
+ </head>
22
+ <body>
23
+ <canvas id="game"></canvas>
24
+ <script type="module" src="game.js"></script>
25
+ </body>
26
+ </html>
@@ -1,151 +0,0 @@
1
- // The agent-facing surface `looop create` writes into every game folder
2
- // (cuqfzo Q4 addendum: no plugin — per-project files are what every agent CLI
3
- // actually reads).
4
- //
5
- // Layout: AGENTS.md is CANONICAL (the emerging cross-vendor standard, read by
6
- // Codex and friends). CLAUDE.md and GEMINI.md are thin pointers that @import
7
- // it, with a plain-text fallback for tools without import support. The looop
8
- // skill lands at .claude/skills/looop/SKILL.md (Claude Code loads project
9
- // skills from there). Because these ship inside @looop-games/cli, the
10
- // instructions always match the CLI/engine version that scaffolded the game —
11
- // the skills-vs-CLI drift a marketplace plugin would reintroduce.
12
- import { mkdirSync, writeFileSync } from 'node:fs';
13
- import { join } from 'node:path';
14
-
15
- export const agentsMd = (name) => `# ${name} — a Looop game
16
-
17
- This folder is a standalone Looop game. You (the agent) build it for a person
18
- who is not a developer — they describe the game in plain language, you make it
19
- real. The game is multiplayer-first: every behaviour must work with other
20
- players in the room (shared state rides the room's authority, never local
21
- mutation).
22
-
23
- ## Commands (run from this folder)
24
-
25
- - \`npx looop dev\` — full local stack: the game at
26
- http://localhost:8000/games/${name}/index.html, multiplayer on :1999, and
27
- platform services (LLM etc.) on :8788. Leave it running; edits hot-reload.
28
- - \`npx looop login\` — connect this machine to the person's Looop account
29
- (needed once per machine; the first \`looop dev\` runs it automatically —
30
- the engine downloads from the platform and every download needs the account).
31
- - \`npx looop publish\` — ship to https://play.looop.games/g/${name}.
32
- \`--slug <alt>\` publishes an A/B copy without touching the live game.
33
-
34
- ## The engine
35
-
36
- \`/shared/...\` imports come from the installed engine at
37
- \`node_modules/@looop-games/engine\` (not an npm dependency — \`looop dev\`
38
- downloads the version pinned in package.json's \`looop.engine\` from the
39
- platform, and reinstalls it automatically if an \`npm install\` prunes it) —
40
- browse \`node_modules/@looop-games/engine/shared/ui/\` for the component library.
41
- **Read \`node_modules/@looop-games/engine/shared/practices/\` (feel.md,
42
- architecture.md, multiplayer.md, qa.md) before writing game code** — that's
43
- the studio's craft knowledge and it applies here.
44
-
45
- The engine is read-only. To modify an engine file for THIS game, copy it to
46
- \`overrides/shared/<same path>\` and edit the copy — dev and publish both
47
- serve your override instead of the official module (imports stay
48
- \`/shared/...\`; never rewrite them). The override is a fork: engine updates
49
- to that file stop arriving until you re-port it.
50
-
51
- ## Verification
52
-
53
- Playwright-style checks belong in \`<aspect>.smoke.mjs\` files (excluded from
54
- publish). Anything with an assertion is a regression test — keep it.
55
- `;
56
-
57
- // CLAUDE.md / GEMINI.md: inline AGENTS.md via @import (both tools support
58
- // it); the second line is the graceful fallback where imports aren't a thing.
59
- const pointerMd = `@AGENTS.md
60
-
61
- (If your tool didn't inline the line above: read AGENTS.md — it is this
62
- project's agent instructions.)
63
- `;
64
-
65
- export const skillMd = () => `---
66
- name: looop
67
- description: Build, run, and publish a multiplayer game on Looop (play.looop.games). Use whenever the user wants to make a game, change their Looop game, preview it, log in, or publish it.
68
- ---
69
-
70
- # Looop — build multiplayer games for play.looop.games
71
-
72
- You are building a game for a person who is probably **not a developer**. They
73
- describe what they want in plain language; you make it real, run it, and show
74
- them. They should never need to learn git, npm, or servers — those are yours.
75
-
76
- Looop games are **multiplayer-first**: the full stack (game + room server +
77
- platform services) runs locally from minute one, and every behaviour must work
78
- with other players in the room. There is no "add multiplayer later."
79
-
80
- ## The commands (run inside the game folder)
81
-
82
- \`\`\`bash
83
- npx @looop-games/cli create <name> # new game folder — multiplayer works out of the box
84
- npx looop dev # local stack: game on :8000, rooms :1999, services :8788
85
- npx looop login # device-flow login — browser opens, user clicks Approve
86
- npx looop publish # live at https://play.looop.games/g/<name>
87
- npx looop publish --slug <alt> # A/B copy at /g/<alt> — never touches the live game
88
- npx looop whoami # which account this machine publishes as
89
- \`\`\`
90
-
91
- - \`<name>\` becomes the URL: lowercase letters, digits, dashes.
92
- - \`looop dev\` hot-reloads on every edit; leave it running in a dedicated
93
- terminal and give the user the URL to open.
94
- - \`login\` is needed once per machine — the first \`looop dev\` triggers it
95
- automatically (the engine downloads from the platform, and every download
96
- needs the user's account). Tell the user: "your browser will open — sign in
97
- and click Approve." The code is shown in the terminal; never handle any
98
- token yourself.
99
-
100
- ## The engine (\`/shared/...\`)
101
-
102
- Game code imports the engine by absolute path, e.g.
103
- \`import { createRoom } from '/shared/ui/room/client.js'\`. Those files come
104
- from the installed engine at \`node_modules/@looop-games/engine\` — downloaded
105
- from the platform by \`looop dev\` (the version is pinned in package.json's
106
- \`looop.engine\`; it is deliberately not an npm dependency):
107
-
108
- - **Browse the library** at \`node_modules/@looop-games/engine/shared/ui/\` —
109
- components for rooms, physics primitives, audio, joysticks, leaderboards,
110
- LLM-driven NPCs, and more. Read a component's README before using it.
111
- - **Read the craft docs** at \`node_modules/@looop-games/engine/shared/practices/\`
112
- (\`feel.md\`, \`architecture.md\`, \`multiplayer.md\`, \`qa.md\`) before writing
113
- game code — they are the studio's accumulated knowledge and they apply to
114
- every game.
115
-
116
- **The engine is read-only.** To change engine behaviour for this game, copy
117
- the file to \`overrides/shared/<same path>\` and edit the copy — both dev and
118
- publish serve your override instead of the official module. Keep the game's
119
- imports as \`/shared/...\`; never rewrite them. An override is a fork: that file
120
- stops receiving engine updates until you re-port it, so prefer game-local code
121
- when possible.
122
-
123
- ## Multiplayer rules (non-negotiable)
124
-
125
- - Any state other players can see (positions, physics, scores, world objects)
126
- rides the room's authority — \`room.update(...)\`, \`room.send('input', ...)\`,
127
- world specs — never local mutation that only one client sees.
128
- - Verify multiplayer with two browser contexts (Playwright), not one.
129
-
130
- ## Verification
131
-
132
- - Write Playwright checks as \`<aspect>.smoke.mjs\` in the game folder; they are
133
- excluded from publish. Anything with an assertion is a regression test — keep it.
134
- - Before saying something works: run it. Boot the game, drive the behaviour,
135
- screenshot visuals and look at them.
136
-
137
- ## Services (LLM etc.)
138
-
139
- Platform services are already wired: in dev, calls to \`/api/llm\` ride the
140
- local shim on :8788 with the user's login; published games call same-origin.
141
- The user never needs an API key — if something asks for one, you're on the
142
- wrong path.
143
- `;
144
-
145
- export function writeAgentFiles(dir, name) {
146
- writeFileSync(join(dir, 'AGENTS.md'), agentsMd(name));
147
- writeFileSync(join(dir, 'CLAUDE.md'), pointerMd);
148
- writeFileSync(join(dir, 'GEMINI.md'), pointerMd);
149
- mkdirSync(join(dir, '.claude', 'skills', 'looop'), { recursive: true });
150
- writeFileSync(join(dir, '.claude', 'skills', 'looop', 'SKILL.md'), skillMd());
151
- }