diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 00000000..fa43b9c9 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,92 @@ +name: Deploy Pages + +# Builds the app-web Adwaita SPA (`gjsify build --app browser`) and deploys it to +# GitHub Pages — the successor to the legacy Jekyll build of the classic tutorial +# (removed in the app-web rewrite). Point Settings → Pages → Source at +# "GitHub Actions" for this workflow to serve the site. + +on: + push: + branches: [main] + paths: + - "packages/app-web/**" + - "packages/learn/**" + - "packages/common-ui/**" + - "packages/core/**" + - "packages/examples/**" + - "gjsify-lock.json" + - ".github/workflows/deploy-pages.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress publish. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build web app + runs-on: ubuntu-latest + # `gjsify build`/`gjsify tsc` spawn `gjs` (unavailable on ubuntu-latest), so + # build in a Fedora 43 container (GJS 1.86 / SpiderMonkey 140) — the same + # environment as the CI type-check job. + container: + image: fedora:43 + steps: + - name: Install container prerequisites (incl. gjs) + run: dnf install -y git tar xz findutils gjs + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Cache gjsify tarball store + uses: actions/cache@v4 + with: + path: ~/.cache/gjsify/tarballs + key: gjsify-tarballs-${{ hashFiles('gjsify-lock.json') }} + restore-keys: gjsify-tarballs- + + - name: Install gjsify CLI + run: npm install -g @gjsify/cli@^0.8.0 + + - name: Install dependencies + run: gjsify install --immutable + + # app-web imports `@learn6502/learn/dist/tutorial.html` (and the examples' + # `.asm`), so the workspace deps that publish built output must be built + # first — the same set the CI type-check job builds. + - name: Build workspace dependencies + run: | + gjsify workspace @learn6502/core build + gjsify workspace @learn6502/examples build + gjsify workspace @learn6502/learn build + + - name: Build web app + run: gjsify workspace @learn6502/app-web build:app + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: packages/app-web/dist-app + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/AGENTS.md b/AGENTS.md index 35bdf254..774dccc9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -310,8 +310,12 @@ Required for R8/ProGuard code shrinking. Missing entries cause **runtime crashes Applies to all files in `packages/app-web/`. -Web app version. Stack: TypeScript + HTML + CSS + Vite + Jekyll (static site generation). -Focus: browser compatibility, responsive design, leverage `6502` core + `common-ui`. +Web app version. An Adwaita single-page app on `@gjsify/adwaita-web`, sharing the +`common-ui` controllers + `core` with app-gnome (the four views are the web twins +of the GNOME views). Production build = `gjsify build --app browser` (→ `dist-app/`, +deployed to GitHub Pages); dev = Vite + HMR (`vite.app.config.ts`). The classic +skilldrick Jekyll tutorial was removed in the app-web rewrite. +Focus: browser compatibility, responsive design, leverage `core` + `common-ui`. ## Translations diff --git a/packages/app-web/.gitignore b/packages/app-web/.gitignore index 05f2afbc..91ebf928 100644 --- a/packages/app-web/.gitignore +++ b/packages/app-web/.gitignore @@ -1,3 +1 @@ -_site -dist dist-app diff --git a/packages/app-web/Gemfile b/packages/app-web/Gemfile deleted file mode 100644 index d2851ce0..00000000 --- a/packages/app-web/Gemfile +++ /dev/null @@ -1,2 +0,0 @@ -source "https://rubygems.org" -gem "github-pages", group: :jekyll_plugins \ No newline at end of file diff --git a/packages/app-web/Gemfile.lock b/packages/app-web/Gemfile.lock deleted file mode 100644 index f85c415c..00000000 --- a/packages/app-web/Gemfile.lock +++ /dev/null @@ -1,320 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (8.0.2) - base64 - benchmark (>= 0.3) - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - colorator (1.1.0) - commonmarker (0.23.11) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) - csv (3.3.4) - dnsruby (1.72.4) - base64 (~> 0.2.0) - logger (~> 1.6.5) - simpleidn (~> 0.2.1) - drb (2.2.1) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - ethon (0.16.0) - ffi (>= 1.15.0) - eventmachine (1.2.7) - execjs (2.10.0) - faraday (2.13.1) - faraday-net_http (>= 2.0, < 3.5) - json - logger - faraday-net_http (3.4.0) - net-http (>= 0.5.0) - ffi (1.17.2-aarch64-linux-gnu) - ffi (1.17.2-aarch64-linux-musl) - ffi (1.17.2-arm-linux-gnu) - ffi (1.17.2-arm-linux-musl) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86-linux-gnu) - ffi (1.17.2-x86-linux-musl) - ffi (1.17.2-x86_64-darwin) - ffi (1.17.2-x86_64-linux-gnu) - ffi (1.17.2-x86_64-linux-musl) - forwardable-extended (2.6.0) - gemoji (4.1.0) - github-pages (232) - github-pages-health-check (= 1.18.2) - jekyll (= 3.10.0) - jekyll-avatar (= 0.8.0) - jekyll-coffeescript (= 1.2.2) - jekyll-commonmark-ghpages (= 0.5.1) - jekyll-default-layout (= 0.1.5) - jekyll-feed (= 0.17.0) - jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.16.1) - jekyll-include-cache (= 0.2.1) - jekyll-mentions (= 1.6.0) - jekyll-optional-front-matter (= 0.3.2) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.16.0) - jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.3) - jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.8.0) - jekyll-sitemap (= 1.4.0) - jekyll-swiss (= 1.0.0) - jekyll-theme-architect (= 0.2.0) - jekyll-theme-cayman (= 0.2.0) - jekyll-theme-dinky (= 0.2.0) - jekyll-theme-hacker (= 0.2.0) - jekyll-theme-leap-day (= 0.2.0) - jekyll-theme-merlot (= 0.2.0) - jekyll-theme-midnight (= 0.2.0) - jekyll-theme-minimal (= 0.2.0) - jekyll-theme-modernist (= 0.2.0) - jekyll-theme-primer (= 0.6.0) - jekyll-theme-slate (= 0.2.0) - jekyll-theme-tactile (= 0.2.0) - jekyll-theme-time-machine (= 0.2.0) - jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.13.0) - kramdown (= 2.4.0) - kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.4) - mercenary (~> 0.3) - minima (= 2.5.1) - nokogiri (>= 1.16.2, < 2.0) - rouge (= 3.30.0) - terminal-table (~> 1.4) - webrick (~> 1.8) - github-pages-health-check (1.18.2) - addressable (~> 2.3) - dnsruby (~> 1.60) - octokit (>= 4, < 8) - public_suffix (>= 3.0, < 6.0) - typhoeus (~> 1.3) - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) - http_parser.rb (0.8.0) - i18n (1.14.7) - concurrent-ruby (~> 1.0) - jekyll (3.10.0) - addressable (~> 2.4) - colorator (~> 1.0) - csv (~> 3.0) - em-websocket (~> 0.5) - i18n (>= 0.7, < 2) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) - liquid (~> 4.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (>= 1.7, < 4) - safe_yaml (~> 1.0) - webrick (>= 1.0) - jekyll-avatar (0.8.0) - jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.2.2) - coffee-script (~> 2.2) - coffee-script-source (~> 1.12) - jekyll-commonmark (1.4.0) - commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.5.1) - commonmarker (>= 0.23.7, < 1.1.0) - jekyll (>= 3.9, < 4.0) - jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.5) - jekyll (>= 3.0, < 5.0) - jekyll-feed (0.17.0) - jekyll (>= 3.7, < 5.0) - jekyll-gist (1.5.0) - octokit (~> 4.2) - jekyll-github-metadata (2.16.1) - jekyll (>= 3.4, < 5.0) - octokit (>= 4, < 7, != 4.4.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-mentions (1.6.0) - html-pipeline (~> 2.3) - jekyll (>= 3.7, < 5.0) - jekyll-optional-front-matter (0.3.2) - jekyll (>= 3.0, < 5.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.3.0) - jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.6.1) - jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.3) - addressable (~> 2.0) - jekyll (>= 3.5, < 5.0) - jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) - rubyzip (>= 1.3.0, < 3.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-sitemap (1.4.0) - jekyll (>= 3.7, < 5.0) - jekyll-swiss (1.0.0) - jekyll-theme-architect (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.6.0) - jekyll (> 3.5, < 5.0) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.0) - jekyll-theme-slate (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.3) - jekyll (>= 3.3, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - jemoji (0.13.0) - gemoji (>= 3, < 5) - html-pipeline (~> 2.2) - jekyll (>= 3.0, < 5.0) - json (2.11.3) - kramdown (2.4.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - liquid (4.0.4) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.6) - mercenary (0.3.6) - mini_portile2 (2.8.8) - minima (2.5.1) - jekyll (>= 3.5, < 5.0) - jekyll-feed (~> 0.9) - jekyll-seo-tag (~> 2.1) - minitest (5.25.5) - net-http (0.6.0) - uri - nokogiri (1.18.8) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-musl) - racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-arm-linux-musl) - racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) - racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-musl) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - pathutil (0.16.2) - forwardable-extended (~> 2.6) - public_suffix (5.1.1) - racc (1.8.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rexml (3.4.1) - rouge (3.30.0) - rubyzip (2.4.1) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - simpleidn (0.2.3) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.4.1) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (1.8.0) - uri (1.0.3) - webrick (1.9.1) - -PLATFORMS - aarch64-linux - aarch64-linux-gnu - aarch64-linux-musl - arm-linux - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86-linux - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - github-pages - -BUNDLED WITH - 2.5.17 diff --git a/packages/app-web/README.md b/packages/app-web/README.md index cd81a325..62361bbc 100644 --- a/packages/app-web/README.md +++ b/packages/app-web/README.md @@ -1,28 +1,34 @@ -# Easy6502 Web App +# Learn 6502 Assembly — Web App [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/) -This package contains the classic web version of Easy6502 by Nick Morgan, a one-stop accessible tutorial on 6502 assembly language programming. It includes a series of worked example programs which you can edit and run in the embedded simulator. +The web version of [Learn 6502 Assembly](../../README.md), built as an Adwaita +single-page app on [`@gjsify/adwaita-web`](https://www.npmjs.com/package/@gjsify/adwaita-web). +It looks and behaves like the native GNOME desktop app (`../app-gnome`), sharing +the same `@learn6502/common-ui` controllers and `@learn6502/core` assembler / +simulator — the four views (Learn, Editor, Debugger, Game Console) are the web +twins of the GNOME views. -This web version is maintained as part of the larger [Learn 6502 Assembly](../../README.md) project, which also provides a modern GNOME desktop application. While the GNOME app offers an enhanced experience, we maintain this web version to preserve the original tutorial's accessibility and simplicity. - -The original version can still be found at http://skilldrick.github.io/easy6502/. +The original skilldrick tutorial can still be found at +http://skilldrick.github.io/easy6502/. ## Development -### Installation +```bash +gjsify install +gjsify run dev:app # Vite dev server with HMR +``` -To run the web version locally, execute the following commands: +Production build (what the GitHub Pages deploy runs): -``` - gjsify install - gjsify run setup - gjsify run build - gjsify run start +```bash +gjsify run build:app # → dist-app/{index.html,app.js} (gjsify build --app browser) +gjsify run preview:app # preview the built output ``` -This will serve the site at http://localhost:4000. +Type-check: `gjsify run check`. ## License -This package is licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/), maintaining the original license from Nick Morgan's work. +Licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/), +maintaining the original license from Nick Morgan's work. diff --git a/packages/app-web/_config.yml b/packages/app-web/_config.yml deleted file mode 100644 index d156409e..00000000 --- a/packages/app-web/_config.yml +++ /dev/null @@ -1,4 +0,0 @@ -safe: true -lsi: false -markdown: kramdown -highlighter: rouge diff --git a/packages/app-web/_includes/end.html b/packages/app-web/_includes/end.html deleted file mode 100644 index cf041b3a..00000000 --- a/packages/app-web/_includes/end.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - -
- - -
-
- - -
-
- -
- - - - - - - -
- -
-
- - - diff --git a/packages/app-web/_includes/snake.html b/packages/app-web/_includes/snake.html deleted file mode 100644 index b6f952ef..00000000 --- a/packages/app-web/_includes/snake.html +++ /dev/null @@ -1,276 +0,0 @@ -{% include start.html %} -; ___ _ __ ___ __ ___ -; / __|_ _ __ _| |_____ / /| __|/ \_ ) -; \__ \ ' \/ _` | / / -_) _ \__ \ () / / -; |___/_||_\__,_|_\_\___\___/___/\__/___| - -; Change direction: W A S D - -define appleL $00 ; screen location of apple, low byte -define appleH $01 ; screen location of apple, high byte -define snakeHeadL $10 ; screen location of snake head, low byte -define snakeHeadH $11 ; screen location of snake head, high byte -define snakeBodyStart $12 ; start of snake body byte pairs -define snakeDirection $02 ; direction (possible values are below) -define snakeLength $03 ; snake length, in bytes - -; Directions (each using a separate bit) -define movingUp 1 -define movingRight 2 -define movingDown 4 -define movingLeft 8 - -; ASCII values of keys controlling the snake -define ASCII_w $77 -define ASCII_a $61 -define ASCII_s $73 -define ASCII_d $64 - -; System variables -define sysRandom $fe -define sysLastKey $ff - - - jsr init - jsr loop - -init: - jsr initSnake - jsr generateApplePosition - rts - - -initSnake: - lda #movingRight ;start direction - sta snakeDirection - - lda #4 ;start length (2 segments) - sta snakeLength - - lda #$11 - sta snakeHeadL - - lda #$10 - sta snakeBodyStart - - lda #$0f - sta $14 ; body segment 1 - - lda #$04 - sta snakeHeadH - sta $13 ; body segment 1 - sta $15 ; body segment 2 - rts - - -generateApplePosition: - ;load a new random byte into $00 - lda sysRandom - sta appleL - - ;load a new random number from 2 to 5 into $01 - lda sysRandom - and #$03 ;mask out lowest 2 bits - clc - adc #2 - sta appleH - - rts - - -loop: - jsr readKeys - jsr checkCollision - jsr updateSnake - jsr drawApple - jsr drawSnake - jsr spinWheels - jmp loop - - -readKeys: - lda sysLastKey - cmp #ASCII_w - beq upKey - cmp #ASCII_d - beq rightKey - cmp #ASCII_s - beq downKey - cmp #ASCII_a - beq leftKey - rts -upKey: - lda #movingDown - bit snakeDirection - bne illegalMove - - lda #movingUp - sta snakeDirection - rts -rightKey: - lda #movingLeft - bit snakeDirection - bne illegalMove - - lda #movingRight - sta snakeDirection - rts -downKey: - lda #movingUp - bit snakeDirection - bne illegalMove - - lda #movingDown - sta snakeDirection - rts -leftKey: - lda #movingRight - bit snakeDirection - bne illegalMove - - lda #movingLeft - sta snakeDirection - rts -illegalMove: - rts - - -checkCollision: - jsr checkAppleCollision - jsr checkSnakeCollision - rts - - -checkAppleCollision: - lda appleL - cmp snakeHeadL - bne doneCheckingAppleCollision - lda appleH - cmp snakeHeadH - bne doneCheckingAppleCollision - - ;eat apple - inc snakeLength - inc snakeLength ;increase length - jsr generateApplePosition -doneCheckingAppleCollision: - rts - - -checkSnakeCollision: - ldx #2 ;start with second segment -snakeCollisionLoop: - lda snakeHeadL,x - cmp snakeHeadL - bne continueCollisionLoop - -maybeCollided: - lda snakeHeadH,x - cmp snakeHeadH - beq didCollide - -continueCollisionLoop: - inx - inx - cpx snakeLength ;got to last section with no collision - beq didntCollide - jmp snakeCollisionLoop - -didCollide: - jmp gameOver -didntCollide: - rts - - -updateSnake: - ldx snakeLength - dex - txa -updateloop: - lda snakeHeadL,x - sta snakeBodyStart,x - dex - bpl updateloop - - lda snakeDirection - lsr - bcs up - lsr - bcs right - lsr - bcs down - lsr - bcs left -up: - lda snakeHeadL - sec - sbc #$20 - sta snakeHeadL - bcc upup - rts -upup: - dec snakeHeadH - lda #$1 - cmp snakeHeadH - beq collision - rts -right: - inc snakeHeadL - lda #$1f - bit snakeHeadL - beq collision - rts -down: - lda snakeHeadL - clc - adc #$20 - sta snakeHeadL - bcs downdown - rts -downdown: - inc snakeHeadH - lda #$6 - cmp snakeHeadH - beq collision - rts -left: - dec snakeHeadL - lda snakeHeadL - and #$1f - cmp #$1f - beq collision - rts -collision: - jmp gameOver - - -drawApple: - ldy #0 - lda sysRandom - sta (appleL),y - rts - - -drawSnake: - ldx snakeLength - lda #0 - sta (snakeHeadL,x) ; erase end of tail - - ldx #0 - lda #1 - sta (snakeHeadL,x) ; paint head - rts - - -spinWheels: - ldx #0 -spinloop: - nop - nop - dex - bne spinloop - rts - - -gameOver: -{% include end.html %} diff --git a/packages/app-web/_includes/start.html b/packages/app-web/_includes/start.html deleted file mode 100644 index 1d31bfb1..00000000 --- a/packages/app-web/_includes/start.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- - - - - - -
- - - - - -
- - -
-
- - -
-
- -
- - - - - - - -
- -
-
-
-
-
-
- - -
\ No newline at end of file diff --git a/packages/app-web/_layouts/basic.html b/packages/app-web/_layouts/basic.html deleted file mode 100644 index 9ce47f1f..00000000 --- a/packages/app-web/_layouts/basic.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - Learn6502 by skilldrick - - - - - -
-
-

