| @@ -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\")" | |||||
| ] | |||||
| } | |||||
| } | |||||
| @@ -2,7 +2,11 @@ | |||||
| "permissions": { | "permissions": { | ||||
| "allow": [ | "allow": [ | ||||
| "Bash(build.bat)", | "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" | |||||
| ] | ] | ||||
| } | } | ||||
| } | } | ||||
| @@ -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 | ## VICE Debug Workflow | ||||
| When debugging a C64 program in VICE, agents should prefer this flow: | When debugging a C64 program in VICE, agents should prefer this flow: | ||||
| @@ -260,3 +286,11 @@ LDA #>my_irq ; high byte | |||||
| STA $0315 | STA $0315 | ||||
| CLI ; enable interrupts | 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 | |||||
| ``` | |||||
| @@ -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/<topic>/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,<high-byte> : 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 | |||||
| @@ -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 | |||||
| ) | |||||
| @@ -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 <stdlib.h> | |||||
| #include <conio.h> | |||||
| #include <c64.h> | |||||
| 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; | |||||
| } | |||||
| @@ -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 | |||||
| ) | |||||
| @@ -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 <c64.h> | |||||
| #include <conio.h> | |||||
| #include <string.h> | |||||
| #include <stdio.h> | |||||
| /* ---------------------------------------------------------------- | |||||
| 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; | |||||
| } | |||||
| @@ -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 | |||||
| @@ -24,7 +24,7 @@ | |||||
| 1000 REM ===== INIT SID ===== | 1000 REM ===== INIT SID ===== | ||||
| 1010 FOR A=54272 TO 54296:POKE A,0:NEXT A : REM SILENCE ALL | 1010 FOR A=54272 TO 54296:POKE A,0:NEXT A : REM SILENCE ALL | ||||
| 1020 POKE 54296,15 : REM MAX VOLUME | 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 | 1040 POKE 54277,9 : REM ATTACK=0 DECAY=9 | ||||
| 1050 POKE 54278,240 : REM SUSTAIN=15 RELEASE=0 | 1050 POKE 54278,240 : REM SUSTAIN=15 RELEASE=0 | ||||
| 1060 RETURN | 1060 RETURN | ||||
Powered by TurnKey Linux.