Compare commits

..

No commits in common. "ac2fc5ff659ccfa279e69910d4f7864dfa5cea2c" and "06c16f80f0a2e7b482773ac4ed7771d528f03753" have entirely different histories.

15 changed files with 200 additions and 857 deletions

112
README.md
View file

@ -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.

View file

@ -9,12 +9,3 @@ services:
- .:/programs:ro
restart: unless-stopped
assembler:
profiles: ["tools"]
build:
context: .
dockerfile: tools/acme/Dockerfile
working_dir: /programs
volumes:
- .:/programs

View file

@ -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"
}
]

View file

@ -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();

View file

@ -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>

View file

@ -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; }
}

View file

@ -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.

View file

@ -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

View file

@ -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;

View file

@ -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.

View file

@ -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

View file

@ -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"]