arcade-ai 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 guolin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # arcade-ai
2
+
3
+ AI-driven scaffold and live studio for Microsoft MakeCode Arcade games.
4
+
5
+ ## 安装与集成方式 (Installation & Integration)
6
+
7
+ ### 1. Claude Code 插件市场 (Claude Code plugin marketplace)
8
+ 你可以在 Claude Code plugin marketplace 中搜索并安装 `arcade-ai`。安装后即可在与 AI 对话时使用对应的技能,帮助生成和实时验证 MakeCode Arcade 游戏代码。
9
+
10
+ ### 2. Trae 智能辅助工具 (.trae)
11
+ 如果你使用 Trae,请将技能配置添加到 `.trae` 相关的规则路径中。初始化脚手架时通过指定 `--tool trae` 会在项目根目录下自动创建 `.trae/project_rules.md`。
12
+
13
+ ### 3. AI 代理常规配置 (AGENTS.md)
14
+ 对于通用的 AI 代理(如 Cline 或 Cursor 等),会在初始化时生成 `AGENTS.md` 文件。该文件定义了只修改 `game/main.ts`、严守 MakeCode Arcade 的 4 个约束、不可臆造 API 等核心规则,请确保将此规则导入你的 AI Agent 配置中。
15
+
16
+ ## 使用方法 (Usage)
17
+
18
+ 1. **项目初始化**:
19
+ ```bash
20
+ npx aca init <your-game-dir> [--tool claude|trae|agents]
21
+ ```
22
+ 2. **启动本地 Studio 双向实时预览服务**:
23
+ ```bash
24
+ cd <your-game-dir>
25
+ npx aca dev
26
+ ```
27
+ 3. **自检 postMessage 协议状态**:
28
+ ```bash
29
+ npx aca check
30
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: arcade-ai
3
+ description: Use when building or modifying a Microsoft MakeCode Arcade game — scaffolds a project, runs a live studio embedding the official editor, and provides an offline API/limits/pitfalls reference.
4
+ ---
5
+
6
+ # MakeCode Arcade AI Studio
7
+
8
+ 帮助用 AI 开发 Microsoft MakeCode Arcade 游戏:脚手架 + 嵌入官方编辑器的实时双向同步 studio
9
+ + 离线参考手册。
10
+
11
+ ## 何时用
12
+ - 用户想做 / 改 arcade 游戏、像素小游戏、makecode 游戏时。
13
+
14
+ ## 怎么用(三个命令)
15
+ 1. 起项目:`npx arcade-ai init <dir> [--tool claude|trae|agents]`
16
+ —— 生成纯 TS 脚手架,并把规则文件与 `reference/` 一并拷进项目。
17
+ 2. 起 studio:在项目目录 `npx aca dev` —— 浏览器实时预览,AI 改 `game/main.ts` 自动刷新;
18
+ 在编辑器里改代码/画精灵也会回写磁盘(双向)。
19
+ 3. 协议自检:`npx aca check` —— 验证官方编辑器握手 + 代码真渲染(联网,需 puppeteer)。
20
+
21
+ ## 写代码硬约束(违反会翻车)
22
+ - 代码只写 `game/main.ts`;资源(精灵/地图,4-bit 16 色)走 `game/assets.json`,不内联大图到 JS。
23
+ - **纯 TS 项目**:`game/` 里不要 `main.blocks`,`pxt.json` 的 `files` 也不列它,
24
+ `preferredEditor` 用 `tsprj`。否则编辑器会开在空白积木视图、看不到代码。
25
+ - `assets.json` 必须是合法 JSON(空资源写 `{}`),不要清空成空文件。
26
+ - **地图**:用命名地图 `tiles.setTilemap(tilemap`level`)`(编辑器自动建、人可画、自动同步回磁盘),
27
+ 或 `createTilemap` 配**内置图块**(`sprites.castle.*`)。❌ 别把内联 `img` 当图块塞进 `createTilemap`,会崩。
28
+ - 不臆造 arcade 不存在的 API。
29
+
30
+ ## 查文档(按需读,不要全量背)
31
+ 本 skill 同级、以及 `aca init` 生成的用户项目内,都有一份 `reference/`:
32
+ - `reference/arcade-api.md` —— sprites / controller / game / tilemap / music / info API 速查
33
+ - `reference/limits.md` —— 屏幕、调色板、内存、不支持的 JS 特性等硬限制
34
+ - `reference/pitfalls.md` —— 已知坑 + postMessage 协议字段(动手前务必扫一遍)
35
+ - `reference/project-format.md` —— pxt.json / assets.json / 文件格式约定
36
+
37
+ 写代码前,遇到不确定的 API 或限制就读对应文件,不要凭记忆。
package/bin/aca.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli.js';
3
+ run(process.argv.slice(2)).then((code) => process.exit(code));
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "arcade-ai",
3
+ "version": "0.1.0",
4
+ "description": "AI-driven scaffold and live studio for Microsoft MakeCode Arcade games",
5
+ "keywords": [
6
+ "makecode",
7
+ "arcade",
8
+ "ai",
9
+ "game",
10
+ "pixel",
11
+ "education"
12
+ ],
13
+ "type": "module",
14
+ "license": "MIT",
15
+ "bin": {
16
+ "aca": "bin/aca.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "files": [
22
+ "bin",
23
+ "src",
24
+ "reference",
25
+ "SKILL.md",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "dependencies": {
30
+ "chokidar": "^3.6.0"
31
+ },
32
+ "optionalDependencies": {
33
+ "puppeteer": "^22.0.0"
34
+ },
35
+ "scripts": {
36
+ "test": "node --test --test-concurrency=1 tests/*.test.js"
37
+ }
38
+ }
@@ -0,0 +1,158 @@
1
+ # MakeCode Arcade API Quick Reference
2
+
3
+ 本文档收集了 Microsoft MakeCode Arcade 中的常用高频 APIs,包含精灵、控制器、游戏循环、瓦片图、音乐和游戏信息等模块。
4
+
5
+ ## 1. 精灵 (Sprites)
6
+ 精灵是游戏中的主体元素(角色、敌人、道具、特效等)。
7
+
8
+ - **创建精灵**:
9
+ `let player = sprites.create(img`...`, SpriteKind.Player)`
10
+ - **控制精灵在屏幕内**:
11
+ `player.setStayInScreen(true)`
12
+ - **设置精灵位置**:
13
+ `player.setPosition(x: number, y: number)`
14
+ - **精灵速度**:
15
+ `player.vx = 50` (X轴速度)
16
+ `player.vy = -20` (Y轴速度)
17
+ - **销毁精灵**:
18
+ `player.destroy(effects.disintegrate, 500)` (支持特效与延迟时间)
19
+ - **碰撞重叠事件**:
20
+ ```typescript
21
+ sprites.onOverlap(SpriteKind.Player, SpriteKind.Enemy, function(player, enemy) {
22
+ enemy.destroy();
23
+ info.changeLifeBy(-1);
24
+ });
25
+ ```
26
+
27
+ ## 2. 控制器 (Controller)
28
+ 控制玩家输入。
29
+
30
+ - **用摇杆移动精灵**:
31
+ `controller.moveSprite(player, vx?, vy?)` (默认速度为 100)
32
+ - **按键事件**:
33
+ ```typescript
34
+ controller.A.onEvent(ControllerButtonEvent.Pressed, function() {
35
+ // 射击子弹
36
+ let projectile = sprites.createProjectileFromSprite(img`...`, player, 200, 0);
37
+ });
38
+ ```
39
+ - **获取当前摇杆倾斜度**:
40
+ `controller.dx()`, `controller.dy()`
41
+
42
+ ## 3. 游戏控制与循环 (Game)
43
+ 控制游戏全局流和定时执行。
44
+
45
+ - **游戏更新循环**(每帧执行):
46
+ ```typescript
47
+ game.onUpdate(function() {
48
+ // 每帧执行的物理逻辑或状态检查
49
+ });
50
+ ```
51
+ - **定时更新循环**(指定毫秒数执行一次):
52
+ ```typescript
53
+ game.onUpdateInterval(1000, function() {
54
+ // 每秒生成一个敌人
55
+ let enemy = sprites.create(img`...`, SpriteKind.Enemy);
56
+ });
57
+ ```
58
+ - **游戏结束**:
59
+ `game.over(win: boolean, effect?: effect)`
60
+ - **显示长文本对话框**:
61
+ `game.showLongText("Hello World!", DialogLayout.Bottom)`
62
+
63
+ ## 4. 瓦片地图 (Tilemap)
64
+ 用于绘制游戏背景和关卡障碍。
65
+
66
+ - **设置当前瓦片地图**:
67
+ `tiles.setCurrentTilemap(tilemap`level1`)`
68
+ - **将精灵移入特定瓦片**:
69
+ `tiles.placeOnRandomTile(sprite, assets.tile`transparency1`)`
70
+ - **碰撞瓦片判定**:
71
+ ```typescript
72
+ scene.onOverlapTile(SpriteKind.Player, assets.tile`lava`, function(sprite, location) {
73
+ info.changeLifeBy(-1);
74
+ });
75
+ ```
76
+
77
+ ## 5. 音乐与音效 (Music)
78
+ - **播放内置音效**:
79
+ `music.play(music.melodyPlayable(music.baDing), music.PlaybackMode.UntilDone)`
80
+ - **播放乐谱简谱**:
81
+ `music.playMelody("C5 B A G F E D C ", 120)`
82
+ - **常用预设音效**:
83
+ `music.pewPew`, `music.jumpUp`, `music.powerUp`, `music.powerDown`
84
+
85
+ ## 6. 游戏信息 (Info)
86
+ 处理分数、生命值和计时器。
87
+
88
+ - **设置/修改分数**:
89
+ `info.setScore(0)`
90
+ `info.changeScoreBy(10)`
91
+ - **设置/修改生命值**:
92
+ `info.setLife(3)`
93
+ `info.changeLifeBy(-1)`
94
+ - **倒计时**:
95
+ `info.startCountdown(30)` (30秒)
96
+ `info.onCountdownEnd(function() { game.over(false) })`
97
+
98
+ ## 7. 地图 Tilemap(两条可靠路线,外加一个会崩的禁区)
99
+
100
+ > ⚠️ **禁区**:`tiles.createTilemap(hex`...`, img`...`, [内联 img tile], ...)` —— **把内联 `img` 当图块会让官方编辑器崩溃**(Oops 重载循环)。编辑器的 tilemap 解析器只接受“命名图块资源”,不接受随手写的内联 img。已实测确认。详见 pitfalls 坑5。
101
+
102
+ ### 路线 A(推荐,AI + 人协作):命名地图 `tilemap`level``
103
+ AI 在代码里引用一个命名地图;编辑器首次打开会**自动创建一张可编辑的空地图**,人在网页地图编辑器里画好后,自动同步回磁盘(`tilemap.g.ts` / `tilemap.g.jres`)并随游戏运行、可反复改。
104
+ ```typescript
105
+ tiles.setTilemap(tilemap`level`) // level 不存在时编辑器会自动创建
106
+ scene.cameraFollowSprite(mySprite)
107
+ ```
108
+
109
+ ### 路线 B(纯代码,用内置图块):`createTilemap` + **内置 tile**
110
+ 图块用 MakeCode 自带的图库(真资源,不会崩),而不是内联 img:
111
+ ```typescript
112
+ const data = Buffer.create(4 + cols * rows);
113
+ data.setNumber(NumberFormat.Int16LE, 0, cols); // 宽
114
+ data.setNumber(NumberFormat.Int16LE, 2, rows); // 高
115
+ data.setUint8(4 + row * cols + col, tileIndex); // 每格图块索引(0 起,指向下面数组)
116
+ const walls = image.create(cols, rows); // 墙体层:非透明像素=墙
117
+ const tm = tiles.createTilemap(
118
+ data, walls,
119
+ [sprites.castle.tileGrass1, sprites.castle.tilePath5], // 内置图块,非内联 img
120
+ TileScale.Sixteen);
121
+ tiles.setCurrentTilemap(tm);
122
+ ```
123
+
124
+ ### 内置图库(直接按名引用,无需手画/手写 jres)
125
+ - `sprites.castle.*` —— 草地/路/石头/房子等:`tileGrass1`、`tileGrass2`、`tileDarkGrass2`、`tilePath5`、`rock0`、`houseBlue` …
126
+ - 还有 `sprites.dungeon.*`、`sprites.builtin.*`、`sprites.food.*`、`sprites.vehicle.*` 等多个图库。
127
+ - **确切名字以编辑器里的图库(image gallery)为准**——打开精灵/地图编辑器点“Gallery”即可浏览全部,别凭记忆臆造名字。
128
+
129
+ ## 8. Looping Background Music & Custom Melodies
130
+ - **Play Custom Notes Looping**: Use `music.stringPlayable` and `music.PlaybackMode.LoopingInBackground`.
131
+ ```typescript
132
+ music.play(
133
+ music.stringPlayable("E G A G C5 B A G ", 120),
134
+ music.PlaybackMode.LoopingInBackground
135
+ );
136
+ ```
137
+ - **Stop All Sounds**: `music.stopAllSounds()`
138
+
139
+ ## 9. Collisions & Overlap Events
140
+ - **Scene Wall Collision Event**: Triggered when a sprite hits a tilemap wall:
141
+ ```typescript
142
+ scene.onHitWall(SpriteKind.Enemy, function(enemy: Sprite, location: tiles.Location) {
143
+ enemy.vx = -enemy.vx; // Bounce back
144
+ });
145
+ ```
146
+ - **Sprite Overlap Event**:
147
+ ```typescript
148
+ sprites.onOverlap(SpriteKind.Player, SpriteKind.Enemy, function(player: Sprite, enemy: Sprite) {
149
+ // Logic here
150
+ });
151
+ ```
152
+ - **Tile Overlap Event**:
153
+ ```typescript
154
+ scene.onOverlapTile(SpriteKind.Player, tileSpike, function(player: Sprite, location: tiles.Location) {
155
+ // Logic here (e.g. respawn player)
156
+ });
157
+ ```
158
+
@@ -0,0 +1,36 @@
1
+ # MakeCode Arcade Limits & Unsupported JS Features
2
+
3
+ Microsoft MakeCode Arcade 专为复古的 8-bit 和 16-bit 掌机设计。虽然它支持使用 TypeScript (JavaScript) 编写游戏,但其底层的物理硬件运行环境、编译器和渲染器都引入了许多限制。
4
+
5
+ ## 1. 硬件与显示限制
6
+
7
+ - **屏幕分辨率 (Screen Resolution)**:
8
+ 固定为 **160 x 120 像素**。所有精灵坐标、屏幕尺寸和瓦片大小都是基于该分辨率进行布局。
9
+ - **16 色调色板 (16-Color Palette)**:
10
+ 底层是 4-bit 颜色深度(Color Depth)。仅支持固定包含 16 种颜色的经典 MS MakeCode Arcade 调色板。
11
+ * 颜色 `0`(在 img 中通常写作 `.`)代表透明色。
12
+ * 精灵图象(Image 字面量)中只能使用这 16 种字符代码(如 `.`, `1`-`9`, `a`-`f`)来代表不同的颜色。
13
+
14
+ ## 2. 内存与精灵数量限制
15
+
16
+ 在运行模拟器或部署在实体 Arcade 硬件(如 PyGamer、PyBadge、Meowbit 等)上时,面临着极度受限的运行资源:
17
+ - **精灵数量上限 (Sprite Count)**:
18
+ 通常在硬件上不建议同时存在超过 **30-50 个活动的精灵**,否则垃圾回收(Garbage Collection)和每帧碰撞检测会导致帧率骤降。
19
+ - **内存限制 (Memory Limit)**:
20
+ 微控制器通常仅有 96KB - 512KB 的 RAM。在 TypeScript 中创建大量的动态数组或嵌套深层对象,或者在循环中频繁实例化临时对象,很容易导致内存溢出崩溃。
21
+
22
+ ## 3. 不支持的 JavaScript (TypeScript) 语言特性
23
+
24
+ MakeCode 拥有自定义的 TypeScript-to-C++ (Static TypeScript) 编译器。它**不包含完整的 JS 运行时环境**(如 V8 或 QuickJS),所以以下特性是不支持或受到极大限制的:
25
+
26
+ - **Async / Await 和 Promise**:
27
+ MakeCode 底层采用轻量级的绿色线程(Fiber)协同运行。原生的 `async/await` 语法和 `Promise` 对象是**不支持**的。
28
+ * *替代方案*:如果需要延迟,请使用 MakeCode 内置的 `pause(ms)` API 挂起当前协程。
29
+ - **内置数据结构限制**:
30
+ 不支持 `Map`、`Set` 以及 `WeakMap` / `WeakSet`。
31
+ * *替代方案*:使用普通的对象 `{ [key: string]: val }` 或数组进行简单的 key-value 映射。
32
+ - **未实现的 JS API**:
33
+ 由于本地无标准的 Web 浏览器环境,任何浏览器和 Node.js 原生的 API(如 `window`, `document`, `fetch`, `console.log` 的完整版,以及任何 DOM 或 Node 内置库)在 Arcade 运行时内均不可用。
34
+ - **动态特性限制**:
35
+ - 不支持 `eval()`、`new Function()` 等动态代码执行。
36
+ - 不支持 `Object.defineProperty` 或 `Proxy` 拦截。
@@ -0,0 +1,72 @@
1
+ # MakeCode Arcade Pitfalls & postMessage Protocol Reference
2
+
3
+ 本文档总结了在使用和集成 Microsoft MakeCode Arcade 官方编辑器时常见的四个关键坑点及对应的解决方案,并对双向同步时所使用的 `postMessage` 协议字段进行了详细说明。
4
+
5
+ ## 常见坑点 (Common Pitfalls)
6
+
7
+ ### 1. 资源配置文件 `assets.json` 不得为空
8
+ * **现象**:当宿主将本地文件推送到 MakeCode 编辑器后,编辑器可能崩溃,显示 "Oops We detected a problem..." 并不断自动刷新。
9
+ * **原因**:因为 `assets.json` 作为美术资源(如瓦片图、贴图、精灵)配置文件,如果是一个空文件(0 字节或 1 字节),编辑器内部的 `JSON.parse` 无法解析空字符串,从而抛出未捕获异常导致崩溃。
10
+ * **解决**:在初始化和保存时,`assets.json` 的内容必须是合法的 JSON 对象 `"{}"`,不得为空文件。
11
+
12
+ ### 2. `pxt.json` 中的 `files` 数组必须与实际文件一致
13
+ * **现象**:修改或同步代码后,编辑器解析警告、代码丢失或无法渲染。
14
+ * **原因**:MakeCode 的同步逻辑严格依赖 `files` 声明。`files` 数组里列出的文件必须都真实存在,反之磁盘上要被编辑器读取的文件也必须在 `files` 里声明,否则关联错乱。
15
+ * **解决**:纯 TypeScript 工作流(本项目默认)声明 `["main.ts", "README.md", "assets.json"]` 即可,**不要列入 `main.blocks`**(见坑 3)。
16
+
17
+ ### 3. ⚠️ 不要推送空的 `main.blocks` / 不要用 `blocksprj`(真机验证纠正)
18
+ * **现象**:编辑器加载成功、文件也同步进去了(README 能显示),**但代码区一片空白,看似一个全新空项目**。
19
+ * **原因**:只要项目里存在 `main.blocks` 文件,编辑器就默认打开**积木(Blocks)视图**;而 AI 生成的代码在 `main.ts` 里、`main.blocks` 是空的,于是积木画布空白、`main.ts` 不显示,且 MakeCode **不会**在加载时自动把 TS 反编译成积木。仅改 header 的 `editor` 字段无法纠正——`main.blocks` 的存在会压倒它。
20
+ * **解决**:做**纯 TypeScript 项目**——磁盘上**不要 `main.blocks`**、`files` 里也不列它,header 用 `editor: "tsprj"`、`pxt.json` 的 `preferredEditor: "tsprj"`。编辑器即会开在 JavaScript 视图并显示 `main.ts`。
21
+ * **教训**:握手成功(编辑器请求 `/api/project`)发生在加载早期,**不等于代码渲染成功**。验证一定要确认编辑器里**真的出现了代码**,否则空白项目会假阳性蒙混过关。`aca check` 已据此加强为两关:握手 + 渲染。
22
+
23
+ ### 4. iframe 加载官方地址时遭遇 404
24
+ * **现象**:使用 `https://arcade.makecode.com/index.html?controller=1` 时页面直接返回 404 导致 iframe 挂起白屏。
25
+ * **原因**:微软官方在线版会安全过滤或缓存拦截带有 `index.html` 路径的请求。
26
+ * **解决**:iframe.src 请求应当使用纯净的根域名路径,即 `https://arcade.makecode.com/?controller=1`(不要带 `index.html`)。
27
+
28
+ ### 5. ⚠️ tilemap 用内联 `img` 当图块 → 编辑器崩溃(实测确认)
29
+ * **现象**:`tiles.createTilemap(hex`...`, img`...`, [tileA, tileB], TileScale.Sixteen)` 里把 `tileA/tileB` 写成**内联 `img` 变量**,编辑器加载后崩溃,弹 "Oops, we detected a problem..." 并不断重载。
30
+ * **原因**:编辑器的 tilemap 字段编辑器会尝试把 `createTilemap` 反编译成可视化地图控件,它只认**命名图块资源**(`myTiles.xxx` / 内置 `sprites.castle.xxx`),遇到随手写的内联 img 图块就解析崩溃。
31
+ * **解决**:地图走两条可靠路线之一(见 arcade-api.md 第 7 节):
32
+ 1. **命名地图** `tiles.setTilemap(tilemap`level`)`——编辑器自动创建空地图,人画好后同步回磁盘;
33
+ 2. **内置图块** `createTilemap(..., [sprites.castle.tileGrass1, ...], ...)`——图块用自带图库,不要内联 img。
34
+ * **补充**:图块/地图数据最终以 base64 的 F4 图片格式存进 `tilemap.g.jres`,由 `tilemap.g.ts` 声明命名空间——这些是编辑器生成的,AI 不要手写 jres。
35
+
36
+ ---
37
+
38
+ ## postMessage 协议字段参考
39
+
40
+ 在宿主与嵌入的 MakeCode iframe 通信中,所有的消息都是通过 HTML5 的 `postMessage` 协议进行的,通常使用 `type: "pxthost"` 来标识由宿主环境与编辑器之间的 workspace 级交互。
41
+
42
+ ### 1. `workspacesync` (同步请求)
43
+ 当编辑器 iframe 初始化加载完成后,它会向宿主发送一个 `action: "workspacesync"` 的消息,向宿主请求本地代码文件。
44
+ - **消息发送方**:编辑器 iframe -> 宿主 window
45
+ - **格式**:
46
+ ```json
47
+ {
48
+ "type": "pxthost",
49
+ "action": "workspacesync"
50
+ }
51
+ ```
52
+ - **宿主响应**:宿主收到后,读取本地文件列表并回复。回复消息也需要包含相同的 `type: "pxthost"` 和 `action: "workspacesync"`,同时附带 `projects` 数组,指定 `header`(其中 `editor: "tsprj"`,见坑 3)和 `text`(包含文件名与文件内容的键值对)。
53
+
54
+ ### 2. `workspacesave` (保存请求)
55
+ 当用户在编辑器中拖拽积木或编辑代码发生改变时,编辑器会触发自动保存,并向宿主发送一个 `action: "workspacesave"` 的消息。
56
+ - **消息发送方**:编辑器 iframe -> 宿主 window
57
+ - **格式**:
58
+ ```json
59
+ {
60
+ "type": "pxthost",
61
+ "action": "workspacesave",
62
+ "project": {
63
+ "header": { ... },
64
+ "text": {
65
+ "main.ts": "...",
66
+ "assets.json": "...",
67
+ "pxt.json": "..."
68
+ }
69
+ }
70
+ }
71
+ ```
72
+ - **宿主处理**:宿主接收到此消息后,解析出其中的 `project.text`,并将其中的文件写回本地磁盘,随后宿主通过暂停监听文件变化来避免写盘引发的回声现象(watcher echo)。
@@ -0,0 +1,55 @@
1
+ # MakeCode Arcade Project Format Reference
2
+
3
+ 本文档介绍了 Microsoft MakeCode Arcade (PXT) 项目的工程格式与文件结构,包括核心配置文件、资源存储模式、自动生成的 TypeScript 代码以及开发时遵守的白名单机制。
4
+
5
+ ## 1. 核心配置文件
6
+
7
+ - **`pxt.json`**:
8
+ 项目的包管理器和工程说明书。它声明了项目的名称、依赖的库和编译的入口文件。
9
+ * `name`:项目名称。
10
+ * `dependencies`:外部包依赖。默认包含 `"device": "*"` 代表核心 API。
11
+ * `files`:非常关键!列出了该项目参与编译的所有文件列表。加了 tilemap/命名资源后,
12
+ 编辑器会自动把生成的 `*.g.ts` 加进这里——同步时要原样带回(见第 3 节)。
13
+ * `preferredEditor`:首选编辑器类型。本项目是纯 TS 工作流,应为 `"tsprj"`(JavaScript 视图)。
14
+ **不要用 `blocksprj`**,否则编辑器开在空白积木视图、看不到 `main.ts` 代码(见 pitfalls 坑3)。
15
+
16
+ - **`assets.json`**:
17
+ 在 MakeCode 项目中用来存放用户设计的所有美术资源(图像、精灵图、瓦片、瓦片图、动画等)。
18
+ * 必须是一个合法的 JSON 对象,内容不得为空。即使没有资源也应初始化为 `"{}"`。
19
+ * 其中的美术资源使用类似 F4(MakeCode 资源版本格式)的结构表示。图像在底层会被编译为 `Image` 字面量。
20
+
21
+ ## 2. 自动生成的 TS 文件 (`*.g.ts`)
22
+
23
+ 当在编辑器的 Assets 管理面板中设计或导入图片、瓦片图后,编辑器在自动保存时会在内存中或输出的工程里动态生成相关的 TypeScript 代码接口:
24
+
25
+ - **`images.g.ts`**:
26
+ 自动生成美术精灵图(Sprite Images)的声明。例如,在 assets 面板建了一个叫 `hero` 的精灵图,它会在代码中生成类似:
27
+ ```typescript
28
+ namespace myImages {
29
+ export const hero = img`...`;
30
+ }
31
+ ```
32
+ 在代码中可通过 `assets.image`hero`` 直接引用。
33
+
34
+ - **`tilemap.g.ts`**:
35
+ 声明编辑的瓦片地图(Tilemap)以及关联的图块(Tiles)。它将复杂的二维图块数组序列化,并暴露常量(如 `tilemap`level1``)。
36
+
37
+ *注意*:这些 `*.g.ts`(以及 tilemap 关联的 `*.g.jres`)是编辑器生成的、AI 不应手改的代码,
38
+ 但**必须参与同步**——否则在编辑器里画的地图/命名资源会丢失。`arcade-ai` 会自动往返这些文件。
39
+
40
+ ## 3. `arcade-ai` 工程同步规则(按扩展名,非固定名单)
41
+
42
+ 本地双向实时同步**不再用固定文件名白名单**(旧版只认 6 个文件,会把 tilemap 生成的
43
+ `*.g.ts` / `*.g.jres` 丢掉,导致地图存不住)。现在 `project-io` 按**扩展名**动态同步项目根下
44
+ 所有项目文件:
45
+
46
+ - **同步的扩展名**:`.ts`、`.js`、`.json`、`.md`、`.blocks`、`.jres`、`.txt`。
47
+ 涵盖 `main.ts`、`pxt.json`、`assets.json`、`README.md`、以及 `tilemap.g.ts`、`tiles.g.jres`
48
+ 等动态生成文件。
49
+ - **自动排除**:
50
+ - 目录 `pxt_modules` / `built` / `node_modules` / `.git`(依赖与产物)。
51
+ - 隐藏文件(`.gitignore`、`.env` 等)。
52
+ - 其它扩展(`.sh`、`.png`、二进制等)。
53
+ - 带路径分隔或 `..` 的名字(防目录遍历,只接受项目根下的纯文件名)。
54
+
55
+ 这样地图、命名精灵等会生成额外文件的功能都能完整往返,同时仍挡住危险/冗余文件。
package/src/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ import { parseArgs as nodeParseArgs } from 'node:util';
2
+
3
+ const COMMANDS = { init: 'init', dev: 'dev', check: 'check' };
4
+
5
+ export function parseArgs(argv) {
6
+ const { values, positionals } = nodeParseArgs({
7
+ args: argv,
8
+ allowPositionals: true,
9
+ strict: false,
10
+ options: {
11
+ tool: { type: 'string' },
12
+ port: { type: 'string' },
13
+ url: { type: 'string' },
14
+ },
15
+ });
16
+ const [command, ...rest] = positionals;
17
+ return { command, positionals: rest, options: values };
18
+ }
19
+
20
+ const HELP = `arcade-ai (aca)
21
+ 用法:
22
+ aca init [dir] [--tool claude|trae|agents] 起脚手架
23
+ aca dev [--port 8080] 起本地 studio
24
+ aca check [--url <makecode-url>] 自检 postMessage 协议`;
25
+
26
+ export async function run(argv) {
27
+ const { command, positionals, options } = parseArgs(argv);
28
+ if (!command) { console.log(HELP); return 0; }
29
+ if (!COMMANDS[command]) { console.error(`未知命令: ${command}\n\n${HELP}`); return 1; }
30
+ const mod = await import(`./commands/${command}.js`);
31
+ return mod.default({ positionals, options });
32
+ }
@@ -0,0 +1,58 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { startStudio } from '../studio/server.js';
4
+
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ const hostHtmlPath = join(here, '..', 'host', 'index.html');
7
+ const gameDir = join(here, '..', 'template', 'game'); // 用内置模板当样本
8
+
9
+ export default async function check(ctx) {
10
+ let puppeteer;
11
+ try { puppeteer = (await import('puppeteer')).default; }
12
+ catch { console.error('需要 puppeteer:npm i -D puppeteer'); return 2; }
13
+
14
+ const studio = await startStudio({ gameDir, port: 0, hostHtmlPath });
15
+ const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
16
+ try {
17
+ const page = await browser.newPage();
18
+ await page.goto(studio.url, { waitUntil: 'networkidle2', timeout: 60000 });
19
+
20
+ // 第 1 关:握手 —— 编辑器触发 workspacesync,host 页据此请求 /api/project。
21
+ await page.waitForResponse((r) => r.url().endsWith('/api/project'), { timeout: 45000 });
22
+ console.log(' · 握手成功:编辑器已发起 workspacesync 并拉取本地项目');
23
+
24
+ // 第 2 关(关键):渲染 —— 等编辑器把 main.ts 真正显示出来。
25
+ // 仅靠握手是不够的:项目可能加载成空白积木视图。必须确认代码可见。
26
+ const editorFrame = await waitForEditorFrame(page);
27
+ const SNIPPET = 'sprites.create';
28
+ await editorFrame.waitForFunction((kw) => {
29
+ const lines = [...document.querySelectorAll('.view-line')].map((l) => l.textContent).join('\n');
30
+ return lines.includes(kw);
31
+ }, { timeout: 30000 }, SNIPPET);
32
+ console.log(' · 渲染成功:编辑器 JS 视图已显示 main.ts 代码');
33
+
34
+ console.log('✅ 协议自检通过:握手 + 代码渲染均成立');
35
+ return 0;
36
+ } catch (e) {
37
+ console.error(`❌ 自检失败:${e.message}`);
38
+ console.error(' (若卡在"渲染"步:多半是项目以空白积木视图打开——检查 pxt.json 是否误含 main.blocks / preferredEditor 是否为 tsprj)');
39
+ return 1;
40
+ } finally {
41
+ await browser.close();
42
+ await studio.close();
43
+ }
44
+ }
45
+
46
+ // 官方编辑器主 frame(排除文档/模拟器子 frame)
47
+ async function waitForEditorFrame(page) {
48
+ const deadline = Date.now() + 30000;
49
+ while (Date.now() < deadline) {
50
+ const f = page.frames().find((fr) =>
51
+ fr.url().includes('makecode.com') &&
52
+ !fr.url().includes('---docs') &&
53
+ !fr.url().includes('---simulator'));
54
+ if (f) return f;
55
+ await new Promise((r) => setTimeout(r, 500));
56
+ }
57
+ throw new Error('未找到官方编辑器 iframe');
58
+ }
@@ -0,0 +1,25 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { spawn } from 'node:child_process';
4
+ import { startStudio } from '../studio/server.js';
5
+
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ const hostHtmlPath = join(here, '..', 'host', 'index.html');
8
+
9
+ function openBrowser(url) {
10
+ const cmd = process.platform === 'darwin' ? 'open'
11
+ : process.platform === 'win32' ? 'cmd' : 'xdg-open';
12
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
13
+ try { spawn(cmd, args, { stdio: 'ignore', detached: true }).unref(); } catch {}
14
+ }
15
+
16
+ export default async function dev(ctx) {
17
+ const cwd = ctx.options._cwd || process.cwd();
18
+ const gameDir = join(cwd, 'game');
19
+ const port = ctx.options._noBlock ? 0 : Number(ctx.options.port || 8080);
20
+ const studio = await startStudio({ gameDir, port, hostHtmlPath });
21
+ if (ctx.options._noBlock) return studio;
22
+ console.log(`🎮 studio: ${studio.url}`);
23
+ openBrowser(studio.url);
24
+ return new Promise(() => {}); // 常驻
25
+ }
@@ -0,0 +1,34 @@
1
+ import { cpSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+ export const templateDir = join(here, '..', 'template');
7
+ // reference 是知识层,随 skill 与 npm 包一起发布(仓库根 /reference)
8
+ export const referenceDir = join(here, '..', '..', 'reference');
9
+
10
+ const RULES = {
11
+ claude: { src: 'claude.md', dest: 'CLAUDE.md' },
12
+ trae: { src: 'project_rules.md', dest: '.trae/project_rules.md' },
13
+ agents: { src: 'agents.md', dest: 'AGENTS.md' },
14
+ };
15
+
16
+ function writeRules(dest, tool) {
17
+ const r = RULES[tool] || RULES.agents;
18
+ const content = readFileSync(join(here, '..', 'rules', r.src), 'utf8');
19
+ const target = join(dest, r.dest);
20
+ mkdirSync(dirname(target), { recursive: true });
21
+ writeFileSync(target, content, 'utf8');
22
+ }
23
+
24
+ export default async function init(ctx) {
25
+ const dest = ctx.positionals[0] || 'arcade-game';
26
+ mkdirSync(dest, { recursive: true });
27
+ cpSync(join(templateDir, 'game'), join(dest, 'game'), { recursive: true });
28
+ copyFileSync(join(templateDir, 'package.json'), join(dest, 'package.json'));
29
+ // 把知识库拷进项目,使其自包含、跨工具可查(Trae/其它 AI 裸读项目也能用)
30
+ cpSync(referenceDir, join(dest, 'reference'), { recursive: true });
31
+ writeRules(dest, ctx.options.tool);
32
+ console.log(`✅ 已创建 ${dest}\n cd ${dest} && npx aca dev`);
33
+ return 0;
34
+ }
@@ -0,0 +1,45 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head><meta charset="utf-8"><title>arcade-ai studio</title>
4
+ <style>html,body{margin:0;height:100%}iframe{width:100%;height:100%;border:0}</style></head>
5
+ <body>
6
+ <iframe id="mc" src="https://arcade.makecode.com/?controller=1"
7
+ sandbox="allow-popups allow-forms allow-scripts allow-same-origin"
8
+ allow="autoplay; gamepad; midi"></iframe>
9
+ <script>
10
+ const iframe = document.getElementById('mc');
11
+
12
+ window.addEventListener('message', async (event) => {
13
+ if (event.source !== iframe.contentWindow) return;
14
+ const msg = event.data; if (!msg) return;
15
+
16
+ if (msg.type === 'pxthost' && msg.action === 'workspacesync') {
17
+ const data = await (await fetch('/api/project')).json();
18
+ msg.projects = [{
19
+ header: {
20
+ id: 'arcade-ai-project', name: 'arcade-game',
21
+ modificationTime: Date.now(), recentUse: Date.now(),
22
+ target: 'arcade', editor: 'tsprj' // JS 视图:AI 写的是 main.ts,须开在 TS 编辑器
23
+ },
24
+ text: data.files
25
+ }];
26
+ iframe.contentWindow.postMessage(msg, '*');
27
+ }
28
+
29
+ if (msg.type === 'pxthost' && msg.action === 'workspacesave') {
30
+ const project = msg.project;
31
+ if (project && project.text) {
32
+ await fetch('/api/save', {
33
+ method: 'POST', headers: { 'content-type': 'application/json' },
34
+ body: JSON.stringify({ files: project.text })
35
+ });
36
+ }
37
+ }
38
+ });
39
+
40
+ // 文件被外部(AI)改动 → 自动 reload iframe,重走 workspacesync(已验证路径)
41
+ const es = new EventSource('/events');
42
+ es.onmessage = (e) => { if (e.data === 'changed') iframe.src = iframe.src; };
43
+ </script>
44
+ </body>
45
+ </html>
@@ -0,0 +1,59 @@
1
+ import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { join, extname, basename } from 'node:path';
3
+
4
+ // MakeCode arcade 项目文件按扩展名同步(项目文件都在项目根,无子目录)。
5
+ // 这样 tilemap/命名资源生成的 *.g.ts / *.g.jres 也能完整往返,而不是被固定白名单丢弃。
6
+ // 纯 JS 工具:不收 .py(编辑器从 main.ts 自动转译的 Python 产物)。
7
+ const SYNC_EXTS = new Set(['.ts', '.js', '.json', '.md', '.blocks', '.jres', '.txt']);
8
+
9
+ // 目录黑名单:依赖、产物,不参与同步。
10
+ const SKIP_DIRS = new Set(['pxt_modules', 'built', 'node_modules', '.git']);
11
+
12
+ // 一个文件名是否该同步:纯文件名(防路径遍历)、非隐藏、扩展在白名单内。
13
+ export function isSyncable(name) {
14
+ if (typeof name !== 'string' || name.length === 0) return false;
15
+ if (name !== basename(name)) return false; // 拒绝 ../x、sub/x、/abs 等
16
+ if (name.startsWith('.')) return false; // 排除 .gitignore 等隐藏文件
17
+ return SYNC_EXTS.has(extname(name).toLowerCase());
18
+ }
19
+
20
+ export function readProject(gameDir) {
21
+ const out = {};
22
+ for (const name of readdirSync(gameDir)) {
23
+ if (SKIP_DIRS.has(name)) continue;
24
+ if (!isSyncable(name)) continue;
25
+ const p = join(gameDir, name);
26
+ if (!statSync(p).isFile()) continue; // 跳过同名目录
27
+ out[name] = readFileSync(p, 'utf8');
28
+ }
29
+ return out;
30
+ }
31
+
32
+ // 让 pxt.json 的 files 与实际同步的文件一致:剔除不同步的条目(如 main.py),
33
+ // 否则磁盘上会出现"声明了却不存在"的悬空引用。
34
+ function sanitizePxtJson(content) {
35
+ try {
36
+ const j = JSON.parse(content);
37
+ if (Array.isArray(j.files)) {
38
+ const cleaned = j.files.filter((f) => isSyncable(f));
39
+ if (cleaned.length !== j.files.length) {
40
+ j.files = cleaned;
41
+ return JSON.stringify(j, null, 4) + '\n';
42
+ }
43
+ }
44
+ } catch { /* 非法 JSON:原样写回,不阻断 */ }
45
+ return content;
46
+ }
47
+
48
+ export function writeProject(gameDir, files, opts = {}) {
49
+ const written = [];
50
+ for (const name of Object.keys(files || {})) {
51
+ if (!isSyncable(name)) continue;
52
+ let content = files[name] ?? '';
53
+ if (name === 'pxt.json') content = sanitizePxtJson(content);
54
+ opts.beforeWrite?.();
55
+ writeFileSync(join(gameDir, name), content, 'utf8');
56
+ written.push(name);
57
+ }
58
+ return written;
59
+ }
@@ -0,0 +1,12 @@
1
+ # 给 AI 的项目规则:MakeCode Arcade 游戏
2
+
3
+ - 游戏代码只写在 `game/main.ts`;资源(精灵/地图,4-bit 16 色)走 `game/assets.json`,不要内联大图到 JS。
4
+ - 这是**纯 TypeScript 项目**:`game/` 里**不要**有 `main.blocks`,`pxt.json` 的 `files` 也不要列它(否则官方编辑器会开在空白积木视图,看不到你的代码)。
5
+ - 改完代码后 `aca dev` 的页面会自动刷新预览;无需手动操作。
6
+ - **精灵**用内联 `img\`\``,AI 直接写。
7
+ - **地图**有两条可靠路线,二选一(细节见 `reference/arcade-api.md` 第7节 / `reference/pitfalls.md` 坑5):
8
+ 1. 命名地图 `tiles.setTilemap(tilemap\`level\`)`——编辑器自动建空地图,人在网页里画好后同步回磁盘;
9
+ 2. `tiles.createTilemap(...)` + **内置图块**(如 `sprites.castle.tileGrass1`),图块用自带图库。
10
+ - ❌ **千万别**把内联 `img` 当图块塞进 `createTilemap`——会让编辑器崩溃(已实测)。
11
+ - 不确定图块/API 名字时查 `reference/` 或编辑器图库,不要臆造不存在的 arcade API。
12
+ - 不要清空 `assets.json`(必须是合法 JSON,空资源写 `{}`),也不要手写 `tilemap.g.jres`(编辑器生成)。
@@ -0,0 +1,12 @@
1
+ # 给 AI 的项目规则:MakeCode Arcade 游戏
2
+
3
+ - 游戏代码只写在 `game/main.ts`;资源(精灵/地图,4-bit 16 色)走 `game/assets.json`,不要内联大图到 JS。
4
+ - 这是**纯 TypeScript 项目**:`game/` 里**不要**有 `main.blocks`,`pxt.json` 的 `files` 也不要列它(否则官方编辑器会开在空白积木视图,看不到你的代码)。
5
+ - 改完代码后 `aca dev` 的页面会自动刷新预览;无需手动操作。
6
+ - **精灵**用内联 `img\`\``,AI 直接写。
7
+ - **地图**有两条可靠路线,二选一(细节见 `reference/arcade-api.md` 第7节 / `reference/pitfalls.md` 坑5):
8
+ 1. 命名地图 `tiles.setTilemap(tilemap\`level\`)`——编辑器自动建空地图,人在网页里画好后同步回磁盘;
9
+ 2. `tiles.createTilemap(...)` + **内置图块**(如 `sprites.castle.tileGrass1`),图块用自带图库。
10
+ - ❌ **千万别**把内联 `img` 当图块塞进 `createTilemap`——会让编辑器崩溃(已实测)。
11
+ - 不确定图块/API 名字时查 `reference/` 或编辑器图库,不要臆造不存在的 arcade API。
12
+ - 不要清空 `assets.json`(必须是合法 JSON,空资源写 `{}`),也不要手写 `tilemap.g.jres`(编辑器生成)。
@@ -0,0 +1,12 @@
1
+ # 给 AI 的项目规则:MakeCode Arcade 游戏
2
+
3
+ - 游戏代码只写在 `game/main.ts`;资源(精灵/地图,4-bit 16 色)走 `game/assets.json`,不要内联大图到 JS。
4
+ - 这是**纯 TypeScript 项目**:`game/` 里**不要**有 `main.blocks`,`pxt.json` 的 `files` 也不要列它(否则官方编辑器会开在空白积木视图,看不到你的代码)。
5
+ - 改完代码后 `aca dev` 的页面会自动刷新预览;无需手动操作。
6
+ - **精灵**用内联 `img\`\``,AI 直接写。
7
+ - **地图**有两条可靠路线,二选一(细节见 `reference/arcade-api.md` 第7节 / `reference/pitfalls.md` 坑5):
8
+ 1. 命名地图 `tiles.setTilemap(tilemap\`level\`)`——编辑器自动建空地图,人在网页里画好后同步回磁盘;
9
+ 2. `tiles.createTilemap(...)` + **内置图块**(如 `sprites.castle.tileGrass1`),图块用自带图库。
10
+ - ❌ **千万别**把内联 `img` 当图块塞进 `createTilemap`——会让编辑器崩溃(已实测)。
11
+ - 不确定图块/API 名字时查 `reference/` 或编辑器图库,不要臆造不存在的 arcade API。
12
+ - 不要清空 `assets.json`(必须是合法 JSON,空资源写 `{}`),也不要手写 `tilemap.g.jres`(编辑器生成)。
@@ -0,0 +1,67 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync } from 'node:fs';
3
+ import { readProject, writeProject } from '../project-io.js';
4
+ import { createWatcher } from './watcher.js';
5
+
6
+ export function startStudio({ gameDir, port = 0, hostHtmlPath }) {
7
+ const clients = new Set();
8
+ const watcher = createWatcher(gameDir, () => {
9
+ for (const res of clients) res.write('data: changed\n\n');
10
+ });
11
+
12
+ const server = createServer((req, res) => {
13
+ const url = req.url.split('?')[0];
14
+ if (req.method === 'GET' && url === '/') {
15
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
16
+ res.end(readFileSync(hostHtmlPath));
17
+ } else if (req.method === 'GET' && url === '/api/project') {
18
+ res.writeHead(200, { 'content-type': 'application/json' });
19
+ res.end(JSON.stringify({ files: readProject(gameDir) }));
20
+ } else if (req.method === 'POST' && url === '/api/save') {
21
+ let body = '';
22
+ req.on('data', (c) => (body += c));
23
+ req.on('end', () => {
24
+ try {
25
+ const { files } = JSON.parse(body || '{}');
26
+ watcher.pause(600);
27
+ const written = writeProject(gameDir, files);
28
+ const incoming = Object.keys(files || {});
29
+ const dropped = incoming.filter((f) => !written.includes(f));
30
+ console.log(`[save] 收到 ${incoming.length} 文件,落盘 ${written.length}`);
31
+ if (dropped.length) console.warn(`[save] ⚠️ 未同步(扩展不支持/不安全): ${dropped.join(', ')}`);
32
+ res.writeHead(200, { 'content-type': 'application/json' });
33
+ res.end(JSON.stringify({ success: true }));
34
+ } catch (e) {
35
+ res.writeHead(500); res.end(String(e));
36
+ }
37
+ });
38
+ } else if (req.method === 'GET' && url === '/events') {
39
+ res.writeHead(200, {
40
+ 'content-type': 'text/event-stream',
41
+ 'cache-control': 'no-cache',
42
+ connection: 'keep-alive',
43
+ });
44
+ res.write('retry: 1000\n\n');
45
+ clients.add(res);
46
+ req.on('close', () => clients.delete(res));
47
+ } else {
48
+ res.writeHead(404); res.end('Not Found');
49
+ }
50
+ });
51
+
52
+ return new Promise((resolve) => {
53
+ server.listen(port, '127.0.0.1', () => {
54
+ const { port: p } = server.address();
55
+ resolve({
56
+ url: `http://127.0.0.1:${p}`,
57
+ port: p,
58
+ async close() {
59
+ for (const r of clients) r.end();
60
+ await watcher.close();
61
+ server.closeAllConnections?.();
62
+ server.close();
63
+ },
64
+ });
65
+ });
66
+ });
67
+ }
@@ -0,0 +1,17 @@
1
+ import chokidar from 'chokidar';
2
+
3
+ export function createWatcher(gameDir, onChange) {
4
+ let pausedUntil = 0;
5
+ let timer = null;
6
+ const watcher = chokidar.watch(gameDir, { ignoreInitial: true });
7
+ const fire = () => {
8
+ if (Date.now() < pausedUntil) return;
9
+ clearTimeout(timer);
10
+ timer = setTimeout(() => { if (Date.now() >= pausedUntil) onChange(); }, 200);
11
+ };
12
+ watcher.on('add', fire).on('change', fire);
13
+ return {
14
+ pause(ms) { pausedUntil = Date.now() + ms; },
15
+ async close() { clearTimeout(timer); await watcher.close(); },
16
+ };
17
+ }
@@ -0,0 +1,2 @@
1
+ # Arcade Game
2
+ 由 arcade-ai 脚手架生成。用 AI 修改 `main.ts`,`aca dev` 实时预览。
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,24 @@
1
+ // 玩家精灵:内联 img(AI 直接写,编辑器里可点开可视化微调)
2
+ let player = sprites.create(img`
3
+ . . . . . . f f f f . . . . . .
4
+ . . . . . f f f f f f . . . . .
5
+ . . . . f f e e e e f f . . . .
6
+ . . . . f e f e e f e f . . . .
7
+ . . . . f e e e e e e f . . . .
8
+ . . . . f e f e e f e f . . . .
9
+ . . . . f e f f f f e f . . . .
10
+ . . . . f e e e e e e f . . . .
11
+ . . . . . f f f f f f . . . . .
12
+ . . . . . . f f f f . . . . . .
13
+ . . . . . f f . . f f . . . . .
14
+ . . . . . . . . . . . . . . . .
15
+ `, SpriteKind.Player)
16
+ controller.moveSprite(player)
17
+ player.setStayInScreen(true)
18
+
19
+ // 地图:引用命名地图 `level`。
20
+ // 编辑器首次打开会自动创建一张空地图——你在网页里点开地图编辑器画好后,
21
+ // 会自动同步回本地磁盘(tilemap.g.ts / tilemap.g.jres),并随游戏运行。
22
+ // 画格子时可直接用内置图块库(如 sprites.castle.tileGrass1),无需手画。
23
+ tiles.setTilemap(tilemap`level`)
24
+ scene.cameraFollowSprite(player)
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "arcade-game",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "dependencies": {
6
+ "device": "*"
7
+ },
8
+ "files": [
9
+ "main.ts",
10
+ "README.md",
11
+ "assets.json"
12
+ ],
13
+ "supportedTargets": [
14
+ "arcade"
15
+ ],
16
+ "preferredEditor": "tsprj",
17
+ "languageRestriction": "javascript-only"
18
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "arcade-game-project",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": { "dev": "aca dev" },
6
+ "devDependencies": { "arcade-ai": "^0.1.0" }
7
+ }