Learn6502

-

- by Nick Morgan, - licensed under - CC BY 4.0 -

-

- Fork me on GitHub -

-
-
{{ content }}
- -
- - - - diff --git a/packages/app-web/_layouts/default.html b/packages/app-web/_layouts/default.html deleted file mode 100644 index a13804f4..00000000 --- a/packages/app-web/_layouts/default.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - Learn6502 by skilldrick - - - - - -
-
-

Learn6502

-

- by Nick Morgan, - licensed under - CC BY 4.0 -

-

- Fork me on GitHub -

- -
-
{{ content }}
- -
- - - - diff --git a/packages/app-web/index.markdown b/packages/app-web/index.markdown deleted file mode 100644 index 823379bd..00000000 --- a/packages/app-web/index.markdown +++ /dev/null @@ -1,793 +0,0 @@ ---- -layout: default ---- - -

Introduction

- -In this tiny ebook I'm going to show you how to get started writing 6502 -assembly language. The 6502 processor was massive in the seventies and -eighties, powering famous computers like the -[BBC Micro](https://en.wikipedia.org/wiki/BBC_Micro), -[Atari 2600](https://en.wikipedia.org/wiki/Atari_2600), -[Commodore 64](https://en.wikipedia.org/wiki/Commodore_64), -[Apple II](https://en.wikipedia.org/wiki/Apple_II), and the [Nintendo Entertainment -System](https://en.wikipedia.org/wiki/Nintendo_Entertainment_System). Bender in -Futurama [has a 6502 processor for a -brain](https://www.transbyte.org/SID/SID-files/Bender_6502.jpg). [Even the -Terminator was programmed in -6502](https://www.pagetable.com/docs/terminator/00-37-23.jpg). - -So, why would you want to learn 6502? It's a dead language isn't it? Well, -so's Latin. And they still teach that. -[Q.E.D.](https://en.wikipedia.org/wiki/Q.E.D.) - -(Actually, I've been reliably informed that 6502 processors are still being -produced by [Western Design Center](https://www.westerndesigncenter.com/wdc/w65c02s-chip.php) -and [sold to hobbyists](https://www.mouser.co.uk/c/?q=65C02), so clearly 6502 -_isn't_ a dead language! Who knew?) - -Seriously though, I think it's valuable to have an understanding of assembly -language. Assembly language is the lowest level of abstraction in computers - -the point at which the code is still readable. Assembly language translates -directly to the bytes that are executed by your computer's processor. -If you understand how it works, you've basically become a computer -[magician](https://www.skilldrick.co.uk/2011/04/magic-in-software-development/). - -Then why 6502? Why not a _useful_ assembly language, like -[x86](https://en.wikipedia.org/wiki/X86)? Well, I don't think learning x86 is -useful. I don't think you'll ever have to _write_ assembly language in your day -job - this is purely an academic exercise, something to expand your mind and -your thinking. 6502 was originally written in a different age, a time when the majority of -developers were writing assembly directly, rather than in these new-fangled -high-level programming languages. So, it was designed to be written by humans. -More modern assembly languages are meant to written by compilers, so let's -leave it to them. Plus, 6502 is _fun_. Nobody ever called x86 _fun_. - -

Our first program

- -So, let's dive in! That thing below is a little [JavaScript 6502 assembler and -simulator](https://github.com/JumpLink/Learn6502/tree/main/packages/core) that I adapted for this book. -Click **Assemble** then **Run** to assemble and run the snippet of assembly language. - -{% include start.html %} -LDA #$01 -STA $0200 -LDA #$05 -STA $0201 -LDA #$08 -STA $0202 -{% include end.html %} - -Hopefully the black area on the right now has three coloured "pixels" at the -top left. (If this doesn't work, you'll probably need to upgrade your browser to -something more modern, like Chrome or Firefox.) - -So, what's this program actually doing? Let's step through it with the -debugger. Hit **Reset**, then check the **Debugger** checkbox to start the -debugger. Click **Step** once. If you were watching carefully, you'll have -noticed that `A=` changed from `$00` to `$01`, and `PC=` changed from `$0600` to -`$0602`. - -Any numbers prefixed with `$` in 6502 assembly language (and by extension, in -this book) are in hexadecimal (hex) format. If you're not familiar with hex -numbers, I recommend you read [the Wikipedia -article](https://en.wikipedia.org/wiki/Hexadecimal). Anything prefixed with `#` -is a literal number value. Any other number refers to a memory location. - -Equipped with that knowledge, you should be able to see that the instruction -`LDA #$01` loads the hex value `$01` into register `A`. I'll go into more -detail on registers in the next section. - -Press **Step** again to execute the second instruction. The top-left pixel of -the game console should now be white. This simulator uses the memory -locations `$0200` to `$05ff` to draw pixels on its display. The values `$00` to -`$0f` represent 16 different colours (`$00` is black and `$01` is white), so -storing the value `$01` at memory location `$0200` draws a white pixel at the -top left corner. This is simpler than how an actual computer would output -video, but it'll do for now. - -So, the instruction `STA $0200` stores the value of the `A` register to memory -location `$0200`. Click **Step** four more times to execute the rest of the -instructions, keeping an eye on the `A` register as it changes. - -### Exercises - -1. Try changing the colour of the three pixels. -2. Change one of the pixels to draw at the bottom-right corner (memory location `$05ff`). -3. Add more instructions to draw extra pixels. - -

Registers and flags

- -We've already had a little look at the processor status section (the bit with -`A`, `PC` etc.), but what does it all mean? - -The first line shows the `A`, `X` and `Y` registers (`A` is often called the -"accumulator"). Each register holds a single byte. Most operations work on the -contents of these registers. - -`SP` is the stack pointer. I won't get into the stack yet, but basically this -register is decremented every time a byte is pushed onto the stack, and -incremented when a byte is popped off the stack. - -`PC` is the program counter - it's how the processor knows at what point in the -program it currently is. It's like the current line number of an executing -script. In the JavaScript simulator the code is assembled starting at memory -location `$0600`, so `PC` always starts there. - -The last section shows the processor flags. Each flag is one bit, so all seven -flags live in a single byte. The flags are set by the processor to give -information about the previous instruction. More on that later. [Read more -about the registers and flags here](https://web.archive.org/web/20210626024532/http://www.obelisk.me.uk/6502/registers.html). - -

Instructions

- -Instructions in assembly language are like a small set of predefined functions. -All instructions take zero or one arguments. Here's some annotated -source code to introduce a few different instructions: - -{% include start.html %} -LDA #$c0 ;Load the hex value $c0 into the A register -TAX ;Transfer the value in the A register to X -INX ;Increment the value in the X register -ADC #$c4 ;Add the hex value $c4 to the A register -BRK ;Break - we're done -{% include end.html %} - -Assemble the code, then turn on the debugger and step through the code, watching -the `A` and `X` registers. Something slightly odd happens on the line `ADC #$c4`. -You might expect that adding `$c4` to `$c0` would give `$184`, but this -processor gives the result as `$84`. What's up with that? - -The problem is, `$184` is too big to fit in a single byte (the max is `$FF`), -and the registers can only hold a single byte. It's OK though; the processor -isn't actually dumb. If you were looking carefully enough, you'll have noticed -that the carry flag was set to `1` after this operation. So that's how you -know. - -In the simulator below **type** (don't paste) the following code: - - LDA #$80 - STA $01 - ADC $01 - -{% include widget.html %} - -An important thing to notice here is the distinction between `ADC #$01` and -`ADC $01`. The first one adds the value `$01` to the `A` register, but the -second adds the value stored at memory location `$01` to the `A` register. - -Assemble, check the **Monitor** checkbox, then step through these three -instructions. The monitor shows a section of memory, and can be helpful to -visualise the execution of programs. `STA $01` stores the value of the `A` -register at memory location `$01`, and `ADC $01` adds the value stored at the -memory location `$01` to the `A` register. `$80 + $80` should equal `$100`, but -because this is bigger than a byte, the `A` register is set to `$00` and the -carry flag is set. As well as this though, the zero flag is set. The zero flag -is set by all instructions where the result is zero. - -A full list of the 6502 instruction set is [available -here](https://web.archive.org/web/20250915114440/http://www.6502.org/tutorials/6502opcodes.html) and -[here](https://web.archive.org/web/20210626042844/http://www.obelisk.me.uk/6502/reference.html) (I usually refer to -both pages as they have their strengths and weaknesses). These pages detail the -arguments to each instruction, which registers they use, and which flags they -set. They are your bible. - -### Exercises - -1. You've seen `TAX`. You can probably guess what `TAY`, `TXA` and `TYA` do, - but write some code to test your assumptions. -2. Rewrite the first example in this section to use the `Y` register instead of - the `X` register. -3. The opposite of `ADC` is `SBC` (subtract with carry). Write a program that - uses this instruction. - -

Branching

- -So far we're only able to write basic programs without any branching logic. -Let's change that. - -6502 assembly language has a bunch of branching instructions, all of which -branch based on whether certain flags are set or not. In this example we'll be -looking at `BNE`: "Branch on not equal". - -{% include start.html %} -LDX #$08 -decrement: -DEX -STX $0200 -CPX #$03 -BNE decrement -STX $0201 -BRK -{% include end.html %} - -First we load the value `$08` into the `X` register. The next line is a label. -Labels just mark certain points in a program so we can return to them later. -After the label we decrement `X`, store it to `$0200` (the top-left pixel), and -then compare it to the value `$03`. -[`CPX`](https://web.archive.org/web/20210626042844/http://www.obelisk.me.uk/6502/reference.html#CPX) compares the -value in the `X` register with another value. If the two values are equal, the -`Z` flag is set to `1`, otherwise it is set to `0`. - -The next line, `BNE decrement`, will shift execution to the decrement label if -the `Z` flag is set to `0` (meaning that the two values in the `CPX` comparison -were not equal), otherwise it does nothing and we store `X` to `$0201`, then -finish the program. - -In assembly language, you'll usually use labels with branch instructions. When -assembled though, this label is converted to a single-byte relative offset (a -number of bytes to go backwards or forwards from the next instruction) so -branch instructions can only go forward and back around 256 bytes. This means -they can only be used to move around local code. For moving further you'll need -to use the jumping instructions. - -### Exercises - -1. The opposite of `BNE` is `BEQ`. Try writing a program that uses `BEQ`. -2. `BCC` and `BCS` ("branch on carry clear" and "branch on carry set") are used - to branch on the carry flag. Write a program that uses one of these two. - -

Addressing modes

- -The 6502 uses a 16-bit address bus, meaning that there are 65536 bytes of -memory available to the processor. Remember that a byte is represented by two -hex characters, so the memory locations are generally represented as `$0000 - -$ffff`. There are various ways to refer to these memory locations, as detailed below. - -With all these examples you might find it helpful to use the memory monitor to -watch the memory change. The monitor takes a starting memory location and a -number of bytes to display from that location. Both of these are hex values. -For example, to display 16 bytes of memory from `$c000`, enter `c000` and `10` -into **Start** and **Length**, respectively. - -### Absolute: `$c000` - -With absolute addressing, the full memory location is used as the argument to the instruction. For example: - - STA $c000 ;Store the value in the accumulator at memory location $c000 - -### Zero page: `$c0` - -All instructions that support absolute addressing (with the exception of the jump -instructions) also have the option to take a single-byte address. This type of -addressing is called "zero page" - only the first page (the first 256 bytes) of -memory is accessible. This is faster, as only one byte needs to be looked up, -and takes up less space in the assembled code as well. - -### Zero page,X: `$c0,X` - -This is where addressing gets interesting. In this mode, a zero page address is given, and then the value of the `X` register is added. Here is an example: - - LDX #$01 ;X is $01 - LDA #$aa ;A is $aa - STA $a0,X ;Store the value of A at memory location $a1 - INX ;Increment X - STA $a0,X ;Store the value of A at memory location $a2 - -If the result of the addition is larger than a single byte, the address wraps around. For example: - - LDX #$05 - STA $ff,X ;Store the value of A at memory location $04 - -### Zero page,Y: `$c0,Y` - -This is the equivalent of zero page,X, but can only be used with `LDX` and `STX`. - -### Absolute,X and absolute,Y: `$c000,X` and `$c000,Y` - -These are the absolute addressing versions of zero page,X and zero page,Y. For example: - - LDX #$01 - STA $0200,X ;Store the value of A at memory location $0201 - -Unlike zero page,Y, absolute,Y can't be used with `STX` but can be used with `LDA` and `STA`. - -### Immediate: `#$c0` - -Immediate addressing doesn't strictly deal with memory addresses - this is the -mode where actual values are used. For example, `LDX #$01` loads the value -`$01` into the `X` register. This is very different to the zero page -instruction `LDX $01` which loads the value at memory location `$01` into the -`X` register. - -### Relative: `$c0` (or label) - -Relative addressing is used for branching instructions. These instructions take -a single byte, which is used as an offset from the following instruction. - -Assemble the following code, then click the **Hexdump** button to see the assembled code. - -{% include start.html %} -LDA #$01 -CMP #$02 -BNE notequal -STA $22 -notequal: -BRK -{% include end.html %} - -The hex should look something like this: - - a9 01 c9 02 d0 02 85 22 00 - -`a9` and `c9` are the processor opcodes for immediate-addressed `LDA` and `CMP` -respectively. `01` and `02` are the arguments to these instructions. `d0` is -the opcode for `BNE`, and its argument is `02`. This means "skip over the next -two bytes" (`85 22`, the assembled version of `STA $22`). Try editing the code -so `STA` takes a two-byte absolute address rather than a single-byte zero page -address (e.g. change `STA $22` to `STA $2222`). Reassemble the code and look at -the hexdump again - the argument to `BNE` should now be `03`, because the -instruction the processor is skipping past is now three bytes long. - -### Implicit - -Some instructions don't deal with memory locations (e.g. `INX` - increment the -`X` register). These are said to have implicit addressing - the argument is -implied by the instruction. - -### Indirect: `($c000)` - -Indirect addressing uses an absolute address to look up another address. The -first address gives the least significant byte of the address, and the -following byte gives the most significant byte. That can be hard to wrap your -head around, so here's an example: - -{% include start.html %} -LDA #$01 -STA $f0 -LDA #$cc -STA $f1 -JMP ($00f0) ;dereferences to $cc01 -{% include end.html %} - -In this example, `$f0` contains the value `$01` and `$f1` contains the value -`$cc`. The instruction `JMP ($f0)` causes the processor to look up the two -bytes at `$f0` and `$f1` (`$01` and `$cc`) and put them together to form the -address `$cc01`, which becomes the new program counter. Assemble and step -through the program above to see what happens. I'll talk more about `JMP` in -the section on [Jumping](#jumping). - -### Indexed indirect: `($c0,X)` - -This one's kinda weird. It's like a cross between zero page,X and indirect. -Basically, you take the zero page address, add the value of the `X` register to -it, then use that to look up a two-byte address. For example: - -{% include start.html %} -LDX #$01 -LDA #$05 -STA $01 -LDA #$07 -STA $02 -LDY #$0a -STY $0705 -LDA ($00,X) -{% include end.html %} - -Memory locations `$01` and `$02` contain the values `$05` and `$07` -respectively. Think of `($00,X)` as `($00 + X)`. In this case `X` is `$01`, so -this simplifies to `($01)`. From here things proceed like standard indirect -addressing - the two bytes at `$01` and `$02` (`$05` and `$07`) are looked up -to form the address `$0705`. This is the address that the `Y` register was -stored into in the previous instruction, so the `A` register gets the same -value as `Y`, albeit through a much more circuitous route. You won't see this -much. - -### Indirect indexed: `($c0),Y` - -Indirect indexed is like indexed indirect but less insane. Instead of adding -the `X` register to the address _before_ dereferencing, the zero page address -is dereferenced, and the `Y` register is added to the resulting address. - -{% include start.html %} -LDY #$01 -LDA #$03 -STA $01 -LDA #$07 -STA $02 -LDX #$0a -STX $0704 -LDA ($01),Y -{% include end.html %} - -In this case, `($01)` looks up the two bytes at `$01` and `$02`: `$03` and -`$07`. These form the address `$0703`. The value of the `Y` register is added -to this address to give the final address `$0704`. - -### Exercise - -1. Try to write code snippets that use each of the 6502 addressing modes. - Remember, you can use the monitor to watch a section of memory. - -

The stack

- -The stack in a 6502 processor is just like any other stack - values are pushed -onto it and popped ("pulled" in 6502 parlance) off it. The current depth of the -stack is measured by the stack pointer, a special register. The stack lives in -memory between `$0100` and `$01ff`. The stack pointer is initially `$ff`, which -points to memory location `$01ff`. When a byte is pushed onto the stack, the -stack pointer becomes `$fe`, or memory location `$01fe`, and so on. - -Two of the stack instructions are `PHA` and `PLA`, "push accumulator" and "pull -accumulator". Below is an example of these two in action. - -{% include start.html %} -LDX #$00 -LDY #$00 -firstloop: -TXA -STA $0200,Y -PHA -INX -INY -CPY #$10 -BNE firstloop ;loop until Y is $10 -secondloop: -PLA -STA $0200,Y -INY -CPY #$20 ;loop until Y is $20 -BNE secondloop -{% include end.html %} - -`X` holds the pixel colour, and `Y` holds the position of the current pixel. -The first loop draws the current colour as a pixel (via the `A` register), -pushes the colour to the stack, then increments the colour and position. The -second loop pops the stack, draws the popped colour as a pixel, then increments -the position. As should be expected, this creates a mirrored pattern. - -

Jumping

- -Jumping is like branching with two main differences. First, jumps are not -conditionally executed, and second, they take a two-byte absolute address. For -small programs, this second detail isn't very important, as you'll mostly be -using labels, and the assembler works out the correct memory location from the -label. For larger programs though, jumping is the only way to move from one -section of the code to another. - -### JMP - -`JMP` is an unconditional jump. Here's a really simple example to show it in action: - -{% include start.html %} -LDA #$03 -JMP there -BRK -BRK -BRK -there: -STA $0200 -{% include end.html %} - -### JSR/RTS - -`JSR` and `RTS` ("jump to subroutine" and "return from subroutine") are a -dynamic duo that you'll usually see used together. `JSR` is used to jump from -the current location to another part of the code. `RTS` returns to the previous -position. This is basically like calling a function and returning. - -The processor knows where to return to because `JSR` pushes the address minus -one of the next instruction onto the stack before jumping to the given -location. `RTS` pops this location, adds one to it, and jumps to that location. -An example: - -{% include start.html %} -JSR init -JSR loop -JSR end - -init: -LDX #$00 -RTS - -loop: -INX -CPX #$05 -BNE loop -RTS - -end: -BRK -{% include end.html %} - -The first instruction causes execution to jump to the `init` label. This sets -`X`, then returns to the next instruction, `JSR loop`. This jumps to the `loop` -label, which increments `X` until it is equal to `$05`. After that we return to -the next instruction, `JSR end`, which jumps to the end of the file. This -illustrates how `JSR` and `RTS` can be used together to create modular code. - -

Creating a game

- -Now, let's put all this knowledge to good use, and make a game! We're going to -be making a really simple version of the classic game 'Snake'. - -Even though this will be a simple version, the code will be substantially larger -than all the previous examples. We will need to keep track of several memory -locations together for the various aspects of the game. We can still do -the necessary bookkeeping throughout the program ourselves, as before, but -on a larger scale that quickly becomes tedious and can also lead to bugs that -are difficult to spot. Instead we'll now let the assembler do some of the -mundane work for us. - -In this assembler, we can define descriptive constants (or symbols) that represent -numbers. The rest of the code can then simply use the constants instead of the -literal number, which immediately makes it obvious what we're dealing with. -You can use letters, digits and underscores in a name. - -Here's an example. Note that immediate operands are still prefixed with a `#`. -{% include start.html %} -define sysRandom $fe ; an address -define a_dozen $0c ; a constant - -LDA sysRandom ; equivalent to "LDA $fe" - -LDX #a_dozen ; equivalent to "LDX #$0c" -{% include end.html %} - -The simulator widget below contains the entire source code of the game. I'll -explain how it works in the following sections. - -[Willem van der Jagt](https://github.com/wkjagt) made a [fully annotated gist -of this source code](https://gist.github.com/wkjagt/9043907), so follow along -with that for more details. - -{% include snake.html %} - -### Overall structure - -After the initial block of comments (lines starting with semicolons), the first -two lines are: - - jsr init - jsr loop - -`init` and `loop` are both subroutines. `init` initializes the game state, and -`loop` is the main game loop. - -The `loop` subroutine itself just calls a number of subroutines sequentially, -before looping back on itself: - - loop: - jsr readkeys - jsr checkCollision - jsr updateSnake - jsr drawApple - jsr drawSnake - jsr spinwheels - jmp loop - -First, `readkeys` checks to see if one of the direction keys (W, A, S, D) was -pressed, and if so, sets the direction of the snake accordingly. Then, -`checkCollision` checks to see if the snake collided with itself or the apple. -`updateSnake` updates the internal representation of the snake, based on its -direction. Next, the apple and snake are drawn. Finally, `spinWheels` makes the -processor do some busy work, to stop the game from running too quickly. Think -of it like a sleep command. The game keeps running until the snake collides -with the wall or itself. - -### Zero page usage - -The zero page of memory is used to store a number of game state variables, as -noted in the comment block at the top of the game. Everything in `$00`, `$01` -and `$10` upwards is a pair of bytes representing a two-byte memory location -that will be looked up using indirect addressing. These memory locations will -all be between `$0200` and `$05ff` - the section of memory corresponding to the -game console. For example, if `$00` and `$01` contained the values `$01` -and `$02`, they would be referring to the second pixel of the display ( -`$0201` - remember, the least significant byte comes first in indirect addressing). - -The first two bytes hold the location of the apple. This is updated every time -the snake eats the apple. Byte `$02` contains the current direction. `1` means -up, `2` right, `4` down, and `8` left. The reasoning behind these numbers will -become clear later. - -Finally, byte `$03` contains the current length of the snake, in terms of bytes -in memory (so a length of 4 means 2 pixels). - -### Initialization - -The `init` subroutine defers to two subroutines, `initSnake` and -`generateApplePosition`. `initSnake` sets the snake direction, length, and then -loads the initial memory locations of the snake head and body. The byte pair at -`$10` contains the screen location of the head, the pair at `$12` contains the -location of the single body segment, and `$14` contains the location of the -tail (the tail is the last segment of the body and is drawn in black to keep -the snake moving). This happens in the following code: - - lda #$11 - sta $10 - lda #$10 - sta $12 - lda #$0f - sta $14 - lda #$04 - sta $11 - sta $13 - sta $15 - -This loads the value `$11` into the memory location `$10`, the value `$10` into -`$12`, and `$0f` into `$14`. It then loads the value `$04` into `$11`, `$13` -and `$15`. This leads to memory like this: - - 0010: 11 04 10 04 0f 04 - -which represents the indirectly-addressed memory locations `$0411`, `$0410` and -`$040f` (three pixels in the middle of the display). I'm labouring this point, -but it's important to fully grok how indirect addressing works. - -The next subroutine, `generateApplePosition`, sets the apple location to a -random position on the display. First, it loads a random byte into the -accumulator (`$fe` is a random number generator in this simulator). This is -stored into `$00`. Next, a different random byte is loaded into the -accumulator, which is then `AND`-ed with the value `$03`. This part requires a -bit of a detour. - -The hex value `$03` is represented in binary as `00000011`. The `AND` opcode -performs a bitwise AND of the argument with the accumulator. For example, if -the accumulator contains the binary value `10101010`, then the result of `AND` -with `00000011` will be `00000010`. - -The effect of this is to mask out the least significant two bits of the -accumulator, setting the others to zero. This converts a number in the range of -0–255 to a number in the range of 0–3. - -After this, the value `2` is added to the accumulator, to create a final random -number in the range 2–5. - -The result of this subroutine is to load a random byte into `$00`, and a random -number between 2 and 5 into `$01`. Because the least significant byte comes -first with indirect addressing, this translates into a memory address between -`$0200` and `$05ff`: the exact range used to draw the display. - -### The game loop - -Nearly all games have at their heart a game loop. All game loops have the same -basic form: accept user input, update the game state, and render the game -state. This loop is no different. - -#### Reading the input - -The first subroutine, `readKeys`, takes the job of accepting user input. The -memory location `$ff` holds the ascii code of the most recent key press in this -simulator. The value is loaded into the accumulator, then compared to `$77` -(the hex code for W), `$64` (D), `$73` (S) and `$61` (A). If any of these -comparisons are successful, the program branches to the appropriate section. -Each section (`upKey`, `rightKey`, etc.) first checks to see if the current -direction is the opposite of the new direction. This requires another little detour. - -As stated before, the four directions are represented internally by the numbers -1, 2, 4 and 8. Each of these numbers is a power of 2, thus they are represented -by a binary number with a single `1`: - - 1 => 0001 (up) - 2 => 0010 (right) - 4 => 0100 (down) - 8 => 1000 (left) - -The `BIT` opcode is similar to `AND`, but the calculation is only used to set -the zero flag - the actual result is discarded. The zero flag is set only if the -result of AND-ing the accumulator with argument is zero. When we're looking at -powers of two, the zero flag will only be set if the two numbers are not the -same. For example, `0001 AND 0001` is not zero, but `0001 AND 0010` is zero. - -So, looking at `upKey`, if the current direction is down (4), the bit test will -be zero. `BNE` means "branch if the zero flag is clear", so in this case we'll -branch to `illegalMove`, which just returns from the subroutine. Otherwise, the -new direction (1 in this case) is stored in the appropriate memory location. - -#### Updating the game state - -The next subroutine, `checkCollision`, defers to `checkAppleCollision` and -`checkSnakeCollision`. `checkAppleCollision` just checks to see if the two -bytes holding the location of the apple match the two bytes holding the -location of the head. If they do, the length is increased and a new apple -position is generated. - -`checkSnakeCollision` loops through the snake's body segments, checking each -byte pair against the head pair. If there is a match, then game over. - -After collision detection, we update the snake's location. This is done at a -high level like so: First, move each byte pair of the body up one position in -memory. Second, update the head according to the current direction. Finally, if -the head is out of bounds, handle it as a collision. I'll illustrate this with -some ascii art. Each pair of brackets contains an x,y coordinate rather than a -pair of bytes for simplicity. - - 0 1 2 3 4 - Head Tail - - [1,5][1,4][1,3][1,2][2,2] Starting position - - [1,5][1,4][1,3][1,2][1,2] Value of (3) is copied into (4) - - [1,5][1,4][1,3][1,3][1,2] Value of (2) is copied into (3) - - [1,5][1,4][1,4][1,3][1,2] Value of (1) is copied into (2) - - [1,5][1,5][1,4][1,3][1,2] Value of (0) is copied into (1) - - [0,5][1,5][1,4][1,3][1,2] Value of (0) is updated based on direction - -At a low level, this subroutine is slightly more complex. First, the length is -loaded into the `X` register, which is then decremented. The snippet below -shows the starting memory for the snake. - - Memory location: $10 $11 $12 $13 $14 $15 - - Value: $11 $04 $10 $04 $0f $04 - -The length is initialized to `4`, so `X` starts off as `3`. `LDA $10,x` loads the -value of `$13` into `A`, then `STA $12,x` stores this value into `$15`. `X` is -decremented, and we loop. Now `X` is `2`, so we load `$12` and store it into -`$14`. This loops while `X` is positive (`BPL` means "branch if positive"). - -Once the values have been shifted down the snake, we have to work out what to -do with the head. The direction is first loaded into `A`. `LSR` means "logical -shift right", or "shift all the bits one position to the right". The least -significant bit is shifted into the carry flag, so if the accumulator is `1`, -after `LSR` it is `0`, with the carry flag set. - -To test whether the direction is `1`, `2`, `4` or `8`, the code continually -shifts right until the carry is set. One `LSR` means "up", two means "right", -and so on. - -The next bit updates the head of the snake depending on the direction. This is -probably the most complicated part of the code, and it's all reliant on how -memory locations map to the screen, so let's look at that in more detail. - -You can think of the screen as four horizontal strips of 32 × 8 pixels. -These strips map to `$0200-$02ff`, `$0300-$03ff`, `$0400-$04ff` and `$0500-$05ff`. -The first rows of pixels are `$0200-$021f`, `$0220-$023f`, `$0240-$025f`, etc. - -As long as you're moving within one of these horizontal strips, things are -simple. For example, to move right, just increment the least significant byte -(e.g. `$0200` becomes `$0201`). To go down, add `$20` (e.g. `$0200` becomes -`$0220`). Left and up are the reverse. - -Going between sections is more complicated, as we have to take into account the -most significant byte as well. For example, going down from `$02e1` should lead -to `$0301`. Luckily, this is fairly easy to accomplish. Adding `$20` to `$e1` -results in `$01` and sets the carry bit. If the carry bit was set, we know we -also need to increment the most significant byte. - -After a move in each direction, we also need to check to see if the head -would become out of bounds. This is handled differently for each direction. For -left and right, we can check to see if the head has effectively "wrapped -around". Going right from `$021f` by incrementing the least significant byte -would lead to `$0220`, but this is actually jumping from the last pixel of the -first row to the first pixel of the second row. So, every time we move right, -we need to check if the new least significant byte is a multiple of `$20`. This -is done using a bit check against the mask `$1f`. Hopefully the illustration -below will show you how masking out the lowest 5 bits reveals whether a number -is a multiple of `$20` or not. - - $20: 0010 0000 - $40: 0100 0000 - $60: 0110 0000 - - $1f: 0001 1111 - -I won't explain in depth how each of the directions work, but the above -explanation should give you enough to work it out with a bit of study. - -#### Rendering the game - -Because the game state is stored in terms of pixel locations, rendering the -game is very straightforward. The first subroutine, `drawApple`, is extremely -simple. It sets `Y` to zero, loads a random colour into the accumulator, then -stores this value into `($00),y`. `$00` is where the location of the apple is -stored, so `($00),y` dereferences to this memory location. Read the "Indirect -indexed" section in [Addressing modes](#addressing) for more details. - -Next comes `drawSnake`. This is pretty simple too - we first undraw the tail -and then draw the head. `X` is set to the length of the snake, so we can index -to the right pixel, and we set `A` to zero then perform the write using the -indexed indirect addressing mode. Then we reload `X` to index to the head, set -`A` to one and store it at `($10,x)`. `$10` stores the two-byte location of -the head, so this draws a white pixel at the current head position. As only -the head and the tail of the snake move, this is enough to keep the snake -moving. - -The last subroutine, `spinWheels`, is just there because the game would run too -fast otherwise. All `spinWheels` does is count `X` down from zero until it hits -zero again. The first `dex` wraps, making `X` `#$ff`. diff --git a/packages/app-web/package.json b/packages/app-web/package.json index b73436f5..aa1bf44e 100644 --- a/packages/app-web/package.json +++ b/packages/app-web/package.json @@ -6,17 +6,12 @@ "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "clear": "rm -rf dist dist-app _site", - "setup": "gem install bundler jekyll && bundle install", - "build:vite": "vite build", - "build": "gjsify run build:vite", + "clear": "rm -rf dist-app", + "build": "gjsify run build:app", "build:app": "gjsify build app.html --app browser --outfile dist-app/app.js --alias @gjsify/adwaita-fonts=@gjsify/empty", "dev:app": "vite --config vite.app.config.ts", "preview:app": "vite preview --config vite.app.config.ts", - "check": "gjsify tsc --noEmit", - "start:app": "gjsify run dist/main.js", - "debug:app": "GTK_DEBUG=interactive gjsify run dist/main.js", - "start": "bundle exec jekyll serve" + "check": "gjsify tsc --noEmit" }, "author": "Pascal Garber ", "license": "GPL-3.0", diff --git a/packages/app-web/params.json b/packages/app-web/params.json deleted file mode 100644 index be3e856a..00000000 --- a/packages/app-web/params.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "Learn6502", - "body": "### Welcome to GitHub Pages.\r\nThis automatic page generator is the easiest way to create beautiful pages for all of your projects. Author your page content here using GitHub Flavored Markdown, select a template crafted by a designer, and publish. After your page is generated, you can check out the new branch:\r\n\r\n```\r\n$ cd your_repo_root/repo_name\r\n$ git fetch origin\r\n$ git checkout gh-pages\r\n```\r\n\r\nIf you're using the GitHub for Mac, simply sync your repository and you'll see the new branch.\r\n\r\n### Designer Templates\r\nWe've crafted some handsome templates for you to use. Go ahead and continue to layouts to browse through them. You can easily go back to edit your page before publishing. After publishing your page, you can revisit the page generator and switch to another theme. Your Page content will be preserved if it remained markdown format.\r\n\r\n### Rather Drive Stick?\r\nIf you prefer to not use the automatic generator, push a branch named `gh-pages` to your repository to create a page manually. In addition to supporting regular HTML content, GitHub Pages support Jekyll, a simple, blog aware static site generator written by our own Tom Preston-Werner. Jekyll makes it easy to create site-wide headers and footers without having to copy them across every page. It also offers intelligent blog support and other advanced templating features.\r\n\r\n### Authors and Contributors\r\nYou can @mention a GitHub username to generate a link to their profile. The resulting `` element will link to the contributor's GitHub Profile. For example: In 2007, Chris Wanstrath (@defunkt), PJ Hyett (@pjhyett), and Tom Preston-Werner (@mojombo) founded GitHub.\r\n\r\n### Support or Contact\r\nHaving trouble with Pages? Check out the documentation at http://help.github.com/pages or contact support@github.com and we’ll help you sort it out.", - "tagline": "", - "google": "UA-3174668-13", - "note": "Don't delete this file! It's used internally to help with page regeneration." -} diff --git a/packages/app-web/simulator.markdown b/packages/app-web/simulator.markdown deleted file mode 100644 index 2c49df28..00000000 --- a/packages/app-web/simulator.markdown +++ /dev/null @@ -1,180 +0,0 @@ ---- -layout: basic ---- - -

Simulator

- -To use the disassembler, click **Assemble**, then **Disassemble**. [Back to Learn6502](index.html). - -{% include start.html %} -start: -jsr init - -loop: -jsr drawMap -jsr genMap -jmp loop - -testMemory: -rts -pha -txa -pha - -lda #0 -ldx $10 -sta $500,x -ldx $78 -lda #1 -sta $500,x -stx $10 - -lda #0 -ldx $11 -sta $500,x -ldx $79 -lda #3 -sta $500,x -stx $11 - -lda #0 -ldx $12 -sta $500,x -ldx $7a -lda #4 -sta $500,x -stx $12 - -lda #0 -ldx $13 -sta $500,x -ldx $7b -lda #4 -sta $500,x -stx $13 - -pla -tax -pla - -rts - -init: -ldx #0 -lda walls - -;draw exactly 256 pixels of wall at top and bottom -drawinitialwalls: -sta $200,x ;draw the top bit of wall -sta $400,x ;draw the bottom bit of wall -dex ;count down from 0 -cpx #0 ;until we hit 0 -bne drawinitialwalls - -lda #$10 -sta $80 -ldx #$0f - -;fill $81-$90 with $10 (initial wall offset) -setinitialwalloffsets: -sta $81,x ; target -dex -bpl setinitialwalloffsets -rts - -;-- - -drawMap: -lda #$00 - sta $78 - lda #$20 - sta $79 - lda #$c0 -sta $7a - lda #$e0 -sta $7b - -ldx #$0f -drawLoop: -lda $81,x -sta $82,x ;shift wall offsets along - -tay -sty $02 ;store current wall offset in $02 -lda pixels,y ;lookup current wall offset in pixels -sta $00 ;and store it in $00 -iny -lda pixels,y ;lookup current wall offset + 1 in pixels -sta $01 ;and store it in $01 -;$00 now points to a two-byte pixel memory location - -lda walls -ldy $78 ;top edge of wall -sta ($00),y -iny -sta ($00),y - -ldy $7b -sta ($00),y ;bottom edge of wall -iny -sta ($00),y - -ldy $79 ;top edge of tunnel -lda #0 ;black for tunnel -sta ($00),y -iny -sta ($00),y - -ldy $7a -sta ($00),y ;bottom edge of tunnel -iny -sta ($00),y - -; move offsets right two pixels -inc $78 -inc $79 -inc $7a -inc $7b -inc $78 -inc $79 -inc $7a -inc $7b -dex -bpl drawLoop -rts - -;--- - -genMap: -lda $80 ;$80 is next wall inflection point - cmp $81 ;$81 is next wall offset - beq newinflectionpoint - lda $80 - clc - sbc $81 ;is next wall offset above or below inflection point? - bpl raisewalls - bmi lowerwalls -newinflectionpoint: - lda $fe - and #$f ;make 4-bit -asl ;double (make even number) -sta $80 ;set $80 to random value -rts -lowerwalls: -dec $81 -dec $81 -rts -raisewalls: -inc $81 -inc $81 -rts - -pixels: -dcb $00,$02,$20,$02,$40,$02,$60,$02 - dcb $80,$02,$a0,$02,$c0,$02,$e0,$02 - dcb $00,$03,$20,$03,$40,$03,$60,$03 - dcb $80,$03,$a0,$03,$c0,$03,$e0,$03 - -walls: -dcb $d -{% include end.html %} diff --git a/packages/app-web/snake.markdown b/packages/app-web/snake.markdown deleted file mode 100644 index e2e780b6..00000000 --- a/packages/app-web/snake.markdown +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: basic ---- - -

Snake6502

-{% include snake.html %} diff --git a/packages/app-web/src/debugger.ts b/packages/app-web/src/debugger.ts deleted file mode 100644 index eadca9f5..00000000 --- a/packages/app-web/src/debugger.ts +++ /dev/null @@ -1,218 +0,0 @@ -import type { Simulator, Memory, Assembler } from "@learn6502/core"; -import { addr2hex, num2hex, throttle } from "@learn6502/core"; - -import { - DebuggerState, - debuggerController, - type DebuggerView, - type HexMonitorOptions, - type MessageConsoleWidget, - type DebugInfoWidget, -} from "@learn6502/common-ui"; - -import { MessageConsole } from "./message-console"; - -// Create a DebugInfoWidget implementation for web -class DebugInfo implements DebugInfoWidget { - constructor(private readonly element: HTMLElement) {} - - public update(simulator: Simulator): void { - const { regA, regX, regY, regP, regPC, regSP } = simulator.info; - let html = "A=$" + num2hex(regA) + " X=$" + num2hex(regX) + " Y=$" + num2hex(regY) + "
"; - html += "SP=$" + num2hex(regSP) + " PC=$" + addr2hex(regPC); - html += "
"; - html += "NV-BDIZC
"; - for (let i = 7; i >= 0; i--) { - html += (regP >> i) & 1; - } - this.element.innerHTML = html; - } -} - -export class Debugger implements DebuggerView { - public state = DebuggerState.INITIAL; - private messageConsole: MessageConsoleWidget; - private debugInfo: DebugInfoWidget; - - constructor( - private readonly node: HTMLElement, - private readonly simulator: Simulator, - private readonly assembler: Assembler, - public readonly memory: Memory, - public readonly options: HexMonitorOptions - ) { - // Initialize widgets - const consoleElement = node.querySelector(".messages"); - const minidebugger = node.querySelector(".minidebugger"); - - if (!consoleElement) { - throw new Error("Console element not found in debugger node"); - } - - if (!minidebugger) { - throw new Error("Debug info element not found in debugger node"); - } - - this.messageConsole = new MessageConsole(consoleElement); - this.debugInfo = new DebugInfo(minidebugger); - - // Initialize service with widgets and core references - debuggerController.init(this.messageConsole, this.debugInfo, this.assembler, this.simulator); - - this.setupEventListeners(); - this.onMonitorRangeChange = this.onMonitorRangeChange.bind(this); - } - - /** - * Toggle the monitor. - * The monitor is the part of the debugger that shows the memory. - * @param state - The state of the monitor. - */ - public toggleMonitor(state: boolean) { - this.state = state ? DebuggerState.ACTIVE : DebuggerState.PAUSED; - } - - /** - * Set the monitor address range. - */ - public setMonitorRange(startAddress: number, length: number) { - this.options.start = startAddress; - this.options.length = length; - } - - /** - * Handle the monitor range change. - */ - public onMonitorRangeChange() { - const $start = this.node.querySelector(".start"), - $length = this.node.querySelector(".length"), - start = parseInt($start?.value || "0", 16), - length = parseInt($length?.value || "0", 16); - - $start?.classList.remove("monitor-invalid"); - $length?.classList.remove("monitor-invalid"); - - const end = start + length - 1; - - if (isNaN(start) || start < 0 || start > 0xffff) { - $start?.classList.add("monitor-invalid"); - console.error("start is invalid", start); - return; - } else if (isNaN(length) || end > 0xffff) { - $length?.classList.add("monitor-invalid"); - console.error("length is invalid", length); - return; - } - - this.setMonitorRange(start, length); - } - - private setupEventListeners() { - this.simulator.on("step", () => { - // If stepper is enabled, update the debug info and the monitor every step - if (this.simulator.stepperEnabled) { - this.update(this.memory, this.simulator); - } - }); - - this.simulator.on("multistep", () => { - this.update(this.memory, this.simulator); - }); - - this.simulator.on("reset", () => { - this.update(this.memory, this.simulator); - }); - this.simulator.on("goto", () => { - this.update(this.memory, this.simulator); - }); - - this.update(this.memory, this.simulator); - } - - public updateMonitor(memory: Memory) { - if (this.state !== DebuggerState.ACTIVE) { - return; - } - - const start = this.options.start; - const length = this.options.length; - let content = ""; - - const end = start + length - 1; - - if (!isNaN(start) && !isNaN(length) && start >= 0 && length > 0 && end <= 0xffff) { - content = memory.format({ - start, - length, - includeAddress: true, - includeSpaces: true, - includeNewline: true, - }); - } else { - content = "Cannot monitor this range. Valid ranges are between $0000 and $ffff, inclusive."; - } - - const monitorNode = this.node.querySelector(".monitor code"); - - if (!monitorNode) { - return; - } - - monitorNode.innerHTML = content; - } - - public updateDebugInfo(simulator: Simulator) { - debuggerController.updateDebugInfo(simulator); - } - - #update(memory: Memory, simulator: Simulator) { - this.updateMonitor(memory); - this.updateDebugInfo(simulator); - } - - /** - * Update the debugger. - * @note This is throttled to 349ms to prevent excessive CPU usage. - * @param memory - The memory to update the hex monitor. - * @param simulator - The simulator to update the debug info. - */ - public update = throttle(this.#update.bind(this), 349); // Prime number - - /** - * Updates the hexdump view - * @param assembler Assembler with assembled code - */ - public updateHexdump(assembler: Assembler): void { - debuggerController.updateHexdump(assembler); - } - - /** - * Updates the disassembled view - * @param assembler Assembler with assembled code - */ - public updateDisassembled(assembler: Assembler): void { - debuggerController.updateDisassembled(assembler); - } - - /** - * Log a message to the debugger console - * @param message Message to log - */ - public log(message: string): void { - debuggerController.log(message); - } - - public reset() { - if (this.messageConsole) { - this.messageConsole.clear(); - } - this.state = DebuggerState.RESET; - } - - /** - * Clean up resources when closing - */ - public close(): void { - // Remove event listeners or perform any cleanup - } -} diff --git a/packages/app-web/src/display.ts b/packages/app-web/src/display.ts deleted file mode 100644 index 73aa54e4..00000000 --- a/packages/app-web/src/display.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type Memory, DisplayAddressRange } from "@learn6502/core"; -import { type DisplayWidget, DEFAULT_COLOR_PALETTE } from "@learn6502/common-ui"; - -/** - * Represents the display for a 6502 simulator. - */ -export class Display implements DisplayWidget { - private ctx: CanvasRenderingContext2D | null = null; - private width: number = 160; - private height: number = 160; - private pixelSize: number = 0; - private numX: number = 32; - private numY: number = 32; - - private palette = DEFAULT_COLOR_PALETTE; - - /** - * Creates a new Display instance. - * @param node - The HTML element that will contain the display. - */ - constructor( - private readonly node: HTMLElement, - private readonly memory: Memory - ) { - this.memory.on("changed", (event) => { - if (event.addr >= DisplayAddressRange.START && event.addr <= DisplayAddressRange.END) { - this.updatePixel(event.addr); - } - }); - } - - /** - * Initializes the display by setting up the canvas and context. - * @throws {Error} If the canvas element is not found. - */ - public initialize(_memory: Memory): void { - const canvas = this.node.querySelector(".screen"); - if (!canvas) { - throw new Error("Canvas not found"); - } - this.width = canvas.width || 160; - this.height = canvas.height || 160; - this.pixelSize = this.width / this.numX; - this.ctx = canvas.getContext("2d"); - this.reset(); - } - - /** - * Resets the display to a black screen. - */ - public reset(): void { - if (!this.ctx) { - return; - } - this.ctx.fillStyle = "black"; - this.ctx.fillRect(0, 0, this.width, this.height); - } - - /** - * Updates a single pixel on the display. - * @param addr - The memory address of the pixel. - * @param memory - The Memory object containing the pixel data. - */ - public updatePixel(addr: number): void { - if (!this.ctx) { - return; - } - this.ctx.fillStyle = this.palette[this.memory.get(addr) & 0x0f]; - const y = Math.floor((addr - DisplayAddressRange.START) / this.numY); - const x = (addr - DisplayAddressRange.START) % this.numX; - this.ctx.fillRect(x * this.pixelSize, y * this.pixelSize, this.pixelSize, this.pixelSize); - } - - /** - * Redraws all pixels on the display based on current memory state. - */ - public drawAllPixels(): void { - for (let addr = DisplayAddressRange.START; addr <= DisplayAddressRange.END; addr++) { - this.updatePixel(addr); - } - } -} diff --git a/packages/app-web/src/game-console.ts b/packages/app-web/src/game-console.ts deleted file mode 100644 index 0b2bfc2f..00000000 --- a/packages/app-web/src/game-console.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { Memory, Labels, Simulator, Assembler, formatMessage } from "@learn6502/core"; -import { Debugger } from "./debugger.js"; -import { Display } from "./display.js"; -import { UIState } from "./ui-state.js"; -import { MessageConsole } from "./message-console.js"; -import { type MessageConsoleWidget, mapKeyboardToGamepadKeyCode } from "@learn6502/common-ui"; - -/** - * Represents the main widget for the 6502 simulator. - */ -export class GameConsole { - private console: MessageConsoleWidget; - private uiState: UIState; - private memory: Memory; - private display: Display; - private labels: Labels; - private simulator: Simulator; - private assembler: Assembler; - private debugger: Debugger; - - /** - * Creates a new GameConsole instance. - * @param node - The root HTML element for the simulator widget. - */ - constructor(private node: HTMLElement) { - this.console = new MessageConsole(node.querySelector(".messages code")!); - this.uiState = new UIState(node); - this.memory = new Memory(); - this.display = new Display(node, this.memory); - this.labels = new Labels(); - this.simulator = new Simulator(this.memory, this.labels); - this.assembler = new Assembler(this.memory, this.labels); - this.debugger = new Debugger(node, this.simulator, this.assembler, this.memory, { - start: 0x00, - length: 0xff, - }); - this.initialize(); - } - - /** - * Initializes the simulator widget and sets up event listeners. - */ - private initialize(): void { - this.stripText(); - this.uiState.initialize(); - this.display.initialize(this.memory); - this.simulator.reset(); - - this.setupEventListeners(); - } - - /** - * Sets up event listeners for various UI elements. - */ - private setupEventListeners(): void { - this.node.querySelector(".assembleButton")?.addEventListener("click", () => { - this.simulator.reset(); - this.labels.reset(); - this.console.clear(); - this.assembler.assembleCode(this.node.querySelector(".code")?.value || ""); - }); - - this.node.querySelector(".mainButton")?.addEventListener("click", () => { - this.simulator.stopStepper(); - this.simulator.runBinary(); - }); - - this.node.querySelector(".resetButton")?.addEventListener("click", () => { - this.simulator.reset(); - }); - - this.node.querySelector(".hexdumpButton")?.addEventListener("click", () => { - this.assembler.hexdump({ - includeAddress: true, - includeSpaces: true, - includeNewline: true, - }); - }); - - this.node.querySelector(".disassembleButton")?.addEventListener("click", () => { - this.assembler.disassemble(); - }); - - this.node.querySelector(".debug")?.addEventListener("change", (e: Event) => { - const debug = (e.target as HTMLInputElement).checked; - if (debug) { - this.uiState.debugOn(); - this.simulator.enableStepper(); - } else { - this.uiState.debugOff(); - this.simulator.stopStepper(); - } - }); - - this.node.querySelector(".monitoring")?.addEventListener("change", (e: Event) => { - const state = (e.target as HTMLInputElement).checked; - this.uiState.toggleMonitor(state); - this.debugger.toggleMonitor(state); - }); - - this.node.querySelectorAll(".start, .length")?.forEach((element) => { - element.addEventListener("blur", this.debugger.onMonitorRangeChange.bind(this.debugger)); - }); - this.node - .querySelector(".stepButton") - ?.addEventListener("click", this.simulator.debugExecStep.bind(this.simulator)); - this.node.querySelector(".gotoButton")?.addEventListener("click", () => { - this.simulator.gotoAddr(this.console.prompt("Enter address or label", "") || ""); - }); - this.node.querySelector(".notesButton")?.addEventListener("click", this.uiState.showNotes.bind(this.uiState)); - - const editor = this.node.querySelector(".code"); - editor?.addEventListener("keypress", () => { - this.simulator.stop(); - }); - editor?.addEventListener("keypress", this.uiState.initialize.bind(this.uiState)); - - document.addEventListener("keypress", (e: KeyboardEvent) => { - let value = 0; - - value = mapKeyboardToGamepadKeyCode(e.key, e.which); - - this.memory.storeKeypress(value); - }); - - // Assembler events - - this.assembler.on("assemble-success", (event) => { - this.uiState.assembleSuccess(); - this.memory.set(this.assembler.getCurrentPC(), 0x00); // Set a null byte at the end of the code - - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.assembler.on("assemble-failure", (event) => { - this.uiState.initialize(); - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.assembler.on("hexdump", (event) => { - this.openPopup(event.message || "", "Hexdump"); - }); - - // @ts-ignore TODO: Fix this, needs a more generic event type - this.assembler.on("disassembly", (event) => { - this.openPopup(event.data.formatted || "", "Disassembly"); - }); - - this.assembler.on("assemble-info", (event) => { - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - // Simulator events - - this.simulator.on("stop", (event) => { - this.uiState.stop(); - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.simulator.on("start", (event) => { - this.uiState.play(); - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.simulator.on("reset", (event) => { - this.display.reset(); - }); - - this.simulator.on("pseudo-op", (event) => { - this.console.log(event.type + ": " + event.message); - }); - - this.simulator.on("simulator-info", (event) => { - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.simulator.on("simulator-failure", (event) => { - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - // Labels events - - this.labels.on("labels-info", (event) => { - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.labels.on("labels-failure", (event) => { - if (event.message) { - this.console.log(formatMessage(event.message, event.params)); - } - }); - - this.debugger.onMonitorRangeChange(); - } - - /** - * Removes leading and trailing whitespace from the code textarea. - */ - private stripText(): void { - const code = this.node.querySelector(".code"); - if (!code) { - return; - } - let text = code.value; - text = text.replace(/^\n+/, "").replace(/\s+$/, ""); - code.value = text; - } - - /** - * Opens a popup window with the given content. - * @param content - The content to display in the popup. - * @param title - The title of the popup window. - */ - private openPopup(content: string, title: string) { - const w = window.open( - "", - title, - "width=500,height=300,resizable=yes,scrollbars=yes,toolbar=no,location=no,menubar=no,status=no" - ); - - if (!w) { - this.console.error("Failed to open popup"); - return; - } - - let html = ""; - html += ""; - html += "" + title + ""; - html += "
";
-
-    html += content;
-
-    html += "
"; - w.document.write(html); - w.document.close(); - } -} diff --git a/packages/app-web/src/main.css b/packages/app-web/src/main.css deleted file mode 100644 index 1e4e2d1a..00000000 --- a/packages/app-web/src/main.css +++ /dev/null @@ -1,4 +0,0 @@ -@import './src/style/layout.css'; -@import './src/style/utils.css'; -@import './src/style/game-console.css'; -@import './src/style/pygment_trac.css'; diff --git a/packages/app-web/src/main.ts b/packages/app-web/src/main.ts deleted file mode 100644 index 25df990a..00000000 --- a/packages/app-web/src/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { GameConsole } from "./game-console.js"; - -document.addEventListener("DOMContentLoaded", function () { - document.querySelectorAll(".widget").forEach(function (widget) { - new GameConsole(widget); - }); -}); diff --git a/packages/app-web/src/message-console.ts b/packages/app-web/src/message-console.ts deleted file mode 100644 index 3b50a9d6..00000000 --- a/packages/app-web/src/message-console.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { MessageConsoleWidget } from "@learn6502/common-ui"; - -export class MessageConsole implements MessageConsoleWidget { - constructor(private readonly node: HTMLElement) {} - - public log(message: string) { - message += "\n"; // allow put operations from the simulator (WDM opcode) - this.node.innerText += message; - } - - public warn(message: string) { - this.node.innerText += `\n\n${message}`; - } - - public error(message: string) { - this.node.innerText += `\n\n${message}`; - } - - public clear() { - this.node.innerHTML = ""; - } - - public prompt(message: string, defaultValue?: string): string | null { - return prompt(message, defaultValue); - } -} diff --git a/packages/app-web/src/style/game-console.css b/packages/app-web/src/style/game-console.css deleted file mode 100644 index eac927c0..00000000 --- a/packages/app-web/src/style/game-console.css +++ /dev/null @@ -1,96 +0,0 @@ -.widget { - width: 600px; - margin: 30px auto; - font-size: 12px; - line-height: 1.5; -} - -.buttons { - margin: 8px 0; -} - -.start, -.length { - width: 50px; -} - -.widget pre { - margin: 0; - padding: 0; - background: inherit; - border: none; -} - -.code { - margin: 0 0 6px 0; - padding: 6px; - border: 1px solid black; - width: 420px; - height: 290px; - font-family: monospace; - overflow: auto; - float: left; -} - -.screen { - float: right; -} - -.debugger { - border: 1px black solid; - margin-top: 6px; - padding: 3px; - padding-top: 8px; - height: 125px; - width: 152px; - text-align: center; - float: right; -} - -.minidebugger { - margin: 0; - margin-top: 6px; - padding: 0; - font-family: monospace; - font-size: 11px; -} - -.monitorControls { - width: 587px; - clear: both; - margin-bottom: 10px; - padding: 0; -} - -.monitorControls input { - margin-right: 0.5em; -} - -.monitor { - margin: 10px 0; - padding: 6px; - border: 1px solid #999; - background-color: #ddd; - width: 587px; - height: 100px; - overflow: auto; - display: none; -} - -.monitor-invalid { - border: 1px inset #c00; - padding: 2px 1px; -} - -.messages { - margin: 0; - padding: 6px; - border: 1px solid #999; - background-color: #eee; - overflow: auto; - width: 587px; - height: 100px; - text-align: left; - font-size: 12px; - color: #444; -} diff --git a/packages/app-web/src/style/layout.css b/packages/app-web/src/style/layout.css deleted file mode 100644 index 58bb5834..00000000 --- a/packages/app-web/src/style/layout.css +++ /dev/null @@ -1,233 +0,0 @@ -body { - padding: 50px; - font: - 14px / 1.5 system-ui, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen-Sans, - Noto, - Ubuntu, - Cantarell, - 'Helvetica Neue', - Arial, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol'; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - color: #222; - margin: 0 0 20px; -} - -p, -ul, -ol, -table, -pre, -dl { - margin: 0 0 20px; -} - -h1, -h2, -h3 { - line-height: 1.1; -} - -h1 { - font-size: 28px; -} - -h2 { - color: #393939; -} - -h3, -h4, -h5, -h6 { - color: #494949; -} - -a, -a code { - color: #33f; - font-weight: 400; - text-decoration: none; -} - -a small { - font-size: 11px; - color: #777; - margin-top: -0.6em; - display: block; -} - -.wrapper { - width: 860px; - margin: 0 auto; -} - -blockquote { - border-left: 1px solid #e5e5e5; - margin: 0; - padding: 0 0 0 20px; - font-style: italic; -} - -code, -pre { - font-family: - Monaco, - Bitstream Vera Sans Mono, - Lucida Console, - Terminal; - color: #333; - font-size: 12px; -} - -pre { - padding: 8px 15px; - background: #f8f8f8; - border-radius: 5px; - border: 1px solid #e5e5e5; - overflow-x: auto; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, -td { - text-align: left; - padding: 5px 10px; - border-bottom: 1px solid #e5e5e5; -} - -dt { - color: #444; - font-weight: 700; -} - -th { - color: #444; -} - -img { - max-width: 100%; -} - -header { - width: 240px; - float: left; - position: fixed; -} - -section { - width: 600px; - float: right; - padding-bottom: 50px; -} - -small { - font-size: 11px; -} - -hr { - border: 0; - background: #e5e5e5; - height: 1px; - margin: 0 0 20px; -} - -footer { - width: 240px; - float: left; - position: fixed; - bottom: 50px; -} - -@media print, screen and (max-width: 960px) { - div.wrapper { - width: auto; - margin: 0; - } - - header, - section, - footer { - float: none; - position: static; - width: auto; - } - - header { - padding-right: 320px; - } - - section { - border: 1px solid #e5e5e5; - border-width: 1px 0; - padding: 20px 0; - margin: 0 0 20px; - } - - header a small { - display: inline; - } - - header ul { - position: absolute; - right: 50px; - top: 52px; - } -} - -@media print, screen and (max-width: 720px) { - body { - word-wrap: break-word; - } - - header { - padding: 0; - } - - header ul, - header p.view { - position: static; - } - - pre, - code { - word-wrap: normal; - } -} - -@media print, screen and (max-width: 480px) { - body { - padding: 15px; - } - - header ul { - display: none; - } -} - -@media print { - body { - padding: 0.4in; - font-size: 12pt; - color: #444; - } -} diff --git a/packages/app-web/src/style/pygment_trac.css b/packages/app-web/src/style/pygment_trac.css deleted file mode 100644 index c0b00513..00000000 --- a/packages/app-web/src/style/pygment_trac.css +++ /dev/null @@ -1,222 +0,0 @@ -.highlight { - background: #ffffff; -} -.highlight .c { - color: #999988; - font-style: italic; -} /* Comment */ -.highlight .err { - color: #a61717; - background-color: #e3d2d2; -} /* Error */ -.highlight .k { - font-weight: bold; -} /* Keyword */ -.highlight .o { - font-weight: bold; -} /* Operator */ -.highlight .cm { - color: #999988; - font-style: italic; -} /* Comment.Multiline */ -.highlight .cp { - color: #999999; - font-weight: bold; -} /* Comment.Preproc */ -.highlight .c1 { - color: #999988; - font-style: italic; -} /* Comment.Single */ -.highlight .cs { - color: #999999; - font-weight: bold; - font-style: italic; -} /* Comment.Special */ -.highlight .gd { - color: #000000; - background-color: #ffdddd; -} /* Generic.Deleted */ -.highlight .gd .x { - color: #000000; - background-color: #ffaaaa; -} /* Generic.Deleted.Specific */ -.highlight .ge { - font-style: italic; -} /* Generic.Emph */ -.highlight .gr { - color: #aa0000; -} /* Generic.Error */ -.highlight .gh { - color: #999999; -} /* Generic.Heading */ -.highlight .gi { - color: #000000; - background-color: #ddffdd; -} /* Generic.Inserted */ -.highlight .gi .x { - color: #000000; - background-color: #aaffaa; -} /* Generic.Inserted.Specific */ -.highlight .go { - color: #888888; -} /* Generic.Output */ -.highlight .gp { - color: #555555; -} /* Generic.Prompt */ -.highlight .gs { - font-weight: bold; -} /* Generic.Strong */ -.highlight .gu { - color: #800080; - font-weight: bold; -} /* Generic.Subheading */ -.highlight .gt { - color: #aa0000; -} /* Generic.Traceback */ -.highlight .kc { - font-weight: bold; -} /* Keyword.Constant */ -.highlight .kd { - font-weight: bold; -} /* Keyword.Declaration */ -.highlight .kn { - font-weight: bold; -} /* Keyword.Namespace */ -.highlight .kp { - font-weight: bold; -} /* Keyword.Pseudo */ -.highlight .kr { - font-weight: bold; -} /* Keyword.Reserved */ -.highlight .kt { - color: #445588; - font-weight: bold; -} /* Keyword.Type */ -.highlight .m { - color: #009999; -} /* Literal.Number */ -.highlight .s { - color: #d14; -} /* Literal.String */ -.highlight .na { - color: #008080; -} /* Name.Attribute */ -.highlight .nb { - color: #0086b3; -} /* Name.Builtin */ -.highlight .nc { - color: #445588; - font-weight: bold; -} /* Name.Class */ -.highlight .no { - color: #008080; -} /* Name.Constant */ -.highlight .ni { - color: #800080; -} /* Name.Entity */ -.highlight .ne { - color: #990000; - font-weight: bold; -} /* Name.Exception */ -.highlight .nf { - color: #990000; - font-weight: bold; -} /* Name.Function */ -.highlight .nn { - color: #555555; -} /* Name.Namespace */ -.highlight .nt { - color: #000080; -} /* Name.Tag */ -.highlight .nv { - color: #008080; -} /* Name.Variable */ -.highlight .ow { - font-weight: bold; -} /* Operator.Word */ -.highlight .w { - color: #bbbbbb; -} /* Text.Whitespace */ -.highlight .mf { - color: #009999; -} /* Literal.Number.Float */ -.highlight .mh { - color: #009999; -} /* Literal.Number.Hex */ -.highlight .mi { - color: #009999; -} /* Literal.Number.Integer */ -.highlight .mo { - color: #009999; -} /* Literal.Number.Oct */ -.highlight .sb { - color: #d14; -} /* Literal.String.Backtick */ -.highlight .sc { - color: #d14; -} /* Literal.String.Char */ -.highlight .sd { - color: #d14; -} /* Literal.String.Doc */ -.highlight .s2 { - color: #d14; -} /* Literal.String.Double */ -.highlight .se { - color: #d14; -} /* Literal.String.Escape */ -.highlight .sh { - color: #d14; -} /* Literal.String.Heredoc */ -.highlight .si { - color: #d14; -} /* Literal.String.Interpol */ -.highlight .sx { - color: #d14; -} /* Literal.String.Other */ -.highlight .sr { - color: #009926; -} /* Literal.String.Regex */ -.highlight .s1 { - color: #d14; -} /* Literal.String.Single */ -.highlight .ss { - color: #990073; -} /* Literal.String.Symbol */ -.highlight .bp { - color: #999999; -} /* Name.Builtin.Pseudo */ -.highlight .vc { - color: #008080; -} /* Name.Variable.Class */ -.highlight .vg { - color: #008080; -} /* Name.Variable.Global */ -.highlight .vi { - color: #008080; -} /* Name.Variable.Instance */ -.highlight .il { - color: #009999; -} /* Literal.Number.Integer.Long */ - -.type-csharp .highlight .k { - color: #0000ff; -} -.type-csharp .highlight .kt { - color: #0000ff; -} -.type-csharp .highlight .nf { - color: #000000; - font-weight: normal; -} -.type-csharp .highlight .nc { - color: #2b91af; -} -.type-csharp .highlight .nn { - color: #000000; -} -.type-csharp .highlight .s { - color: #a31515; -} -.type-csharp .highlight .sc { - color: #a31515; -} diff --git a/packages/app-web/src/style/utils.css b/packages/app-web/src/style/utils.css deleted file mode 100644 index f4d336fd..00000000 --- a/packages/app-web/src/style/utils.css +++ /dev/null @@ -1,3 +0,0 @@ -.hidden { - display: none; -} diff --git a/packages/app-web/src/types/state.ts b/packages/app-web/src/types/state.ts deleted file mode 100644 index c8d6b3ad..00000000 --- a/packages/app-web/src/types/state.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface State { - assemble: boolean; - run?: [boolean, string]; - reset: boolean; - hexdump: boolean; - disassemble: boolean; - debug: [boolean, boolean]; -} diff --git a/packages/app-web/src/ui-state.ts b/packages/app-web/src/ui-state.ts deleted file mode 100644 index 66616c36..00000000 --- a/packages/app-web/src/ui-state.ts +++ /dev/null @@ -1,182 +0,0 @@ -import type { State } from "./types/state.js"; - -/** - * Manages the user interface for the 6502 simulator. - */ -export class UIState { - private _currentState: State; - - // Define the various UI states - private start: State = { - assemble: true, - run: [false, "Run"], - reset: false, - hexdump: false, - disassemble: false, - debug: [false, false], - }; - - private assembled: State = { - assemble: false, - run: [true, "Run"], - reset: true, - hexdump: true, - disassemble: true, - debug: [true, false], - }; - - private running: State = { - assemble: false, - run: [true, "Stop"], - reset: true, - hexdump: false, - disassemble: false, - debug: [true, false], - }; - - private debugging: State = { - assemble: false, - reset: true, - hexdump: true, - disassemble: true, - debug: [true, true], - }; - - private postDebugging: State = { - assemble: false, - reset: true, - hexdump: true, - disassemble: true, - debug: [true, false], - }; - - /** - * Creates a new UI instance. - * @param node - The root HTML element containing the UI components. - */ - constructor(private node: HTMLElement) { - this._currentState = this.start; - } - - /** - * Initializes the UI to its starting state. - */ - public initialize(): void { - this.setState(this.start); - } - - /** - * Sets the UI to the running state. - */ - public play(): void { - this.setState(this.running); - } - - /** - * Sets the UI to the assembled state (stopped). - */ - public stop(): void { - this.setState(this.assembled); - } - - /** - * Enables debugging mode in the UI. - */ - public debugOn(): void { - this.setState(this.debugging); - } - - /** - * Disables debugging mode in the UI. - */ - public debugOff(): void { - this.setState(this.postDebugging); - } - - /** - * Sets the UI to the assembled state after successful assembly. - */ - public assembleSuccess(): void { - this.setState(this.assembled); - } - - /** - * Toggles the visibility of the monitor. - * @param enable - Whether to show or hide the monitor. - */ - public toggleMonitor(enable: boolean): void { - const monitor = this.node.querySelector(".monitor"); - if (!monitor) { - return; - } - monitor.style.display = enable ? "block" : "none"; - } - - /** - * Displays notes in the messages area. - */ - public showNotes(): void { - const messagesCode = this.node.querySelector(".messages code"); - const notes = this.node.querySelector(".notes"); - if (messagesCode && notes) { - messagesCode.innerHTML = notes.innerHTML; - } - } - - public get currentState(): State { - return this._currentState; - } - - /** - * Updates the UI elements based on the current state. - * @param state - The new state to apply to the UI. - */ - private setState(state: State): void { - const assembleButton = this.node.querySelector(".assembleButton"); - const mainButton = this.node.querySelector(".mainButton"); - const resetButton = this.node.querySelector(".resetButton"); - const hexdumpButton = this.node.querySelector(".hexdumpButton"); - const disassembleButton = this.node.querySelector(".disassembleButton"); - const debug = this.node.querySelector(".debug"); - const stepButton = this.node.querySelector(".stepButton"); - const gotoButton = this.node.querySelector(".gotoButton"); - - if (assembleButton) { - assembleButton.disabled = !state.assemble; - } - - if (state.run) { - if (mainButton) { - mainButton.disabled = !state.run[0]; - mainButton.value = state.run[1]; - } - } - - if (resetButton) { - resetButton.disabled = !state.reset; - } - - if (hexdumpButton) { - hexdumpButton.disabled = !state.hexdump; - } - - if (disassembleButton) { - disassembleButton.disabled = !state.disassemble; - } - - if (debug) { - debug.disabled = !state.debug[0]; - debug.checked = state.debug[1]; - } - - if (stepButton) { - stepButton.disabled = !state.debug[1]; - } - - if (gotoButton) { - gotoButton.disabled = !state.debug[1]; - } - - this._currentState = state; - } -} diff --git a/packages/app-web/tsconfig.json b/packages/app-web/tsconfig.json index e506ee18..6e0fc326 100644 --- a/packages/app-web/tsconfig.json +++ b/packages/app-web/tsconfig.json @@ -15,7 +15,6 @@ "diagnostics": true }, "files": [ - "src/main.ts", "src/app/main.ts", "src/types/adwaita-fonts.d.ts", "src/types/html-module.d.ts", diff --git a/packages/app-web/vite.config.js b/packages/app-web/vite.config.js deleted file mode 100644 index d0ebe957..00000000 --- a/packages/app-web/vite.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import { defineConfig } from "vite"; - -export default defineConfig({ - clearScreen: false, - css: { - transformer: "lightningcss", - }, - build: { - assetsDir: ".", - outDir: "dist", - minify: false, - rollupOptions: { - input: ["src/main.ts", "src/main.css"], - output: { - // No hash in the filenames - entryFileNames: `assets/[name].js`, - chunkFileNames: `assets/[name].js`, - assetFileNames: `assets/[name].[ext]`, - }, - }, - cssMinify: "lightningcss", - }, -});