Expand collection to thirty programs

This commit is contained in:
Christian Johannesen 2026-10-16 02:27:04 +02:00
parent ac2fc5ff65
commit 7a583ee313
21 changed files with 1271 additions and 117 deletions

View file

@ -1,38 +1,43 @@
# C64 Program 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.
Thirty 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, music, utilities, and visual effects. Every entry is self-contained and runs on an ordinary PAL or NTSC C64 without a cartridge or BASIC extension.
Browse and run the collection at [c64.p20.dev](https://c64.p20.dev).
## 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 | Released | Category | Language | Controls | Description | Source | Run |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Meteor Dodge | 2026-10-07 | Arcade | BASIC | `A`, `D`, `Q` | Dodge falling meteors and build your score. | [BAS](meteor-dodge/meteor-dodge.bas) | [Run](https://c64.p20.dev/program/meteor-dodge) |
| Code Breaker | 2026-10-07 | Logic | BASIC | `0`-`9`, Return, `Q` | Deduce a four-digit secret in ten guesses. | [BAS](code-breaker/code-breaker.bas) | [Run](https://c64.p20.dev/program/code-breaker) |
| Submarine Hunt | 2026-10-07 | Strategy | BASIC | Row, Column, Return | Find three hidden submarines with limited sonar shots. | [BAS](submarine-hunt/submarine-hunt.bas) | [Run](https://c64.p20.dev/program/submarine-hunt) |
| Lunar Lander | 2026-10-07 | Simulation | BASIC | `0`-`9`, Return, `-1` | Choose engine burns and touch down gently. | [BAS](lunar-lander/lunar-lander.bas) | [Run](https://c64.p20.dev/program/lunar-lander) |
| Memory Matrix | 2026-10-07 | Memory | BASIC | `1`-`4`, `Q` | Repeat a growing sequence of flashing quadrants. | [BAS](memory-matrix/memory-matrix.bas) | [Run](https://c64.p20.dev/program/memory-matrix) |
| Robot Maze | 2026-10-07 | Puzzle | BASIC | `W`, `A`, `S`, `D`, `Q` | Navigate a freshly generated maze to its exit. | [BAS](robot-maze/robot-maze.bas) | [Run](https://c64.p20.dev/program/robot-maze) |
| Music Box | 2026-10-07 | Music | BASIC | `A`-`K`, `1`-`3`, `Q` | Play the SID chip as a small computer keyboard. | [BAS](music-box/music-box.bas) | [Run](https://c64.p20.dev/program/music-box) |
| Pixel Painter | 2026-10-07 | Creative | BASIC | Cursor keys, Space, `C`, `Q` | Draw character-cell pictures on the screen. | [BAS](pixel-painter/pixel-painter.bas) | [Run](https://c64.p20.dev/program/pixel-painter) |
| Stock Trader | 2026-10-07 | Simulation | BASIC | `B`, `S`, `W`, `Q` | Trade a volatile stock over twenty market days. | [BAS](stock-trader/stock-trader.bas) | [Run](https://c64.p20.dev/program/stock-trader) |
| Starfield | 2026-10-07 | Demo | BASIC | `1`-`3`, `Q` | Watch and control a layered animated starfield. | [BAS](starfield/starfield.bas) | [Run](https://c64.p20.dev/program/starfield) |
| Snake Trail | 2026-10-07 | 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](https://c64.p20.dev/program/snake-trail) |
| Dice Poker | 2026-10-07 | Dice | BASIC | `1`-`5`, Return, `Q` | Keep your best dice and chase a winning poker combination. | [BAS](dice-poker/dice-poker.bas) | [Run](https://c64.p20.dev/program/dice-poker) |
| Word Scramble | 2026-10-07 | Word | BASIC | `A`-`Z`, Return, `Q` | Unscramble five computer-themed words from helpful clues. | [BAS](word-scramble/word-scramble.bas) | [Run](https://c64.p20.dev/program/word-scramble) |
| Nim Challenge | 2026-10-07 | Strategy | BASIC | `1`-`3`, `0` | Outthink the computer in the classic game of twenty-one sticks. | [BAS](nim-challenge/nim-challenge.bas) | [Run](https://c64.p20.dev/program/nim-challenge) |
| Reaction Timer | 2026-10-07 | Reflex | BASIC | Any key, `Q` | Test your reactions across five unpredictable timed rounds. | [BAS](reaction-timer/reaction-timer.bas) | [Run](https://c64.p20.dev/program/reaction-timer) |
| Morse Trainer | 2026-10-07 | Educational | BASIC | `A`-`Z`, Return, `Q` | Learn Morse letters by reading and hearing their signals. | [BAS](morse-trainer/morse-trainer.bas) | [Run](https://c64.p20.dev/program/morse-trainer) |
| Kaleidoscope | 2026-10-07 | Demo | BASIC | Space, `C`, `Q` | Grow colorful symmetrical patterns across the character screen. | [BAS](kaleidoscope/kaleidoscope.bas) | [Run](https://c64.p20.dev/program/kaleidoscope) |
| World Quiz | 2026-10-07 | Trivia | BASIC | `1`-`3`, `Q` | Take a seven-stop tour of world geography and landmarks. | [BAS](world-quiz/world-quiz.bas) | [Run](https://c64.p20.dev/program/world-quiz) |
| Life Lab | 2026-10-07 | Simulation | BASIC | Space, `R`, `Q` | Observe generations of Conway's cellular automaton evolve. | [BAS](life-lab/life-lab.bas) | [Run](https://c64.p20.dev/program/life-lab) |
| Cipher Wheel | 2026-10-07 | Utility | BASIC | `E`, `D`, `N`, `Q` | Encode and decode secret messages with a Caesar shift. | [BAS](cipher-wheel/cipher-wheel.bas) | [Run](https://c64.p20.dev/program/cipher-wheel) |
| Sprite Playground | 2026-10-16 | 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](https://c64.p20.dev/program/sprite-playground) |
| Raster Bars | 2026-10-16 | Demo | Machine Code | `1`-`3`, `C`, `Q` | Paint animated bands of color with cycle-aware VIC-II raster timing. | [ASM](raster-bars/raster-bars.asm) | [Run](https://c64.p20.dev/program/raster-bars) |
| SID Arpeggiator | 2026-10-16 | 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](https://c64.p20.dev/program/sid-arpeggiator) |
| Paddle Ball | 2026-10-16 | Arcade | Machine Code | `A`, `D`, Space, `Q` | Keep a fast-moving ball in play with a screen-edge paddle. | [ASM](paddle-ball/paddle-ball.asm) | [Run](https://c64.p20.dev/program/paddle-ball) |
| Smooth Scroller | 2026-10-16 | Demo | Machine Code | `+`, `-`, `C`, `Q` | Send a message gliding across the screen with VIC-II fine scrolling. | [ASM](smooth-scroller/smooth-scroller.asm) | [Run](https://c64.p20.dev/program/smooth-scroller) |
| Fireworks | 2026-10-16 | Demo | Machine Code | Space, `C`, `Q` | Launch colorful PETSCII fireworks into a night sky. | [ASM](fireworks/fireworks.asm) | [Run](https://c64.p20.dev/program/fireworks) |
| Jiffy Clock | 2026-10-16 | Utility | Machine Code | `R`, Space, `Q` | Turn the C64 jiffy timer into a large live digital clock. | [ASM](jiffy-clock/jiffy-clock.asm) | [Run](https://c64.p20.dev/program/jiffy-clock) |
| Typing Sprint | 2026-10-16 | Word | Machine Code | `A`-`Z`, Space, `Q` | Race the clock while copying a Commodore-themed phrase. | [ASM](typing-sprint/typing-sprint.asm) | [Run](https://c64.p20.dev/program/typing-sprint) |
| PETSCII Aquarium | 2026-10-16 | Simulation | Machine Code | `F`, `1`-`3`, `Q` | Watch a tiny animated aquarium and feed its roaming fish. | [ASM](petscii-aquarium/petscii-aquarium.asm) | [Run](https://c64.p20.dev/program/petscii-aquarium) |
| Fifteen Puzzle | 2026-10-16 | Puzzle | Machine Code | `W`, `A`, `S`, `D`, `R`, `Q` | Slide numbered tiles until the classic four-by-four board is restored. | [ASM](fifteen-puzzle/fifteen-puzzle.asm) | [Run](https://c64.p20.dev/program/fifteen-puzzle) |
## Running the programs
@ -42,31 +47,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 visit the hosted collection at [c64.p20.dev](https://c64.p20.dev). 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.
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`.
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 overview renders programs in small batches and is ready to scale without creating every card at once. The default host port is configured as `8134` in `.env`.
On a Commodore 64, transfer a PRG to disk or another supported medium, then use:
```text
LOAD"METEOR-DODGE.PRG",8
LOAD"PADDLE-BALL.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.
Substitute the filename and device number as needed. 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:
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
@ -78,11 +74,7 @@ The tools profile is opt-in and is not started by the normal app command.
Each program directory contains a loadable `.prg` and its readable source:
- 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`.
- 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 ordinary PAL and NTSC C64
hardware as well as VICE-compatible emulators.
All programs are self-contained and designed for ordinary PAL and NTSC C64 hardware as well as VICE-compatible emulators.

View file

@ -0,0 +1,155 @@
!cpu 6510
* = $0801
!word basic_end
!word 10
!byte $9e
!text "2061"
!byte 0
basic_end: !word 0
* = $080d
start:
lda #147
jsr $ffd2
ldx #0
title_loop:
lda title,x
beq shuffle
jsr $ffd2
inx
bne title_loop
shuffle:
ldx #0
copy_board:
lda shuffled,x
sta board,x
inx
cpx #16
bne copy_board
lda #13
sta blank
main:
jsr draw_board
wait_key:
jsr $ffe4
beq wait_key
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #82
beq shuffle
cmp #87
bne key_down
lda blank
cmp #12
bcs wait_key
clc
adc #4
jmp move_tile
key_down:
cmp #83
bne key_left
lda blank
cmp #4
bcc wait_key
sec
sbc #4
jmp move_tile
key_left:
cmp #65
bne key_right
lda blank
and #3
cmp #3
beq wait_key
lda blank
clc
adc #1
jmp move_tile
key_right:
cmp #68
bne wait_key
lda blank
and #3
beq wait_key
lda blank
sec
sbc #1
move_tile:
tax
ldy blank
lda board,x
sta board,y
lda #0
sta board,x
stx blank
jmp main
draw_board:
ldx #0
draw_loop:
stx tile_index
lda positions_lo,x
sta $fb
lda positions_hi,x
sta $fc
lda board,x
beq draw_blank
jsr split_digits
ldy #0
lda tens
beq leading_space
ora #$30
bne store_first
leading_space:
lda #32
store_first:
sta ($fb),y
iny
lda ones
ora #$30
sta ($fb),y
jmp next_tile
draw_blank:
ldy #0
lda #32
sta ($fb),y
iny
sta ($fb),y
next_tile:
ldx tile_index
inx
cpx #16
bne draw_loop
rts
split_digits:
ldy #0
split_loop:
cmp #10
bcc split_done
sec
sbc #10
iny
bne split_loop
split_done:
sty tens
sta ones
rts
exit:
lda #147
jsr $ffd2
rts
title: !byte 147
!text "FIFTEEN PUZZLE WASD SLIDE R RESET Q QUIT"
!byte 13,13,13,0
shuffled: !byte 1,2,3,4,5,6,7,8,9,10,11,12,13,0,14,15
board: !fill 16,0
positions_lo:
!for row,0,3 { !for col,0,3 { !byte <($04f5+row*80+col*5) } }
positions_hi:
!for row,0,3 { !for col,0,3 { !byte >($04f5+row*80+col*5) } }
blank: !byte 0
tile_index: !byte 0
tens: !byte 0
ones: !byte 0

Binary file not shown.

169
fireworks/fireworks.asm Normal file
View file

@ -0,0 +1,169 @@
!cpu 6510
* = $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 #0
sta $d020
sta $d021
lda #147
jsr $ffd2
ldx #0
print_title:
lda title,x
beq setup
jsr $ffd2
inx
bne print_title
setup:
lda #7
sta center_x
lda #5
sta burst_color
main:
jsr $ffe4
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #67
bne launch_key
inc burst_color
lda burst_color
and #15
sta burst_color
launch_key:
cmp #32
bne main
jsr launch
jmp main
launch:
lda #22
sta rocket_y
rise:
ldx center_x
ldy rocket_y
lda #46
jsr plot
jsr delay
ldx center_x
ldy rocket_y
lda #32
jsr plot
dec rocket_y
lda rocket_y
cmp #7
bne rise
ldx #0
burst_loop:
stx particle_index
lda burst_dx,x
clc
adc center_x
sta px
lda burst_dy,x
clc
adc rocket_y
tay
ldx px
lda #42
jsr plot
ldx particle_index
inx
cpx #12
bne burst_loop
jsr long_delay
ldx #0
clear_burst:
stx particle_index
lda burst_dx,x
clc
adc center_x
sta px
lda burst_dy,x
clc
adc rocket_y
tay
ldx px
lda #32
jsr plot
ldx particle_index
inx
cpx #12
bne clear_burst
lda center_x
clc
adc #11
cmp #35
bcc save_center
sbc #31
save_center:
sta center_x
rts
plot:
sta char_value
lda row_lo,y
sta $fb
lda row_hi,y
sta $fc
txa
tay
lda char_value
sta ($fb),y
lda $fb
clc
adc #<$d400
sta $fd
lda $fc
adc #>$d400
sta $fe
lda burst_color
sta ($fd),y
rts
delay:
ldx #8
delay_outer:
ldy #0
delay_inner:
dey
bne delay_inner
dex
bne delay_outer
rts
long_delay:
ldx #45
bne delay_outer
exit:
lda old_border
sta $d020
lda old_background
sta $d021
lda #147
jsr $ffd2
rts
title: !byte 147
!text "FIREWORKS SPACE LAUNCH C COLOR Q QUIT"
!byte 13,0
burst_dx: !byte 0,1,2,$ff,$fe,0,0,1,2,$ff,$fe,0
burst_dy: !byte $fe,$ff,0,$ff,0,2,1,1,2,1,2,$ff
row_lo: !for i,0,24 { !byte <($0400+i*40) }
row_hi: !for i,0,24 { !byte >($0400+i*40) }
center_x: !byte 0
rocket_y: !byte 0
burst_color: !byte 0
particle_index: !byte 0
px: !byte 0
char_value: !byte 0
old_border: !byte 0
old_background: !byte 0

BIN
fireworks/fireworks.prg Normal file

Binary file not shown.

124
jiffy-clock/jiffy-clock.asm Normal file
View file

@ -0,0 +1,124 @@
!cpu 6510
* = $0801
!word basic_end
!word 10
!byte $9e
!text "2061"
!byte 0
basic_end: !word 0
* = $080d
start:
lda #147
jsr $ffd2
ldx #0
print_title:
lda title,x
beq reset
jsr $ffd2
inx
bne print_title
reset:
lda #0
sta frames
sta seconds
sta minutes
sta paused
lda $a2
sta last_jiffy
main:
jsr wait_frame
jsr $ffe4
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #82
beq reset
cmp #32
bne update
lda paused
eor #1
sta paused
update:
lda paused
bne display
lda $a2
cmp last_jiffy
beq display
sta last_jiffy
inc frames
lda frames
cmp #60
bne display
lda #0
sta frames
inc seconds
lda seconds
cmp #60
bne display
lda #0
sta seconds
inc minutes
lda minutes
cmp #100
bne display
lda #0
sta minutes
display:
lda minutes
jsr split_digits
lda tens
ora #$30
sta $05f0
lda ones
ora #$30
sta $05f1
lda #58
sta $05f2
lda seconds
jsr split_digits
lda tens
ora #$30
sta $05f3
lda ones
ora #$30
sta $05f4
jmp main
split_digits:
ldx #0
subtract_ten:
cmp #10
bcc split_done
sec
sbc #10
inx
bne subtract_ten
split_done:
stx tens
sta ones
rts
wait_frame:
lda #250
wait_raster:
cmp $d012
bne wait_raster
wait_leave:
cmp $d012
beq wait_leave
rts
exit:
lda #147
jsr $ffd2
rts
title: !byte 147
!text "JIFFY CLOCK R RESET SPACE PAUSE Q QUIT"
!byte 13,13,13,13,13,13,13,13,13,13,0
frames: !byte 0
seconds: !byte 0
minutes: !byte 0
paused: !byte 0
last_jiffy: !byte 0
tens: !byte 0
ones: !byte 0

BIN
jiffy-clock/jiffy-clock.prg Normal file

Binary file not shown.

189
paddle-ball/paddle-ball.asm Normal file
View file

@ -0,0 +1,189 @@
!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
title_loop:
lda title,x
beq new_ball
jsr $ffd2
inx
bne title_loop
new_ball:
lda #20
sta paddle_x
lda #20
sta ball_x
lda #10
sta ball_y
lda #1
sta dx
sta dy
main_loop:
jsr erase_objects
jsr read_key
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #65
bne check_right
lda paddle_x
beq check_right
dec paddle_x
check_right:
cmp #68
bne update_ball
lda paddle_x
cmp #34
beq update_ball
inc paddle_x
update_ball:
lda ball_x
clc
adc dx
sta ball_x
beq bounce_x
cmp #39
bne move_y
bounce_x:
lda dx
eor #$fe
clc
adc #1
sta dx
move_y:
lda ball_y
clc
adc dy
sta ball_y
beq bounce_y
cmp #22
bne draw
lda ball_x
cmp paddle_x
bcc missed
sec
sbc paddle_x
cmp #6
bcc bounce_y
missed:
jsr draw_objects
wait_serve:
jsr $ffe4
cmp #81
bne serve_key
jmp exit
serve_key:
cmp #32
bne wait_serve
jmp new_ball
bounce_y:
lda dy
eor #$fe
clc
adc #1
sta dy
draw:
jsr draw_objects
ldx #6
delay_outer:
ldy #0
delay_inner:
dey
bne delay_inner
dex
bne delay_outer
jmp main_loop
erase_objects:
lda #32
jsr draw_ball
ldx #5
erase_paddle:
txa
pha
clc
adc paddle_x
tax
ldy #23
lda #32
jsr plot
pla
tax
dex
bpl erase_paddle
rts
draw_objects:
lda #81
jsr draw_ball
ldx #5
draw_paddle:
txa
pha
clc
adc paddle_x
tax
ldy #23
lda #160
jsr plot
pla
tax
dex
bpl draw_paddle
rts
draw_ball:
pha
ldx ball_x
ldy ball_y
pla
plot:
sta char_value
lda row_lo,y
sta $fb
lda row_hi,y
sta $fc
txa
tay
lda char_value
sta ($fb),y
rts
read_key:
jsr $ffe4
rts
exit:
lda old_border
sta $d020
lda old_background
sta $d021
lda #147
jsr $ffd2
rts
title: !byte 147
!text "PADDLE BALL A/D MOVE SPACE SERVE Q QUIT"
!byte 13,0
row_lo: !for i,0,24 { !byte <($0400+i*40) }
row_hi: !for i,0,24 { !byte >($0400+i*40) }
paddle_x: !byte 0
ball_x: !byte 0
ball_y: !byte 0
dx: !byte 0
dy: !byte 0
char_value: !byte 0
old_border: !byte 0
old_background: !byte 0

BIN
paddle-ball/paddle-ball.prg Normal file

Binary file not shown.

View file

@ -0,0 +1,161 @@
!cpu 6510
* = $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 #6
sta $d020
lda #14
sta $d021
lda #147
jsr $ffd2
ldx #0
title_loop:
lda title,x
beq setup
jsr $ffd2
inx
bne title_loop
setup:
lda #4
sta speed
lda #0
sta tick
sta food_y
main:
jsr wait_frame
jsr $ffe4
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #70
bne speed_keys
lda #3
sta food_x
lda #2
sta food_y
speed_keys:
cmp #49
bne speed_two
lda #7
sta speed
speed_two:
cmp #50
bne speed_three
lda #4
sta speed
speed_three:
cmp #51
bne animate
lda #2
sta speed
animate:
inc tick
lda tick
cmp speed
bcc main
lda #0
sta tick
ldx #0
fish_loop:
stx fish_index
lda fish_x,x
sta old_x
ldy fish_y,x
ldx old_x
lda #32
jsr plot
ldx fish_index
inc fish_x,x
lda fish_x,x
cmp #39
bcc draw_fish
lda #0
sta fish_x,x
draw_fish:
sta old_x
ldy fish_y,x
ldx old_x
lda #62
jsr plot
ldx fish_index
inx
cpx #5
bne fish_loop
lda food_y
bne move_food
jmp main
move_food:
ldx food_x
ldy food_y
lda #32
jsr plot
inc food_y
lda food_y
cmp #22
bcc draw_food
lda #0
sta food_y
jmp main
draw_food:
tay
ldx food_x
lda #46
jsr plot
jmp main
plot:
sta char_value
lda row_lo,y
sta $fb
lda row_hi,y
sta $fc
txa
tay
lda char_value
sta ($fb),y
rts
wait_frame:
lda #250
wait_raster:
cmp $d012
bne wait_raster
wait_leave:
cmp $d012
beq wait_leave
rts
exit:
lda old_border
sta $d020
lda old_background
sta $d021
lda #147
jsr $ffd2
rts
title: !byte 147
!text "PETSCII AQUARIUM F FEED 1-3 SPEED Q QUIT"
!byte 13,0
fish_x: !byte 2,9,16,23,30
fish_y: !byte 5,9,13,17,20
row_lo: !for i,0,24 { !byte <($0400+i*40) }
row_hi: !for i,0,24 { !byte >($0400+i*40) }
speed: !byte 0
tick: !byte 0
food_x: !byte 0
food_y: !byte 0
fish_index: !byte 0
old_x: !byte 0
char_value: !byte 0
old_border: !byte 0
old_background: !byte 0

Binary file not shown.

View file

@ -1,140 +1,182 @@
[
{
"title": "Meteor Dodge", "category": "Arcade", "language": "basic",
"title": "Meteor Dodge", "category": "Arcade", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Code Breaker", "category": "Logic", "language": "basic",
"title": "Code Breaker", "category": "Logic", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Submarine Hunt", "category": "Strategy", "language": "basic",
"title": "Submarine Hunt", "category": "Strategy", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Lunar Lander", "category": "Simulation", "language": "basic",
"title": "Lunar Lander", "category": "Simulation", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Memory Matrix", "category": "Memory", "language": "basic",
"title": "Memory Matrix", "category": "Memory", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Robot Maze", "category": "Puzzle", "language": "basic",
"title": "Robot Maze", "category": "Puzzle", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Music Box", "category": "Music", "language": "basic",
"title": "Music Box", "category": "Music", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Pixel Painter", "category": "Creative", "language": "basic",
"title": "Pixel Painter", "category": "Creative", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Stock Trader", "category": "Simulation", "language": "basic",
"title": "Stock Trader", "category": "Simulation", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Starfield", "category": "Demo", "language": "basic",
"title": "Starfield", "category": "Demo", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Snake Trail", "category": "Arcade", "language": "basic",
"title": "Snake Trail", "category": "Arcade", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Dice Poker", "category": "Dice", "language": "basic",
"title": "Dice Poker", "category": "Dice", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Word Scramble", "category": "Word", "language": "basic",
"title": "Word Scramble", "category": "Word", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Nim Challenge", "category": "Strategy", "language": "basic",
"title": "Nim Challenge", "category": "Strategy", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Reaction Timer", "category": "Reflex", "language": "basic",
"title": "Reaction Timer", "category": "Reflex", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Morse Trainer", "category": "Educational", "language": "basic",
"title": "Morse Trainer", "category": "Educational", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Kaleidoscope", "category": "Demo", "language": "basic",
"title": "Kaleidoscope", "category": "Demo", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "World Quiz", "category": "Trivia", "language": "basic",
"title": "World Quiz", "category": "Trivia", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Life Lab", "category": "Simulation", "language": "basic",
"title": "Life Lab", "category": "Simulation", "language": "basic", "releaseDate": "2026-10-07",
"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"
},
{
"title": "Cipher Wheel", "category": "Utility", "language": "basic",
"title": "Cipher Wheel", "category": "Utility", "language": "basic", "releaseDate": "2026-10-07",
"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",
"title": "Sprite Playground", "category": "Creative", "language": "machine-code", "releaseDate": "2026-10-16",
"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",
"title": "Raster Bars", "category": "Demo", "language": "machine-code", "releaseDate": "2026-10-16",
"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",
"title": "SID Arpeggiator", "category": "Music", "language": "machine-code", "releaseDate": "2026-10-16",
"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"
},
{
"title": "Paddle Ball", "category": "Arcade", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Keep a fast-moving ball in play with a screen-edge paddle.",
"controls": [{"keys":["A","D"],"description":"Move the paddle left or right"},{"keys":["SPACE"],"description":"Serve a new ball"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "paddle-ball/paddle-ball.prg", "source": "paddle-ball/paddle-ball.asm"
},
{
"title": "Smooth Scroller", "category": "Demo", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Send a message gliding across the screen with VIC-II fine scrolling.",
"controls": [{"keys":["+","-"],"description":"Increase or decrease scroll speed"},{"keys":["C"],"description":"Change text color"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "smooth-scroller/smooth-scroller.prg", "source": "smooth-scroller/smooth-scroller.asm"
},
{
"title": "Fireworks", "category": "Demo", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Launch colorful PETSCII fireworks into a night sky.",
"controls": [{"keys":["SPACE"],"description":"Launch a firework"},{"keys":["C"],"description":"Choose the next burst color"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "fireworks/fireworks.prg", "source": "fireworks/fireworks.asm"
},
{
"title": "Jiffy Clock", "category": "Utility", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Turn the C64 jiffy timer into a large live digital clock.",
"controls": [{"keys":["R"],"description":"Reset the elapsed time"},{"keys":["SPACE"],"description":"Pause or resume the clock"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "jiffy-clock/jiffy-clock.prg", "source": "jiffy-clock/jiffy-clock.asm"
},
{
"title": "Typing Sprint", "category": "Word", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Race the clock while copying a Commodore-themed phrase.",
"controls": [{"keys":["A-Z"],"description":"Type the highlighted letters"},{"keys":["SPACE"],"description":"Start a new sprint"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "typing-sprint/typing-sprint.prg", "source": "typing-sprint/typing-sprint.asm"
},
{
"title": "PETSCII Aquarium", "category": "Simulation", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Watch a tiny animated aquarium and feed its roaming fish.",
"controls": [{"keys":["F"],"description":"Drop fish food into the tank"},{"keys":["1","2","3"],"description":"Select animation speed"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "petscii-aquarium/petscii-aquarium.prg", "source": "petscii-aquarium/petscii-aquarium.asm"
},
{
"title": "Fifteen Puzzle", "category": "Puzzle", "language": "machine-code", "releaseDate": "2026-10-16",
"description": "Slide numbered tiles until the classic four-by-four board is restored.",
"controls": [{"keys":["W","A","S","D"],"description":"Slide a tile toward the empty space"},{"keys":["R"],"description":"Shuffle the board again"},{"keys":["Q"],"description":"Return to BASIC"}],
"path": "fifteen-puzzle/fifteen-puzzle.prg", "source": "fifteen-puzzle/fifteen-puzzle.asm"
}
]

View file

@ -16,25 +16,18 @@ 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 programReleaseDate = document.querySelector('#program-release-date');
const programControls = document.querySelector('#program-controls');
const programSentinel = document.querySelector('#program-sentinel');
let allPrograms = [];
let filteredPrograms = [];
let visibleProgramCount = 0;
const PROGRAM_BATCH_SIZE = 12;
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' }
Arcade: '🎮', Creative: '✎', Demo: '✦', Dice: '⚄', Educational: '⌁',
Logic: '◆', Memory: '◫', Music: '♪', Puzzle: '⌘', Reflex: '⚡',
Simulation: '⚙', Strategy: '♟', Trivia: '?', Utility: '⚒', Word: 'Aa'
};
const languageLabels = {
@ -52,14 +45,13 @@ function categoryFromRoute() {
}
function createCategoryPill(category) {
const meta = categoryMeta[category] || { icon: '●', color: '#a5acb8' };
const iconName = categoryMeta[category] || '●';
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;
icon.textContent = iconName;
const label = document.createElement('span');
label.textContent = category;
pill.append(icon, label);
@ -90,21 +82,11 @@ function searchText(program) {
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 programs = allPrograms
.filter(program => !selectedCategory || program.category === selectedCategory)
.filter(program => !selectedLanguage || program.language === selectedLanguage)
.filter(program => !query || searchText(program).includes(query))
.sort((left, right) => {
if (sortOrder.value === 'category') return left.category.localeCompare(right.category) || left.title.localeCompare(right.title);
return left.title.localeCompare(right.title);
});
function formatReleaseDate(value) {
return new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' }).format(new Date(`${value}T00:00:00Z`));
}
programsElement.replaceChildren();
for (const program of programs) {
function createProgramCard(program) {
const card = document.createElement('article');
card.className = 'program-card';
const programUrl = `/program/${encodeURIComponent(program.slug)}`;
@ -124,6 +106,10 @@ function renderPrograms() {
description.className = 'program-description';
description.textContent = program.description;
const releaseDate = document.createElement('p');
releaseDate.className = 'release-date';
releaseDate.textContent = `Released ${formatReleaseDate(program.releaseDate)}`;
const actions = document.createElement('div');
actions.className = 'program-actions';
const run = document.createElement('a');
@ -137,11 +123,39 @@ function renderPrograms() {
source.disabled = !program.source;
source.addEventListener('click', () => showSource(program));
actions.append(run, source);
card.append(badges, title, description, actions);
programsElement.append(card);
}
card.append(badges, title, description, releaseDate, actions);
return card;
}
function appendProgramBatch() {
const nextCount = Math.min(visibleProgramCount + PROGRAM_BATCH_SIZE, filteredPrograms.length);
const fragment = document.createDocumentFragment();
for (const program of filteredPrograms.slice(visibleProgramCount, nextCount)) fragment.append(createProgramCard(program));
programsElement.append(fragment);
visibleProgramCount = nextCount;
programSentinel.hidden = visibleProgramCount >= filteredPrograms.length;
status.className = 'status';
status.textContent = `${programs.length} of ${allPrograms.length} programs shown`;
status.textContent = `${visibleProgramCount} of ${filteredPrograms.length} matching programs shown · ${allPrograms.length} total`;
}
function renderPrograms() {
const query = searchInput.value.trim().toLowerCase();
const selectedCategory = categoryFilter.value;
const selectedLanguage = languageFilter.value;
filteredPrograms = allPrograms
.filter(program => !selectedCategory || program.category === selectedCategory)
.filter(program => !selectedLanguage || program.language === selectedLanguage)
.filter(program => !query || searchText(program).includes(query))
.sort((left, right) => {
if (sortOrder.value === 'category') return left.category.localeCompare(right.category) || left.title.localeCompare(right.title);
if (sortOrder.value === 'newest') return right.releaseDate.localeCompare(left.releaseDate) || left.title.localeCompare(right.title);
if (sortOrder.value === 'oldest') return left.releaseDate.localeCompare(right.releaseDate) || left.title.localeCompare(right.title);
return left.title.localeCompare(right.title);
});
programsElement.replaceChildren();
visibleProgramCount = 0;
appendProgramBatch();
}
async function showSource(program) {
@ -197,6 +211,7 @@ async function loadPrograms() {
function renderProgramDetails(program) {
programBadges.replaceChildren(createCategoryPill(program.category), createLanguageBadge(program.language));
programDescription.textContent = program.description;
programReleaseDate.textContent = `Released ${formatReleaseDate(program.releaseDate)}`;
programControls.replaceChildren();
for (const control of program.controls) {
const row = document.createElement('div');
@ -280,6 +295,11 @@ window.addEventListener('popstate', () => {
}
});
const programObserver = new IntersectionObserver(entries => {
if (entries.some(entry => entry.isIntersecting) && visibleProgramCount < filteredPrograms.length) appendProgramBatch();
}, { rootMargin: '500px 0px' });
programObserver.observe(programSentinel);
function setView(view) {
const list = view === 'list';
programsElement.classList.toggle('list-view', list);

View file

@ -4,6 +4,7 @@
<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.">
<link rel="canonical" href="https://c64.p20.dev/">
<title>C64 Program Collection</title>
<link rel="stylesheet" href="/styles.css">
</head>
@ -39,6 +40,8 @@
<select id="sort-order">
<option value="title">Sort: Title</option>
<option value="category">Sort: Category</option>
<option value="newest">Sort: Newest first</option>
<option value="oldest">Sort: Oldest first</option>
</select>
</label>
<div class="view-switcher" role="group" aria-label="View style">
@ -49,6 +52,7 @@
</div>
<p id="status" class="status">Loading programs…</p>
<div id="programs" class="program-grid" aria-live="polite"></div>
<div id="program-sentinel" class="program-sentinel" aria-hidden="true"></div>
</section>
<section id="player" class="player" hidden>
<div class="player-heading">
@ -60,6 +64,7 @@
<a class="secondary button" href="/">Back to library</a>
</div>
<p id="program-description" class="program-page-description"></p>
<p id="program-release-date" class="release-date"></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>

View file

@ -36,14 +36,16 @@ h1 { margin: 0; font-size: clamp(2rem, 7vw, 4rem); line-height: 1; }
.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; }
.category-pill { border: 1px solid #b8b1e755; background: #17113b99; color: #d8d3f4; text-decoration: none; transition: background .15s, color .15s, transform .15s; }
.category-pill:hover { border-color: var(--c64-lightest-blue); background: var(--c64-light-blue); color: #fff; transform: translateY(-1px); }
.category-pill:focus-visible { outline: 3px solid #b8b1e788; 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-title { margin: 1rem 0 0; font-size: 1.2rem; }
.program-description { margin: .55rem 0 0; color: #d2cef0; font-size: .92rem; line-height: 1.45; }
.release-date { margin: .7rem 0 0; color: #aaa5ce; font-size: .78rem; }
.program-sentinel { height: 1px; }
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 +55,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: minmax(180px, auto) minmax(150px, .8fr) minmax(220px, 1.5fr) minmax(110px, auto) minmax(180px, 1fr); align-items: center; gap: 1rem; }
.program-grid.list-view .program-title, .program-grid.list-view .program-description, .program-grid.list-view .release-date, .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; }

View file

@ -4,7 +4,14 @@ set -eu
for source in \
sprite-playground/sprite-playground.asm \
raster-bars/raster-bars.asm \
sid-arpeggiator/sid-arpeggiator.asm
sid-arpeggiator/sid-arpeggiator.asm \
paddle-ball/paddle-ball.asm \
smooth-scroller/smooth-scroller.asm \
fireworks/fireworks.asm \
jiffy-clock/jiffy-clock.asm \
typing-sprint/typing-sprint.asm \
petscii-aquarium/petscii-aquarium.asm \
fifteen-puzzle/fifteen-puzzle.asm
do
output="${source%.asm}.prg"
echo "Assembling $source -> $output"

View file

@ -24,6 +24,7 @@ function fetchPrograms() {
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 (!/^\d{4}-\d{2}-\d{2}$/.test(program.releaseDate) || Number.isNaN(Date.parse(`${program.releaseDate}T00:00:00Z`))) throw new Error(`Invalid release date for ${program.title}`);
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}`);
@ -126,6 +127,6 @@ createServer(async (request, response) => {
else response.end();
}
}).listen(port, '0.0.0.0', () => {
console.log(`C64 BASIC runner listening on port ${port}`);
console.log(`C64 Program Collection listening on port ${port}`);
});

View file

@ -0,0 +1,128 @@
!cpu 6510
* = $0801
!word basic_end
!word 10
!byte $9e
!text "2061"
!byte 0
basic_end: !word 0
* = $080d
start:
lda $d016
sta old_d016
lda $d020
sta old_border
lda #147
jsr $ffd2
ldx #0
header:
lda title,x
beq setup
jsr $ffd2
inx
bne header
setup:
lda #7
sta fine
lda #3
sta speed
lda #0
sta tick
sta message_pos
lda #1
sta text_color
main:
jsr wait_frame
jsr $ffe4
cmp #81
bne not_quit
jmp exit
not_quit:
cmp #43
bne slower
lda speed
cmp #1
beq slower
dec speed
slower:
cmp #45
bne color_key
lda speed
cmp #8
beq color_key
inc speed
color_key:
cmp #67
bne animate
inc text_color
lda text_color
and #15
sta text_color
animate:
inc tick
lda tick
cmp speed
bcc main
lda #0
sta tick
dec fine
lda fine
and #7
sta fine
lda old_d016
and #$f8
ora fine
sta $d016
lda fine
cmp #7
bne main
ldx #0
shift:
lda $0619,x
sta $0618,x
inx
cpx #39
bne shift
ldy message_pos
lda message,y
bne put_char
ldy #0
sty message_pos
lda message
put_char:
sta $063f
lda text_color
sta $da3f
inc message_pos
jmp main
wait_frame:
lda #250
wait_raster:
cmp $d012
bne wait_raster
wait_leave:
cmp $d012
beq wait_leave
rts
exit:
lda old_d016
sta $d016
lda old_border
sta $d020
lda #147
jsr $ffd2
rts
title: !byte 147
!text "SMOOTH SCROLLER +/- SPEED C COLOR Q QUIT"
!byte 13,13,0
message: !scr " WELCOME TO THE C64 PROGRAM COLLECTION BUILT WITH 6502 MACHINE CODE "
!byte 0
fine: !byte 0
speed: !byte 0
tick: !byte 0
message_pos: !byte 0
text_color: !byte 0
old_d016: !byte 0
old_border: !byte 0

Binary file not shown.

View file

@ -0,0 +1,159 @@
!cpu 6510
* = $0801
!word basic_end
!word 10
!byte $9e
!text "2061"
!byte 0
basic_end: !word 0
* = $080d
start:
lda #147
jsr $ffd2
ldx #0
title_loop:
lda title,x
beq ready
jsr $ffd2
inx
bne title_loop
ready:
ldx #0
ready_loop:
lda ready_text,x
beq wait_start
jsr $ffd2
inx
bne ready_loop
wait_start:
jsr $ffe4
cmp #81
bne start_key
jmp exit
start_key:
cmp #32
bne wait_start
new_round:
lda #147
jsr $ffd2
ldx #0
prompt_loop:
lda prompt,x
beq setup
jsr $ffd2
inx
bne prompt_loop
setup:
lda #0
sta position
sta mistakes
sta frames
sta seconds
lda $a2
sta last_jiffy
main:
lda $a2
cmp last_jiffy
beq read_key
sta last_jiffy
inc frames
lda frames
cmp #60
bne read_key
lda #0
sta frames
inc seconds
read_key:
jsr $ffe4
beq main
cmp #81
bne type_key
jmp exit
type_key:
ldx position
cmp target,x
beq correct
inc mistakes
lda #18
jsr $ffd2
lda #42
jsr $ffd2
lda #146
jsr $ffd2
jmp main
correct:
jsr $ffd2
inc position
ldx position
lda target,x
bne main
lda #13
jsr $ffd2
ldx #0
done_loop:
lda done_text,x
beq show_time
jsr $ffd2
inx
bne done_loop
show_time:
lda seconds
ldx #0
time_tens:
cmp #10
bcc print_time
sec
sbc #10
inx
bne time_tens
print_time:
pha
txa
beq print_ones
ora #$30
jsr $ffd2
print_ones:
pla
ora #$30
jsr $ffd2
ldx #0
seconds_loop:
lda seconds_text,x
beq wait_again
jsr $ffd2
inx
bne seconds_loop
wait_again:
jsr $ffe4
cmp #81
bne again_key
jmp exit
again_key:
cmp #32
bne wait_again
jmp new_round
exit:
lda #147
jsr $ffd2
rts
title: !byte 147
!text "TYPING SPRINT COPY THE PHRASE WITHOUT ERRORS"
!byte 13,13,0
ready_text: !text "SPACE STARTS A SPRINT Q QUITS"
!byte 0
prompt: !byte 147
!text "TYPE: COMMODORE SIXTY FOUR"
!byte 13,13,0
target: !text "COMMODORE SIXTY FOUR"
!byte 0
done_text: !text "FINISH IN "
!byte 0
seconds_text: !text " SECONDS SPACE RUNS AGAIN"
!byte 0
position: !byte 0
mistakes: !byte 0
frames: !byte 0
seconds: !byte 0
last_jiffy: !byte 0

Binary file not shown.