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