Compare commits
No commits in common. "ac2fc5ff659ccfa279e69910d4f7864dfa5cea2c" and "06c16f80f0a2e7b482773ac4ed7771d528f03753" have entirely different histories.
ac2fc5ff65
...
06c16f80f0
15 changed files with 200 additions and 857 deletions
112
README.md
112
README.md
|
|
@ -1,38 +1,34 @@
|
|||
# C64 Program Collection
|
||||
# C64 BASIC Collection
|
||||
|
||||
Twenty-three small, polished programs for the Commodore 64, written in stock
|
||||
Commodore BASIC V2 and 6502 machine code. The collection mixes arcade action,
|
||||
puzzles, simulations, creative tools, sound, and visual effects. Every entry is
|
||||
self-contained and runs on an ordinary PAL or NTSC C64 without a cartridge or
|
||||
BASIC extension.
|
||||
Twenty small, polished programs for the Commodore 64, written entirely in stock
|
||||
Commodore BASIC V2. The collection mixes arcade action, puzzles, simulations,
|
||||
creative tools, sound, and visual effects. No cartridge, BASIC extension, or
|
||||
machine-language payload is required.
|
||||
|
||||
## Programs
|
||||
|
||||
| Program | Category | Language | Controls | Description | Source | Run locally |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Meteor Dodge | Arcade | BASIC | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [BAS](meteor-dodge/meteor-dodge.bas) | [Run](http://localhost:8134/program/meteor-dodge) |
|
||||
| Code Breaker | Logic | BASIC | `0`-`9`, Return, `Q` | Deduce a four-digit secret in ten guesses. | [BAS](code-breaker/code-breaker.bas) | [Run](http://localhost:8134/program/code-breaker) |
|
||||
| Submarine Hunt | Strategy | BASIC | Row, Column, Return | Find three hidden submarines with limited sonar shots. | [BAS](submarine-hunt/submarine-hunt.bas) | [Run](http://localhost:8134/program/submarine-hunt) |
|
||||
| Lunar Lander | Simulation | BASIC | `0`-`9`, Return, `-1` | Choose engine burns and touch down gently. | [BAS](lunar-lander/lunar-lander.bas) | [Run](http://localhost:8134/program/lunar-lander) |
|
||||
| Memory Matrix | Memory | BASIC | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [BAS](memory-matrix/memory-matrix.bas) | [Run](http://localhost:8134/program/memory-matrix) |
|
||||
| Robot Maze | Puzzle | BASIC | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [BAS](robot-maze/robot-maze.bas) | [Run](http://localhost:8134/program/robot-maze) |
|
||||
| Music Box | Music | BASIC | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [BAS](music-box/music-box.bas) | [Run](http://localhost:8134/program/music-box) |
|
||||
| Pixel Painter | Creative | BASIC | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [BAS](pixel-painter/pixel-painter.bas) | [Run](http://localhost:8134/program/pixel-painter) |
|
||||
| Stock Trader | Simulation | BASIC | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [BAS](stock-trader/stock-trader.bas) | [Run](http://localhost:8134/program/stock-trader) |
|
||||
| Starfield | Demo | BASIC | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [BAS](starfield/starfield.bas) | [Run](http://localhost:8134/program/starfield) |
|
||||
| Snake Trail | Arcade | BASIC | `W`, `A`, `S`, `D`, `Q` | Guide a growing snake through a field of food and danger. | [BAS](snake-trail/snake-trail.bas) | [Run](http://localhost:8134/program/snake-trail) |
|
||||
| Dice Poker | Dice | BASIC | `1`-`5`, Return, `Q` | Keep your best dice and chase a winning poker combination. | [BAS](dice-poker/dice-poker.bas) | [Run](http://localhost:8134/program/dice-poker) |
|
||||
| Word Scramble | Word | BASIC | `A`-`Z`, Return, `Q` | Unscramble five computer-themed words from helpful clues. | [BAS](word-scramble/word-scramble.bas) | [Run](http://localhost:8134/program/word-scramble) |
|
||||
| Nim Challenge | Strategy | BASIC | `1`, `2`, `3`, `0` | Outthink the computer in the classic game of twenty-one sticks. | [BAS](nim-challenge/nim-challenge.bas) | [Run](http://localhost:8134/program/nim-challenge) |
|
||||
| Reaction Timer | Reflex | BASIC | Any key, `Q` | Test your reactions across five unpredictable timed rounds. | [BAS](reaction-timer/reaction-timer.bas) | [Run](http://localhost:8134/program/reaction-timer) |
|
||||
| Morse Trainer | Educational | BASIC | `A`-`Z`, Return, `Q` | Learn Morse letters by reading and hearing their signals. | [BAS](morse-trainer/morse-trainer.bas) | [Run](http://localhost:8134/program/morse-trainer) |
|
||||
| Kaleidoscope | Demo | BASIC | Space, `C`, `Q` | Grow colorful symmetrical patterns across the character screen. | [BAS](kaleidoscope/kaleidoscope.bas) | [Run](http://localhost:8134/program/kaleidoscope) |
|
||||
| World Quiz | Trivia | BASIC | `1`, `2`, `3`, `Q` | Take a seven-stop tour of world geography and landmarks. | [BAS](world-quiz/world-quiz.bas) | [Run](http://localhost:8134/program/world-quiz) |
|
||||
| Life Lab | Simulation | BASIC | Space, `R`, `Q` | Observe generations of Conway's cellular automaton evolve. | [BAS](life-lab/life-lab.bas) | [Run](http://localhost:8134/program/life-lab) |
|
||||
| Cipher Wheel | Utility | BASIC | `E`, `D`, `N`, `Q` | Encode and decode secret messages with a Caesar shift. | [BAS](cipher-wheel/cipher-wheel.bas) | [Run](http://localhost:8134/program/cipher-wheel) |
|
||||
| Sprite Playground | Creative | Machine Code | `W`, `A`, `S`, `D`, `C`, `Q` | Move and recolor a hardware sprite in a responsive VIC-II playground. | [ASM](sprite-playground/sprite-playground.asm) | [Run](http://localhost:8134/program/sprite-playground) |
|
||||
| Raster Bars | Demo | Machine Code | `1`, `2`, `3`, `C`, `Q` | Paint animated bands of color with cycle-aware VIC-II raster timing. | [ASM](raster-bars/raster-bars.asm) | [Run](http://localhost:8134/program/raster-bars) |
|
||||
| SID Arpeggiator | Music | Machine Code | `A`-`K`, `1`-`3`, `+`, `-`, Space, `Q` | Shape a looping SID arpeggio with notes, waveforms, and tempo controls. | [ASM](sid-arpeggiator/sid-arpeggiator.asm) | [Run](http://localhost:8134/program/sid-arpeggiator) |
|
||||
| Program | Type | Controls | Description | Run locally |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Meteor Dodge](meteor-dodge/meteor-dodge.bas) | Arcade | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [Run program](http://localhost:8134/program/meteor-dodge) |
|
||||
| [Code Breaker](code-breaker/code-breaker.bas) | Logic | `0`-`9`, `Q` | Deduce a four-digit secret in ten guesses. | [Run program](http://localhost:8134/program/code-breaker) |
|
||||
| [Submarine Hunt](submarine-hunt/submarine-hunt.bas) | Strategy | Row, Column, `Q` | Find three hidden submarines with limited sonar shots. | [Run program](http://localhost:8134/program/submarine-hunt) |
|
||||
| [Lunar Lander](lunar-lander/lunar-lander.bas) | Simulation | `0`-`9`, `Q` | Choose engine burns and touch down gently. | [Run program](http://localhost:8134/program/lunar-lander) |
|
||||
| [Memory Matrix](memory-matrix/memory-matrix.bas) | Memory | `1`, `2`, `3`, `4`, `Q` | Repeat a growing sequence of flashing quadrants. | [Run program](http://localhost:8134/program/memory-matrix) |
|
||||
| [Robot Maze](robot-maze/robot-maze.bas) | Puzzle | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [Run program](http://localhost:8134/program/robot-maze) |
|
||||
| [Music Box](music-box/music-box.bas) | Music | `A`-`K`, `1`, `2`, `3`, `Q` | Play the SID chip as a small computer keyboard. | [Run program](http://localhost:8134/program/music-box) |
|
||||
| [Pixel Painter](pixel-painter/pixel-painter.bas) | Creative | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [Run program](http://localhost:8134/program/pixel-painter) |
|
||||
| [Stock Trader](stock-trader/stock-trader.bas) | Simulation | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [Run program](http://localhost:8134/program/stock-trader) |
|
||||
| [Starfield](starfield/starfield.bas) | Demo | `1`, `2`, `3`, `Q` | Watch and control a layered animated starfield. | [Run program](http://localhost:8134/program/starfield) |
|
||||
| [Snake Trail](snake-trail/snake-trail.bas) | Arcade | `W`, `A`, `S`, `D`, `Q` | Guide a growing snake through a field of food and danger. | [Run program](http://localhost:8134/program/snake-trail) |
|
||||
| [Dice Poker](dice-poker/dice-poker.bas) | Dice | `1`-`5`, Return, `Q` | Keep your best dice and chase a winning poker combination. | [Run program](http://localhost:8134/program/dice-poker) |
|
||||
| [Word Scramble](word-scramble/word-scramble.bas) | Word | `A`-`Z`, Return, `Q` | Unscramble five computer-themed words from helpful clues. | [Run program](http://localhost:8134/program/word-scramble) |
|
||||
| [Nim Challenge](nim-challenge/nim-challenge.bas) | Strategy | `1`, `2`, `3`, `0` | Outthink the computer in the classic game of twenty-one sticks. | [Run program](http://localhost:8134/program/nim-challenge) |
|
||||
| [Reaction Timer](reaction-timer/reaction-timer.bas) | Reflex | Any key, `Q` | Test your reactions across five unpredictable timed rounds. | [Run program](http://localhost:8134/program/reaction-timer) |
|
||||
| [Morse Trainer](morse-trainer/morse-trainer.bas) | Educational | `A`-`Z`, Return, `Q` | Learn Morse letters by reading and hearing their signals. | [Run program](http://localhost:8134/program/morse-trainer) |
|
||||
| [Kaleidoscope](kaleidoscope/kaleidoscope.bas) | Demo | Space, `C`, `Q` | Grow colorful symmetrical patterns across the character screen. | [Run program](http://localhost:8134/program/kaleidoscope) |
|
||||
| [World Quiz](world-quiz/world-quiz.bas) | Trivia | `1`, `2`, `3`, `Q` | Take a seven-stop tour of world geography and landmarks. | [Run program](http://localhost:8134/program/world-quiz) |
|
||||
| [Life Lab](life-lab/life-lab.bas) | Simulation | Space, `R`, `Q` | Observe generations of Conway's cellular automaton evolve. | [Run program](http://localhost:8134/program/life-lab) |
|
||||
| [Cipher Wheel](cipher-wheel/cipher-wheel.bas) | Utility | `E`, `D`, `N`, `Q` | Encode and decode secret messages with a Caesar shift. | [Run program](http://localhost:8134/program/cipher-wheel) |
|
||||
|
||||
## Running the programs
|
||||
|
||||
|
|
@ -42,16 +38,22 @@ Start the local browser app with Docker Compose:
|
|||
docker compose up --build -d
|
||||
```
|
||||
|
||||
Open [http://localhost:8134](http://localhost:8134), or use a **Run** link in
|
||||
the table. The app runs the VICE x64sc core through WebAssembly and downloads
|
||||
the selected PRG through its own backend. Click inside the emulator to enable
|
||||
keyboard input and sound. Direct keyboard input is enabled by default, mouse
|
||||
locking is disabled, and moving the pointer anywhere over the emulator reveals
|
||||
its control bar.
|
||||
Open [http://localhost:8134](http://localhost:8134), or click a **Run program**
|
||||
link in the table. The app runs the VICE x64sc core through WebAssembly and
|
||||
downloads the selected PRG through its own backend, avoiding browser CORS
|
||||
restrictions. Click inside the emulator to enable keyboard input and sound.
|
||||
Direct keyboard input is enabled by default, mouse locking is disabled, and
|
||||
moving the pointer anywhere over the emulator reveals its control bar.
|
||||
|
||||
The repository is mounted read-only at `/programs`. Add a manifest entry and
|
||||
its source and PRG files, then click **Refresh**; the app does not need to be
|
||||
rebuilt. The default host port is configured as `8134` in `.env`.
|
||||
Docker Compose mounts this repository read-only at `/programs`, and the app
|
||||
serves PRG files from that local mount. There is no runtime dependency on the
|
||||
Git server and no browser CORS boundary. Add a program to `programs.json` and
|
||||
place its PRG in the referenced path; it appears after clicking **Refresh list**
|
||||
without rebuilding the image. The default host port is configured as `8134` in
|
||||
`.env`.
|
||||
|
||||
Each directory contains an editable, line-numbered `.bas` listing and a matching
|
||||
tokenized `.prg` file. The PRGs use the normal C64 BASIC load address, `$0801`.
|
||||
|
||||
On a Commodore 64, transfer a PRG to disk or another supported medium, then use:
|
||||
|
||||
|
|
@ -60,29 +62,17 @@ LOAD"METEOR-DODGE.PRG",8
|
|||
RUN
|
||||
```
|
||||
|
||||
Substitute the filename and device number as needed. The machine-code PRGs
|
||||
include a small BASIC `SYS` launcher, so the same `RUN` command starts them.
|
||||
|
||||
## Building machine-code programs
|
||||
|
||||
The assembly sources use ACME syntax. Build every machine-code PRG with the
|
||||
isolated tools profile; no local assembler installation is required:
|
||||
|
||||
```text
|
||||
docker compose --profile tools run --rm assembler
|
||||
```
|
||||
|
||||
The tools profile is opt-in and is not started by the normal app command.
|
||||
Substitute the filename and device number as needed. In VICE or another C64
|
||||
emulator, drag a PRG into the emulator window or use its autostart command.
|
||||
|
||||
## Source format
|
||||
|
||||
Each program directory contains a loadable `.prg` and its readable source:
|
||||
The `.bas` files are plain ASCII for comfortable reading and editing on a modern
|
||||
computer. They use uppercase BASIC V2 keywords and avoid embedded control-code
|
||||
glyphs. Their corresponding `.prg` files contain the tokenized PETSCII program
|
||||
that the C64 loads directly.
|
||||
|
||||
- BASIC programs use line-numbered, uppercase `.bas` files compatible with
|
||||
stock Commodore BASIC V2. Their PRGs are tokenized at the normal `$0801`
|
||||
BASIC load address.
|
||||
- Machine-code programs use commented ACME `.asm` files. Their PRGs also load
|
||||
at `$0801`, where a short BASIC line starts the 6502 code with `SYS`.
|
||||
All programs are self-contained and designed for an ordinary PAL or NTSC
|
||||
Commodore 64. Randomized programs seed BASIC's random-number generator from the
|
||||
machine clock, so every session is different.
|
||||
|
||||
All programs are self-contained and designed for ordinary PAL and NTSC C64
|
||||
hardware as well as VICE-compatible emulators.
|
||||
|
|
|
|||
|
|
@ -9,12 +9,3 @@ services:
|
|||
- .:/programs:ro
|
||||
restart: unless-stopped
|
||||
|
||||
assembler:
|
||||
profiles: ["tools"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: tools/acme/Dockerfile
|
||||
working_dir: /programs
|
||||
volumes:
|
||||
- .:/programs
|
||||
|
||||
|
|
|
|||
159
programs.json
159
programs.json
|
|
@ -1,140 +1,143 @@
|
|||
[
|
||||
{
|
||||
"title": "Meteor Dodge", "category": "Arcade", "language": "basic",
|
||||
"title": "Meteor Dodge",
|
||||
"type": "Arcade",
|
||||
"description": "Dodge falling meteors and build your score.",
|
||||
"controls": [{"keys":["A"],"description":"Move left"},{"keys":["D"],"description":"Move right"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "meteor-dodge/meteor-dodge.prg", "source": "meteor-dodge/meteor-dodge.bas"
|
||||
"controls": ["A", "D", "Q"],
|
||||
"path": "meteor-dodge/meteor-dodge.prg"
|
||||
},
|
||||
{
|
||||
"title": "Code Breaker", "category": "Logic", "language": "basic",
|
||||
"title": "Code Breaker",
|
||||
"type": "Logic",
|
||||
"description": "Deduce a four-digit secret in ten guesses.",
|
||||
"controls": [{"keys":["0-9"],"description":"Enter a four-digit guess"},{"keys":["RETURN"],"description":"Submit the guess"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "code-breaker/code-breaker.prg", "source": "code-breaker/code-breaker.bas"
|
||||
"controls": ["0-9", "Q"],
|
||||
"path": "code-breaker/code-breaker.prg"
|
||||
},
|
||||
{
|
||||
"title": "Submarine Hunt", "category": "Strategy", "language": "basic",
|
||||
"title": "Submarine Hunt",
|
||||
"type": "Strategy",
|
||||
"description": "Find three hidden submarines with limited sonar shots.",
|
||||
"controls": [{"keys":["ROW","COLUMN"],"description":"Enter sonar coordinates"},{"keys":["RETURN"],"description":"Fire a sonar shot"},{"keys":["0","0"],"description":"Quit"}],
|
||||
"path": "submarine-hunt/submarine-hunt.prg", "source": "submarine-hunt/submarine-hunt.bas"
|
||||
"controls": ["ROW", "COLUMN", "Q"],
|
||||
"path": "submarine-hunt/submarine-hunt.prg"
|
||||
},
|
||||
{
|
||||
"title": "Lunar Lander", "category": "Simulation", "language": "basic",
|
||||
"title": "Lunar Lander",
|
||||
"type": "Simulation",
|
||||
"description": "Choose engine burns and touch down gently.",
|
||||
"controls": [{"keys":["0-9"],"description":"Choose engine burn strength"},{"keys":["RETURN"],"description":"Commit the burn"},{"keys":["-1"],"description":"Quit"}],
|
||||
"path": "lunar-lander/lunar-lander.prg", "source": "lunar-lander/lunar-lander.bas"
|
||||
"controls": ["0-9", "Q"],
|
||||
"path": "lunar-lander/lunar-lander.prg"
|
||||
},
|
||||
{
|
||||
"title": "Memory Matrix", "category": "Memory", "language": "basic",
|
||||
"title": "Memory Matrix",
|
||||
"type": "Memory",
|
||||
"description": "Repeat a growing sequence of flashing quadrants.",
|
||||
"controls": [{"keys":["1","2","3","4"],"description":"Repeat the flashed quadrant"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "memory-matrix/memory-matrix.prg", "source": "memory-matrix/memory-matrix.bas"
|
||||
"controls": ["1", "2", "3", "4", "Q"],
|
||||
"path": "memory-matrix/memory-matrix.prg"
|
||||
},
|
||||
{
|
||||
"title": "Robot Maze", "category": "Puzzle", "language": "basic",
|
||||
"title": "Robot Maze",
|
||||
"type": "Puzzle",
|
||||
"description": "Navigate a freshly generated maze to its exit.",
|
||||
"controls": [{"keys":["W","A","S","D"],"description":"Move the robot"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "robot-maze/robot-maze.prg", "source": "robot-maze/robot-maze.bas"
|
||||
"controls": ["W", "A", "S", "D", "Q"],
|
||||
"path": "robot-maze/robot-maze.prg"
|
||||
},
|
||||
{
|
||||
"title": "Music Box", "category": "Music", "language": "basic",
|
||||
"title": "Music Box",
|
||||
"type": "Music",
|
||||
"description": "Play the SID chip as a small computer keyboard.",
|
||||
"controls": [{"keys":["A-K"],"description":"Play notes"},{"keys":["1","2","3"],"description":"Select square, saw, or triangle wave"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "music-box/music-box.prg", "source": "music-box/music-box.bas"
|
||||
"controls": ["A-K", "1", "2", "3", "Q"],
|
||||
"path": "music-box/music-box.prg"
|
||||
},
|
||||
{
|
||||
"title": "Pixel Painter", "category": "Creative", "language": "basic",
|
||||
"title": "Pixel Painter",
|
||||
"type": "Creative",
|
||||
"description": "Draw character-cell pictures on the screen.",
|
||||
"controls": [{"keys":["CURSOR KEYS"],"description":"Move the drawing cursor"},{"keys":["SPACE"],"description":"Draw a pixel"},{"keys":["C"],"description":"Clear the canvas"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "pixel-painter/pixel-painter.prg", "source": "pixel-painter/pixel-painter.bas"
|
||||
"controls": ["CURSOR KEYS", "SPACE", "C", "Q"],
|
||||
"path": "pixel-painter/pixel-painter.prg"
|
||||
},
|
||||
{
|
||||
"title": "Stock Trader", "category": "Simulation", "language": "basic",
|
||||
"title": "Stock Trader",
|
||||
"type": "Simulation",
|
||||
"description": "Trade a volatile stock over twenty market days.",
|
||||
"controls": [{"keys":["B"],"description":"Buy shares"},{"keys":["S"],"description":"Sell shares"},{"keys":["W"],"description":"Wait for the next day"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "stock-trader/stock-trader.prg", "source": "stock-trader/stock-trader.bas"
|
||||
"controls": ["B", "S", "W", "Q"],
|
||||
"path": "stock-trader/stock-trader.prg"
|
||||
},
|
||||
{
|
||||
"title": "Starfield", "category": "Demo", "language": "basic",
|
||||
"title": "Starfield",
|
||||
"type": "Demo",
|
||||
"description": "Watch and control a layered animated starfield.",
|
||||
"controls": [{"keys":["1","2","3"],"description":"Select slow, medium, or fast speed"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "starfield/starfield.prg", "source": "starfield/starfield.bas"
|
||||
"controls": ["1", "2", "3", "Q"],
|
||||
"path": "starfield/starfield.prg"
|
||||
},
|
||||
{
|
||||
"title": "Snake Trail", "category": "Arcade", "language": "basic",
|
||||
"title": "Snake Trail",
|
||||
"type": "Arcade",
|
||||
"description": "Guide a growing snake through a field of food and danger.",
|
||||
"controls": [{"keys":["W","A","S","D"],"description":"Steer the snake"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "snake-trail/snake-trail.prg", "source": "snake-trail/snake-trail.bas"
|
||||
"controls": ["W", "A", "S", "D", "Q"],
|
||||
"path": "snake-trail/snake-trail.prg"
|
||||
},
|
||||
{
|
||||
"title": "Dice Poker", "category": "Dice", "language": "basic",
|
||||
"title": "Dice Poker",
|
||||
"type": "Dice",
|
||||
"description": "Keep your best dice and chase a winning poker combination.",
|
||||
"controls": [{"keys":["1-5"],"description":"Choose dice positions to keep"},{"keys":["RETURN"],"description":"Roll the remaining dice"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "dice-poker/dice-poker.prg", "source": "dice-poker/dice-poker.bas"
|
||||
"controls": ["1-5", "RETURN", "Q"],
|
||||
"path": "dice-poker/dice-poker.prg"
|
||||
},
|
||||
{
|
||||
"title": "Word Scramble", "category": "Word", "language": "basic",
|
||||
"title": "Word Scramble",
|
||||
"type": "Word",
|
||||
"description": "Unscramble five computer-themed words from helpful clues.",
|
||||
"controls": [{"keys":["A-Z"],"description":"Type your answer"},{"keys":["RETURN"],"description":"Submit the answer"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "word-scramble/word-scramble.prg", "source": "word-scramble/word-scramble.bas"
|
||||
"controls": ["A-Z", "RETURN", "Q"],
|
||||
"path": "word-scramble/word-scramble.prg"
|
||||
},
|
||||
{
|
||||
"title": "Nim Challenge", "category": "Strategy", "language": "basic",
|
||||
"title": "Nim Challenge",
|
||||
"type": "Strategy",
|
||||
"description": "Outthink the computer in the classic game of twenty-one sticks.",
|
||||
"controls": [{"keys":["1","2","3"],"description":"Choose how many sticks to take"},{"keys":["0"],"description":"Quit"}],
|
||||
"path": "nim-challenge/nim-challenge.prg", "source": "nim-challenge/nim-challenge.bas"
|
||||
"controls": ["1", "2", "3", "0"],
|
||||
"path": "nim-challenge/nim-challenge.prg"
|
||||
},
|
||||
{
|
||||
"title": "Reaction Timer", "category": "Reflex", "language": "basic",
|
||||
"title": "Reaction Timer",
|
||||
"type": "Reflex",
|
||||
"description": "Test your reactions across five unpredictable timed rounds.",
|
||||
"controls": [{"keys":["ANY KEY"],"description":"React when NOW appears"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "reaction-timer/reaction-timer.prg", "source": "reaction-timer/reaction-timer.bas"
|
||||
"controls": ["ANY KEY", "Q"],
|
||||
"path": "reaction-timer/reaction-timer.prg"
|
||||
},
|
||||
{
|
||||
"title": "Morse Trainer", "category": "Educational", "language": "basic",
|
||||
"title": "Morse Trainer",
|
||||
"type": "Educational",
|
||||
"description": "Learn Morse letters by reading and hearing their signals.",
|
||||
"controls": [{"keys":["A-Z"],"description":"Enter the decoded letter"},{"keys":["RETURN"],"description":"Submit the answer"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "morse-trainer/morse-trainer.prg", "source": "morse-trainer/morse-trainer.bas"
|
||||
"controls": ["A-Z", "RETURN", "Q"],
|
||||
"path": "morse-trainer/morse-trainer.prg"
|
||||
},
|
||||
{
|
||||
"title": "Kaleidoscope", "category": "Demo", "language": "basic",
|
||||
"title": "Kaleidoscope",
|
||||
"type": "Demo",
|
||||
"description": "Grow colorful symmetrical patterns across the character screen.",
|
||||
"controls": [{"keys":["SPACE"],"description":"Start a new pattern"},{"keys":["C"],"description":"Change color"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "kaleidoscope/kaleidoscope.prg", "source": "kaleidoscope/kaleidoscope.bas"
|
||||
"controls": ["SPACE", "C", "Q"],
|
||||
"path": "kaleidoscope/kaleidoscope.prg"
|
||||
},
|
||||
{
|
||||
"title": "World Quiz", "category": "Trivia", "language": "basic",
|
||||
"title": "World Quiz",
|
||||
"type": "Trivia",
|
||||
"description": "Take a seven-stop tour of world geography and landmarks.",
|
||||
"controls": [{"keys":["1","2","3"],"description":"Choose an answer"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "world-quiz/world-quiz.prg", "source": "world-quiz/world-quiz.bas"
|
||||
"controls": ["1", "2", "3", "Q"],
|
||||
"path": "world-quiz/world-quiz.prg"
|
||||
},
|
||||
{
|
||||
"title": "Life Lab", "category": "Simulation", "language": "basic",
|
||||
"title": "Life Lab",
|
||||
"type": "Simulation",
|
||||
"description": "Observe generations of Conway's cellular automaton evolve.",
|
||||
"controls": [{"keys":["SPACE"],"description":"Pause or continue"},{"keys":["R"],"description":"Create a new random colony"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "life-lab/life-lab.prg", "source": "life-lab/life-lab.bas"
|
||||
"controls": ["SPACE", "R", "Q"],
|
||||
"path": "life-lab/life-lab.prg"
|
||||
},
|
||||
{
|
||||
"title": "Cipher Wheel", "category": "Utility", "language": "basic",
|
||||
"title": "Cipher Wheel",
|
||||
"type": "Utility",
|
||||
"description": "Encode and decode secret messages with a Caesar shift.",
|
||||
"controls": [{"keys":["E","D"],"description":"Choose encode or decode"},{"keys":["N"],"description":"Enter another message"},{"keys":["Q"],"description":"Quit"}],
|
||||
"path": "cipher-wheel/cipher-wheel.prg", "source": "cipher-wheel/cipher-wheel.bas"
|
||||
},
|
||||
{
|
||||
"title": "Sprite Playground", "category": "Creative", "language": "machine-code",
|
||||
"description": "Move and recolor a hardware sprite in a responsive VIC-II playground.",
|
||||
"controls": [{"keys":["W","A","S","D"],"description":"Move the sprite"},{"keys":["C"],"description":"Change sprite color"},{"keys":["Q"],"description":"Return to BASIC"}],
|
||||
"path": "sprite-playground/sprite-playground.prg", "source": "sprite-playground/sprite-playground.asm"
|
||||
},
|
||||
{
|
||||
"title": "Raster Bars", "category": "Demo", "language": "machine-code",
|
||||
"description": "Paint animated bands of color with cycle-aware VIC-II raster timing.",
|
||||
"controls": [{"keys":["1","2","3"],"description":"Select slow, medium, or fast movement"},{"keys":["C"],"description":"Change color palette"},{"keys":["Q"],"description":"Return to BASIC"}],
|
||||
"path": "raster-bars/raster-bars.prg", "source": "raster-bars/raster-bars.asm"
|
||||
},
|
||||
{
|
||||
"title": "SID Arpeggiator", "category": "Music", "language": "machine-code",
|
||||
"description": "Shape a looping SID arpeggio with notes, waveforms, and tempo controls.",
|
||||
"controls": [{"keys":["A-K"],"description":"Select the root note"},{"keys":["1","2","3"],"description":"Select triangle, saw, or pulse wave"},{"keys":["+","-"],"description":"Increase or decrease tempo"},{"keys":["SPACE"],"description":"Pause or resume playback"},{"keys":["Q"],"description":"Return to BASIC"}],
|
||||
"path": "sid-arpeggiator/sid-arpeggiator.prg", "source": "sid-arpeggiator/sid-arpeggiator.asm"
|
||||
"controls": ["E", "D", "N", "Q"],
|
||||
"path": "cipher-wheel/cipher-wheel.prg"
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
182
public/app.js
182
public/app.js
|
|
@ -4,102 +4,47 @@ const status = document.querySelector('#status');
|
|||
const programsElement = document.querySelector('#programs');
|
||||
const refreshButton = document.querySelector('#refresh');
|
||||
const searchInput = document.querySelector('#search');
|
||||
const categoryFilter = document.querySelector('#category-filter');
|
||||
const languageFilter = document.querySelector('#language-filter');
|
||||
const typeFilter = document.querySelector('#type-filter');
|
||||
const sortOrder = document.querySelector('#sort-order');
|
||||
const gridViewButton = document.querySelector('#grid-view');
|
||||
const listViewButton = document.querySelector('#list-view');
|
||||
const sourceModal = document.querySelector('#source-modal');
|
||||
const sourceTitle = document.querySelector('#source-title');
|
||||
const sourceKind = document.querySelector('#source-kind');
|
||||
const sourceStatus = document.querySelector('#source-status');
|
||||
const sourceCode = document.querySelector('#source-code');
|
||||
const programBadges = document.querySelector('#program-badges');
|
||||
const programDescription = document.querySelector('#program-description');
|
||||
const programControls = document.querySelector('#program-controls');
|
||||
let allPrograms = [];
|
||||
|
||||
const categoryMeta = {
|
||||
Arcade: { icon: '🎮', color: '#d65f5f' },
|
||||
Creative: { icon: '✎', color: '#65b96d' },
|
||||
Demo: { icon: '✦', color: '#75a9dc' },
|
||||
Dice: { icon: '⚄', color: '#d8d4c4' },
|
||||
Educational: { icon: '⌁', color: '#b98a64' },
|
||||
Logic: { icon: '◆', color: '#67b6bd' },
|
||||
Memory: { icon: '◫', color: '#9b6bd6' },
|
||||
Music: { icon: '♪', color: '#d668a4' },
|
||||
Puzzle: { icon: '⌘', color: '#d6c85c' },
|
||||
Reflex: { icon: '⚡', color: '#ef725f' },
|
||||
Simulation: { icon: '⚙', color: '#6d8ed6' },
|
||||
Strategy: { icon: '♟', color: '#d98f45' },
|
||||
Trivia: { icon: '?', color: '#dfaa45' },
|
||||
Utility: { icon: '⚒', color: '#a5acb8' },
|
||||
Word: { icon: 'Aa', color: '#55b7a4' }
|
||||
function titleFromPath(path) {
|
||||
const filename = path.split('/').at(-1).replace(/\.prg$/i, '');
|
||||
return filename.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
||||
}
|
||||
|
||||
const typeIcons = {
|
||||
Arcade: '🎮',
|
||||
Logic: '◆',
|
||||
Strategy: '♟',
|
||||
Simulation: '⚙',
|
||||
Memory: '◫',
|
||||
Puzzle: '⌘',
|
||||
Music: '♪',
|
||||
Creative: '✎',
|
||||
Demo: '✦',
|
||||
Dice: '⚄',
|
||||
Word: 'Aa',
|
||||
Reflex: '⚡',
|
||||
Educational: '⌁',
|
||||
Trivia: '?',
|
||||
Utility: '⚒'
|
||||
};
|
||||
|
||||
const languageLabels = {
|
||||
basic: 'BASIC',
|
||||
'machine-code': 'Machine Code'
|
||||
};
|
||||
|
||||
function slugify(value) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
function categoryFromRoute() {
|
||||
const match = location.pathname.match(/^\/category\/([a-z0-9]+(?:-[a-z0-9]+)*)\/?$/);
|
||||
return match?.[1] || '';
|
||||
}
|
||||
|
||||
function createCategoryPill(category) {
|
||||
const meta = categoryMeta[category] || { icon: '●', color: '#a5acb8' };
|
||||
const pill = document.createElement('a');
|
||||
pill.className = 'category-pill';
|
||||
pill.href = `/category/${slugify(category)}`;
|
||||
pill.style.setProperty('--category-color', meta.color);
|
||||
const icon = document.createElement('span');
|
||||
icon.setAttribute('aria-hidden', 'true');
|
||||
icon.textContent = meta.icon;
|
||||
const label = document.createElement('span');
|
||||
label.textContent = category;
|
||||
pill.append(icon, label);
|
||||
pill.setAttribute('aria-label', `Show all ${category} programs`);
|
||||
return pill;
|
||||
}
|
||||
|
||||
function createLanguageBadge(language) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = `language-badge language-${language}`;
|
||||
badge.textContent = languageLabels[language] || language;
|
||||
return badge;
|
||||
}
|
||||
|
||||
function createKeyGroup(keys) {
|
||||
const group = document.createElement('span');
|
||||
group.className = 'key-group';
|
||||
for (const keyName of keys) {
|
||||
const key = document.createElement('kbd');
|
||||
key.textContent = keyName;
|
||||
group.append(key);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
function searchText(program) {
|
||||
const controls = program.controls.flatMap(control => [...control.keys, control.description]);
|
||||
return `${program.title} ${program.description} ${program.category} ${languageLabels[program.language]} ${controls.join(' ')}`.toLowerCase();
|
||||
}
|
||||
|
||||
function renderPrograms() {
|
||||
const query = searchInput.value.trim().toLowerCase();
|
||||
const selectedCategory = categoryFilter.value;
|
||||
const selectedLanguage = languageFilter.value;
|
||||
const selectedType = typeFilter.value;
|
||||
const programs = allPrograms
|
||||
.filter(program => !selectedCategory || program.category === selectedCategory)
|
||||
.filter(program => !selectedLanguage || program.language === selectedLanguage)
|
||||
.filter(program => !query || searchText(program).includes(query))
|
||||
.filter(program => !selectedType || program.type === selectedType)
|
||||
.filter(program => !query || `${program.title} ${program.description} ${program.type} ${program.controls.join(' ')}`.toLowerCase().includes(query))
|
||||
.sort((left, right) => {
|
||||
if (sortOrder.value === 'category') return left.category.localeCompare(right.category) || left.title.localeCompare(right.title);
|
||||
if (sortOrder.value === 'type') return left.type.localeCompare(right.type) || left.title.localeCompare(right.title);
|
||||
return left.title.localeCompare(right.title);
|
||||
});
|
||||
|
||||
|
|
@ -112,9 +57,9 @@ function renderPrograms() {
|
|||
if (!event.target.closest('a, button')) location.href = programUrl;
|
||||
});
|
||||
|
||||
const badges = document.createElement('div');
|
||||
badges.className = 'card-badges';
|
||||
badges.append(createCategoryPill(program.category), createLanguageBadge(program.language));
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'type-pill';
|
||||
pill.textContent = `${typeIcons[program.type] || '●'} ${program.type}`;
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'program-title';
|
||||
|
|
@ -124,6 +69,15 @@ function renderPrograms() {
|
|||
description.className = 'program-description';
|
||||
description.textContent = program.description;
|
||||
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'controls';
|
||||
controls.setAttribute('aria-label', `Controls: ${program.controls.join(', ')}`);
|
||||
for (const control of program.controls) {
|
||||
const key = document.createElement('kbd');
|
||||
key.textContent = control;
|
||||
controls.append(key);
|
||||
}
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'program-actions';
|
||||
const run = document.createElement('a');
|
||||
|
|
@ -137,16 +91,14 @@ function renderPrograms() {
|
|||
source.disabled = !program.source;
|
||||
source.addEventListener('click', () => showSource(program));
|
||||
actions.append(run, source);
|
||||
card.append(badges, title, description, actions);
|
||||
card.append(pill, title, description, controls, actions);
|
||||
programsElement.append(card);
|
||||
}
|
||||
status.className = 'status';
|
||||
status.textContent = `${programs.length} of ${allPrograms.length} programs shown`;
|
||||
}
|
||||
|
||||
async function showSource(program) {
|
||||
sourceTitle.textContent = program.title;
|
||||
sourceKind.textContent = program.language === 'machine-code' ? 'Assembly source' : 'BASIC V2 source';
|
||||
sourceStatus.hidden = false;
|
||||
sourceStatus.className = 'status';
|
||||
sourceStatus.textContent = 'Loading source…';
|
||||
|
|
@ -165,15 +117,6 @@ async function showSource(program) {
|
|||
}
|
||||
}
|
||||
|
||||
function populateFilters(programs) {
|
||||
const categories = [...new Set(programs.map(program => program.category))].sort();
|
||||
categoryFilter.replaceChildren(new Option('All categories', ''), ...categories.map(category => new Option(category, category)));
|
||||
const routeSlug = categoryFromRoute();
|
||||
const routeCategory = categories.find(category => slugify(category) === routeSlug);
|
||||
if (routeCategory) categoryFilter.value = routeCategory;
|
||||
else categoryFilter.value = '';
|
||||
}
|
||||
|
||||
async function loadPrograms() {
|
||||
status.hidden = false;
|
||||
status.className = 'status';
|
||||
|
|
@ -184,7 +127,10 @@ async function loadPrograms() {
|
|||
if (!response.ok) throw new Error('The program list could not be loaded.');
|
||||
const { programs } = await response.json();
|
||||
allPrograms = programs;
|
||||
populateFilters(programs);
|
||||
const currentType = typeFilter.value;
|
||||
const types = [...new Set(programs.map(program => program.type))].sort();
|
||||
typeFilter.replaceChildren(new Option('All types', ''), ...types.map(type => new Option(type, type)));
|
||||
if (types.includes(currentType)) typeFilter.value = currentType;
|
||||
renderPrograms();
|
||||
} catch (error) {
|
||||
status.className = 'status error';
|
||||
|
|
@ -194,31 +140,18 @@ async function loadPrograms() {
|
|||
}
|
||||
}
|
||||
|
||||
function renderProgramDetails(program) {
|
||||
programBadges.replaceChildren(createCategoryPill(program.category), createLanguageBadge(program.language));
|
||||
programDescription.textContent = program.description;
|
||||
programControls.replaceChildren();
|
||||
for (const control of program.controls) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'control-row';
|
||||
const description = document.createElement('span');
|
||||
description.textContent = control.description;
|
||||
row.append(createKeyGroup(control.keys), description);
|
||||
programControls.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
function startEmulator(program) {
|
||||
const programPath = program.path;
|
||||
library.hidden = true;
|
||||
player.hidden = false;
|
||||
document.title = `${program.title} — C64 Program Collection`;
|
||||
document.querySelector('#program-title').textContent = program.title;
|
||||
renderProgramDetails(program);
|
||||
const title = program.title || titleFromPath(programPath);
|
||||
document.title = `${title} — C64 BASIC Collection`;
|
||||
document.querySelector('#program-title').textContent = title;
|
||||
|
||||
window.EJS_player = '#game';
|
||||
window.EJS_core = 'vice_x64sc';
|
||||
window.EJS_gameUrl = `/api/program?path=${encodeURIComponent(program.path)}`;
|
||||
window.EJS_gameName = program.path.split('/').at(-1);
|
||||
window.EJS_gameUrl = `/api/program?path=${encodeURIComponent(programPath)}`;
|
||||
window.EJS_gameName = programPath.split('/').at(-1);
|
||||
window.EJS_pathtodata = 'https://cdn.emulatorjs.org/stable/data/';
|
||||
window.EJS_startOnLoaded = true;
|
||||
window.EJS_language = 'en-US';
|
||||
|
|
@ -255,17 +188,9 @@ async function loadProgramPage(slug) {
|
|||
}
|
||||
}
|
||||
|
||||
function setCategory(category) {
|
||||
categoryFilter.value = category;
|
||||
const url = category ? `/category/${slugify(category)}` : '/';
|
||||
history.pushState({}, '', url);
|
||||
renderPrograms();
|
||||
}
|
||||
|
||||
refreshButton.addEventListener('click', loadPrograms);
|
||||
searchInput.addEventListener('input', renderPrograms);
|
||||
categoryFilter.addEventListener('change', () => setCategory(categoryFilter.value));
|
||||
languageFilter.addEventListener('change', renderPrograms);
|
||||
typeFilter.addEventListener('change', renderPrograms);
|
||||
sortOrder.addEventListener('change', renderPrograms);
|
||||
gridViewButton.addEventListener('click', () => setView('grid'));
|
||||
listViewButton.addEventListener('click', () => setView('list'));
|
||||
|
|
@ -273,12 +198,6 @@ document.querySelector('#close-source').addEventListener('click', () => sourceMo
|
|||
sourceModal.addEventListener('click', event => {
|
||||
if (event.target === sourceModal) sourceModal.close();
|
||||
});
|
||||
window.addEventListener('popstate', () => {
|
||||
if (!library.hidden) {
|
||||
populateFilters(allPrograms);
|
||||
renderPrograms();
|
||||
}
|
||||
});
|
||||
|
||||
function setView(view) {
|
||||
const list = view === 'list';
|
||||
|
|
@ -298,3 +217,4 @@ if (legacyProgramPath) {
|
|||
location.replace(`/program/${encodeURIComponent(slug)}`);
|
||||
} else if (programPage) loadProgramPage(decodeURIComponent(programPage[1]));
|
||||
else loadPrograms();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,42 +3,34 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Explore BASIC and machine-code programs for the Commodore 64 in a browser-based WASM VICE emulator.">
|
||||
<title>C64 Program Collection</title>
|
||||
<meta name="description" content="Run the C64 BASIC Collection in a browser-based WASM VICE emulator.">
|
||||
<title>C64 BASIC Collection</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<a class="brand" href="/">C64 Program Collection</a>
|
||||
<a class="brand" href="/">C64 BASIC Collection</a>
|
||||
<span>VICE x64sc · WebAssembly</span>
|
||||
</header>
|
||||
<main>
|
||||
<section id="library" class="panel">
|
||||
<p class="eyebrow">Program library</p>
|
||||
<h1>Small programs. Pure 8-bit fun.</h1>
|
||||
<p class="lede">Explore a growing collection of games, puzzles, creative tools, and audiovisual experiments written in Commodore 64 BASIC and 6502 machine code. Pick something that catches your eye, run it instantly, or open the source to see how it works.</p>
|
||||
<p class="lede">Explore a growing collection of games, puzzles, creative tools, and audiovisual experiments written in Commodore 64 BASIC. Pick something that catches your eye, run it instantly, or open the source to see how every line works.</p>
|
||||
<div class="library-toolbar" aria-label="Program library controls">
|
||||
<label class="search-field">
|
||||
<span class="sr-only">Search programs</span>
|
||||
<input id="search" type="search" placeholder="Search programs…" autocomplete="off">
|
||||
</label>
|
||||
<label>
|
||||
<span class="sr-only">Filter by category</span>
|
||||
<select id="category-filter"><option value="">All categories</option></select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="sr-only">Filter by language</span>
|
||||
<select id="language-filter">
|
||||
<option value="">All languages</option>
|
||||
<option value="basic">BASIC</option>
|
||||
<option value="machine-code">Machine Code</option>
|
||||
</select>
|
||||
<span class="sr-only">Filter by type</span>
|
||||
<select id="type-filter"><option value="">All types</option></select>
|
||||
</label>
|
||||
<label>
|
||||
<span class="sr-only">Sort programs</span>
|
||||
<select id="sort-order">
|
||||
<option value="title">Sort: Title</option>
|
||||
<option value="category">Sort: Category</option>
|
||||
<option value="type">Sort: Type</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="view-switcher" role="group" aria-label="View style">
|
||||
|
|
@ -55,24 +47,18 @@
|
|||
<div>
|
||||
<p class="eyebrow">Now running</p>
|
||||
<h1 id="program-title"></h1>
|
||||
<div id="program-badges" class="program-badges"></div>
|
||||
</div>
|
||||
<a class="secondary button" href="/">Back to library</a>
|
||||
</div>
|
||||
<p id="program-description" class="program-page-description"></p>
|
||||
<p class="hint">Click inside the emulator to enable keyboard input and sound.</p>
|
||||
<div id="game"></div>
|
||||
<p id="emulator-error" class="error" hidden></p>
|
||||
<section class="instructions-panel" aria-labelledby="instructions-title">
|
||||
<h2 id="instructions-title">Controls</h2>
|
||||
<div id="program-controls" class="control-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
<dialog id="source-modal" class="source-modal">
|
||||
<div class="modal-heading">
|
||||
<div>
|
||||
<p id="source-kind" class="eyebrow">Program source</p>
|
||||
<p class="eyebrow">BASIC V2 source</p>
|
||||
<h2 id="source-title">Program source</h2>
|
||||
</div>
|
||||
<button id="close-source" class="icon-button" type="button" aria-label="Close source">×</button>
|
||||
|
|
|
|||
|
|
@ -33,17 +33,11 @@ h1 { margin: 0; font-size: clamp(2rem, 7vw, 4rem); line-height: 1; }
|
|||
.program-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1rem; }
|
||||
.program-card { display: flex; min-height: 270px; flex-direction: column; align-items: flex-start; padding: 1.25rem; border: 1px solid #b8b1e733; border-radius: 14px; background: #211a59d9; color: #fff; cursor: pointer; transition: transform .15s, border-color .15s, background .15s; }
|
||||
.program-card:hover { transform: translateY(-3px); border-color: var(--c64-light-blue); }
|
||||
.program-card:has(.card-action:hover), .program-card:has(.category-pill:hover) { transform: none; background: #29216bd9; }
|
||||
.card-badges, .program-badges { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; }
|
||||
.category-pill, .language-badge { display: inline-flex; gap: .4rem; align-items: center; min-height: 28px; padding: .3rem .65rem; border-radius: 999px; font-size: .72rem; font-weight: 800; letter-spacing: .04em; line-height: 1; text-transform: uppercase; }
|
||||
.category-pill { border: 1px solid color-mix(in srgb, var(--category-color) 72%, white); background: color-mix(in srgb, var(--category-color) 24%, transparent); color: color-mix(in srgb, var(--category-color) 42%, white); text-decoration: none; transition: background .15s, color .15s, transform .15s; }
|
||||
.category-pill:hover { background: color-mix(in srgb, var(--category-color) 58%, #17113b); color: #fff; transform: translateY(-1px); }
|
||||
.category-pill:focus-visible { outline: 3px solid color-mix(in srgb, var(--category-color) 55%, white); outline-offset: 2px; }
|
||||
.language-badge { border: 1px solid #b8b1e755; background: #17113b99; color: #d8d3f4; }
|
||||
.language-machine-code { border-color: #d98f4588; background: #d98f4526; color: #f4c997; }
|
||||
.language-basic { border-color: #75a9dc77; background: #75a9dc22; color: #c8e1f7; }
|
||||
.program-card:has(.card-action:hover) { transform: none; background: #29216bd9; }
|
||||
.type-pill { display: inline-flex; gap: .4rem; align-items: center; padding: .3rem .65rem; border: 1px solid #b8b1e766; border-radius: 999px; background: #7869c433; color: #d8d3f4; font-size: .75rem; font-weight: 750; letter-spacing: .04em; text-transform: uppercase; }
|
||||
.program-title { margin: 1rem 0 0; font-size: 1.2rem; }
|
||||
.program-description { margin: .55rem 0 0; color: #d2cef0; font-size: .92rem; line-height: 1.45; }
|
||||
.controls { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; }
|
||||
kbd { min-width: 1.8rem; padding: .25rem .45rem; border: 1px solid var(--c64-light-blue); border-bottom-width: 3px; border-radius: 5px; background: #e9e7f7; color: #211a59; font: 700 .72rem ui-monospace, monospace; text-align: center; }
|
||||
.program-actions { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 1rem; margin-top: auto; padding-top: 1.25rem; }
|
||||
.card-action { display: inline-flex; align-items: center; justify-content: center; gap: .45rem; min-width: 94px; min-height: 38px; margin: 0; padding: .5rem .75rem; border: 1px solid #b8b1e766; border-radius: 8px; background: var(--c64-blue); color: #eeecfa; font: inherit; font-size: .84rem; font-weight: 700; line-height: 1; text-decoration: none; cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
|
||||
|
|
@ -53,8 +47,8 @@ kbd { min-width: 1.8rem; padding: .25rem .45rem; border: 1px solid var(--c64-lig
|
|||
.play-icon { font-size: .78rem; }
|
||||
.code-icon { font: 800 .8rem ui-monospace, monospace; }
|
||||
.program-grid.list-view { grid-template-columns: 1fr; }
|
||||
.program-grid.list-view .program-card { min-height: 0; display: grid; grid-template-columns: minmax(180px, auto) minmax(150px, .8fr) minmax(220px, 1.5fr) minmax(180px, 1fr); align-items: center; gap: 1rem; }
|
||||
.program-grid.list-view .program-title, .program-grid.list-view .program-description, .program-grid.list-view .program-actions { margin: 0; padding: 0; }
|
||||
.program-grid.list-view .program-card { min-height: 0; display: grid; grid-template-columns: auto minmax(150px, .8fr) minmax(220px, 1.5fr) minmax(180px, 1fr); align-items: center; gap: 1rem; }
|
||||
.program-grid.list-view .program-title, .program-grid.list-view .program-description, .program-grid.list-view .controls, .program-grid.list-view .program-actions { margin: 0; padding: 0; }
|
||||
.source-modal { width: min(900px, calc(100% - 2rem)); height: min(80vh, 760px); padding: 0; border: 1px solid #b8b1e744; border-radius: 14px; background: var(--c64-deep-blue); color: #fff; box-shadow: 0 30px 80px #000a; }
|
||||
.source-modal::backdrop { background: #090621d9; backdrop-filter: blur(5px); }
|
||||
.modal-heading { position: sticky; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; border-bottom: 1px solid #b8b1e733; background: #17113bf5; }
|
||||
|
|
@ -67,13 +61,6 @@ kbd { min-width: 1.8rem; padding: .25rem .45rem; border: 1px solid var(--c64-lig
|
|||
button:disabled { opacity: .5; cursor: wait; }
|
||||
.player-heading { display: flex; align-items: end; justify-content: space-between; gap: 1rem; }
|
||||
.player-heading h1 { font-size: clamp(1.8rem, 5vw, 3rem); }
|
||||
.program-badges { margin-top: .85rem; }
|
||||
.program-page-description { max-width: 760px; margin: 1.5rem 0 0; color: #d2cef0; font-size: 1.08rem; line-height: 1.6; }
|
||||
.instructions-panel { max-width: 760px; margin: 1.5rem auto 0; padding: 1.1rem 1.25rem; border: 1px solid #b8b1e738; border-radius: 12px; background: #211a59d9; }
|
||||
.instructions-panel h2 { margin: 0 0 .85rem; font-size: 1rem; letter-spacing: .08em; text-transform: uppercase; }
|
||||
.control-list { display: grid; gap: .7rem; }
|
||||
.control-row { display: grid; grid-template-columns: minmax(150px, auto) 1fr; gap: 1rem; align-items: center; color: #d2cef0; }
|
||||
.key-group { display: flex; flex-wrap: wrap; gap: .35rem; }
|
||||
#game { width: min(100%, 960px); height: min(75vw, 720px); min-height: 480px; margin: 1.5rem auto 0; background: #000; }
|
||||
.error { color: #ffb4ab; }
|
||||
@media (max-width: 600px) {
|
||||
|
|
@ -82,6 +69,5 @@ button:disabled { opacity: .5; cursor: wait; }
|
|||
.library-toolbar { top: .5rem; }
|
||||
.library-toolbar label, .library-toolbar select { flex: 1 1 100%; width: 100%; }
|
||||
.program-grid.list-view .program-card { display: flex; }
|
||||
.control-row { grid-template-columns: 1fr; gap: .45rem; }
|
||||
#game { min-height: 60vh; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
!cpu 6510
|
||||
|
||||
; Self-starting BASIC line: 10 SYS2061
|
||||
* = $0801
|
||||
!word basic_end
|
||||
!word 10
|
||||
!byte $9e
|
||||
!text "2061"
|
||||
!byte 0
|
||||
basic_end:
|
||||
!word 0
|
||||
|
||||
* = $080d
|
||||
start:
|
||||
lda $d020
|
||||
sta old_border
|
||||
lda $d021
|
||||
sta old_background
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
ldx #0
|
||||
print_title:
|
||||
lda title,x
|
||||
beq initialize
|
||||
jsr $ffd2
|
||||
inx
|
||||
bne print_title
|
||||
initialize:
|
||||
lda #4
|
||||
sta speed
|
||||
lda #0
|
||||
sta phase
|
||||
sta tick
|
||||
sta palette
|
||||
main_loop:
|
||||
ldx #0
|
||||
draw_bar:
|
||||
lda raster_lines,x
|
||||
wait_line:
|
||||
cmp $d012
|
||||
bne wait_line
|
||||
txa
|
||||
clc
|
||||
adc phase
|
||||
and #7
|
||||
tay
|
||||
lda palette
|
||||
beq use_color
|
||||
tya
|
||||
clc
|
||||
adc #8
|
||||
tay
|
||||
use_color:
|
||||
lda colors,y
|
||||
sta $d020
|
||||
sta $d021
|
||||
inx
|
||||
cpx #8
|
||||
bne draw_bar
|
||||
lda old_background
|
||||
sta $d021
|
||||
lda old_border
|
||||
sta $d020
|
||||
inc tick
|
||||
lda tick
|
||||
cmp speed
|
||||
bcc read_key
|
||||
lda #0
|
||||
sta tick
|
||||
inc phase
|
||||
lda phase
|
||||
and #7
|
||||
sta phase
|
||||
read_key:
|
||||
jsr $ffe4
|
||||
beq main_loop
|
||||
cmp #'Q'
|
||||
beq exit
|
||||
cmp #'C'
|
||||
beq change_palette
|
||||
cmp #'1'
|
||||
beq slow
|
||||
cmp #'2'
|
||||
beq medium
|
||||
cmp #'3'
|
||||
beq fast
|
||||
jmp main_loop
|
||||
slow:
|
||||
lda #8
|
||||
bne set_speed
|
||||
medium:
|
||||
lda #4
|
||||
bne set_speed
|
||||
fast:
|
||||
lda #1
|
||||
set_speed:
|
||||
sta speed
|
||||
jmp main_loop
|
||||
change_palette:
|
||||
lda palette
|
||||
eor #1
|
||||
sta palette
|
||||
jmp main_loop
|
||||
exit:
|
||||
lda old_border
|
||||
sta $d020
|
||||
lda old_background
|
||||
sta $d021
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
rts
|
||||
title:
|
||||
!text " RASTER BARS",13,13
|
||||
!text "1 2 3 CHANGE SPEED",13
|
||||
!text "C CHANGE PALETTE",13
|
||||
!text "Q RETURN TO BASIC",13,0
|
||||
raster_lines: !byte 72,84,96,108,120,132,144,156
|
||||
colors: !byte 2,8,10,7,1,7,10,8, 6,14,3,13,1,13,3,14
|
||||
speed: !byte 4
|
||||
phase: !byte 0
|
||||
tick: !byte 0
|
||||
palette: !byte 0
|
||||
old_border: !byte 0
|
||||
old_background: !byte 0
|
||||
Binary file not shown.
|
|
@ -1,12 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
for source in \
|
||||
sprite-playground/sprite-playground.asm \
|
||||
raster-bars/raster-bars.asm \
|
||||
sid-arpeggiator/sid-arpeggiator.asm
|
||||
do
|
||||
output="${source%.asm}.prg"
|
||||
echo "Assembling $source -> $output"
|
||||
acme --format cbm --outfile "$output" "$source"
|
||||
done
|
||||
22
server.mjs
22
server.mjs
|
|
@ -21,31 +21,18 @@ function sendJson(response, status, value, headOnly = false) {
|
|||
function fetchPrograms() {
|
||||
const programs = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
if (!Array.isArray(programs)) throw new Error('programs.json must contain an array');
|
||||
const slugs = new Set();
|
||||
return programs.map(program => {
|
||||
if (!program.title || !program.category || !program.description) throw new Error('Every program must include title, category, and description');
|
||||
if (!['basic', 'machine-code'].includes(program.language)) throw new Error(`Invalid language for ${program.title}`);
|
||||
if (!Array.isArray(program.controls) || !program.controls.length || program.controls.some(control => !Array.isArray(control.keys) || !control.keys.length || !control.description)) {
|
||||
throw new Error(`Invalid controls for ${program.title}`);
|
||||
}
|
||||
if (!program.path?.toLowerCase().endsWith('.prg')) throw new Error('Every program must reference a PRG file');
|
||||
const filePath = resolve(programDirectory, program.path);
|
||||
if (!filePath.startsWith(programDirectory + sep) || !existsSync(filePath) || !statSync(filePath).isFile()) {
|
||||
throw new Error(`Program file is missing: ${program.path}`);
|
||||
}
|
||||
const sourcePath = program.source;
|
||||
const expectedExtension = program.language === 'basic' ? '.bas' : '.asm';
|
||||
if (!sourcePath?.toLowerCase().endsWith(expectedExtension)) throw new Error(`Invalid source file for ${program.title}`);
|
||||
const sourcePath = program.source || program.path.replace(/\.prg$/i, '.bas');
|
||||
const sourceFilePath = resolve(programDirectory, sourcePath);
|
||||
if (!sourceFilePath.startsWith(programDirectory + sep) || !existsSync(sourceFilePath) || !statSync(sourceFilePath).isFile()) {
|
||||
throw new Error(`Source file is missing: ${sourcePath}`);
|
||||
}
|
||||
const slug = program.path.split('/')[0];
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) || slugs.has(slug)) throw new Error(`Invalid or duplicate program slug: ${slug}`);
|
||||
slugs.add(slug);
|
||||
return {
|
||||
...program,
|
||||
slug,
|
||||
slug: program.path.split('/')[0],
|
||||
source: sourceFilePath.startsWith(programDirectory + sep) && existsSync(sourceFilePath) ? sourcePath : null,
|
||||
size: statSync(filePath).size
|
||||
};
|
||||
});
|
||||
|
|
@ -87,8 +74,7 @@ function sendSource(response, programPath, headOnly = false) {
|
|||
|
||||
function serveStatic(response, pathname, headOnly = false) {
|
||||
const isProgramPage = /^\/program\/[a-z0-9]+(?:-[a-z0-9]+)*\/?$/.test(pathname);
|
||||
const isCategoryPage = /^\/category\/[a-z0-9]+(?:-[a-z0-9]+)*\/?$/.test(pathname);
|
||||
const requested = pathname === '/' || isProgramPage || isCategoryPage ? 'index.html' : pathname.slice(1);
|
||||
const requested = pathname === '/' || isProgramPage ? 'index.html' : pathname.slice(1);
|
||||
const filePath = normalize(join(publicDirectory, requested));
|
||||
if (!filePath.startsWith(publicDirectory) || !existsSync(filePath) || !statSync(filePath).isFile()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
!cpu 6510
|
||||
|
||||
; Self-starting BASIC line: 10 SYS2061
|
||||
* = $0801
|
||||
!word basic_end
|
||||
!word 10
|
||||
!byte $9e
|
||||
!text "2061"
|
||||
!byte 0
|
||||
basic_end:
|
||||
!word 0
|
||||
|
||||
* = $080d
|
||||
start:
|
||||
ldx #0
|
||||
save_sid:
|
||||
lda $d400,x
|
||||
sta old_sid,x
|
||||
inx
|
||||
cpx #25
|
||||
bne save_sid
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
ldx #0
|
||||
print_title:
|
||||
lda title,x
|
||||
beq initialize
|
||||
jsr $ffd2
|
||||
inx
|
||||
bne print_title
|
||||
initialize:
|
||||
ldx #0
|
||||
clear_sid:
|
||||
lda #0
|
||||
sta $d400,x
|
||||
inx
|
||||
cpx #25
|
||||
bne clear_sid
|
||||
lda #9
|
||||
sta $d405
|
||||
lda #$f0
|
||||
sta $d406
|
||||
lda #$0f
|
||||
sta $d418
|
||||
lda #0
|
||||
sta root
|
||||
sta step
|
||||
sta tick
|
||||
sta paused
|
||||
lda #6
|
||||
sta tempo
|
||||
lda #$21
|
||||
sta waveform
|
||||
jsr play_note
|
||||
main_loop:
|
||||
jsr wait_frame
|
||||
jsr read_key
|
||||
lda paused
|
||||
bne main_loop
|
||||
inc tick
|
||||
lda tick
|
||||
cmp tempo
|
||||
bcc main_loop
|
||||
lda #0
|
||||
sta tick
|
||||
inc step
|
||||
lda step
|
||||
and #3
|
||||
sta step
|
||||
jsr play_note
|
||||
jmp main_loop
|
||||
read_key:
|
||||
jsr $ffe4
|
||||
beq key_done
|
||||
cmp #'Q'
|
||||
bne not_quit
|
||||
jmp exit
|
||||
not_quit:
|
||||
cmp #' '
|
||||
beq toggle_pause
|
||||
cmp #'1'
|
||||
beq triangle
|
||||
cmp #'2'
|
||||
beq saw
|
||||
cmp #'3'
|
||||
beq pulse
|
||||
cmp #'+'
|
||||
beq faster
|
||||
cmp #'-'
|
||||
beq slower
|
||||
ldx #0
|
||||
find_root:
|
||||
cmp key_codes,x
|
||||
beq set_root
|
||||
inx
|
||||
cpx #8
|
||||
bne find_root
|
||||
key_done:
|
||||
rts
|
||||
set_root:
|
||||
stx root
|
||||
jsr play_note
|
||||
rts
|
||||
triangle:
|
||||
lda #$11
|
||||
bne set_waveform
|
||||
saw:
|
||||
lda #$21
|
||||
bne set_waveform
|
||||
pulse:
|
||||
lda #$41
|
||||
set_waveform:
|
||||
sta waveform
|
||||
sta $d404
|
||||
rts
|
||||
faster:
|
||||
lda tempo
|
||||
cmp #2
|
||||
beq key_done
|
||||
dec tempo
|
||||
rts
|
||||
slower:
|
||||
lda tempo
|
||||
cmp #12
|
||||
beq key_done
|
||||
inc tempo
|
||||
rts
|
||||
toggle_pause:
|
||||
lda paused
|
||||
eor #1
|
||||
sta paused
|
||||
beq resume
|
||||
lda waveform
|
||||
and #$fe
|
||||
sta $d404
|
||||
rts
|
||||
resume:
|
||||
jsr play_note
|
||||
rts
|
||||
play_note:
|
||||
ldx step
|
||||
lda arp_offsets,x
|
||||
clc
|
||||
adc root
|
||||
tay
|
||||
lda frequencies_low,y
|
||||
sta $d400
|
||||
lda frequencies_high,y
|
||||
sta $d401
|
||||
lda waveform
|
||||
sta $d404
|
||||
rts
|
||||
wait_frame:
|
||||
wait_bottom:
|
||||
lda $d012
|
||||
cmp #250
|
||||
bne wait_bottom
|
||||
wait_top:
|
||||
lda $d012
|
||||
cmp #10
|
||||
bne wait_top
|
||||
rts
|
||||
exit:
|
||||
ldx #0
|
||||
restore_sid:
|
||||
lda old_sid,x
|
||||
sta $d400,x
|
||||
inx
|
||||
cpx #25
|
||||
bne restore_sid
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
; Discard the JSR read_key return address, then return from SYS.
|
||||
pla
|
||||
pla
|
||||
rts
|
||||
title:
|
||||
!text " SID ARPEGGIATOR",13,13
|
||||
!text "A-K SELECT ROOT NOTE",13
|
||||
!text "1 2 3 TRIANGLE SAW PULSE",13
|
||||
!text "+ - CHANGE TEMPO",13
|
||||
!text "SPACE PAUSE OR RESUME",13
|
||||
!text "Q RETURN TO BASIC",13,0
|
||||
key_codes: !text "ASDFGHJK"
|
||||
arp_offsets: !byte 0,4,7,12
|
||||
frequencies_low:
|
||||
!byte <4389,<4650,<4927,<5220,<5530,<5859,<6207,<6577,<6968,<7382
|
||||
!byte <7821,<8286,<8779,<9301,<9854,<10440,<11060,<11718,<12414,<13154
|
||||
frequencies_high:
|
||||
!byte >4389,>4650,>4927,>5220,>5530,>5859,>6207,>6577,>6968,>7382
|
||||
!byte >7821,>8286,>8779,>9301,>9854,>10440,>11060,>11718,>12414,>13154
|
||||
root: !byte 0
|
||||
step: !byte 0
|
||||
tick: !byte 0
|
||||
tempo: !byte 6
|
||||
waveform: !byte $21
|
||||
paused: !byte 0
|
||||
old_sid: !fill 25,0
|
||||
Binary file not shown.
|
|
@ -1,177 +0,0 @@
|
|||
!cpu 6510
|
||||
|
||||
; Self-starting BASIC line: 10 SYS2061
|
||||
* = $0801
|
||||
!word basic_end
|
||||
!word 10
|
||||
!byte $9e
|
||||
!text "2061"
|
||||
!byte 0
|
||||
basic_end:
|
||||
!word 0
|
||||
|
||||
* = $080d
|
||||
start:
|
||||
lda $d020
|
||||
sta old_border
|
||||
lda $d021
|
||||
sta old_background
|
||||
lda $d015
|
||||
sta old_sprite_enable
|
||||
lda $d000
|
||||
sta old_sprite_x
|
||||
lda $d001
|
||||
sta old_sprite_y
|
||||
lda $d010
|
||||
sta old_sprite_x_msb
|
||||
lda $d027
|
||||
sta old_sprite_color
|
||||
lda $07f8
|
||||
sta old_sprite_pointer
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
ldx #0
|
||||
print_title:
|
||||
lda title,x
|
||||
beq setup_sprite
|
||||
jsr $ffd2
|
||||
inx
|
||||
bne print_title
|
||||
setup_sprite:
|
||||
lda #$30
|
||||
sta $07f8
|
||||
lda #120
|
||||
sta sprite_x
|
||||
sta $d000
|
||||
sta sprite_y
|
||||
sta $d001
|
||||
lda #0
|
||||
sta $d010
|
||||
lda #1
|
||||
sta sprite_color
|
||||
sta $d027
|
||||
lda $d015
|
||||
ora #1
|
||||
sta $d015
|
||||
main_loop:
|
||||
jsr wait_frame
|
||||
jsr $ffe4
|
||||
beq main_loop
|
||||
cmp #'Q'
|
||||
bne not_quit
|
||||
jmp exit
|
||||
not_quit:
|
||||
cmp #'C'
|
||||
beq change_color
|
||||
cmp #'W'
|
||||
beq move_up
|
||||
cmp #'S'
|
||||
beq move_down
|
||||
cmp #'A'
|
||||
beq move_left
|
||||
cmp #'D'
|
||||
beq move_right
|
||||
jmp main_loop
|
||||
move_up:
|
||||
lda sprite_y
|
||||
cmp #58
|
||||
bcc main_loop
|
||||
sec
|
||||
sbc #2
|
||||
sta sprite_y
|
||||
sta $d001
|
||||
jmp main_loop
|
||||
move_down:
|
||||
lda sprite_y
|
||||
cmp #228
|
||||
bcs main_loop
|
||||
clc
|
||||
adc #2
|
||||
sta sprite_y
|
||||
sta $d001
|
||||
jmp main_loop
|
||||
move_left:
|
||||
lda sprite_x
|
||||
cmp #26
|
||||
bcc main_loop
|
||||
sec
|
||||
sbc #2
|
||||
sta sprite_x
|
||||
sta $d000
|
||||
jmp main_loop
|
||||
move_right:
|
||||
lda sprite_x
|
||||
cmp #238
|
||||
bcs main_loop
|
||||
clc
|
||||
adc #2
|
||||
sta sprite_x
|
||||
sta $d000
|
||||
jmp main_loop
|
||||
change_color:
|
||||
inc sprite_color
|
||||
lda sprite_color
|
||||
and #15
|
||||
bne set_color
|
||||
lda #1
|
||||
sta sprite_color
|
||||
set_color:
|
||||
sta $d027
|
||||
jmp main_loop
|
||||
wait_frame:
|
||||
wait_bottom:
|
||||
lda $d012
|
||||
cmp #250
|
||||
bne wait_bottom
|
||||
wait_top:
|
||||
lda $d012
|
||||
cmp #10
|
||||
bne wait_top
|
||||
rts
|
||||
exit:
|
||||
lda old_sprite_enable
|
||||
sta $d015
|
||||
lda old_sprite_x
|
||||
sta $d000
|
||||
lda old_sprite_y
|
||||
sta $d001
|
||||
lda old_sprite_x_msb
|
||||
sta $d010
|
||||
lda old_sprite_color
|
||||
sta $d027
|
||||
lda old_sprite_pointer
|
||||
sta $07f8
|
||||
lda old_border
|
||||
sta $d020
|
||||
lda old_background
|
||||
sta $d021
|
||||
lda #147
|
||||
jsr $ffd2
|
||||
rts
|
||||
title:
|
||||
!text " SPRITE PLAYGROUND",13,13
|
||||
!text "W A S D MOVE THE HARDWARE SPRITE",13
|
||||
!text "C CHANGE COLOR",13
|
||||
!text "Q RETURN TO BASIC",13,0
|
||||
sprite_x: !byte 120
|
||||
sprite_y: !byte 120
|
||||
sprite_color: !byte 1
|
||||
old_border: !byte 0
|
||||
old_background: !byte 0
|
||||
old_sprite_enable: !byte 0
|
||||
old_sprite_x: !byte 0
|
||||
old_sprite_y: !byte 0
|
||||
old_sprite_x_msb: !byte 0
|
||||
old_sprite_color: !byte 0
|
||||
old_sprite_pointer: !byte 0
|
||||
|
||||
* = $0c00
|
||||
sprite_data:
|
||||
!byte $00,$18,$00, $00,$3c,$00, $00,$7e,$00
|
||||
!byte $00,$ff,$00, $01,$ff,$80, $03,$ff,$c0
|
||||
!byte $07,$ff,$e0, $0f,$ff,$f0, $1f,$ff,$f8
|
||||
!byte $3f,$ff,$fc, $7f,$ff,$fe, $ff,$ff,$ff
|
||||
!byte $7f,$ff,$fe, $3f,$ff,$fc, $1f,$ff,$f8
|
||||
!byte $0f,$ff,$f0, $07,$ff,$e0, $03,$ff,$c0
|
||||
!byte $01,$ff,$80, $00,$ff,$00, $00,$7e,$00
|
||||
!byte 0
|
||||
Binary file not shown.
|
|
@ -1,8 +0,0 @@
|
|||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends acme \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /programs
|
||||
CMD ["sh", "scripts/build-machine-code.sh"]
|
||||
Loading…
Add table
Reference in a new issue