From 388b505e0b89311f0aaadc0c7583ff9f1e77928d Mon Sep 17 00:00:00 2001 From: Fam Zheng Date: Mon, 4 May 2026 15:12:22 +0100 Subject: [PATCH] =?UTF-8?q?app=20#1=20simpleasm:=20=E4=BB=8E=20oci=20?= =?UTF-8?q?=E8=BF=81=E8=BF=87=E6=9D=A5=EF=BC=8Casm.famzheng.me=20=E5=B7=B2?= =?UTF-8?q?=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 FastAPI 重写为 axum + rusqlite (musl static, 2.8MB) - 前端原样搬运 (Vue3 + Vite + Pinia + vue-router + vite-plugin-yaml) - k8s: cube-simpleasm ns + 1Gi PVC (k3s local-path) + Recreate strategy - CI: 复刻 deploy-cube.yml,按 apps/simpleasm/** 触发 - cube 门户里 simpleasm 状态从 pending 改成 live - 数据冷启 (Fam 拍板不带历史进度) --- .gitea/workflows/deploy-simpleasm.yml | 51 + Cargo.lock | 141 ++ Cargo.toml | 4 + apps/cube/frontend/src/apps.ts | 6 +- apps/simpleasm/Cargo.toml | 16 + apps/simpleasm/Dockerfile | 8 + apps/simpleasm/frontend/index.html | 15 + apps/simpleasm/frontend/package-lock.json | 1214 +++++++++++++++++ apps/simpleasm/frontend/package.json | 20 + apps/simpleasm/frontend/src/App.vue | 57 + .../frontend/src/components/CodeEditor.vue | 106 ++ .../frontend/src/components/LevelComplete.vue | 106 ++ .../frontend/src/components/MachineState.vue | 139 ++ .../frontend/src/components/OutputConsole.vue | 72 + .../frontend/src/components/TutorialPanel.vue | 99 ++ apps/simpleasm/frontend/src/lib/levels.js | 5 + .../simpleasm/frontend/src/lib/levels/01.yaml | 50 + .../simpleasm/frontend/src/lib/levels/02.yaml | 45 + .../simpleasm/frontend/src/lib/levels/03.yaml | 53 + .../simpleasm/frontend/src/lib/levels/04.yaml | 47 + .../simpleasm/frontend/src/lib/levels/05.yaml | 58 + .../simpleasm/frontend/src/lib/levels/06.yaml | 54 + .../simpleasm/frontend/src/lib/levels/07.yaml | 62 + .../simpleasm/frontend/src/lib/levels/08.yaml | 79 ++ .../simpleasm/frontend/src/lib/levels/09.yaml | 52 + .../simpleasm/frontend/src/lib/levels/10.yaml | 81 ++ apps/simpleasm/frontend/src/lib/vm.js | 375 +++++ apps/simpleasm/frontend/src/main.js | 9 + apps/simpleasm/frontend/src/router/index.js | 12 + apps/simpleasm/frontend/src/stores/game.js | 84 ++ .../frontend/src/views/LevelSelectView.vue | 110 ++ .../frontend/src/views/LevelView.vue | 270 ++++ .../frontend/src/views/WelcomeView.vue | 158 +++ apps/simpleasm/frontend/vite.config.js | 12 + apps/simpleasm/k8s/deployment.yaml | 58 + apps/simpleasm/k8s/ingress.yaml | 18 + apps/simpleasm/k8s/namespace.yaml | 4 + apps/simpleasm/k8s/pvc.yaml | 12 + apps/simpleasm/k8s/service.yaml | 12 + apps/simpleasm/src/main.rs | 214 +++ 40 files changed, 3985 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/deploy-simpleasm.yml create mode 100644 apps/simpleasm/Cargo.toml create mode 100644 apps/simpleasm/Dockerfile create mode 100644 apps/simpleasm/frontend/index.html create mode 100644 apps/simpleasm/frontend/package-lock.json create mode 100644 apps/simpleasm/frontend/package.json create mode 100644 apps/simpleasm/frontend/src/App.vue create mode 100644 apps/simpleasm/frontend/src/components/CodeEditor.vue create mode 100644 apps/simpleasm/frontend/src/components/LevelComplete.vue create mode 100644 apps/simpleasm/frontend/src/components/MachineState.vue create mode 100644 apps/simpleasm/frontend/src/components/OutputConsole.vue create mode 100644 apps/simpleasm/frontend/src/components/TutorialPanel.vue create mode 100644 apps/simpleasm/frontend/src/lib/levels.js create mode 100644 apps/simpleasm/frontend/src/lib/levels/01.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/02.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/03.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/04.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/05.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/06.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/07.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/08.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/09.yaml create mode 100644 apps/simpleasm/frontend/src/lib/levels/10.yaml create mode 100644 apps/simpleasm/frontend/src/lib/vm.js create mode 100644 apps/simpleasm/frontend/src/main.js create mode 100644 apps/simpleasm/frontend/src/router/index.js create mode 100644 apps/simpleasm/frontend/src/stores/game.js create mode 100644 apps/simpleasm/frontend/src/views/LevelSelectView.vue create mode 100644 apps/simpleasm/frontend/src/views/LevelView.vue create mode 100644 apps/simpleasm/frontend/src/views/WelcomeView.vue create mode 100644 apps/simpleasm/frontend/vite.config.js create mode 100644 apps/simpleasm/k8s/deployment.yaml create mode 100644 apps/simpleasm/k8s/ingress.yaml create mode 100644 apps/simpleasm/k8s/namespace.yaml create mode 100644 apps/simpleasm/k8s/pvc.yaml create mode 100644 apps/simpleasm/k8s/service.yaml create mode 100644 apps/simpleasm/src/main.rs diff --git a/.gitea/workflows/deploy-simpleasm.yml b/.gitea/workflows/deploy-simpleasm.yml new file mode 100644 index 0000000..099eaf0 --- /dev/null +++ b/.gitea/workflows/deploy-simpleasm.yml @@ -0,0 +1,51 @@ +name: deploy simpleasm +# asm.famzheng.me — 汇编教学小游戏。host shell runner(fam 用户)。 + +on: + push: + branches: [master] + paths: + - 'apps/simpleasm/**' + - 'crates/cube-core/**' + - 'Cargo.toml' + - 'Cargo.lock' + - '.gitea/workflows/deploy-simpleasm.yml' + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + env: + APP: simpleasm + IMAGE: registry.famzheng.me/mochi/simpleasm + steps: + - uses: actions/checkout@v4 + + - name: Resolve image tag + id: tag + run: | + echo "sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT" + + - name: Build rust (musl static) + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo build --release --target x86_64-unknown-linux-musl -p "$APP" + + - name: Build frontend + run: | + cd "apps/$APP/frontend" + npm ci + npm run build + + - name: Build & push image + env: + REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + echo "$REGISTRY_TOKEN" | docker login registry.famzheng.me -u mochi --password-stdin + docker build -f "apps/$APP/Dockerfile" -t "$IMAGE:${{ steps.tag.outputs.sha }}" . + docker push "$IMAGE:${{ steps.tag.outputs.sha }}" + + - name: Roll out to k3s + run: | + kubectl -n "cube-$APP" set image "deploy/$APP" "$APP=$IMAGE:${{ steps.tag.outputs.sha }}" + kubectl -n "cube-$APP" rollout status "deploy/$APP" --timeout=120s diff --git a/Cargo.lock b/Cargo.lock index dc21489..f54e099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -95,6 +107,16 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -130,6 +152,24 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -178,6 +218,24 @@ dependencies = [ "slab", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + [[package]] name = "http" version = "1.4.0" @@ -282,6 +340,17 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.14" @@ -395,6 +464,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -439,6 +514,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -464,6 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -531,6 +621,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -541,6 +637,19 @@ dependencies = [ "libc", ] +[[package]] +name = "simpleasm" +version = "0.1.0" +dependencies = [ + "axum", + "cube-core", + "rusqlite", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "slab" version = "0.4.12" @@ -777,6 +886,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -798,6 +919,26 @@ dependencies = [ "windows-link", ] +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 34191c4..b57fa32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/cube-core", "apps/cube", + "apps/simpleasm", ] [workspace.package] @@ -17,6 +18,9 @@ tower = "0.5" tower-http = { version = "0.6", features = ["fs", "trace"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +rusqlite = { version = "0.32", features = ["bundled"] } [profile.release] opt-level = "z" diff --git a/apps/cube/frontend/src/apps.ts b/apps/cube/frontend/src/apps.ts index f549cac..109b95d 100644 --- a/apps/cube/frontend/src/apps.ts +++ b/apps/cube/frontend/src/apps.ts @@ -33,9 +33,9 @@ export const apps: App[] = [ { slug: 'simpleasm', name: 'simpleasm', - description: '汇编教学/玩具。从 oci 迁移中(原 asm.oci.euphon.net)。', - url: 'https://simpleasm.famzheng.me', - status: 'pending', + description: '汇编教学小游戏。', + url: 'https://asm.famzheng.me', + status: 'live', }, { slug: 'guitar', diff --git a/apps/simpleasm/Cargo.toml b/apps/simpleasm/Cargo.toml new file mode 100644 index 0000000..f72b2f7 --- /dev/null +++ b/apps/simpleasm/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "simpleasm" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "asm.famzheng.me — 汇编教学小游戏,玩家进度持久化在 sqlite" + +[dependencies] +cube-core = { path = "../../crates/cube-core" } +axum = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +rusqlite = { workspace = true } diff --git a/apps/simpleasm/Dockerfile b/apps/simpleasm/Dockerfile new file mode 100644 index 0000000..865bcdf --- /dev/null +++ b/apps/simpleasm/Dockerfile @@ -0,0 +1,8 @@ +# simpleasm — asm.famzheng.me +# Build context = repo root(同 cube),所以路径都是 apps/simpleasm/... +# Rust + frontend 都在 host 上 build,容器只是拼装。 +FROM scratch +COPY target/x86_64-unknown-linux-musl/release/simpleasm /simpleasm +COPY apps/simpleasm/frontend/dist /dist +EXPOSE 8080 +ENTRYPOINT ["/simpleasm"] diff --git a/apps/simpleasm/frontend/index.html b/apps/simpleasm/frontend/index.html new file mode 100644 index 0000000..381cd1d --- /dev/null +++ b/apps/simpleasm/frontend/index.html @@ -0,0 +1,15 @@ + + + + + + Simple ASM - Assembly Adventure + + + + + +
+ + + diff --git a/apps/simpleasm/frontend/package-lock.json b/apps/simpleasm/frontend/package-lock.json new file mode 100644 index 0000000..0105a2c --- /dev/null +++ b/apps/simpleasm/frontend/package-lock.json @@ -0,0 +1,1214 @@ +{ + "name": "simpleasm", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simpleasm", + "version": "1.0.0", + "dependencies": { + "@modyfi/vite-plugin-yaml": "^1.1.1", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@modyfi/vite-plugin-yaml": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@modyfi/vite-plugin-yaml/-/vite-plugin-yaml-1.1.1.tgz", + "integrity": "sha512-rEbfFNlMGLKpAYs2RsfLAhxCHFa6M4QKHHk0A4EYcCJAUwFtFO6qiEdLjUGUTtnRUxAC7GxxCa+ZbeUILSDvqQ==", + "dependencies": { + "@rollup/pluginutils": "5.1.0", + "js-yaml": "4.1.0", + "tosource": "2.0.0-alpha.3" + }, + "peerDependencies": { + "vite": ">=3.2.7" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tosource": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/tosource/-/tosource-2.0.0-alpha.3.tgz", + "integrity": "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==", + "engines": { + "node": ">=10" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/apps/simpleasm/frontend/package.json b/apps/simpleasm/frontend/package.json new file mode 100644 index 0000000..dc09618 --- /dev/null +++ b/apps/simpleasm/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "simpleasm", + "private": true, + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@modyfi/vite-plugin-yaml": "^1.1.1", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.4.0" + } +} diff --git a/apps/simpleasm/frontend/src/App.vue b/apps/simpleasm/frontend/src/App.vue new file mode 100644 index 0000000..f648b03 --- /dev/null +++ b/apps/simpleasm/frontend/src/App.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/simpleasm/frontend/src/components/CodeEditor.vue b/apps/simpleasm/frontend/src/components/CodeEditor.vue new file mode 100644 index 0000000..0746b4b --- /dev/null +++ b/apps/simpleasm/frontend/src/components/CodeEditor.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/components/LevelComplete.vue b/apps/simpleasm/frontend/src/components/LevelComplete.vue new file mode 100644 index 0000000..4d45b5b --- /dev/null +++ b/apps/simpleasm/frontend/src/components/LevelComplete.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/components/MachineState.vue b/apps/simpleasm/frontend/src/components/MachineState.vue new file mode 100644 index 0000000..05c109f --- /dev/null +++ b/apps/simpleasm/frontend/src/components/MachineState.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/components/OutputConsole.vue b/apps/simpleasm/frontend/src/components/OutputConsole.vue new file mode 100644 index 0000000..386cb08 --- /dev/null +++ b/apps/simpleasm/frontend/src/components/OutputConsole.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/components/TutorialPanel.vue b/apps/simpleasm/frontend/src/components/TutorialPanel.vue new file mode 100644 index 0000000..48470da --- /dev/null +++ b/apps/simpleasm/frontend/src/components/TutorialPanel.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/lib/levels.js b/apps/simpleasm/frontend/src/lib/levels.js new file mode 100644 index 0000000..baa2c1a --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels.js @@ -0,0 +1,5 @@ +const modules = import.meta.glob('./levels/*.yaml', { eager: true }) + +export const levels = Object.values(modules) + .map(m => m.default) + .sort((a, b) => a.id - b.id) diff --git a/apps/simpleasm/frontend/src/lib/levels/01.yaml b/apps/simpleasm/frontend/src/lib/levels/01.yaml new file mode 100644 index 0000000..dda2095 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/01.yaml @@ -0,0 +1,50 @@ +id: 1 +title: Meet the Registers +subtitle: The robot's memory slots +description: Learn the MOV instruction to load values into registers + +tutorial: + - title: What is a register? + text: > + The CPU is the brain of the computer, and **registers** are the tiny drawers + right next to it — the fastest storage there is! Our machine has 8 registers: + **R0** through **R7**. + - title: The MOV instruction + text: > + `MOV` puts a number into a register. Numbers are prefixed with **#** to mark + them as immediate values: + code: | + MOV R0, #42 ; put 42 into R0 + MOV R1, #100 ; put 100 into R1 + - title: The XHLT instruction + text: > + Every program ends with `XHLT` (halt = stop) to tell the machine "we're done!" + code: | + MOV R0, #42 + XHLT + +goal: Put the number **42** into the **R0** register + +initialState: {} + +testCases: + - init: {} + expected: + registers: + R0: 42 + +hints: + - "MOV format: MOV register, #number" + - "Try: MOV R0, #???" + - "Answer: MOV R0, #42 then XHLT" + +starThresholds: [2, 3, 5] + +starterCode: | + ; Put 42 into the R0 register + ; Tip: prefix numbers with # + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/02.yaml b/apps/simpleasm/frontend/src/lib/levels/02.yaml new file mode 100644 index 0000000..a768c87 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/02.yaml @@ -0,0 +1,45 @@ +id: 2 +title: Data Mover +subtitle: Copying between registers +description: Learn how to copy data between registers + +tutorial: + - title: Register-to-register copy + text: > + MOV can also **copy** the value of one register into another (no # needed): + code: | + MOV R1, R0 ; copy R0 into R1 + - title: It's a copy, not a move! + text: > + Despite being called "MOV" (move), it's actually a **copy**. After it runs + R0 still holds its value, and R1 now matches it. + +goal: R0 already holds **7** — copy it into both **R1** and **R2** + +initialState: + registers: + R0: 7 + +testCases: + - init: {} + expected: + registers: + R0: 7 + R1: 7 + R2: 7 + +hints: + - "MOV register, register — copies right into left" + - "MOV R1, R0 copies R0 into R1" + - "Answer: MOV R1, R0 / MOV R2, R0 / XHLT" + +starThresholds: [3, 4, 6] + +starterCode: | + ; R0 = 7 + ; Copy R0 into R1 and R2 + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/03.yaml b/apps/simpleasm/frontend/src/lib/levels/03.yaml new file mode 100644 index 0000000..04123ff --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/03.yaml @@ -0,0 +1,53 @@ +id: 3 +title: Add and Subtract +subtitle: The power of three operands +description: Learn the ADD and SUB instructions + +tutorial: + - title: ADD — three-operand addition + text: > + ARM-style addition is neat: **three operands**! The first is the destination, + the other two are the values being combined: + code: | + ADD R2, R0, R1 ; R2 = R0 + R1 + ADD R0, R0, #10 ; R0 = R0 + 10 + - title: SUB — subtraction + text: > + SUB works the same way, three operands: + code: | + SUB R2, R0, R1 ; R2 = R0 - R1 + SUB R0, R0, #5 ; R0 = R0 - 5 + - title: Why three operands? + text: > + You can drop the result straight into a new register, **no extra copy needed**! + +goal: R0=**15**, R1=**27** — compute R0+R1 into **R2** (R0 and R1 unchanged) + +initialState: + registers: + R0: 15 + R1: 27 + +testCases: + - init: {} + expected: + registers: + R0: 15 + R1: 27 + R2: 42 + +hints: + - "First operand of ADD is the destination, the next two are added" + - "ADD R2, R0, R1 — result goes in R2" + - "Answer: ADD R2, R0, R1 / XHLT" + +starThresholds: [2, 3, 5] + +starterCode: | + ; R0=15, R1=27 + ; Compute R0 + R1 into R2 + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/04.yaml b/apps/simpleasm/frontend/src/lib/levels/04.yaml new file mode 100644 index 0000000..a066671 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/04.yaml @@ -0,0 +1,47 @@ +id: 4 +title: Multiply and Divide +subtitle: Stronger arithmetic +description: Learn the MUL and XDIV instructions + +tutorial: + - title: MUL — multiplication + text: > + MUL is also three operands: + code: | + MOV R0, #6 + MOV R1, #7 + MUL R2, R0, R1 ; R2 = 6 × 7 = 42 + - title: XDIV — integer division + text: > + XDIV does integer division (the fractional part is discarded): + code: | + MOV R0, #100 + MOV R1, #4 + XDIV R2, R0, R1 ; R2 = 100 ÷ 4 = 25 + +goal: Compute **6 × 7** into R0 and **100 ÷ 4** into R1 + +initialState: {} + +testCases: + - init: {} + expected: + registers: + R0: 42 + R1: 25 + +hints: + - "MOV the numbers into registers first, then MUL/XDIV" + - "MUL R0, R2, R3 puts R2×R3 into R0" + - "Answer: MOV R2, #6 / MOV R3, #7 / MUL R0, R2, R3 / MOV R2, #100 / MOV R3, #4 / XDIV R1, R2, R3 / XHLT" + +starThresholds: [7, 9, 12] + +starterCode: | + ; Compute 6×7 into R0 + ; Compute 100÷4 into R1 + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/05.yaml b/apps/simpleasm/frontend/src/lib/levels/05.yaml new file mode 100644 index 0000000..7a90034 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/05.yaml @@ -0,0 +1,58 @@ +id: 5 +title: Bitwise Magic +subtitle: The secret life of 0s and 1s +description: Learn the AND, ORR, EOR, and MVN instructions + +tutorial: + - title: The binary world + text: > + Computers store everything as **0**s and **1**s. 42 in binary is `00101010`. + The state panel on the right shows the binary form of every register! + - title: AND — both must be 1 + text: > + AND compares bit by bit, returning 1 only when both bits are 1. Great for + "extracting" specific bits: + code: | + ; 11111111 (255) + ; AND 00001111 (15) + ; = 00001111 (15) + AND R0, R0, #15 + - title: Other bitwise ops + text: > + **ORR** — 1 if either bit is 1 (OR) + + **EOR** — 1 if the bits differ (XOR) + + **MVN** — flip every bit (NOT) + code: | + ORR R0, R0, #240 ; set the upper 4 bits + EOR R0, R0, #255 ; flip the lower 8 bits + MVN R0, R0 ; flip every bit + +goal: R0 = **255** (binary 11111111). Use AND to extract the **lower 4 bits** so R0 becomes **15** + +initialState: + registers: + R0: 255 + +testCases: + - init: {} + expected: + registers: + R0: 15 + +hints: + - "AND keeps the bits you want and clears the rest" + - "The mask for the lower 4 bits is 15 (binary 00001111)" + - "Answer: AND R0, R0, #15 / XHLT" + +starThresholds: [2, 3, 5] + +starterCode: | + ; R0 = 255 (binary 11111111) + ; Use AND to extract the lower 4 bits + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/06.yaml b/apps/simpleasm/frontend/src/lib/levels/06.yaml new file mode 100644 index 0000000..1a3399c --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/06.yaml @@ -0,0 +1,54 @@ +id: 6 +title: Shift Operations +subtitle: A dance of bits +description: Learn the LSL and LSR instructions + +tutorial: + - title: LSL — Logical Shift Left + text: > + All bits move left, zeros fill in on the right. **Shifting left by 1 = multiplying by 2**. + Shifting left by 3 = multiplying by 8: + code: | + ; 5 = 00000101 + LSL R0, R0, #1 ; 00001010 = 10 (×2) + LSL R0, R0, #1 ; 00010100 = 20 (×2) + - title: LSR — Logical Shift Right + text: > + All bits move right, zeros fill in on the left. **Shifting right by 1 = dividing by 2**: + code: | + MOV R0, #40 + LSR R0, R0, #1 ; 20 (÷2) + LSR R0, R0, #2 ; 5 (÷4) + - title: A programmer's trick + text: > + On a real ARM CPU, shifts are far cheaper than multiply/divide! + `LSL R0, R0, #3` beats `MUL R0, R0, #8`. + +goal: R0 = **5**. Use **shift only** to make it **40** (40 = 5 × 8 = 5 × 2³) + +initialState: + registers: + R0: 5 + +testCases: + - init: {} + expected: + registers: + R0: 40 + +hints: + - "8 = 2³ — multiplying by 8 is shifting left by 3" + - "LSL R0, R0, #3" + - "It's just one instruction!" + +starThresholds: [2, 3, 5] +blockedOps: [MUL, XDIV] + +starterCode: | + ; R0 = 5 + ; Use LSL to turn R0 into 40 (no MUL allowed) + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/07.yaml b/apps/simpleasm/frontend/src/lib/levels/07.yaml new file mode 100644 index 0000000..19ecc61 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/07.yaml @@ -0,0 +1,62 @@ +id: 7 +title: Memory Read and Write +subtitle: Open up the bigger storage +description: Learn the LDR and STR instructions + +tutorial: + - title: What is memory? + text: > + 8 registers isn't a lot! **Memory** is like a row of 256 lockers, each with + its own number (0-255). + - title: LDR — load from memory + text: > + Put the address into a register, then use `LDR` to read from there: + code: | + MOV R1, #0 ; address = 0 + LDR R0, [R1] ; R0 = memory[0] + - title: STR — store into memory + text: > + `STR` writes a register's value into memory: + code: | + MOV R1, #5 ; address = 5 + STR R0, [R1] ; memory[5] = R0 + - title: Offset addressing + text: > + You can also add an offset: `[R1, #4]` means address R1+4: + code: | + MOV R1, #0 + LDR R0, [R1, #0] ; memory[0] + LDR R2, [R1, #1] ; memory[1] + +goal: memory[0]=**10**, memory[1]=**20** — store their sum into **memory[2]** + +initialState: + memory: + 0: 10 + 1: 20 + +testCases: + - init: {} + expected: + memory: + 2: 30 + +hints: + - "LDR the values into registers, add them, then STR the result back" + - "MOV R3, #0 sets a base address; LDR R0, [R3, #0] reads the first value" + - "Answer: MOV R3, #0 / LDR R0, [R3, #0] / LDR R1, [R3, #1] / ADD R2, R0, R1 / STR R2, [R3, #2] / XHLT" + +starThresholds: [6, 8, 10] + +starterCode: | + ; memory[0]=10, memory[1]=20 + ; Compute the sum and store into memory[2] + ; + ; Tip: MOV an address into a register first + ; then use LDR/STR to read/write memory + + + XHLT + +showMemory: true +memoryRange: [0, 15] diff --git a/apps/simpleasm/frontend/src/lib/levels/08.yaml b/apps/simpleasm/frontend/src/lib/levels/08.yaml new file mode 100644 index 0000000..6967d97 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/08.yaml @@ -0,0 +1,79 @@ +id: 8 +title: Compare and Branch +subtitle: Letting the program decide +description: Learn CMP and conditional branch instructions + +tutorial: + - title: Up to now... + text: > + Programs ran top to bottom in order. With **branching**, the program can + finally make decisions! + - title: CMP — compare + text: > + `CMP` compares two values and remembers the result (the values themselves + are unchanged): + code: | + CMP R0, #10 ; compare R0 with 10 + - title: Conditional branches + text: > + After comparing, use **B** (Branch) to jump: + code: | + BEQ label ; jump if Equal + BNE label ; jump if Not Equal + BGT label ; jump if Greater Than + BLT label ; jump if Less Than + B label ; unconditional jump + - title: Labels + text: > + A **label** is a marker in your code that branches jump to. Add a colon after the name: + code: | + CMP R0, #10 + BGT big + MOV R1, #0 ; R0 <= 10 + B done ; skip the next part + big: + MOV R1, #1 ; R0 > 10 + done: + XHLT + +goal: R0=**15**. If R0 > 10 then R1 = **1**; otherwise R1 = **0** + +initialState: + registers: + R0: 15 + +testCases: + - init: + registers: + R0: 15 + expected: + registers: + R1: 1 + - init: + registers: + R0: 5 + expected: + registers: + R1: 0 + - init: + registers: + R0: 10 + expected: + registers: + R1: 0 + +hints: + - "Default R1 to 0, then compare R0 with 10" + - "If R0 > 10, jump to a label that sets R1 to 1" + - "Answer: MOV R1, #0 / CMP R0, #10 / BLE done / MOV R1, #1 / done: XHLT" + +starThresholds: [5, 7, 9] + +starterCode: | + ; If R0 > 10 then R1 = 1 + ; Otherwise R1 = 0 + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/09.yaml b/apps/simpleasm/frontend/src/lib/levels/09.yaml new file mode 100644 index 0000000..4dfc105 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/09.yaml @@ -0,0 +1,52 @@ +id: 9 +title: Loops +subtitle: The power of repetition +description: Use branches to build a loop + +tutorial: + - title: What is a loop? + text: > + A loop runs the same code **over and over**. In assembly, a loop is just + **branching back to a label**! + - title: Loop structure + text: > + ① initialize ② do work ③ update the counter ④ test + branch back: + code: | + MOV R4, #0 ; ① initialize + loop: ; loop start + ADD R4, R4, #1 ; ②③ counter += 1 + CMP R4, #5 ; ④ reached 5? + BLE loop ; not yet, jump back + XHLT + - title: Watch out! + text: > + Forget to update the counter and you've made an **infinite loop** + (don't worry — execution stops automatically after 10000 steps). + +goal: Compute **1+2+3+...+10** into **R0** (the answer is 55) + +initialState: {} + +testCases: + - init: {} + expected: + registers: + R0: 55 + +hints: + - "R0 accumulates the sum, R4 is the counter (1 to 10)" + - "Loop body: ADD R0, R0, R4 / ADD R4, R4, #1 / CMP R4, #10 / BLE loop" + - "Full: MOV R0, #0 / MOV R4, #1 / loop: ADD R0, R0, R4 / ADD R4, R4, #1 / CMP R4, #10 / BLE loop / XHLT" + +starThresholds: [7, 9, 12] + +starterCode: | + ; Compute 1+2+3+...+10 + ; Result goes in R0 + ; + ; Tip: use a register as the counter + + + XHLT + +showMemory: false diff --git a/apps/simpleasm/frontend/src/lib/levels/10.yaml b/apps/simpleasm/frontend/src/lib/levels/10.yaml new file mode 100644 index 0000000..e745615 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/levels/10.yaml @@ -0,0 +1,81 @@ +id: 10 +title: Final Challenge +subtitle: Find the maximum +description: Bring all your skills together! + +tutorial: + - title: The last level! + text: > + You've learned registers, arithmetic, bitwise ops, memory, branches and loops. + Now combine **all of it**! + - title: The challenge + text: > + Memory addresses 0-4 hold five numbers. Find the **maximum value** and its + **position**. You'll need: a loop + memory reads + compare-and-branch. + - title: Approach + text: | + 1. Assume the first number is the largest (R0 = memory[0], R1 = position 0) + 2. Loop through the remaining numbers + 3. If a larger one shows up, update both the max and its position + 4. Continue until all 5 numbers are checked + code: | + ; Pseudocode: + ; R0 = max = mem[0] + ; R1 = maxIdx = 0 + ; for R4 = 1 to 4: + ; R5 = mem[R4] + ; if R5 > R0: R0=R5, R1=R4 + +goal: memory[0..4] holds 5 numbers — store the **maximum** in **R0** and its **position** in **R1** + +initialState: + memory: + 0: 5 + 1: 3 + 2: 8 + 3: 1 + 4: 7 + +testCases: + - init: + memory: + 0: 5 + 1: 3 + 2: 8 + 3: 1 + 4: 7 + expected: + registers: + R0: 8 + R1: 2 + - init: + memory: + 0: 1 + 1: 9 + 2: 4 + 3: 9 + 4: 2 + expected: + registers: + R0: 9 + R1: 1 + +hints: + - "R0 = max, R1 = position, R4 = loop counter, R5 = current value" + - "Use MOV R3, R4 / LDR R5, [R3] to read mem[R4]" + - "Loop body: MOV R3, R4 / LDR R5, [R3] / CMP R5, R0 / BLE skip / MOV R0, R5 / MOV R1, R4 / skip: ADD R4, R4, #1 / CMP R4, #5 / BLT loop" + +starThresholds: [12, 15, 20] + +starterCode: | + ; memory[0..4] = [5, 3, 8, 1, 7] + ; Find the max into R0, the position into R1 + ; + ; Tip: use R4 as the loop variable + ; use MOV + LDR to read memory + + + XHLT + +showMemory: true +memoryRange: [0, 15] diff --git a/apps/simpleasm/frontend/src/lib/vm.js b/apps/simpleasm/frontend/src/lib/vm.js new file mode 100644 index 0000000..1b91686 --- /dev/null +++ b/apps/simpleasm/frontend/src/lib/vm.js @@ -0,0 +1,375 @@ +const REGISTERS = ['R0','R1','R2','R3','R4','R5','R6','R7'] +const OPCODES = [ + 'MOV','ADD','SUB','MUL','XDIV','XMOD', + 'AND','ORR','EOR','MVN','LSL','LSR', + 'CMP','B','BEQ','BNE','BGT','BLT','BGE','BLE', + 'LDR','STR','PUSH','POP','XOUT','XHLT','NOP', +] +const BRANCH_OPS = ['B','BEQ','BNE','BGT','BLT','BGE','BLE'] +const MAX_STEPS = 10000 + +function parseNumber(s) { + s = s.trim() + if (/^0x[0-9a-fA-F]+$/i.test(s)) return parseInt(s, 16) + if (/^0b[01]+$/i.test(s)) return parseInt(s.slice(2), 2) + if (/^-?\d+$/.test(s)) return parseInt(s, 10) + return null +} + +function splitOperands(str) { + const result = []; let cur = ''; let depth = 0 + for (const ch of str) { + if (ch === '[' || ch === '{') depth++ + if (ch === ']' || ch === '}') depth-- + if (ch === ',' && depth === 0) { result.push(cur.trim()); cur = '' } + else cur += ch + } + if (cur.trim()) result.push(cur.trim()) + return result +} + +function parseOperand(s) { + s = s.trim() + const upper = s.toUpperCase() + if (REGISTERS.includes(upper)) return { type: 'reg', value: upper } + + // Immediate: #42, #0xFF, #0b101 + if (s.startsWith('#')) { + const n = parseNumber(s.slice(1)) + if (n !== null) return { type: 'imm', value: n } + } + + // Memory: [R0], [R0, #4], [#addr] + const memMatch = s.match(/^\[([^\],]+)(?:,\s*(.+))?\]$/) + if (memMatch) { + const base = memMatch[1].trim() + const baseUp = base.toUpperCase() + if (REGISTERS.includes(baseUp)) { + let offset = 0 + if (memMatch[2]) { + let off = memMatch[2].trim() + if (off.startsWith('#')) off = off.slice(1) + offset = parseNumber(off) || 0 + } + return { type: 'mem', base: baseUp, offset } + } + // [#addr] direct + let addr = base.startsWith('#') ? base.slice(1) : base + const n = parseNumber(addr) + if (n !== null) return { type: 'mem_direct', addr: n & 0xFF } + } + + // {R0} for PUSH/POP + const braceMatch = s.match(/^\{(.+)\}$/) + if (braceMatch) { + const inner = braceMatch[1].trim().toUpperCase() + if (REGISTERS.includes(inner)) return { type: 'reg', value: inner } + } + + // Label + if (/^[a-zA-Z_]\w*$/.test(s)) return { type: 'label', value: s.toLowerCase() } + + // Bare number (lenient) + const n = parseNumber(s) + if (n !== null) return { type: 'imm', value: n } + + return { type: 'unknown', raw: s } +} + +export function parse(code) { + const lines = code.split('\n') + const instructions = [] + const labels = {} + const errors = [] + + // Pass 1: find labels + let idx = 0 + for (let i = 0; i < lines.length; i++) { + let line = lines[i] + const ci = line.indexOf(';'); if (ci >= 0) line = line.slice(0, ci) + line = line.trim(); if (!line) continue + const lm = line.match(/^([a-zA-Z_]\w*)\s*:(.*)$/) + if (lm) { labels[lm[1].toLowerCase()] = idx; line = lm[2].trim(); if (!line) continue } + idx++ + } + + // Pass 2: parse instructions + for (let i = 0; i < lines.length; i++) { + let line = lines[i] + const ci = line.indexOf(';'); if (ci >= 0) line = line.slice(0, ci) + line = line.trim(); if (!line) continue + const lm = line.match(/^([a-zA-Z_]\w*)\s*:(.*)$/) + if (lm) { line = lm[2].trim(); if (!line) continue } + + const parts = line.match(/^(\w+)(?:\s+(.*))?$/) + if (!parts) { errors.push({ line: i, msg: `Syntax error: ${lines[i].trim()}` }); continue } + + const opcode = parts[1].toUpperCase() + if (!OPCODES.includes(opcode)) { errors.push({ line: i, msg: `Unknown instruction: ${parts[1]}` }); continue } + + let operands = [] + if (parts[2]) { + operands = splitOperands(parts[2]).map(parseOperand) + const bad = operands.find(o => o.type === 'unknown') + if (bad) { errors.push({ line: i, msg: `Unrecognized operand: ${bad.raw}` }); continue } + } + instructions.push({ opcode, operands, srcLine: i }) + } + return { instructions, labels, errors } +} + +export function createVM() { + const state = { + registers: { R0:0,R1:0,R2:0,R3:0,R4:0,R5:0,R6:0,R7:0 }, + flags: { zero:false, carry:false, negative:false, overflow:false }, + memory: new Array(256).fill(0), + stack: [], + pc: 0, halted: false, error: null, + output: [], input: [], + stepCount: 0, instructions: [], labels: {}, + cmpA: 0, cmpB: 0, + } + + function getVal(op) { + switch (op.type) { + case 'reg': return state.registers[op.value] + case 'imm': return op.value + case 'mem': return state.memory[(state.registers[op.base] + (op.offset||0)) & 0xFF] + case 'mem_direct': return state.memory[op.addr] + default: throw new Error(`Cannot read: ${JSON.stringify(op)}`) + } + } + + function setReg(op, val, changes) { + val = ((val % 65536) + 65536) % 65536 + if (op.type !== 'reg') throw new Error('Destination must be a register') + const old = state.registers[op.value] + state.registers[op.value] = val + changes.push({ type: 'reg', name: op.value, old, val }) + } + + function setMem(addr, val, changes) { + addr = addr & 0xFF + val = ((val % 65536) + 65536) % 65536 + const old = state.memory[addr] + state.memory[addr] = val + changes.push({ type: 'mem', addr, old, val }) + } + + function updateFlags(val) { + val = ((val % 65536) + 65536) % 65536 + state.flags.zero = val === 0 + state.flags.negative = (val & 0x8000) !== 0 + } + + function condMet(op) { + const a = state.cmpA, b = state.cmpB + switch (op) { + case 'B': return true + case 'BEQ': return a === b + case 'BNE': return a !== b + case 'BGT': return a > b + case 'BLT': return a < b + case 'BGE': return a >= b + case 'BLE': return a <= b + } + return false + } + + // For 2-or-3 operand arithmetic: if 3 ops → Rd = op(Rn, Rm); if 2 ops → Rd = op(Rd, Rm) + function arith3(ops, fn) { + if (ops.length >= 3) return fn(getVal(ops[1]), getVal(ops[2])) + return fn(getVal(ops[0]), getVal(ops[1])) + } + + function step() { + if (state.halted || state.error) return null + if (state.pc >= state.instructions.length) { state.halted = true; return null } + if (state.stepCount >= MAX_STEPS) { + state.error = 'Exceeded maximum step count (10000) — likely an infinite loop!'; return null + } + + const instr = state.instructions[state.pc] + const { opcode, operands: ops } = instr + const changes = [] + let jumped = false + state.stepCount++ + + try { + switch (opcode) { + case 'NOP': break + case 'XHLT': state.halted = true; break + + case 'MOV': + setReg(ops[0], getVal(ops[1]), changes) + break + + case 'ADD': { const r = arith3(ops, (a,b) => a+b); state.flags.carry = r > 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break } + case 'SUB': { const r = arith3(ops, (a,b) => a-b); state.flags.carry = r < 0; setReg(ops[0], r, changes); updateFlags(r); break } + case 'MUL': { const r = arith3(ops, (a,b) => a*b); state.flags.carry = r > 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break } + case 'XDIV': { + const r = arith3(ops, (a,b) => { if(b===0) throw new Error('Division by zero!'); return Math.floor(a/b) }) + setReg(ops[0], r, changes); updateFlags(r); break + } + case 'XMOD': { + const r = arith3(ops, (a,b) => { if(b===0) throw new Error('Division by zero!'); return a%b }) + setReg(ops[0], r, changes); updateFlags(r); break + } + + case 'AND': { const r = arith3(ops, (a,b) => a&b); setReg(ops[0], r, changes); updateFlags(r); break } + case 'ORR': { const r = arith3(ops, (a,b) => a|b); setReg(ops[0], r, changes); updateFlags(r); break } + case 'EOR': { const r = arith3(ops, (a,b) => a^b); setReg(ops[0], r, changes); updateFlags(r); break } + case 'MVN': { const r = (~getVal(ops[1])) & 0xFFFF; setReg(ops[0], r, changes); updateFlags(r); break } + case 'LSL': { const r = arith3(ops, (a,b) => (a << b) & 0xFFFF); setReg(ops[0], r, changes); updateFlags(r); break } + case 'LSR': { const r = arith3(ops, (a,b) => a >>> b); setReg(ops[0], r, changes); updateFlags(r); break } + + case 'CMP': { + state.cmpA = getVal(ops[0]); state.cmpB = getVal(ops[1]) + const d = state.cmpA - state.cmpB + state.flags.zero = d === 0; state.flags.negative = d < 0; state.flags.carry = state.cmpA < state.cmpB + break + } + + case 'B': case 'BEQ': case 'BNE': case 'BGT': case 'BLT': case 'BGE': case 'BLE': { + if (condMet(opcode)) { + const lbl = ops[0].value + if (state.labels[lbl] === undefined) throw new Error(`Unknown label: ${lbl}`) + state.pc = state.labels[lbl]; jumped = true + } + break + } + + case 'LDR': { + const memOp = ops[1] + let addr + if (memOp.type === 'mem') addr = (state.registers[memOp.base] + (memOp.offset||0)) & 0xFF + else if (memOp.type === 'mem_direct') addr = memOp.addr + else throw new Error('LDR needs a memory address, e.g. [R0] or [R0, #4]') + const v = state.memory[addr] + setReg(ops[0], v, changes) + changes.push({ type: 'mem_read', addr }) + break + } + case 'STR': { + const memOp = ops[1] + let addr + if (memOp.type === 'mem') addr = (state.registers[memOp.base] + (memOp.offset||0)) & 0xFF + else if (memOp.type === 'mem_direct') addr = memOp.addr + else throw new Error('STR needs a memory address, e.g. [R0] or [R0, #4]') + setMem(addr, getVal(ops[0]), changes) + break + } + + case 'PUSH': { + if (state.stack.length >= 64) throw new Error('Stack overflow!') + const v = getVal(ops[0]) + state.stack.push(v) + changes.push({ type: 'stack_push', val: v }) + break + } + case 'POP': { + if (state.stack.length === 0) throw new Error('Stack is empty!') + setReg(ops[0], state.stack.pop(), changes) + changes.push({ type: 'stack_pop' }) + break + } + + case 'XOUT': { + const v = getVal(ops[0]) + state.output.push(v) + changes.push({ type: 'output', val: v }) + break + } + } + } catch (e) { + state.error = `Line ${instr.srcLine + 1}: ${e.message}` + return { pc: state.pc, instr, changes, error: state.error } + } + + if (!jumped) state.pc++ + return { pc: state.pc, instr, changes, srcLine: instr.srcLine } + } + + function loadProgram(code) { + const result = parse(code) + if (result.errors.length > 0) { state.error = result.errors[0].msg; return result } + state.instructions = result.instructions + state.labels = result.labels + state.pc = 0; state.halted = false; state.error = null; state.stepCount = 0 + state.output = []; state.stack = [] + state.flags = { zero:false, carry:false, negative:false, overflow:false } + state.cmpA = 0; state.cmpB = 0 + return result + } + + function run() { + while (!state.halted && !state.error && state.stepCount < MAX_STEPS) step() + if (state.stepCount >= MAX_STEPS && !state.halted && !state.error) + state.error = 'Exceeded maximum step count (10000) — likely an infinite loop!' + } + + function reset() { + state.registers = { R0:0,R1:0,R2:0,R3:0,R4:0,R5:0,R6:0,R7:0 } + state.flags = { zero:false, carry:false, negative:false, overflow:false } + state.memory = new Array(256).fill(0) + state.stack = []; state.pc = 0; state.halted = false; state.error = null + state.output = []; state.input = []; state.stepCount = 0 + state.cmpA = 0; state.cmpB = 0 + } + + return { state, step, run, loadProgram, reset } +} + +export function countInstructions(code) { + let c = 0 + for (const line of code.split('\n')) { + let l = line; const ci = l.indexOf(';'); if (ci >= 0) l = l.slice(0, ci); l = l.trim() + if (!l) continue + const lm = l.match(/^[a-zA-Z_]\w*\s*:(.*)$/); if (lm) { l = lm[1].trim(); if (!l) continue } + c++ + } + return c +} + +export function validateLevel(level, vm) { + const s = vm.state + if (s.error) return { passed: false, msg: s.error } + + for (const tc of level.testCases) { + vm.reset() + if (level.initialState) { + if (level.initialState.registers) Object.assign(vm.state.registers, level.initialState.registers) + if (level.initialState.memory) for (const [a,v] of Object.entries(level.initialState.memory)) vm.state.memory[+a] = v + if (level.initialState.input) vm.state.input = [...level.initialState.input] + } + if (tc.init) { + if (tc.init.registers) Object.assign(vm.state.registers, tc.init.registers) + if (tc.init.memory) for (const [a,v] of Object.entries(tc.init.memory)) vm.state.memory[+a] = v + if (tc.init.input) vm.state.input = [...tc.init.input] + } + + vm.run() + if (vm.state.error) return { passed: false, msg: vm.state.error } + + const exp = tc.expected + if (exp.registers) { + for (const [r,v] of Object.entries(exp.registers)) { + if (vm.state.registers[r] !== v) + return { passed: false, msg: `${r} should be ${v}, but got ${vm.state.registers[r]}` } + } + } + if (exp.memory) { + for (const [a,v] of Object.entries(exp.memory)) { + if (vm.state.memory[+a] !== v) + return { passed: false, msg: `memory[${a}] should be ${v}, but got ${vm.state.memory[+a]}` } + } + } + if (exp.output) { + for (let i = 0; i < exp.output.length; i++) { + if (vm.state.output[i] !== exp.output[i]) + return { passed: false, msg: `Output #${i+1} should be ${exp.output[i]}, but got ${vm.state.output[i] ?? 'none'}` } + } + } + } + return { passed: true, msg: 'Passed!' } +} diff --git a/apps/simpleasm/frontend/src/main.js b/apps/simpleasm/frontend/src/main.js new file mode 100644 index 0000000..c2b1175 --- /dev/null +++ b/apps/simpleasm/frontend/src/main.js @@ -0,0 +1,9 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router/index.js' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/apps/simpleasm/frontend/src/router/index.js b/apps/simpleasm/frontend/src/router/index.js new file mode 100644 index 0000000..ddc290e --- /dev/null +++ b/apps/simpleasm/frontend/src/router/index.js @@ -0,0 +1,12 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const routes = [ + { path: '/', name: 'welcome', component: () => import('../views/WelcomeView.vue') }, + { path: '/levels', name: 'levels', component: () => import('../views/LevelSelectView.vue') }, + { path: '/level/:id', name: 'level', component: () => import('../views/LevelView.vue'), props: true }, +] + +export default createRouter({ + history: createWebHistory(), + routes, +}) diff --git a/apps/simpleasm/frontend/src/stores/game.js b/apps/simpleasm/frontend/src/stores/game.js new file mode 100644 index 0000000..4516653 --- /dev/null +++ b/apps/simpleasm/frontend/src/stores/game.js @@ -0,0 +1,84 @@ +import { defineStore } from 'pinia' + +export const useGameStore = defineStore('game', { + state: () => ({ + playerName: localStorage.getItem('asm_playerName') || '', + playerId: parseInt(localStorage.getItem('asm_playerId')) || null, + progress: JSON.parse(localStorage.getItem('asm_progress') || '{}'), + }), + + getters: { + isLoggedIn: (state) => !!state.playerName && !!state.playerId, + totalStars: (state) => Object.values(state.progress).reduce((s, p) => s + (p.stars || 0), 0), + levelsCompleted: (state) => Object.values(state.progress).filter(p => p.completed).length, + isLevelUnlocked() { + return (levelId) => { + if (levelId === 1) return true + return !!this.progress[levelId - 1]?.completed + } + }, + }, + + actions: { + async login(name) { + try { + const res = await fetch('/api/players', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }) + const data = await res.json() + this.playerName = data.name + this.playerId = data.id + if (data.progress) { + this.progress = { ...this.progress, ...data.progress } + } + this._persist() + return true + } catch { + // offline mode - just save locally + this.playerName = name + this.playerId = Date.now() + this._persist() + return true + } + }, + + async saveProgress(levelId, stars, code) { + const existing = this.progress[levelId] + const bestStars = Math.max(stars, existing?.stars || 0) + this.progress[levelId] = { completed: true, stars: bestStars, code } + this._persist() + + try { + await fetch('/api/progress', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + player_id: this.playerId, + level_id: levelId, + stars: bestStars, + code, + }), + }) + } catch { + // ok, saved locally + } + }, + + logout() { + this.playerName = '' + this.playerId = null + this.progress = {} + localStorage.removeItem('asm_playerName') + localStorage.removeItem('asm_playerId') + localStorage.removeItem('asm_progress') + }, + + _persist() { + localStorage.setItem('asm_playerName', this.playerName) + localStorage.setItem('asm_playerId', String(this.playerId)) + localStorage.setItem('asm_progress', JSON.stringify(this.progress)) + }, + }, +}) diff --git a/apps/simpleasm/frontend/src/views/LevelSelectView.vue b/apps/simpleasm/frontend/src/views/LevelSelectView.vue new file mode 100644 index 0000000..872f20e --- /dev/null +++ b/apps/simpleasm/frontend/src/views/LevelSelectView.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/views/LevelView.vue b/apps/simpleasm/frontend/src/views/LevelView.vue new file mode 100644 index 0000000..d5278a1 --- /dev/null +++ b/apps/simpleasm/frontend/src/views/LevelView.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/apps/simpleasm/frontend/src/views/WelcomeView.vue b/apps/simpleasm/frontend/src/views/WelcomeView.vue new file mode 100644 index 0000000..55cdd37 --- /dev/null +++ b/apps/simpleasm/frontend/src/views/WelcomeView.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/apps/simpleasm/frontend/vite.config.js b/apps/simpleasm/frontend/vite.config.js new file mode 100644 index 0000000..3c13822 --- /dev/null +++ b/apps/simpleasm/frontend/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import yaml from '@modyfi/vite-plugin-yaml' + +export default defineConfig({ + plugins: [vue(), yaml()], + server: { + proxy: { + '/api': 'http://localhost:8080' + } + } +}) diff --git a/apps/simpleasm/k8s/deployment.yaml b/apps/simpleasm/k8s/deployment.yaml new file mode 100644 index 0000000..72f94cc --- /dev/null +++ b/apps/simpleasm/k8s/deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: simpleasm + namespace: cube-simpleasm + labels: + app: simpleasm +spec: + replicas: 1 + strategy: + # PVC 是 RWO,rolling 上线时新旧 pod 抢 PVC 会卡住,直接 Recreate + type: Recreate + selector: + matchLabels: + app: simpleasm + template: + metadata: + labels: + app: simpleasm + spec: + imagePullSecrets: + - name: registry-creds + containers: + - name: simpleasm + image: registry.famzheng.me/mochi/simpleasm:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + env: + - name: DB_PATH + value: /data/app.db + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 1 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 15 + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 200m + memory: 64Mi + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: simpleasm-data diff --git a/apps/simpleasm/k8s/ingress.yaml b/apps/simpleasm/k8s/ingress.yaml new file mode 100644 index 0000000..dc56a58 --- /dev/null +++ b/apps/simpleasm/k8s/ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: simpleasm + namespace: cube-simpleasm +spec: + ingressClassName: traefik + rules: + - host: asm.famzheng.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: simpleasm + port: + number: 80 diff --git a/apps/simpleasm/k8s/namespace.yaml b/apps/simpleasm/k8s/namespace.yaml new file mode 100644 index 0000000..ecf9665 --- /dev/null +++ b/apps/simpleasm/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cube-simpleasm diff --git a/apps/simpleasm/k8s/pvc.yaml b/apps/simpleasm/k8s/pvc.yaml new file mode 100644 index 0000000..f8ad702 --- /dev/null +++ b/apps/simpleasm/k8s/pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: simpleasm-data + namespace: cube-simpleasm +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + # storageClassName 留空 → 走 k3s 默认 local-path(hostPath,单节点足够) diff --git a/apps/simpleasm/k8s/service.yaml b/apps/simpleasm/k8s/service.yaml new file mode 100644 index 0000000..0694226 --- /dev/null +++ b/apps/simpleasm/k8s/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: simpleasm + namespace: cube-simpleasm +spec: + selector: + app: simpleasm + ports: + - name: http + port: 80 + targetPort: 8080 diff --git a/apps/simpleasm/src/main.rs b/apps/simpleasm/src/main.rs new file mode 100644 index 0000000..9923627 --- /dev/null +++ b/apps/simpleasm/src/main.rs @@ -0,0 +1,214 @@ +//! asm.famzheng.me — 汇编教学小游戏。 +//! +//! 4 个 endpoint: +//! - `GET /api/health` 前端 ping 用 +//! - `POST /api/players` 按 name upsert,返回 id + 已有进度 map +//! - `POST /api/progress` 单关卡 upsert(stars 只能增加,code 覆盖) +//! - `GET /api/leaderboard` top 50(按 total_stars desc, levels_completed desc) +//! +//! 静态前端 + SPA fallback 由 cube-core::base 处理。 + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json as JsonResp}, + routing::{get, post}, + Json, Router, +}; +use rusqlite::{params, Connection, OptionalExtension}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +type Db = Arc>; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + cube_core::init_tracing(); + + let db_path = std::env::var("DB_PATH").unwrap_or_else(|_| "/data/app.db".into()); + let dist = std::env::var("SIMPLEASM_DIST_DIR").unwrap_or_else(|_| "/dist".into()); + + let conn = Connection::open(&db_path).expect("open sqlite"); + conn.execute_batch( + "PRAGMA journal_mode=WAL; + CREATE TABLE IF NOT EXISTS players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS progress ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id INTEGER NOT NULL, + level_id INTEGER NOT NULL, + stars INTEGER NOT NULL DEFAULT 0, + code TEXT, + completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (player_id) REFERENCES players(id), + UNIQUE (player_id, level_id) + );", + ) + .expect("init schema"); + tracing::info!(%db_path, "sqlite ready"); + + let db: Db = Arc::new(Mutex::new(conn)); + + let api = Router::new() + .route("/health", get(|| async { "ok" })) + .route("/players", post(create_or_get_player)) + .route("/progress", post(save_progress)) + .route("/leaderboard", get(leaderboard)) + .with_state(db); + + let app = cube_core::base(dist).nest("/api", api); + cube_core::serve(app, 8080).await +} + +#[derive(Deserialize)] +struct PlayerCreate { + name: String, +} + +#[derive(Deserialize)] +struct ProgressSave { + player_id: i64, + level_id: i64, + stars: i64, + #[serde(default)] + code: String, +} + +#[derive(Serialize)] +struct ProgressItem { + stars: i64, + code: String, + completed: bool, +} + +#[derive(Serialize)] +struct LeaderboardRow { + name: String, + total_stars: i64, + levels_completed: i64, +} + +/// `POST /api/players` — 按 name upsert。返回 `{id, name, progress: {level_id: {...}}}`。 +async fn create_or_get_player( + State(db): State, + Json(data): Json, +) -> Result { + let name = data.name.trim().to_string(); + if name.is_empty() { + return Err(AppError::bad_request("Name cannot be empty")); + } + + let conn = db.lock().unwrap(); + let player_id: i64 = match conn + .query_row( + "SELECT id FROM players WHERE name = ?1", + params![name], + |r| r.get(0), + ) + .optional()? + { + Some(id) => id, + None => { + conn.execute("INSERT INTO players (name) VALUES (?1)", params![name])?; + conn.last_insert_rowid() + } + }; + + let mut stmt = conn.prepare( + "SELECT level_id, stars, code FROM progress WHERE player_id = ?1", + )?; + let mut rows = stmt.query(params![player_id])?; + let mut progress: HashMap = HashMap::new(); + while let Some(r) = rows.next()? { + let level_id: i64 = r.get(0)?; + let stars: i64 = r.get(1)?; + let code: String = r.get::<_, Option>(2)?.unwrap_or_default(); + progress.insert( + level_id.to_string(), + ProgressItem { stars, code, completed: true }, + ); + } + + Ok(JsonResp(json!({ + "id": player_id, + "name": name, + "progress": progress, + }))) +} + +/// `POST /api/progress` — 单关 upsert。stars 取 max(old, new),code 永远覆盖。 +async fn save_progress( + State(db): State, + Json(data): Json, +) -> Result { + let conn = db.lock().unwrap(); + conn.execute( + "INSERT INTO progress (player_id, level_id, stars, code) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT (player_id, level_id) + DO UPDATE SET stars = MAX(stars, excluded.stars), + code = excluded.code, + completed_at = CURRENT_TIMESTAMP", + params![data.player_id, data.level_id, data.stars, data.code], + )?; + Ok(JsonResp(json!({ "success": true }))) +} + +/// `GET /api/leaderboard` — top 50 by (total_stars desc, levels_completed desc)。 +async fn leaderboard(State(db): State) -> Result { + let conn = db.lock().unwrap(); + let mut stmt = conn.prepare( + "SELECT p.name, COALESCE(SUM(pr.stars), 0) AS total_stars, + COUNT(pr.id) AS levels_completed + FROM players p + LEFT JOIN progress pr ON p.id = pr.player_id + GROUP BY p.id + ORDER BY total_stars DESC, levels_completed DESC + LIMIT 50", + )?; + let rows = stmt + .query_map([], |r| { + Ok(LeaderboardRow { + name: r.get(0)?, + total_stars: r.get(1)?, + levels_completed: r.get(2)?, + }) + })? + .collect::, _>>()?; + Ok(JsonResp(json!(rows))) +} + +enum AppError { + BadRequest(String), + Db(rusqlite::Error), +} + +impl AppError { + fn bad_request(msg: impl Into) -> Self { + Self::BadRequest(msg.into()) + } +} + +impl From for AppError { + fn from(e: rusqlite::Error) -> Self { + Self::Db(e) + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + match self { + Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg).into_response(), + Self::Db(e) => { + tracing::error!(error = %e, "sqlite error"); + (StatusCode::INTERNAL_SERVER_ERROR, "db error").into_response() + } + } + } +}