castle-web-cli 0.4.54 → 0.4.56
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/dist/agent-prompts.d.ts +2 -2
- package/dist/agent-prompts.js +16 -16
- package/dist/bundle.js +2 -1
- package/dist/serve.js +2 -1
- package/dist/vitePlugins.d.ts +2 -0
- package/dist/vitePlugins.js +27 -0
- package/kits/basic-2d/CLAUDE.md +4 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +15 -1
- package/kits/basic-2d/engine/scene.js +35 -0
- package/package.json +1 -1
package/dist/agent-prompts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface PromptMessage {
|
|
2
|
-
role:
|
|
2
|
+
role: "user" | "assistant";
|
|
3
3
|
text: string;
|
|
4
4
|
}
|
|
5
5
|
export interface PromptTask {
|
|
@@ -28,6 +28,6 @@ export declare function buildTaskPrompt(opts: {
|
|
|
28
28
|
progressPath: string;
|
|
29
29
|
notesPath: string;
|
|
30
30
|
depsSummary?: string;
|
|
31
|
-
backend?:
|
|
31
|
+
backend?: "cursor" | "claude";
|
|
32
32
|
}): string;
|
|
33
33
|
export declare const CLAUDE_TASK_SYSTEM_REMINDER = "Castle background task agent: work autonomously to completion -- never pause to ask questions or wait for confirmation; finish the task end-to-end, then stop. Prefer the quickest viable change that fully does the job.";
|
package/dist/agent-prompts.js
CHANGED
|
@@ -14,10 +14,10 @@ Hard rules:
|
|
|
14
14
|
- You NEVER edit files or run state-changing commands. All building and fixing happens through background task agents -- always hand the longer work to them.
|
|
15
15
|
- You are the fast lane: get to your final reply as quickly as possible. When the user reports something broken, do NOT dig into the code to diagnose it first -- spawn a task whose job is to investigate AND fix it. Only read deck files when your reply itself needs them (answering a question about the deck, grounding a claim -- never make things up); never read as pre-work before spawning a task.
|
|
16
16
|
- Launch a SET of small steps the user tests one by one -- a pipeline, never one big task they wait on, never untestable fragments. One interacting mechanic = one task (paddle + ball + bricks = one playable core, not three). First step = the smallest genuinely playable thing; later steps build it out. Match breadth to ambition ("basic" = a few steps; "go wild" = many). You're optimizing the user's taste and feedback -- more small testable steps = more points where they steer it into something theirs.
|
|
17
|
-
- The whole goal:
|
|
18
|
-
-
|
|
17
|
+
- The whole goal: every piece of work TESTABLE in actual gameplay ASAP. Start every task as early as possible and run them in PARALLEL. Do NOT break tasks down by which files they touch, and never add \`after:\` just to avoid two tasks editing the same file -- tasks make surgical edits and overlap fine. The only real dependency between tasks is INFORMATION: a task is blocked only when it needs a fact it does not yet have.
|
|
18
|
+
- Dissolve those information-dependencies with a CONTRACT decided up front, instead of serializing tasks. Before spawning, pin the shared names the tasks will agree on -- scene file names, behavior names, drawing file names, tags -- and give each parallel task the exact names it must CREATE or REFERENCE. Then they all run at once and converge to a working game with NO follow-up wiring task: the scene task places actors referencing behaviors and drawings by their agreed names before those files even exist; the behavior tasks create those behavior names; the drawing tasks create those drawing files. (Art: pin drawings/ship.drawing up front -> the scene task points an actor at it while another task draws it, in parallel; no place-it-later task.) Fall back to a separate waiting wire-in task (\`after:\` the maker, on the least possible) ONLY when a task truly needs another's actual produced output, not just its agreed name. Working out this breakdown -- what to parallelize, which names to pin up front -- is the core of your job and differs per game.
|
|
19
19
|
- Write each task prompt for the quickest viable, testable change that still delivers a meaningful step up. Dive deep only if the user asked.
|
|
20
|
-
- Real, editable assets: drawings (drawings/*.drawing pixel art) for game objects, characters, and scenery
|
|
20
|
+
- Real, editable assets: drawings (drawings/*.drawing pixel art) for game objects, characters, and scenery as real actors; data-driven UI (health/score/HUD) and effects stay code. Get the game playable instantly with placeholder blocks (block.drawing + tint) -- even for pong/breakout -- then bring in the real drawings (wired in per the rule above). Never say "greybox" or other dev jargon to the user.
|
|
21
21
|
- To spawn a background task, include a fenced block in your reply:
|
|
22
22
|
|
|
23
23
|
\`\`\`castle-task
|
|
@@ -29,7 +29,7 @@ the deck's own docs for framework/API detail, so never restate recipes,
|
|
|
29
29
|
file layouts, or implementation steps.
|
|
30
30
|
\`\`\`
|
|
31
31
|
|
|
32
|
-
- Use \`after:\` only
|
|
32
|
+
- Use \`after:\` only for the fallback case above -- a task that genuinely needs another's produced output (it may reference tasks spawned in this same reply, by title). Never for file-contention; independent tasks must NOT wait on each other.
|
|
33
33
|
|
|
34
34
|
Keeping the board clean. The background-tasks list below IS the board the user sees -- every row, with its id and status. Be diligent about removing rows that no longer belong, using these two fences:
|
|
35
35
|
|
|
@@ -59,20 +59,20 @@ Conversation style:
|
|
|
59
59
|
function renderTranscript(messages) {
|
|
60
60
|
const recent = messages.slice(-TRANSCRIPT_LIMIT);
|
|
61
61
|
if (recent.length === 0)
|
|
62
|
-
return
|
|
62
|
+
return "(no conversation yet)";
|
|
63
63
|
return recent
|
|
64
|
-
.map((m) => `${m.role ===
|
|
65
|
-
.join(
|
|
64
|
+
.map((m) => `${m.role === "user" ? "user" : "you"}: ${m.text}`)
|
|
65
|
+
.join("\n\n");
|
|
66
66
|
}
|
|
67
67
|
function renderTasks(tasks) {
|
|
68
68
|
if (tasks.length === 0)
|
|
69
|
-
return
|
|
69
|
+
return "(none yet)";
|
|
70
70
|
return tasks
|
|
71
71
|
.map((t) => {
|
|
72
|
-
const notes = t.notes.trim() ? ` -- notes: ${t.notes.trim()}` :
|
|
72
|
+
const notes = t.notes.trim() ? ` -- notes: ${t.notes.trim()}` : "";
|
|
73
73
|
return `- [${t.status} ${t.progress}%] ${t.title} (${t.id})${notes}`;
|
|
74
74
|
})
|
|
75
|
-
.join(
|
|
75
|
+
.join("\n");
|
|
76
76
|
}
|
|
77
77
|
export function buildRouterPrompt(opts) {
|
|
78
78
|
return `${ROUTER_RULES}
|
|
@@ -98,14 +98,14 @@ export function userTurnInstruction(opts) {
|
|
|
98
98
|
}
|
|
99
99
|
parts.push(`The user just said:\n\n${opts.text}`);
|
|
100
100
|
if (opts.attachments && opts.attachments.length > 0) {
|
|
101
|
-
parts.push(`The user attached image file(s), saved in the deck at: ${opts.attachments.join(
|
|
101
|
+
parts.push(`The user attached image file(s), saved in the deck at: ${opts.attachments.join(", ")}. Open them with your read tool and take them into account; pass the paths along to task agents that need them.`);
|
|
102
102
|
}
|
|
103
|
-
return parts.join(
|
|
103
|
+
return parts.join("\n\n");
|
|
104
104
|
}
|
|
105
105
|
export function buildTaskPrompt(opts) {
|
|
106
106
|
const deps = opts.depsSummary
|
|
107
107
|
? `\n\nThis task waited on earlier tasks:\n${opts.depsSummary}\n`
|
|
108
|
-
:
|
|
108
|
+
: "";
|
|
109
109
|
// Claude-only: collapsing the wrap-up (progress 90 + restart + notes) into
|
|
110
110
|
// one shell call reliably saves 1-2 serial ~7s turns there. Cursor's
|
|
111
111
|
// composer sometimes reacts to the same rule with MORE calls, so it stays
|
|
@@ -113,9 +113,9 @@ export function buildTaskPrompt(opts) {
|
|
|
113
113
|
// (We deliberately do NOT tell agents to overwrite files via `cat > … <<EOF`
|
|
114
114
|
// -- blind whole-file rewrites made parallel agents clobber each other's
|
|
115
115
|
// edits. Read-then-edit is slower but safe; that is the right tradeoff.)
|
|
116
|
-
const wrapUp = opts.backend ===
|
|
116
|
+
const wrapUp = opts.backend === "claude"
|
|
117
117
|
? `\n- Wrap up in ONE tool call, not several: once your last file edit is done, combine the 90-progress write, the final \`npm run restart\`, and writing the notes file into a single shell command (\`;\`-separated so the notes land even if the restart hiccups). Then stop -- no extra turns after it.`
|
|
118
|
-
:
|
|
118
|
+
: "";
|
|
119
119
|
return `You are a background build agent for the Castle deck "${opts.deckLabel}" (current directory). A separate conversation agent dispatched you with one task. Follow the deck's CLAUDE.md / AGENTS.md conventions, and reload the served deck after changes (\`npm run restart\`).
|
|
120
120
|
|
|
121
121
|
Your task (id ${opts.taskId}): ${opts.title}
|
|
@@ -137,4 +137,4 @@ Operating rules:
|
|
|
137
137
|
}
|
|
138
138
|
// Appended to claude task agents' system prompt (portable replacement for the
|
|
139
139
|
// machine-specific /goal slash command): commit to autonomous completion.
|
|
140
|
-
export const CLAUDE_TASK_SYSTEM_REMINDER =
|
|
140
|
+
export const CLAUDE_TASK_SYSTEM_REMINDER = "Castle background task agent: work autonomously to completion -- never pause to ask questions or wait for confirmation; finish the task end-to-end, then stop. Prefer the quickest viable change that fully does the job.";
|
package/dist/bundle.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { build } from 'vite';
|
|
3
3
|
import { viteSingleFile } from 'vite-plugin-singlefile';
|
|
4
|
+
import { sceneFilesPlugin } from './vitePlugins.js';
|
|
4
5
|
export async function bundleProject(dir) {
|
|
5
6
|
const result = await build({
|
|
6
7
|
root: dir,
|
|
7
|
-
plugins: [viteSingleFile()],
|
|
8
|
+
plugins: [viteSingleFile(), sceneFilesPlugin()],
|
|
8
9
|
build: {
|
|
9
10
|
write: false,
|
|
10
11
|
rollupOptions: {
|
package/dist/serve.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createServer } from 'vite';
|
|
|
6
6
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
7
7
|
import { createIdeServer, DECK_FOCUS_SCRIPT } from './ide.js';
|
|
8
8
|
import { createAgentServer } from './agent.js';
|
|
9
|
+
import { sceneFilesPlugin } from './vitePlugins.js';
|
|
9
10
|
import * as config from './config.js';
|
|
10
11
|
function isPortFree(port) {
|
|
11
12
|
return new Promise((resolve) => {
|
|
@@ -194,7 +195,7 @@ export async function serve(dir, options = {}) {
|
|
|
194
195
|
process.on('exit', () => agentServer.shutdown());
|
|
195
196
|
const vite = await createServer({
|
|
196
197
|
root: projectDir,
|
|
197
|
-
plugins: [castlePlugin(wsPort, ideServer, agentServer)],
|
|
198
|
+
plugins: [castlePlugin(wsPort, ideServer, agentServer), sceneFilesPlugin()],
|
|
198
199
|
server: {
|
|
199
200
|
port,
|
|
200
201
|
strictPort: true,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
// Safety net for `.scene` / `.drawing` files imported as JS modules.
|
|
3
|
+
//
|
|
4
|
+
// These files are DATA (JSON), not modules. The blessed way to read them is the
|
|
5
|
+
// engine's file APIs (`scene.readFromFile(name)` / the `?raw` glob in
|
|
6
|
+
// engine/files.js). But agents sometimes still write a plain
|
|
7
|
+
// `import scene from './scenes/main.scene'`, which Vite would otherwise serve as
|
|
8
|
+
// raw JSON-as-JS and the browser throws "Unexpected token ':'". This plugin
|
|
9
|
+
// makes that import resolve to the parsed object so it just works.
|
|
10
|
+
//
|
|
11
|
+
// `?raw` (and any other query suffix) is left untouched so the engine's
|
|
12
|
+
// `import.meta.glob(..., { query: '?raw' })` still gets raw text.
|
|
13
|
+
export function sceneFilesPlugin() {
|
|
14
|
+
return {
|
|
15
|
+
name: 'castle-scene-files',
|
|
16
|
+
load(id) {
|
|
17
|
+
const [filePath, query] = id.split('?');
|
|
18
|
+
if (query !== undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (filePath.endsWith('.scene') || filePath.endsWith('.drawing')) {
|
|
21
|
+
const text = fs.readFileSync(filePath, 'utf-8');
|
|
22
|
+
return `export default JSON.parse(${JSON.stringify(text)});\n`;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
package/kits/basic-2d/CLAUDE.md
CHANGED
|
@@ -17,7 +17,7 @@ The deck is already serving when you start (`castle-web init` set that up; see `
|
|
|
17
17
|
1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), `npm run restart`, then add the next piece. Do NOT write the whole game in one shot.
|
|
18
18
|
2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
|
|
19
19
|
3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual drawings (`drawings/*.drawing`) and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into drawings.
|
|
20
|
-
4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.
|
|
20
|
+
4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.loadFromFile('gameover.scene')` (reads the file and transitions) on play / win / level change. NEVER `import` a `.scene` file as a module — scene files are data, not modules; use `scene.readFromFile` / `scene.loadFromFile`.
|
|
21
21
|
|
|
22
22
|
Card size is **500 wide × 700 tall** (origin top-left, +y is down).
|
|
23
23
|
|
|
@@ -107,6 +107,9 @@ Rules: every actor needs a unique `id` (any string) and almost always a `Layout`
|
|
|
107
107
|
- `scene.spawnActor({ components: { Layout: {...}, MyBehavior: {...} } })` — add a new actor at runtime. Returns the actor (with auto-minted `id` and `runtime = {}`). Use this; don't push to `scene.data.actors` by hand.
|
|
108
108
|
- `scene.despawnActor(id)` — remove an actor at runtime. Use this; don't `splice` + `delete` by hand.
|
|
109
109
|
- `scene.status` — string you can set/read for game-state ('playing', 'gameover', ...).
|
|
110
|
+
- `scene.load(sceneData)` — replace the running scene with the given scene data object.
|
|
111
|
+
- `scene.readFromFile(name)` — read and parse a scene file (`'gameover.scene'`, `'levels/2.scene'`), returning its scene data. Use this instead of importing a `.scene` file.
|
|
112
|
+
- `scene.loadFromFile(name)` — read a scene file and transition the running scene to it. The way to switch screens/levels and to restart, e.g. `scene.loadFromFile('main.scene')`.
|
|
110
113
|
- `actor.runtime` — per-instance scratchpad for transient state across frames (e.g. velocity, trail history). Not serialized.
|
|
111
114
|
|
|
112
115
|
## Input shortcuts
|
|
@@ -19,13 +19,22 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
19
19
|
configureSceneCanvas(canvas, ctx);
|
|
20
20
|
const runtime = makeScene(sceneData, behaviorClasses, drawings).clone();
|
|
21
21
|
runtimeRef.current = runtime;
|
|
22
|
+
// Store BOTH the physical code ('KeyX', 'ArrowLeft', 'Space') and the
|
|
23
|
+
// logical key ('x', 'ArrowLeft', ' ') so behaviors can match either. Codes
|
|
24
|
+
// are what the kit docs/examples use; keeping key too is forgiving.
|
|
22
25
|
const onKeyDown = (event) => {
|
|
26
|
+
runtime.keys.add(event.code);
|
|
23
27
|
runtime.keys.add(event.key);
|
|
24
28
|
};
|
|
25
29
|
const onKeyUp = (event) => {
|
|
30
|
+
runtime.keys.delete(event.code);
|
|
26
31
|
runtime.keys.delete(event.key);
|
|
27
32
|
};
|
|
28
33
|
const onPointerDown = (event) => {
|
|
34
|
+
// The play surface listens on window keydown, so it only gets keys when
|
|
35
|
+
// its document/iframe holds focus. Grab focus on any click into the game
|
|
36
|
+
// (and on mount below) so the keyboard works inside the launcher embed.
|
|
37
|
+
canvas.focus();
|
|
29
38
|
runtime.setPointerFromScreen(canvas, event.clientX, event.clientY, true);
|
|
30
39
|
canvas.setPointerCapture(event.pointerId);
|
|
31
40
|
};
|
|
@@ -46,6 +55,7 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
46
55
|
canvas.addEventListener('pointermove', onPointerMove);
|
|
47
56
|
canvas.addEventListener('pointerup', onPointerUp);
|
|
48
57
|
canvas.addEventListener('pointercancel', onPointerUp);
|
|
58
|
+
canvas.focus();
|
|
49
59
|
const stopLoop = startPlayerLoop(canvas, ctx, runtime);
|
|
50
60
|
return () => {
|
|
51
61
|
stopLoop();
|
|
@@ -61,7 +71,11 @@ export function ScenePlayer({ sceneData, drawings, behaviorClasses }) {
|
|
|
61
71
|
}, []);
|
|
62
72
|
return (
|
|
63
73
|
<div style={{ position: 'fixed', inset: 0, background: '#000' }}>
|
|
64
|
-
<canvas
|
|
74
|
+
<canvas
|
|
75
|
+
ref={canvasRef}
|
|
76
|
+
tabIndex={0}
|
|
77
|
+
style={{ width: '100%', height: '100%', display: 'block', outline: 'none' }}
|
|
78
|
+
/>
|
|
65
79
|
<SceneUI getRuntime={getRuntime} />
|
|
66
80
|
<TouchControls getKeys={getKeys} />
|
|
67
81
|
</div>
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import { initialFiles, parseJsonFile } from './files';
|
|
2
|
+
|
|
1
3
|
const CARD_WIDTH = 500;
|
|
2
4
|
const CARD_HEIGHT = 700;
|
|
3
5
|
|
|
4
6
|
export const cardSize = { width: CARD_WIDTH, height: CARD_HEIGHT };
|
|
5
7
|
|
|
8
|
+
// Resolve a scene-file name to its key in the loaded file map. Accepts
|
|
9
|
+
// 'main.scene', 'scenes/main.scene', './scenes/main.scene', or 'main' -- the
|
|
10
|
+
// `.scene` extension and a leading `scenes/` are both optional.
|
|
11
|
+
function resolveSceneFileKey(name) {
|
|
12
|
+
const key = String(name).replace(/^\.?\//, '');
|
|
13
|
+
const candidates = [key];
|
|
14
|
+
if (!key.endsWith('.scene')) candidates.push(`${key}.scene`);
|
|
15
|
+
for (const candidate of [...candidates]) {
|
|
16
|
+
if (!candidate.startsWith('scenes/')) candidates.push(`scenes/${candidate}`);
|
|
17
|
+
}
|
|
18
|
+
return candidates.find((candidate) => initialFiles[candidate] !== undefined) ?? key;
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
export class SceneRuntime {
|
|
7
22
|
constructor(sceneData, behaviors, drawings) {
|
|
8
23
|
this.behaviors = new Map(behaviors.map((Behavior) => [Behavior.behaviorName, Behavior]));
|
|
@@ -35,6 +50,26 @@ export class SceneRuntime {
|
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
|
|
53
|
+
// Read and parse a scene file by name, returning its scene data. Use this
|
|
54
|
+
// instead of `import x from './scenes/foo.scene'` -- scene files are data, not
|
|
55
|
+
// JS modules, so importing them is unsupported.
|
|
56
|
+
readFromFile(name) {
|
|
57
|
+
const key = resolveSceneFileKey(name);
|
|
58
|
+
const text = initialFiles[key];
|
|
59
|
+
if (text === undefined) {
|
|
60
|
+
throw new Error(`scene file not found: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
const { value, error } = parseJsonFile(key, text);
|
|
63
|
+
if (error) throw new Error(error);
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Transition the running scene to the scene stored in the named file (read it
|
|
68
|
+
// fresh, then load it). The building block for "go to scene" / restart.
|
|
69
|
+
loadFromFile(name) {
|
|
70
|
+
this.load(this.readFromFile(name));
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
clone() {
|
|
39
74
|
return new SceneRuntime(this.serialize(), [...this.behaviors.values()], this.drawings);
|
|
40
75
|
}
|