diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..3296c99 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(where cl65 *)", + "Bash(where x64sc *)", + "Bash(cl65 --version)", + "Bash(command -v cl65)", + "Bash(command -v x64sc)", + "Bash(command -v x64)", + "Bash(ls \"/c/Program Files/\" 2>/dev/null | grep -i vice; ls \"/c/Program Files \\(x86\\)/\" 2>/dev/null | grep -i vice; find /d/Development -iname \"x64sc*\" 2>/dev/null | head -20)", + "Read(//c/Program Files \\\\\\(x86\\\\\\)//**)", + "Bash(cl65 -t c64 -O -o linked-list-test.prg linked-list-test.c)", + "Bash(\"/c/Program Files/GTK3VICE-3.10-win64/bin/x64sc.exe\" -autostart linked-list-test.prg -autostart-warp -warp -remotemonitor -remotemonitoraddress ip4://127.0.0.1:6510)", + "Bash(echo \"launched pid $!\")", + "Bash(ps -W)", + "Bash(powershell -NoProfile -Command \"Get-Process x64sc -ErrorAction SilentlyContinue | Select-Object Id,ProcessName\")", + "Bash(powershell -NoProfile -Command \"Get-NetTCPConnection -LocalPort 6510 -ErrorAction SilentlyContinue\")", + "Bash(tasklist //FI \"IMAGENAME eq x64sc.exe\")" + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 46b187e..88bfc0a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,11 @@ "permissions": { "allow": [ "Bash(build.bat)", - "PowerShell(Copy-Item \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mousedemo.c\" \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mouse_driver_demo.c\" -Force)" + "PowerShell(Copy-Item \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mousedemo.c\" \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mouse_driver_demo.c\" -Force)", + "Bash(mkdir -p \"C:\\\\Users\\\\danielc.NTP\\\\.claude\\\\projects\\\\d--Development-Commodore-C64-dev\\\\memory\")" + ], + "additionalDirectories": [ + "C:\\Users\\danielc.NTP\\.claude\\projects\\d--Development-Commodore-C64-dev" ] } } diff --git a/AGENTS.md b/AGENTS.md index 0ad9a58..465037d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -211,6 +211,32 @@ When an AI agent receives a C64 development question: --- +## Development Conventions + +- Write machine addresses in hexadecimal with a `$` prefix (for example, `$D000` and `$FFD2`). Include decimal equivalents when an address is used with BASIC `PEEK` or `POKE` (for example, `$D000` = `53248`). +- Check memory banking before accessing `$A000–$FFFF`; the CPU may see ROM, RAM, I/O, or character ROM depending on the 6510 port at `$0001`. +- Prefer Kernal routines for standard I/O and use zero page for variables that benefit from faster access. Keep BASIC's memory ceiling below machine code that BASIC might otherwise overwrite. +- State whether timing-sensitive code assumes PAL or NTSC, especially for raster timing and sound frequencies. + +### Hardware Limits to Check + +| Resource | Limit or behavior | +|----------|-------------------| +| Default BASIC program area | `$0801–$9FFF` (38,911 addressable bytes; usable free space is lower) | +| 6510 hardware stack | `$0100–$01FF` (256 bytes) | +| Default screen matrix | `$0400–$07E7` (1,000 visible character cells) | +| Color RAM | `$D800–$DBE7` (1,000 visible 4-bit color cells when I/O is mapped in) | +| VIC-II address space | One 16KB bank at a time, selected by CIA #2 at `$DD00`, bits 0–1 | +| Sprite data | 64 bytes per block; at most 256 block positions in a 16KB VIC bank | +| SID reads | `$D400–$D418` are write-only; `$D419–$D41C` provide paddle, oscillator 3, and envelope 3 readings | +| Character ROM | CPU-readable at `$D000–$DFFF` only under the appropriate `$0001` banking configuration | + +### Development Tools + +VICE is the emulator used by the examples in this repository. Common modern assemblers include KickAssembler, ACME, ca65, and 64tass; CBM .prg Studio is a Windows IDE, C64 debugger provides visual debugging with VICE integration, and Exomizer and Pucrunch are executable crunchers. Period tools include SUPERMON, Mikro Assembler, PAL, and Simon's BASIC. + +--- + ## VICE Debug Workflow When debugging a C64 program in VICE, agents should prefer this flow: @@ -260,3 +286,11 @@ LDA #>my_irq ; high byte STA $0315 CLI ; enable interrupts ``` + +### Assembly program skeleton +```asm + *=$C000 ; assemble at $C000 (49152) +START JSR $FF81 ; CINT: initialize the screen editor + ; program code goes here + RTS ; return to BASIC when entered with SYS 49152 +``` diff --git a/CLAUDE.md b/CLAUDE.md index d757808..eef4bd2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,155 +1 @@ -# CLAUDE.md — Commodore 64 Development Agent Configuration - -> This file configures AI agents (Claude and others) to assist with Commodore 64 application development. -> **Start here.** Then follow pointers to AGENTS.md and the skill files. - ---- - -## Purpose - -This repository is a structured knowledge base for developing software on the **Commodore 64**, compiled from 11 canonical C64 reference books by a collective of 150 expert C64 developers. It also includes supplemental VICE 3.10 emulator workflow guidance for modern testing and debugging. - -## Primary Reference - -→ **Read [`AGENTS.md`](./AGENTS.md) first.** It contains: -- Complete system overview and memory map -- Skill directory index -- Kernal jump table quick reference -- Zero-page location table -- Common development patterns -- Source book citations - -## Repository Structure - -``` -c64-dev-knowledge/ -├── CLAUDE.md ← You are here -├── AGENTS.md ← Primary AI reference (read this first) -├── skills/ -│ ├── 6510-assembly/SKILL.md ← 6510 CPU, opcodes, addressing -│ ├── memory-map/SKILL.md ← Memory layout, banking, zero page -│ ├── graphics-vic2/SKILL.md ← VIC-II chip, sprites, modes -│ ├── sound-sid/SKILL.md ← SID 6581 sound chip -│ ├── disk-io-1541/SKILL.md ← 1541 disk drive programming -│ ├── kernal-routines/SKILL.md ← OS routines and vectors -│ ├── basic-programming/SKILL.md ← BASIC 2.0 and extensions -│ ├── interrupts/SKILL.md ← IRQ, NMI, CIA timers -│ ├── compiler-design/SKILL.md ← Assemblers, compilers for C64 -│ └── vice-emulator/SKILL.md ← VICE launch, autostart, monitor, automation -├── reference/ -│ ├── memory-map-complete.md ← Full address space reference -│ ├── 6510-opcodes.md ← Complete opcode table -│ ├── kernal-jumplist.md ← Kernal routine details -│ ├── vic-registers.md ← VIC-II register map -│ ├── sid-registers.md ← SID register map -│ ├── cia-registers.md ← CIA 6526 register map -│ ├── disk-dos-commands.md ← 1541 DOS commands -│ └── vice-usage.md ← VICE 3.10 quick reference -└── examples/ - ├── hello-world.asm ← Assembly hello world - ├── sprite-demo.bas ← Sprite programming in BASIC - ├── sound-demo.bas ← SID sound in BASIC - ├── disk-access.bas ← File I/O examples - ├── irq-handler.asm ← Custom IRQ handler - ├── kernal-io.asm ← Kernal I/O routines - ├── vice-autostart-c64os.bat ← Launch c64os.prg in x64sc - ├── vice-debug-c64os.bat ← Launch c64os.prg with native monitor - ├── vice-monitor-c64os.txt ← VICE monitor playback commands - └── vice-monitor-c64os-debug.txt ← Repeatable VICE debug session -``` - -## Agent Instructions - -### When a user asks a C64 development question: - -1. **Consult `AGENTS.md`** for the system overview and to identify which skill applies -2. **Load the relevant `SKILL.md`** from `skills//SKILL.md` -3. **Reference specific values** from files in `reference/` -4. **Use examples** from `examples/` as starting points for code -5. **If the task involves emulator execution or debugging**, load `skills/vice-emulator/SKILL.md` - -When the task is specifically about debugging a program in VICE, prefer: - -- `x64sc` over the older `x64` -- `-nativemonitor -keepmonopen -refreshonbreak` -- `sidefx off` before inspecting hardware-mapped locations -- watchpoints on `$0001`, `$0314/$0315`, and other critical machine state -- repeatable playback files from `examples/vice-monitor-c64os-debug.txt` - -### Topic Routing - -| User asks about... | → Load skill | -|--------------------|-------------| -| 6502/6510 assembly, opcodes, registers, addressing | `skills/6510-assembly/SKILL.md` | -| Memory layout, PEEK/POKE locations, banking | `skills/memory-map/SKILL.md` | -| Sprites, text modes, bitmap, colors, VIC chip | `skills/graphics-vic2/SKILL.md` | -| Music, sound effects, SID chip | `skills/sound-sid/SKILL.md` | -| Disk operations, loading, saving, file types | `skills/disk-io-1541/SKILL.md` | -| OS calls, LOAD/SAVE from ML, I/O from assembly | `skills/kernal-routines/SKILL.md` | -| BASIC programming, PEEK/POKE tricks, adding commands | `skills/basic-programming/SKILL.md` | -| IRQ handlers, NMI, CIA timers, raster bars | `skills/interrupts/SKILL.md` | -| Writing assemblers, compilers, parsers | `skills/compiler-design/SKILL.md` | -| Running, testing, or automating code in VICE | `skills/vice-emulator/SKILL.md` | - -### Expert Conventions (apply to all answers) - -- **Always use hex addresses** with `$` prefix (e.g., `$D000`, `$FFD2`) -- **Always note decimal equivalents** for BASIC PEEK/POKE (e.g., `53248`) -- **Always check memory banking** before giving addresses in the $A000–$FFFF range -- **Prefer Kernal routines** over custom code for standard I/O operations -- **Use zero page** for frequently-accessed variables (faster indexed addressing) -- **Protect ML code** from BASIC with `POKE 56, : CLR` -- **Note NTSC/PAL differences** for timing-sensitive code (raster, sound frequencies) - -## Hardware Constraints - -Always keep these limits in mind when generating code or advice: - -- **RAM available to BASIC**: ~38KB ($0801–$9FFF = 38911 bytes) -- **Free RAM under I/O**: $C000–$CFFF (4KB, always RAM) -- **6510 stack**: $0100–$01FF (256 bytes, hardware fixed) -- **Screen RAM**: $0400–$07E7 (default, 1000 bytes) -- **Color RAM**: $D800–$DBE7 (always at this address, 1000 nybbles) -- **VIC-II sees**: One 16KB bank at a time (bank selected via CIA #2 $DD00 bits 0-1) -- **SID registers**: Write-only except $D419–$D41C (paddles/misc) -- **Character ROM**: Mapped at $D000 when I/O is banked out (not normally accessible via CPU) -- **Max sprite data blocks**: 256 blocks × 64 bytes = 16KB within VIC bank - -## Quick-Start Code Templates - -### BASIC Program with ML Routine -```basic -1 REM C64 ML TEMPLATE -10 ML=49152 : REM $C000 - safe ML area -20 POKE 56,192 : CLR : REM PROTECT ML AREA -30 FOR I=ML TO ML+N-1 : READ D : POKE I,D : NEXT -40 SYS ML : REM RUN THE ROUTINE -50 REM ... your BASIC code ... -60000 DATA 96 : REM RTS (minimal routine placeholder) -``` - -### Assembly Program Skeleton -```asm - ; C64 Assembly Program Template - ; Assemble to $C000 (49152) - *=$C000 - -START JSR $FF81 ; CINT - init screen - ; ... your code ... - RTS ; return to BASIC (if SYS'd) - ; or JMP $FCE2 to warm-start BASIC -``` - -## Emulator/Development Tool Notes - -Modern C64 development typically uses: -- **VICE** (emulator) — accurate cycle-level emulation -- **KickAssembler**, **ACME**, **CA65**, **64tass** — modern assemblers -- **CBM .prg Studio** — Windows IDE -- **C64 debugger** — visual debugger with VICE integration -- **Exomizer**, **Pucrunch** — executable crunchers - -For period-accurate development (on hardware or accurate emulation): -- **SUPERMON** — machine language monitor (from ROM books) -- **Mikro Assembler**, **PAL** — period assemblers -- **Simon's BASIC** — extended BASIC with ML-callable routines +@AGENTS.md \ No newline at end of file diff --git a/examples/linked-list-test-build.bat b/examples/linked-list-test-build.bat new file mode 100644 index 0000000..ea6b9d5 --- /dev/null +++ b/examples/linked-list-test-build.bat @@ -0,0 +1,21 @@ +@echo off +REM Build linked-list-test.c with cc65 and optionally launch in VICE x64sc +REM Requirements: cc65 on PATH, VICE x64sc on PATH or in GTK3VICE install dir + +set SRC=linked-list-test.c +set PRG=linked-list-test.prg +set VICE=x64sc + +echo Compiling %SRC% ... +cl65 -t c64 -O -o %PRG% %SRC% +if errorlevel 1 ( + echo BUILD FAILED + exit /b 1 +) + +echo Build OK: %PRG% + +if "%1"=="-run" ( + echo Launching in VICE ... + "%VICE%" -autostart %PRG% -warp -autostartprgmode 1 +) diff --git a/examples/linked-list-test.c b/examples/linked-list-test.c new file mode 100644 index 0000000..07c80e4 --- /dev/null +++ b/examples/linked-list-test.c @@ -0,0 +1,199 @@ +/* linked-list-test.c - singly linked list on the C64, built with cc65 + * + * Build: + * cl65 -t c64 -O -o linked-list-test.prg linked-list-test.c + * + * Exercises malloc()/free()-backed linked list operations (append, + * count, sum, search, delete, reverse, free) at sizes from 1000 to + * 2000 nodes, then runs an allocator capacity test to show how many + * 4-byte nodes actually fit in real C64 RAM under this cc65 memory + * config (program loads at $0801, heap tops out at $D000). + */ + +#include +#include +#include + +typedef struct Node { + int value; + struct Node *next; +} Node; + +/* ---- basic list operations ------------------------------------- */ + +/* Appends 1..count to a new list. Returns NULL (and frees any partial + list) if malloc() runs out of memory partway through. */ +static Node *build_list(unsigned int count) +{ + Node *head = NULL, *tail = NULL, *n; + unsigned int i; + + for (i = 0; i < count; i++) { + n = malloc(sizeof(Node)); + if (!n) { + while (head) { n = head; head = head->next; free(n); } + return NULL; + } + n->value = (int)(i + 1); + n->next = NULL; + if (head) tail->next = n; else head = n; + tail = n; + } + return head; +} + +static unsigned int count_list(Node *head) +{ + unsigned int c = 0; + while (head) { c++; head = head->next; } + return c; +} + +static long sum_list(Node *head) +{ + long s = 0; + while (head) { s += head->value; head = head->next; } + return s; +} + +static Node *list_find(Node *head, int value) +{ + while (head) { + if (head->value == value) return head; + head = head->next; + } + return NULL; +} + +/* Removes the first node matching value. Returns 1 if removed. */ +static unsigned char list_delete(Node **headp, int value) +{ + Node *prev = NULL, *cur = *headp; + while (cur) { + if (cur->value == value) { + if (prev) prev->next = cur->next; else *headp = cur->next; + free(cur); + return 1; + } + prev = cur; + cur = cur->next; + } + return 0; +} + +static void list_reverse(Node **headp) +{ + Node *prev = NULL, *cur = *headp, *next; + while (cur) { + next = cur->next; + cur->next = prev; + prev = cur; + cur = next; + } + *headp = prev; +} + +static void free_list(Node *head) +{ + Node *n; + while (head) { n = head; head = head->next; free(n); } +} + +/* ---- one pass of tests at a given size --------------------------- */ + +static unsigned char test_size(unsigned int n) +{ + Node *head; + unsigned int c; + long s, expected; + int mid; + unsigned char ok = 1; + + head = build_list(n); + if (!head) { + textcolor(COLOR_RED); + cprintf("N=%4u ALLOC FAIL\n\r", n); + return 0; + } + + c = count_list(head); + s = sum_list(head); + expected = ((long)n * (n + 1)) / 2; + if (c != n || s != expected) ok = 0; + + mid = (int)(n / 2); + if (!list_find(head, mid)) ok = 0; + if (list_find(head, (int)(n + 500))) ok = 0; + + if (!list_delete(&head, mid)) ok = 0; + if (list_find(head, mid)) ok = 0; + if (count_list(head) != n - 1) ok = 0; + + list_reverse(&head); + if (!head || head->value != (int)n) ok = 0; + + free_list(head); + + textcolor(ok ? COLOR_GREEN : COLOR_RED); + cprintf("N=%4u %s\n\r", n, ok ? "PASS" : "FAIL"); + return ok; +} + +/* ---- how many nodes really fit in RAM? --------------------------- */ + +static void capacity_test(void) +{ + Node *head = NULL, *tail = NULL, *n; + unsigned int count = 0; + + textcolor(COLOR_WHITE); + cprintf("\n\rCAPACITY TEST (malloc until NULL)...\n\r"); + + for (;;) { + n = malloc(sizeof(Node)); + if (!n) break; + n->value = (int)count; + n->next = NULL; + if (head) tail->next = n; else head = n; + tail = n; + count++; + } + + textcolor(COLOR_YELLOW); + cprintf("MAX NODES ALLOCATED: %u\n\r", count); + free_list(head); +} + +/* ---- driver -------------------------------------------------------- */ + +int main(void) +{ + static const unsigned int sizes[5] = { 1000, 1250, 1500, 1750, 2000 }; + unsigned char i, passed = 0; + + clrscr(); + bgcolor(COLOR_BLACK); + bordercolor(COLOR_BLACK); + textcolor(COLOR_CYAN); + cprintf("C64 LINKED LIST TEST (cc65)\n\r"); + cprintf("===========================\n\r"); + textcolor(COLOR_WHITE); + cprintf("append/count/sum/find/delete/reverse\n\r\n\r"); + + for (i = 0; i < 5; i++) { + passed += test_size(sizes[i]); + } + + textcolor(COLOR_WHITE); + cprintf("\n\r%u/5 SIZE TESTS PASSED\n\r", passed); + + capacity_test(); + + textcolor(COLOR_CYAN); + cprintf("\n\rDONE - PRESS ANY KEY\n\r"); + cgetc(); + + bgcolor(COLOR_BLUE); + bordercolor(COLOR_LIGHTBLUE); + return 0; +} diff --git a/examples/linked-list-test.prg b/examples/linked-list-test.prg new file mode 100644 index 0000000..eca5766 Binary files /dev/null and b/examples/linked-list-test.prg differ diff --git a/examples/moonlander-build.bat b/examples/moonlander-build.bat new file mode 100644 index 0000000..3cec7a1 --- /dev/null +++ b/examples/moonlander-build.bat @@ -0,0 +1,21 @@ +@echo off +REM Build moonlander.c with cc65 and optionally launch in VICE x64sc +REM Requirements: cc65 on PATH, VICE x64sc on PATH or in GTK3VICE install dir + +set SRC=moonlander.c +set PRG=moonlander.prg +set VICE=x64sc + +echo Compiling %SRC% ... +cl65 -t c64 -O -o %PRG% %SRC% +if errorlevel 1 ( + echo BUILD FAILED + exit /b 1 +) + +echo Build OK: %PRG% + +if "%1"=="-run" ( + echo Launching in VICE ... + "%VICE%" -autostart %PRG% -warp -autostartprgmode 1 +) diff --git a/examples/moonlander.c b/examples/moonlander.c new file mode 100644 index 0000000..3f22c4c --- /dev/null +++ b/examples/moonlander.c @@ -0,0 +1,465 @@ +/* moonlander.c – Lunar Lander for Commodore 64 + * + * Build: + * cl65 -t c64 -O -o moonlander.prg moonlander.c + * + * Controls (joystick port 2): + * FIRE = upward thrust + * LEFT/RIGHT = horizontal thrust + * + * Land gently on the green pad to score points. + * Hard landing or missing the pad = CRASH. + */ + +#include +#include +#include +#include + +/* ---------------------------------------------------------------- + Hardware register macros + ---------------------------------------------------------------- */ +#define SP0X (*(unsigned char*)0xD000) +#define SP0Y (*(unsigned char*)0xD001) +#define MSIGX (*(unsigned char*)0xD010) /* MSB of sprite X coords */ +#define SPENA (*(unsigned char*)0xD015) /* sprite enable bits */ +#define SP0COL (*(unsigned char*)0xD027) /* sprite 0 color */ +#define BGCOL (*(unsigned char*)0xD021) /* background color */ +#define BORDER (*(unsigned char*)0xD020) /* border color */ +#define RASTER (*(unsigned char*)0xD012) /* current raster line */ + +#define SCREEN ((unsigned char*)0x0400) /* screen RAM */ +#define COLRAM ((unsigned char*)0xD800) /* color RAM */ +#define SPR0PTR (*(unsigned char*)0x07F8) /* sprite 0 data pointer */ + +/* CIA #1 $DC00 – joystick port 2 (active LOW) */ +#define JOY2 (*(unsigned char*)0xDC00) +#define JOY_PORT2_UP 0x01 +#define JOY_PORT2_DOWN 0x02 +#define JOY_PORT2_LEFT 0x04 +#define JOY_PORT2_RIGHT 0x08 +#define JOY_PORT2_FIRE 0x10 + +/* SID voice 1 */ +#define SID_FLO (*(unsigned char*)0xD400) +#define SID_FHI (*(unsigned char*)0xD401) +#define SID_CT (*(unsigned char*)0xD404) +#define SID_AD (*(unsigned char*)0xD405) +#define SID_SR (*(unsigned char*)0xD406) +#define SID_VOL (*(unsigned char*)0xD418) + +/* ---------------------------------------------------------------- + Layout constants + ---------------------------------------------------------------- */ +#define COLS 40 +#define ROWS 25 +#define TERRAIN_ROW 22 /* text row: top of terrain surface */ +#define PAD_COL 18 /* landing pad left column */ +#define PAD_W 4 /* landing pad width in characters */ + +/* Sprite block 13 sits at $0340 (tape buffer area, safe to reuse) */ +#define SPR_BLOCK 13 +#define SPR_MEM ((unsigned char*)0x0340) + +/* Sprite Y where terrain surface begins (NTSC top ≈ 50, row*8 px) */ +#define GROUND_Y (50 + TERRAIN_ROW * 8) /* = 226 */ + +/* Landing pad pixel X bounds (left border ≈ 24px for NTSC) */ +#define PAD_PIX_L (24 + PAD_COL * 8) /* 168 */ +#define PAD_PIX_R (24 + (PAD_COL + PAD_W) * 8) /* 200 */ + +/* Fixed-point scale: multiply all positions/velocities by SCALE */ +#define SCALE 4 + +/* ---------------------------------------------------------------- + Lander sprite: 24×21 hi-res, 63 bytes + 1 padding + ---------------------------------------------------------------- + Looks like a rocket/lander body with two splayed landing legs. + ---------------------------------------------------------------- */ +static const unsigned char spr_lander[64] = { + 0x00, 0x18, 0x00, /* row 0 top nozzle */ + 0x00, 0x3C, 0x00, /* row 1 */ + 0x00, 0x7E, 0x00, /* row 2 */ + 0x01, 0xFF, 0x80, /* row 3 */ + 0x03, 0xFF, 0xC0, /* row 4 */ + 0x07, 0xFF, 0xE0, /* row 5 wide body */ + 0x03, 0xFF, 0xC0, /* row 6 */ + 0x01, 0xFF, 0x80, /* row 7 */ + 0x00, 0xFF, 0x00, /* row 8 */ + 0x00, 0x7E, 0x00, /* row 9 */ + 0x00, 0x3C, 0x00, /* row 10 */ + 0x00, 0x3C, 0x00, /* row 11 */ + 0x00, 0x3C, 0x00, /* row 12 */ + 0x01, 0xFF, 0x80, /* row 13 lower body */ + 0x03, 0xFF, 0xC0, /* row 14 */ + 0x07, 0xFF, 0xE0, /* row 15 */ + 0x06, 0x00, 0x60, /* row 16 leg struts */ + 0x0C, 0x00, 0x30, /* row 17 */ + 0x18, 0x00, 0x18, /* row 18 */ + 0x30, 0x00, 0x0C, /* row 19 */ + 0x60, 0x00, 0x06, /* row 20 footpads */ + 0x00 /* padding byte */ +}; + +/* ---------------------------------------------------------------- + Thrust exhaust sprite: 24×21, placed below lander when firing + ---------------------------------------------------------------- */ +static const unsigned char spr_exhaust[64] = { + 0x00, 0x18, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0x7E, 0x00, + 0x01, 0xFF, 0x80, + 0x01, 0xE7, 0x80, + 0x00, 0xC3, 0x00, + 0x00, 0x42, 0x00, + 0x00, 0x3C, 0x00, + 0x00, 0x18, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + 0x00 +}; + +/* ---------------------------------------------------------------- + Game state + ---------------------------------------------------------------- */ +static int px, py; /* lander sprite position × SCALE */ +static int vx, vy; /* velocity × SCALE */ +static int fuel; /* remaining fuel × SCALE */ +static int score; +static unsigned char crashed; +static unsigned char landed; + +/* ---------------------------------------------------------------- + Helpers + ---------------------------------------------------------------- */ + +/* Sync to raster line 250 – gives one tick per TV frame (≈50/60 Hz) */ +static void wait_frame(void) +{ + while (RASTER != 250) {} + while (RASTER == 250) {} +} + +static void sid_thrust_on(void) +{ + SID_VOL = 0x0F; + SID_FLO = 0x60; + SID_FHI = 0x00; + SID_CT = 0x81; /* noise waveform + gate */ + SID_AD = 0x00; + SID_SR = 0xF0; +} + +static void sid_off(void) +{ + SID_CT = 0x00; /* release gate */ +} + +static void sid_silence_all(void) +{ + (*(unsigned char*)0xD404) = 0x00; + (*(unsigned char*)0xD40B) = 0x00; + (*(unsigned char*)0xD412) = 0x00; + SID_VOL = 0x0F; +} + +/* Draw two rows of terrain: solid blocks with a flat pad gap */ +static void draw_terrain(void) +{ + unsigned char c; + for (c = 0; c < COLS; c++) { + unsigned int r1 = (unsigned int)TERRAIN_ROW * COLS + c; + unsigned int r2 = (unsigned int)(TERRAIN_ROW + 1) * COLS + c; + + if (c >= PAD_COL && c < PAD_COL + PAD_W) { + SCREEN[r1] = 0x2D; /* '-' in screen code = pad marker */ + COLRAM[r1] = 5; /* green */ + } else { + SCREEN[r1] = 0xA0; /* solid block */ + COLRAM[r1] = 11; /* dark grey */ + } + SCREEN[r2] = 0xA0; + COLRAM[r2] = 11; + } +} + +static void draw_stars(void) +{ + static const unsigned char sr[] = { 2, 4, 7, 10, 13, 3, 8, 15, 19, 6 }; + static const unsigned char sc[] = { 5, 15, 30, 8, 35, 22, 3, 18, 10, 37 }; + unsigned char i; + for (i = 0; i < 10; i++) { + unsigned int p = (unsigned int)sr[i] * COLS + sc[i]; + SCREEN[p] = 0x2E; /* '.' */ + COLRAM[p] = 1; /* white */ + } +} + +/* Write sprite 0 position registers from internal fixed-point state */ +static void update_sprite(void) +{ + unsigned int sx = (unsigned int)(px / SCALE); + unsigned int sy = (unsigned int)(py / SCALE); + + if (sx > 255) { + SP0X = (unsigned char)(sx - 256); + MSIGX = MSIGX | 0x01; + } else { + SP0X = (unsigned char)sx; + MSIGX = MSIGX & 0xFE; + } + SP0Y = (unsigned char)(sy > 247 ? 247 : sy); +} + +/* Update exhaust sprite 1 (shown only while thrusting) */ +static void update_exhaust(unsigned char thrusting) +{ + if (thrusting) { + unsigned int sx = (unsigned int)(px / SCALE); + unsigned int sy = (unsigned int)(py / SCALE) + 18; + if (sx > 255) { + (*(unsigned char*)0xD002) = (unsigned char)(sx - 256); + MSIGX = MSIGX | 0x02; + } else { + (*(unsigned char*)0xD002) = (unsigned char)sx; + MSIGX = MSIGX & 0xFD; + } + (*(unsigned char*)0xD003) = (unsigned char)(sy > 247 ? 247 : sy); + SPENA = SPENA | 0x02; + } else { + SPENA = SPENA & 0xFD; /* hide exhaust sprite */ + } +} + +static void draw_hud(void) +{ + gotoxy(0, 0); + cprintf("FUEL:%4d VY:%3d VX:%3d SC:%5d", + fuel / SCALE, vy / SCALE, vx / SCALE, score); +} + +static void center_msg(const char *msg, unsigned char row) +{ + unsigned char len = (unsigned char)strlen(msg); + unsigned char col = (COLS - len) / 2; + gotoxy(col, row); + textcolor(COLOR_WHITE); + cputs(msg); +} + +/* ---------------------------------------------------------------- + main + ---------------------------------------------------------------- */ +int main(void) +{ + unsigned char joy; + unsigned char thrusting; + unsigned char gravity_tick; + unsigned char play_again; + int lsx, lsy; /* lander sprite X/Y (not scaled) for collision */ + + /* ---- Title screen ------------------------------------------ */ + clrscr(); + bgcolor(COLOR_BLACK); + bordercolor(COLOR_BLACK); + textcolor(COLOR_CYAN); + center_msg("* MOONLANDER *", 4); + textcolor(COLOR_YELLOW); + center_msg("GUIDE YOUR SHIP TO THE GREEN PAD", 7); + textcolor(COLOR_WHITE); + center_msg("FIRE = UPWARD THRUST", 9); + center_msg("LEFT/RIGHT = HORIZONTAL THRUST", 10); + center_msg("LAND GENTLY TO SCORE POINTS", 11); + center_msg("HARD LANDING OR MISSING PAD", 12); + center_msg("MEANS CERTAIN DOOM!", 13); + textcolor(COLOR_GREEN); + center_msg("PRESS FIRE TO BEGIN", 17); + + /* Park lander off-screen during title */ + memcpy(SPR_MEM, spr_lander, 64); + memcpy((unsigned char*)0x0380, spr_exhaust, 64); + SPR0PTR = SPR_BLOCK; /* block 13 = lander */ + (*(unsigned char*)0x07F9) = 14; /* block 14 = exhaust */ + SP0COL = 7; /* yellow lander */ + (*(unsigned char*)0xD028) = 8; /* orange exhaust */ + SP0X = 0; SP0Y = 251; + SPENA = 0x01; + sid_silence_all(); + + while (JOY2 & JOY_PORT2_FIRE) {} /* wait for fire press */ + while (!(JOY2 & JOY_PORT2_FIRE)) {} /* wait for release */ + + play_again = 1; + do { + /* ---- Game setup -------------------------------------------- */ + clrscr(); + bgcolor(COLOR_BLACK); + bordercolor(COLOR_BLACK); + textcolor(COLOR_WHITE); + + draw_terrain(); + draw_stars(); + + px = 160 * SCALE; + py = 55 * SCALE; + vx = 0; + vy = 1; /* start with gentle downward drift */ + fuel = 200 * SCALE; + score = 0; + crashed = 0; + landed = 0; + gravity_tick = 0; + + SPENA = 0x01; + update_sprite(); + update_exhaust(0); + + /* ---- Main game loop ---------------------------------------- */ + while (!crashed && !landed) { + wait_frame(); + + joy = JOY2; + thrusting = 0; + + /* Upward thrust from fire button */ + if (!(joy & JOY_PORT2_FIRE) && fuel > 0) { + vy -= 5; + fuel -= SCALE; + thrusting = 1; + sid_thrust_on(); + } else { + sid_off(); + } + + /* Horizontal thrust */ + if (!(joy & JOY_PORT2_LEFT) && fuel > 0) { + vx -= 2; + fuel -= 1; + } + if (!(joy & JOY_PORT2_RIGHT) && fuel > 0) { + vx += 2; + fuel -= 1; + } + if (fuel < 0) fuel = 0; + + /* Very gentle gravity: +1 sub-pixel every fourth frame */ + gravity_tick++; + if (gravity_tick >= 4) { + gravity_tick = 0; + vy += 1; + } + + /* Light horizontal damping (simulate RCS / air drag) */ + if (vx > 0) vx--; + else if (vx < 0) vx++; + + /* Clamp velocities */ + if (vy > 32) vy = 32; + if (vy < -24) vy = -24; + if (vx > 16) vx = 16; + if (vx < -16) vx = -16; + + /* Integrate position */ + px += vx; + py += vy; + + /* Clamp to visible playfield (X: ~24..295 sprite pixels) */ + if (px < 24 * SCALE) { px = 24 * SCALE; vx = -vx / 2; } + if (px > 295 * SCALE) { px = 295 * SCALE; vx = -vx / 2; } + + /* ---- Ground collision ---------------------------------- */ + lsx = (int)(px / SCALE); + lsy = (int)(py / SCALE); + + /* Sprite is 21 px tall; check if bottom row reaches terrain */ + if (lsy + 21 >= GROUND_Y) { + py = (GROUND_Y - 21) * SCALE; + lsy = GROUND_Y - 21; + + /* Is lander center (X+12) over the pad? */ + if ((lsx + 12) >= PAD_PIX_L && (lsx + 12) <= PAD_PIX_R) { + /* Safe if slow enough: vy <= 8, |vx| <= 6 */ + if (vy <= 8 && vx >= -6 && vx <= 6) { + score = 1000 - (vy / SCALE) * 40 + (fuel / SCALE); + if (score < 0) score = 0; + landed = 1; + } else { + crashed = 1; + } + } else { + crashed = 1; /* missed the pad */ + } + } + + /* Don't let lander go above row 1 (HUD row) */ + if (py < 58 * SCALE) { py = 58 * SCALE; vy = 1; } + + update_sprite(); + update_exhaust(thrusting); + draw_hud(); + } + + /* ---- Game over --------------------------------------------- */ + sid_off(); + + if (landed) { + SPENA = 0x01; /* keep lander visible */ + textcolor(COLOR_GREEN); + center_msg("** SAFE LANDING! **", 9); + { + char buf[24]; + sprintf(buf, "YOUR SCORE: %d", score); + textcolor(COLOR_YELLOW); + center_msg(buf, 11); + } + textcolor(COLOR_WHITE); + center_msg("NICE FLYING, PILOT!", 13); + } else { + SPENA = 0x00; /* hide sprite – it exploded */ + /* Draw explosion at impact site */ + { + unsigned char ex_col = (unsigned char)((px / SCALE - 24) / 8); + unsigned int ep; + if (ex_col >= COLS) ex_col = COLS - 1; + ep = (unsigned int)(TERRAIN_ROW - 1) * COLS + ex_col; + SCREEN[ep] = 0x51; /* '*' in PETSCII screen code */ + COLRAM[ep] = 2; /* red */ + } + textcolor(COLOR_RED); + center_msg("** CRASH! **", 9); + textcolor(COLOR_WHITE); + center_msg("NO SCORE FOR YOU.", 11); + } + + textcolor(COLOR_GREEN); + center_msg("FIRE = PLAY AGAIN", 16); + textcolor(COLOR_WHITE); + center_msg("LEFT = QUIT", 17); + + while (!(JOY2 & JOY_PORT2_FIRE) || !(JOY2 & JOY_PORT2_LEFT)) {} + do { + joy = JOY2; + } while ((joy & JOY_PORT2_FIRE) && (joy & JOY_PORT2_LEFT)); + play_again = !(joy & JOY_PORT2_FIRE); + while (!(JOY2 & JOY_PORT2_FIRE) || !(JOY2 & JOY_PORT2_LEFT)) {} + } while (play_again); + + /* ---- Return to BASIC --------------------------------------- */ + SPENA = 0x00; + sid_silence_all(); + clrscr(); + bgcolor(6); /* restore C64 default blue */ + bordercolor(14); + + return 0; +} diff --git a/examples/moonlander.prg b/examples/moonlander.prg new file mode 100644 index 0000000..ac7883e Binary files /dev/null and b/examples/moonlander.prg differ diff --git a/examples/run.bat b/examples/run.bat new file mode 100644 index 0000000..cff3840 --- /dev/null +++ b/examples/run.bat @@ -0,0 +1,18 @@ +@echo off +setlocal + +set "VICE_EXE=C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" +set "PRG=%~dp0moonlander.prg" + +if not exist "%VICE_EXE%" ( + echo VICE x64sc was not found at: + echo %VICE_EXE% + exit /b 1 +) + +if not exist "%PRG%" ( + call "%~dp0moonlander-build.bat" + if errorlevel 1 exit /b 1 +) + +"%VICE_EXE%" -autostart "%PRG%" -autostart-warp -autostartprgmode 1 diff --git a/examples/sprite-music-demo.bas b/examples/sprite-music-demo.bas index b5cb8a2..c593fea 100644 --- a/examples/sprite-music-demo.bas +++ b/examples/sprite-music-demo.bas @@ -24,7 +24,7 @@ 1000 REM ===== INIT SID ===== 1010 FOR A=54272 TO 54296:POKE A,0:NEXT A : REM SILENCE ALL 1020 POKE 54296,15 : REM MAX VOLUME -1030 POKE 54272,0:POKE 54273,40 : REM VOICE 1 FREQ ~440Hz (A4) +1030 POKE 54272,0:POKE 54273,40 1040 POKE 54277,9 : REM ATTACK=0 DECAY=9 1050 POKE 54278,240 : REM SUSTAIN=15 RELEASE=0 1060 RETURN