Skip to content

Commit 29ed931

Browse files
feat(moonscratch): improve compatibility (#102)
* feat(moonscratch): switch to frame-based stepping API - add internal event loop spec under packages/moonscratch/docs - replace step(dtMs) with stepFrame/runFrames/runForTime/runUntilIdle - add initialNowMs and vm_set_now_ms support - update MoonBit API to vm_step_frame - refresh tests, docs, and AGENTS requirements Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> * feat: add support for WARP * feat: add renderer * fix * fix * fix * fix(moonscratch): use Scratch compare semantics for lt/gt - compare numeric operands as numbers before string fallback - add regression test for negative numeric comparisons Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> * fix(moonscratch): respect pen transparency on stroke rasterization - avoid repeated alpha overdraw when drawing pen lines - add regression test for semi-transparent overlay stroke Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> * chore(moonscratch): always build js target before test/build scripts - ensure _build/js bindings are up to date for viewer/runtime behavior Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> * fix(moonscratch): align pen rasterization with scratch line blending * fix * fix * breaking * perf: 読み込みの高速化 * fix * fix * perf: add precompile * perf: better AOT * perf: AOT * fix --------- Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
1 parent 11da86a commit 29ed931

87 files changed

Lines changed: 9855 additions & 912 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,6 @@
4848
* `bun fmt`: apply formatting
4949
* `bun typecheck`: run type checking
5050
* `bun ref:build`: update the docs
51+
* when changes include `packages/moonscratch`, you also have to run:
52+
* `moon check` (in `packages/moonscratch`)
53+
* `bun run test` (in `packages/moonscratch`)

a.svg

Lines changed: 1 addition & 0 deletions
Loading

bun.lock

Lines changed: 444 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/moonscratch/AGENTS.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ You can browse and install extra skills here:
1515
- In the toplevel directory, there is a `moon.mod.json` file listing module
1616
metadata.
1717

18-
* src/ にあるテストはユニットテストのみで、マクロなテストは test/ に書く
18+
* Tests under `src/` are unit tests only; macro-style tests should be written in `test/`.
1919

2020
## AGENTS.md Maintenance
2121

22-
- AGENTS.md はルートだけでなく、責務が分かれる主要ディレクトリにも配置する。
23-
- エージェントはプロジェクト構成や運用ルールの変更を検知したら、関連する AGENTS.md を必要に応じて自律的に更新し、現状と乖離させない。
22+
- Place `AGENTS.md` files not only at the root but also in major directories where responsibilities diverge.
23+
- Agents should autonomously update related `AGENTS.md` files when project structure or operating rules change, keeping instructions synchronized with the current state.
2424

2525
## Coding convention
2626

@@ -77,3 +77,8 @@ You can browse and install extra skills here:
7777
- `bun run test` to run tests. `vite-plus/test` API is same as `vitest`.
7878
- `bun run build` to build the project.
7979
- `bun typecheck` to typecheck the code using `tsgo`.
80+
- If you add or modify tests, run `bun run test` before reporting completion.
81+
- Required before finishing any change in `packages/moonscratch`:
82+
- `bun typecheck`
83+
- `moon check`
84+
- `bun run test`

packages/moonscratch/PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ JS からは Wasm ラッパ API で利用可能にする。
3131
- new/start/green_flag/step/post_io/broadcast/stop_all/snapshot/dispose
3232
- JS API
3333
- createHeadlessVM({projectJson, assets, options, host})
34-
- start(), greenFlag(), step(dtMs), postIO(), onEffect(), getSnapshot()
34+
- start(), greenFlag(), setTime(nowMs), stepFrame(frameCount?), postIO(), onEffect(), getSnapshot()
3535

3636
### Implementation
3737

packages/moonscratch/README.mbt.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ test {
2525
Err(_) => fail("failed to create vm")
2626
}
2727
@moonscratch.vm_start(vm)
28-
let report = @moonscratch.vm_step(vm, 16)
28+
@moonscratch.vm_set_time(vm, 33)
29+
let report = @moonscratch.vm_step_frame(vm, 1)
2930
let snapshot_json = @moonscratch.vm_snapshot_json(vm)
3031
inspect(report.active_threads >= 0, content="true")
3132
inspect(snapshot_json.contains("targets"), content="true")
@@ -56,7 +57,8 @@ import {
5657

5758
const vm = createHeadlessVM({ projectJson, assets, options })
5859
vm.greenFlag()
59-
vm.step(16)
60+
vm.setTime(Date.now())
61+
vm.stepFrame()
6062
const effects = vm.takeEffects()
6163
const snapshot = vm.snapshot()
6264

packages/moonscratch/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/moonscratch/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# nakasyou/moonscratch
2+
3+
Headless Scratch runtime in MoonBit.
4+
5+
## What is implemented
6+
7+
- Loads Scratch 3 `project.json` (+ optional assets metadata map)
8+
- Can fill missing costume assets from `cdn.scratch.mit.edu` (async API)
9+
- Runs scripts from hats (`event_whenflagclicked`, `event_whenbroadcastreceived`)
10+
- Supports core opcodes for:
11+
- `event`, `control`, `data`, `operator`
12+
- subset of `motion`, `looks`, `sound`, `sensing`
13+
- Emits side effects as JSON event queue (`vm_take_effects_json`)
14+
15+
## MoonBit API
16+
17+
```mbt check
18+
///|
19+
test {
20+
let precompiled_result = try? @moonscratch.vm_compile_from_json(
21+
"{\"targets\":[{\"isStage\":true,\"name\":\"Stage\",\"variables\":{},\"lists\":{},\"blocks\":{}}]}",
22+
)
23+
let precompiled = match precompiled_result {
24+
Ok(value) => value
25+
Err(_) => fail("failed to precompile project")
26+
}
27+
let vm_result = try? @moonscratch.vm_new_from_compiled(precompiled)
28+
let vm = match vm_result {
29+
Ok(value) => value
30+
Err(_) => fail("failed to create vm")
31+
}
32+
@moonscratch.vm_start(vm)
33+
@moonscratch.vm_set_time(vm, 33)
34+
let report = @moonscratch.vm_step_frame(vm)
35+
let snapshot_json = @moonscratch.vm_snapshot_json(vm)
36+
inspect(report.active_threads >= 0, content="true")
37+
inspect(snapshot_json.contains("targets"), content="true")
38+
}
39+
```
40+
41+
## JavaScript setup
42+
43+
Build JS artifacts:
44+
45+
```bash
46+
moon build --target js
47+
```
48+
49+
Use the JS wrapper:
50+
51+
```bash
52+
node js/example.mjs
53+
```
54+
55+
Wrapper file: `js/headless-vm.mjs`
56+
57+
```js
58+
import {
59+
createHeadlessVM,
60+
createPrecompiledProject,
61+
createHeadlessVMWithScratchAssets,
62+
} from './js/headless-vm.mjs'
63+
64+
const precompiled = createPrecompiledProject({ projectJson, assets })
65+
const vm = createHeadlessVM({ precompiled, options })
66+
vm.greenFlag()
67+
vm.setTime(Date.now())
68+
vm.stepFrame()
69+
const effects = vm.takeEffects()
70+
const snapshot = vm.snapshot()
71+
72+
const vmWithScratchAssets = await createHeadlessVMWithScratchAssets({
73+
projectJson,
74+
assets,
75+
options,
76+
})
77+
```
78+
79+
## Verification
80+
81+
```bash
82+
moon check
83+
moon test
84+
```

packages/moonscratch/a.png

1.92 KB
Loading
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { Project } from 'hikkaku'
2+
import {
3+
add,
4+
addToList,
5+
and,
6+
changeVariableBy,
7+
divide,
8+
equals,
9+
gt,
10+
ifThen,
11+
lt,
12+
mathop,
13+
mod,
14+
multiply,
15+
or,
16+
repeat,
17+
repeatUntil,
18+
setVariableTo,
19+
subtract,
20+
whenFlagClicked,
21+
} from 'hikkaku/blocks'
22+
import { bench, describe } from 'vite-plus/test'
23+
import { createHeadlessVM, createPrecompiledProject } from '../js/vm/factory.ts'
24+
25+
await import('scratch-storage')
26+
27+
const benchOptions = {
28+
time: 3000,
29+
warmupTime: 1000,
30+
}
31+
32+
const runUntilFinished = (vm: {
33+
greenFlag(): void
34+
stepFrame(): { stopReason: string }
35+
}) => {
36+
vm.greenFlag()
37+
while (true) {
38+
const result = vm.stepFrame()
39+
if (result.stopReason === 'finished') {
40+
break
41+
}
42+
}
43+
}
44+
45+
describe('calc', () => {
46+
const project = new Project()
47+
const acc = project.stage.createVariable('acc', 0)
48+
const angle = project.stage.createVariable('angle', 0)
49+
50+
project.stage.run(() => {
51+
whenFlagClicked(() => {
52+
setVariableTo(acc, 0)
53+
setVariableTo(angle, 0)
54+
repeat(50000, () => {
55+
setVariableTo(
56+
acc,
57+
add(
58+
acc.get(),
59+
multiply(mathop('sin', angle.get()), mathop('cos', angle.get())),
60+
),
61+
)
62+
changeVariableBy(angle, subtract(divide(angle.get(), 2), -1))
63+
})
64+
})
65+
})
66+
67+
const projectJson = project.toScratch()
68+
const precompiled = createPrecompiledProject({ projectJson })
69+
const vm = createHeadlessVM({
70+
precompiled,
71+
initialNowMs: 0,
72+
options: {
73+
stepTimeoutTicks: 1000000,
74+
turbo: true,
75+
},
76+
})
77+
78+
bench(
79+
'moonscratch',
80+
() => {
81+
runUntilFinished(vm)
82+
},
83+
benchOptions,
84+
)
85+
})
86+
87+
describe('calc primes', () => {
88+
const project = new Project()
89+
const primes = project.stage.createList('primes', [])
90+
const candidate = project.stage.createVariable('candidate', 2)
91+
const divisor = project.stage.createVariable('divisor', 0)
92+
const isPrime = project.stage.createVariable('is prime', 0)
93+
const primeCount = project.stage.createVariable('prime count', 0)
94+
95+
project.stage.run(() => {
96+
whenFlagClicked(() => {
97+
setVariableTo(primeCount, 0)
98+
setVariableTo(candidate, 2)
99+
setVariableTo(divisor, 2)
100+
setVariableTo(isPrime, 0)
101+
repeatUntil(equals(primeCount.get(), 100), () => {
102+
setVariableTo(isPrime, 1)
103+
setVariableTo(divisor, 2)
104+
repeatUntil(
105+
or(
106+
gt(divisor.get(), mathop('sqrt', candidate.get())),
107+
equals(isPrime.get(), 0),
108+
),
109+
() => {
110+
ifThen(
111+
and(
112+
equals(mod(candidate.get(), divisor.get()), 0),
113+
lt(divisor.get(), candidate.get()),
114+
),
115+
() => {
116+
setVariableTo(isPrime, 0)
117+
},
118+
)
119+
changeVariableBy(divisor, 1)
120+
},
121+
)
122+
ifThen(equals(isPrime.get(), 1), () => {
123+
addToList(primes, candidate.get())
124+
changeVariableBy(primeCount, 1)
125+
})
126+
changeVariableBy(candidate, 1)
127+
})
128+
})
129+
})
130+
131+
const projectJson = project.toScratch()
132+
const precompiled = createPrecompiledProject({ projectJson })
133+
const vm = createHeadlessVM({
134+
precompiled,
135+
initialNowMs: 0,
136+
options: {
137+
stepTimeoutTicks: 1000000,
138+
turbo: true,
139+
},
140+
})
141+
142+
bench(
143+
'moonscratch',
144+
() => {
145+
runUntilFinished(vm)
146+
},
147+
benchOptions,
148+
)
149+
})

0 commit comments

Comments
 (0)