| @@ -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": { | |||
| "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" | |||
| ] | |||
| } | |||
| } | |||
| @@ -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 | |||
| ``` | |||
| @@ -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 ===== | |||
| 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 | |||
Powered by TurnKey Linux.