/* 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; }