@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.
- package/bin/looop.mjs +16 -0
- package/lib/bundle-primitives.mjs +77 -0
- package/lib/bundle-primitives.test.mjs +95 -0
- package/lib/create.mjs +114 -81
- package/lib/create.test.mjs +353 -12
- package/lib/dev.mjs +27 -2
- package/lib/dev.test.mjs +52 -0
- package/lib/engine.mjs +2 -2
- package/lib/feedback.mjs +87 -0
- package/lib/feedback.test.mjs +144 -0
- package/lib/npm.mjs +38 -0
- package/lib/npm.test.mjs +61 -0
- package/lib/publish.mjs +18 -3
- package/lib/publish.test.mjs +44 -0
- package/lib/test-cmd.mjs +135 -0
- package/lib/test-cmd.test.mjs +118 -0
- package/lib/update.mjs +51 -0
- package/lib/update.test.mjs +98 -0
- package/package.json +5 -2
- package/template/.claude/skills/build/SKILL.md +240 -0
- package/template/.claude/skills/engine/SKILL.md +59 -0
- package/template/.claude/skills/feedback/SKILL.md +71 -0
- package/template/.claude/skills/qa/SKILL.md +53 -0
- package/template/.claude/skills/todo/SKILL.md +54 -0
- package/template/.claude/skills/update-handbook/SKILL.md +69 -0
- package/template/AGENTS.md +86 -0
- package/template/CLAUDE.md +4 -0
- package/template/GEMINI.md +4 -0
- package/template/boot.smoke.mjs +21 -0
- package/template/game.js +53 -0
- package/template/gitignore +2 -0
- package/template/handbook/design.md +7 -0
- package/template/handbook/feel.md +8 -0
- package/template/handbook/qa.md +9 -0
- package/template/index.html +26 -0
- package/lib/agent-files.mjs +0 -151
|
@@ -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,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');
|
package/template/game.js
ADDED
|
@@ -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,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>
|
package/lib/agent-files.mjs
DELETED
|
@@ -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
|
-
}
|