From c9034b3762ae8a4035dc6aed17504130cbfa779f Mon Sep 17 00:00:00 2001 From: Luxferre Date: Tue, 20 Jan 2026 10:55:26 +0200 Subject: [PATCH] published GB port source --- README.md | 1 + gb/Makefile | 8 + gb/ezgb.h | 232 +++++++++++++++++++++++ gb/gbscoundrel.c | 474 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 715 insertions(+) create mode 100644 gb/Makefile create mode 100644 gb/ezgb.h create mode 100644 gb/gbscoundrel.c diff --git a/README.md b/README.md index 8bf7b56..8766949 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This is a collection of my own ports of the [Scoundrel](http://stfj.net/art/2011 * [ANSI C89 port](c/scoundrel.c) - also compatible with CC65 and Z88DK (use the provided Makefile to build Scoundrel for Commodore 16/64/128/Plus-4/PET, Atari 400/800/XL/XE, ZX Spectrum, MSX and Apple II/IIe; a JRE installation on the host system is required to build Apple disk images) * [NES port](nes/nescoundrel.c) - gamepad-oriented, considered stable but some impovements may come +* [Game Boy port](gb/gbscoundrel.c) - gamepad-oriented, considered stable but some impovements may come ## Ports NOT included in this repo for various reasons diff --git a/gb/Makefile b/gb/Makefile new file mode 100644 index 0000000..b40d5d4 --- /dev/null +++ b/gb/Makefile @@ -0,0 +1,8 @@ +CFILES = gbscoundrel.c +OUTFILE = scoundrel.gb + +$(OUTFILE): $(CFILES) + zcc +gb -create-app -Os $(CFILES) -o $(OUTFILE) + +clean: + rm -f *.gb *.bin *.o diff --git a/gb/ezgb.h b/gb/ezgb.h new file mode 100644 index 0000000..74c1080 --- /dev/null +++ b/gb/ezgb.h @@ -0,0 +1,232 @@ +#ifndef EZGB_H +#define EZGB_H +/** + EZgb: a set of helpers for easy GB indie development in pure C + for Z88DK (in text mode) + + Created by Luxferre in 2026, released into public domain +*/ + +#include +#include +#include + +#define u8 unsigned char +#define u16 unsigned short +#define s8 signed char +#define s16 signed short + +/* Generic constants */ + +#define SCREEN_WIDTH 20 +#define SCREEN_HEIGHT 18 +#define SCREEN_CENTER_X (SCREEN_WIDTH >> 1) +#define SCREEN_CENTER_Y (SCREEN_HEIGHT >> 1) +#define NL "\n" + +#define EZJOY_UP 1 +#define EZJOY_DN 2 +#define EZJOY_LT 3 +#define EZJOY_RT 4 +#define EZJOY_A 5 +#define EZJOY_B 6 +#define EZJOY_SL 7 +#define EZJOY_ST 8 + +/* RNG stuff */ +static u16 _ez_entropy = 0; + +u16 ezrand() { + _ez_entropy = 1 + ((_ez_entropy >> 15) | (_ez_entropy << 1)); + return ((u16) rand()) ^ _ez_entropy; +} + +u8 joy_input_wait() { + u8 last_joy = 0, joy, pressed; + /* wait until the previous key is released */ + while(joypad()); + /* wait for a new press */ + while(1) { + ++_ez_entropy; + joy = joypad(); + pressed = joy & ~last_joy; + if(pressed & J_UP) return EZJOY_UP; + if(pressed & J_DOWN) return EZJOY_DN; + if(pressed & J_LEFT) return EZJOY_LT; + if(pressed & J_RIGHT) return EZJOY_RT; + if(pressed & J_A) return EZJOY_A; + if(pressed & J_B) return EZJOY_B; + if(pressed & J_SELECT) return EZJOY_SL; + if(pressed & J_START) return EZJOY_ST; + last_joy = joy; + wait_vbl_done(); + } +} + +/* wait until Start is pressed */ +void wait_for_start() { + u8 joy; + while(1) { + ++_ez_entropy; + joy = joypad(); + if(joy & J_START) break; + wait_vbl_done(); + } +} + +/* randomize until Start is pressed */ +void wait_for_start_rand() { + u8 joy; +#ifdef _randomize + _randomize(); +#endif + _ez_entropy = rand(); + while(1) { + ++_ez_entropy; + joy = joypad(); + if(joy & J_START) break; + wait_vbl_done(); + } + srand(_ez_entropy); +} + +/* Sound methods */ + +void init_sound() { + NR52_REG = 0x80; + NR50_REG = 0x77; + NR51_REG = 0xFF; +} + +u16 get_timer_by_freq(u16 freq) { + return (u16) (2048 - (131072 / freq)); +} + +void play_pulse(u8 channel, u16 freq, u16 duration) { + u16 tm = get_timer_by_freq(freq); + + if(channel == 0) { + NR10_REG = 0x00; + NR11_REG = 0x80 | duration; + NR12_REG = 0xF3; + NR13_REG = tm & 255; + NR14_REG = 0xC0 | (tm >> 8); + } else { + NR21_REG = 0x80 | duration; + NR22_REG = 0xF3; + NR23_REG = tm & 255; + NR24_REG = 0xC0 | (tm >> 8); + } +} + +void play_jingle(u16 n1f1, + u16 n1f2, + u16 n2f1, + u16 n2f2, + u16 n3f1, + u16 n3f2) { + u16 i, dl = 2600; + play_pulse(0, n1f1, 1); + play_pulse(1, n1f2, 1); + for(i=0;i 1) { + for(i=n-1;i>0;--i) { + j = (u16) (ezrand()%(i+1)); + t = array[j]; + array[j] = array[i]; + array[i] = t; + } + } +} + +/* Delay by n cycles */ +void delay_cycles(u16 n) { + u16 i; + for(i=0;i= 0x30 && s[i] < 0x3A) + s[i] -= 0x20; + } +} + +void cputcxy(u8 x, u8 y, char c) { + gotoxy(x, y); + cputc(c); +} + +void cputsxy(u8 x, u8 y, char *s) { + gotoxy(x, y); + cprintf("%s", s); +} + +void chlinexy(u8 x, u8 y, u8 l) { + u8 i; + gotoxy(x, y); + for(i=0;i +#include +#include +#include "ezgb.h" + +/* Sound section */ + +void beep_start() { + play_jingle(220, 440, 440, 660, 660, 880); +} +void beep_interact() { + play_jingle(440, 660, 220, 880, 440, 660); +} +void beep_enemy() { + play_jingle(440, 220, 220, 110, 110, 110); +} +void beep_weapon() { + play_jingle(660, 880, 440, 880, 660, 880); +} +void beep_potion() { + play_jingle(440, 660, 660, 990, 660, 880); +} +void beep_run() { + play_jingle(660, 880, 440, 660, 220, 440); +} +void beep_win() { + play_jingle(55, 110, 110, 220, 220, 440); + delay_cycles(6000); + play_jingle(440, 660, 660, 880, 880, 1760); + delay_cycles(6000); + play_jingle(880, 660, 660, 880, 880, 1760); +} +void beep_lose() { + play_jingle(110, 55, 220, 110, 110, 110); + delay_cycles(6000); + play_jingle(110, 110, 110, 110, 110, 110); +} +void beep_err() { + play_jingle(330, 220, 330, 220, 330, 220); +} + +/* print helper utils */ + +static char numbuf[6]; +void printnum(s16 num) { + memset(numbuf, 0, sizeof(numbuf)); + snprintf(numbuf, sizeof(numbuf), "%02d", num); + convdigits(numbuf, sizeof(numbuf)); + cprintf("%s", numbuf); +} + +/* Main game logic */ + +void game_startup() { + u8 i, j, x = 0; + termreset(); + DISPLAY_OFF; + cputs( " " NL +" \x0e\x0e\x0e\x1d \x0e\x0e\x0e\x1d \x0e\x0e\x0e" NL +" \x0f \x0f \x0f \x0f" NL +" \x0e+\x0e \x0f + \x0f + \x0f" NL +" \x0f \x0f \x0f \x0f" NL +" \x1e\x0e\x0e\x0e \x0e\x0e\x0e\x1f \x0e\x0e\x0e " NL + +" \x1d \x1c \x1c \x1c \x1c\x0e\x0e\x0e" NL +" \x0f \x0f \x0f+ \x0f \x0f \x0f" NL +" \x0f + \x0f \x0f + \x0f + \x0f" NL +" \x0f \x0f \x0f +\x0f \x0f \x0f" NL +" \x0e\x0e\x0e \x1f \x1f \x1e\x0e\x0e\x0e" NL + +" \x1c\x0e\x0e\x0e \x1c\x0e\x0e\x0e\x1d \x1c\x1d" NL +" \x0f \x0f \x0f \x0f\x1f" NL +" \x0f\x0e+\x0e \x0f\x0e+\x0e \x0f +" NL +" \x0f + \x0f \x0f \x1c\x1d" NL +" \x0f + \x1e\x0e\x0e\x0e\x1f \x1e\x0e\x0e\x0e\x1f"); + cputsxy(1, SCREEN_HEIGHT - 1, "Luxferre"); + cputsxy(SCREEN_WIDTH-5, SCREEN_HEIGHT - 1, "2026"); + DISPLAY_ON; + wait_vbl_done(); + wait_for_start_rand(); + beep_start(); + termreset(); +} + +/* Win screen display */ +void win_screen(s16 score) { + u8 i, j; + termreset(); + cputs( \ +" \x1d \x1c \x0e\x0e\x0e \x1c \x1c" NL +" \x0f \x0f \x0f \x0f+ \x0f" NL +" \x0f + \x0f + \x0f + \x0f" NL +" \x0f+ +\x0f \x0f \x0f +\x0f" NL +" \x1e\x1f \x1e\x1f \x0e\x0e\x0e \x1f \x1f"); + cputsxy(2, 6, "Congratulations!"); + cputsxy(0, 7, " You made it across"); + cputsxy(0, 8, " the dangerous"); + cputsxy(0, 9, " dungeon alive!"); + gotoxy(0, 11); + cprintf(" Your score: "); + printnum(score); + chlinexy(0, 12, SCREEN_WIDTH); + cputs(" By: Luxferre, 2026"); + cputs(NL NL " Orig: Zach Gage," NL " Kurt Bieg, 2011"); + wait_vbl_done(); + beep_win(); + wait_for_start(); +} + +/* UI choices */ +#define CH1 0 +#define CH2 1 +#define CH3 2 +#define CH4 3 +#define CH_R 4 +#define CH_Q 5 +#define CH_INVAL 6 + +/** +Deck/room representation: +0 is empty (this value cannot be in the deck, only in the room), +1 to 13 is a monster, +14 to 22 is a weapon, +23 to 31 is a potion + +Monster level: x + 1 +Weapon level: x - 12 +Potion level: x - 21 +*/ + +/* Game stats */ + +/* main deck and room */ +static s8 deck[44], room[] = {0, 0, 0, 0}; + +/* status message */ +static char status_msg[(SCREEN_WIDTH)]; +static char bufmsg[(SCREEN_WIDTH)]; + +/* tracking variables */ +static s16 hp = 20; +static u8 deck_idx = 0, rooms_cleared = 0, + weapon = 0, durability = 0, can_run = 1, + engaged = 0, potion_drank = 0, last_potion_val = 0; + +/* Input method */ +/* up 1, left 2, right 3, down 4, a run, start quit */ +u8 get_choice() { + switch(joy_input_wait()) { + case EZJOY_UP: return CH1; + case EZJOY_LT: return CH2; + case EZJOY_RT: return CH3; + case EZJOY_DN: return CH4; + case EZJOY_ST: return CH_Q; + default: return CH_R; + } +} + +/* Draw a room character according to the index and value */ +void render_room_item(u8 i, u8 v) { + u8 topleft_x, topleft_y, cheight=4, cwidth=6; + if(i == 0) { + topleft_x = 7; + topleft_y = 1; + } else if(i == 1) { + topleft_x = 0; + topleft_y = 6; + } else if(i == 2) { + topleft_x = 14; + topleft_y = 6; + } else if(i == 3) { + topleft_x = 7; + topleft_y = 11; + } + if(v == 0) {} /* don't render anything */ + else { + /* draw the rectangle */ + rect(topleft_x, topleft_y, cwidth, cheight); + gotoxy(topleft_x + 1, topleft_y + 1); + /* draw text */ + if(v < 14) { + cputs("ENMY"); + } else if(v < 23) { + cputs("WEAP"); + } else { + cputs("POTI"); + } + gotoxy(topleft_x + 2, topleft_y + 2); + if(v < 14) printnum(v + 1); + else if(v < 23) printnum(v - 12); + else printnum(v - 21); + } +} + +/* Render main game screen */ +void render_main_scene() { + u8 i, deck_count = 0; + for(i=0;i 1) { + switch(joy_input_wait()) { + case EZJOY_A: res = 1; break; + case EZJOY_B: res = 0; break; + default: res = 2; + } + } + beep_interact(); + return res; +} + +/* Output method */ +void print_msg(char *s) { + memset(status_msg, 0, sizeof(status_msg)); + strncpy(status_msg, s, sizeof(status_msg)); + render_main_scene(); +} + +/* Room item handling methods */ + +s16 handle_monster(s16 val) { + s16 healthlost = val; + char monster_msg[(SCREEN_WIDTH-1)]; + beep_enemy(); + if(weapon > 0 && val <= durability) { + s16 weapondiff = val - weapon; + if(weapondiff < 0) weapondiff = 0; + snprintf(monster_msg, sizeof(status_msg), "(%d) Use weapon?", val); + if(confirm(monster_msg)) { + healthlost = weapondiff; + durability = val - 1; + if(durability < 2) { + durability = weapon = 0; + } + } + } + hp -= healthlost; + if(hp < 0) hp = 0; + snprintf(status_msg, sizeof(status_msg), "Lost %d HP", healthlost); + return val; +} + +void handle_weapon(u8 val) { + weapon = val; + snprintf(status_msg, sizeof(status_msg), "Equipped %d-lvl wpn", weapon); + durability = 14; /* reset durability to max limit */ + beep_weapon(); +} + +void handle_potion(u8 val) { + if(potion_drank) { + print_msg("Potion broken!"); + beep_err(); + } else { + potion_drank = 1; + hp += val; + if(hp > 20) hp = 20; + snprintf(status_msg, sizeof(status_msg), "Drank %d-lvl potion", val); + if(deck_idx >= sizeof(deck) || deck[deck_idx] == 0) + last_potion_val = val; + beep_potion(); + } +} + +/* Main gameplay */ +s16 game() { + u16 i; + s16 score = -208; /* score storage */ + s16 item, room_sum; /* true item value */ + s16 last_room = 0, last_room_clr = 0; /* last room markers */ + u8 ch; + deck_idx = 0; /* reset the deck index */ + /* Prefill the deck */ + for(i=0;i<13;i++) deck[i] = deck[i+13] = i+1; + for(i=26;i<44;i++) deck[i] = i - 12; + /* Shuffle the deck */ + shuffle(deck, sizeof(deck)); + /* prepare all other parameters */ + hp = 20; + can_run = 1; + weapon = durability = engaged = rooms_cleared = 0; + potion_drank = last_potion_val = 0; + for(i=0;i 0 && !last_room_clr) { + /* fill in the new room unless engaged */ + if(!engaged) { + for(i=0;i= sizeof(deck) || deck[deck_idx] == 0) { + last_room = 1; + can_run = 0; + } else { + room[i] = deck[deck_idx]; + deck[deck_idx] = 0; /* remove from deck */ + deck_idx++; + /* Check immediately if that was the last card */ + if(deck_idx >= sizeof(deck) || deck[deck_idx] == 0) { + last_room = 1; + can_run = 0; + } + } + } + } + } + + /* display the stats and room */ + render_main_scene(); + ch = get_choice(); + + /* Logic validation */ + if(ch == CH_R) { + if(!can_run || engaged) ch = CH_INVAL; /* Cannot run if disabled or engaged */ + } else if(ch < CH_R && !room[(int)ch]) { + ch = CH_INVAL; /* empty item */ + } + + if(ch == CH_Q) { + hp = 0; + break; + } else if(ch == CH_INVAL) { + print_msg("Invalid input!"); + beep_err(); + continue; + } else if(ch == CH_R) { /* run mechanics */ + /* Compact and rearrange the deck */ + char temp_deck[sizeof(deck)]; + s16 t_idx = 0; + + /* 1. Collect all valid remaining cards from deck */ + for(i=0; i 0) { + if(room_sum == 0) { + if(last_room) last_room_clr = 1; + else { + engaged = 0; + potion_drank = 0; + can_run = 1; + rooms_cleared++; + } + } else if(!last_room && engaged == 3) { + engaged = 0; + potion_drank = 0; + can_run = 1; + rooms_cleared++; + } + } + } + } + /* Score the result */ + if(hp > 0) { /* win */ + score = hp; + if(hp == 20) score += last_potion_val; + } + return score; +} + +/* Entry point */ +s16 main() { + s16 score; + init_sound(); + while(1) { + termreset(); + game_startup(); + while(1) { + score = game(); + if(score > 0) { + win_screen(score); + break; + } else { + beep_lose(); + snprintf(bufmsg, sizeof(bufmsg), "(%d) Restart?", score); + if(!confirm(bufmsg)) break; + } + } + wait_vbl_done(); + } + return 0; +}