From fa2bfd4cf70926f619a97a17573f93e8f86c6060 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Thu, 8 Jan 2026 17:22:53 +0200 Subject: [PATCH] NES porting started --- README.md | 2 +- nes/Makefile | 9 + nes/nescoundrel.c | 504 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 nes/Makefile create mode 100644 nes/nescoundrel.c diff --git a/README.md b/README.md index b3cde69..8dbef84 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This is a collection of my own ports of the [Scoundrel](http://stfj.net/art/2011 ## Ports included in this repo * [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, not everything works yet (sound, color etc) ## Ports NOT included in this repo for various reasons @@ -28,7 +29,6 @@ This is a collection of my own ports of the [Scoundrel](http://stfj.net/art/2011 ### Ultimate goals * CHIP-8 (standard mode) -* NES (CC65, text/pseudo-graphics mode) * Sega Mega Drive (SGDK) ### Hypothetical ports diff --git a/nes/Makefile b/nes/Makefile new file mode 100644 index 0000000..c777e9f --- /dev/null +++ b/nes/Makefile @@ -0,0 +1,9 @@ + +CFILES = nescoundrel.c +OUTFILE = scoundrel.nes + +$(OUTFILE): $(CFILES) + cl65 -t nes -O -Os $(CFILES) -o $(OUTFILE) + +clean: + rm -f $(OUTFILE) *.o diff --git a/nes/nescoundrel.c b/nes/nescoundrel.c new file mode 100644 index 0000000..9471506 --- /dev/null +++ b/nes/nescoundrel.c @@ -0,0 +1,504 @@ +/** + nescoundrel.c: a NES port of the Scoundrel roguelike card game + using the official ruleset only + Created by Luxferre in 2026, released into public domain +*/ + +#include +#include +#include +#include +#include +#include + +#define SCREEN_WIDTH 32 +#define SCREEN_HEIGHT 28 +#define SCREEN_CENTER_X (SCREEN_WIDTH >> 1) +#define SCREEN_CENTER_Y (SCREEN_HEIGHT >> 1) + +#define KEY_UP 1 +#define KEY_DN 2 +#define KEY_LT 3 +#define KEY_RT 4 +#define KEY_A 5 +#define KEY_B 6 +#define KEY_SL 7 +#define KEY_ST 8 + +int joy_input_wait() { + unsigned char last_joy = 0, joy, pressed; + /* wait until the previous key is released */ + while(joy_read(JOY_1)); + /* wait for a new press */ + while(1) { + joy = joy_read(JOY_1); + pressed = joy & ~last_joy; + if(pressed & JOY_UP_MASK) return KEY_UP; + if(pressed & JOY_DOWN_MASK) return KEY_DN; + if(pressed & JOY_LEFT_MASK) return KEY_LT; + if(pressed & JOY_RIGHT_MASK) return KEY_RT; + if(pressed & JOY_BTN_1_MASK) return KEY_A; + if(pressed & JOY_BTN_2_MASK) return KEY_B; + if(pressed & JOY_BTN_3_MASK) return KEY_SL; + if(pressed & JOY_BTN_4_MASK) return KEY_ST; + last_joy = joy; + waitvsync(); + } +} + +void game_startup() { + unsigned int entropy; + unsigned char joy; +#ifdef _randomize + _randomize(); +#endif + entropy = rand(); + clrscr(); + bgcolor(COLOR_BLACK); + textcolor(0); + gotoxy(7, 2); + cputs("Luxferre presents"); + gotoxy(0, 3); + cputs( \ +" _______ _______ _______ \r\n" \ +" | _ | _ | _ |\r\n" \ +" | 1___|. 1___|. | |\r\n" \ +" |____ |. |___|. | |\r\n" \ +" |: 1 |: 1 |: 1 |\r\n" \ +" |::.. . |::.. . |::.. . |\r\n" \ +" `-------`-------`-------'\r\n"); + cputs( \ +" ___ ___ ______ ______\r\n" \ +" | Y | _ \\| _ \\ \r\n" \ +" |. | |. | |. | \\ \r\n" \ +" |. | |. | |. | \\\r\n" \ +" |: 1 |: | |: 1 /\r\n" \ +" |::.. . |::.| |::.. . / \r\n" \ +" `-------`--- ---`------' \r\n"); + cputs( \ +" _______ _______ ___ \r\n" \ +" | _ | _ | | \r\n" \ +" |. l |. 1___|. | \r\n" \ +" |. _ |. __)_|. |___ \r\n" \ +" |: | |: 1 |: 1 |\r\n" \ +" |::.|:. |::.. . |::.. . |\r\n" \ +" `--- ---`-------`-------'"); + gotoxy(SCREEN_CENTER_X - 6, SCREEN_HEIGHT - 3); + cputs("Press Start"); + gotoxy(SCREEN_WIDTH-5, SCREEN_HEIGHT - 2); + cputs("2026"); + joy_install(joy_static_stddrv); + while(1) { + entropy++; + joy = joy_read(JOY_1); + if(JOY_START(joy)) break; + waitvsync(); + } + srand(entropy); + clrscr(); +} + +/* Win screen display */ +void win_screen(int score) { + clrscr(); + gotoxy(0, 3); + cputs(" ___ ___ ___ ______ \r\n" \ +" | Y | | _ \\ \r\n" \ +" |. | |. |. | |\r\n" \ +" |. / \\ |. |. | |\r\n" \ +" |: |: |: | |\r\n" \ +" |::.|:. |::.|::.| |\r\n" \ +" `--- ---`---`--- ---'"); + gotoxy(0, 12); + cputs(" Congratulations! You made it\r\n through the dangerous\r\n dungeon alive!\r\n\r\n"); + cprintf(" Your score: %d\r\n\r\n", score); + chlinexy(0, 17, SCREEN_WIDTH); + cputs("Created by Luxferre in 2026\r\nReleased into public domain\r\n"); + cputs("\r\nOriginal concept by Zach Gage\r\nand Kurt Bieg, 2011"); + chlinexy(0, 23, SCREEN_WIDTH); + cputsxy(10, 25, "Press Start"); + while(joy_input_wait() != KEY_ST); +} + +/* 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 char deck[44], room[] = {0, 0, 0, 0}; + +/* status message */ +static char status_msg[(SCREEN_WIDTH-1)]; + +/* tracking variables */ +static short deck_idx = 0, hp = 20, 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 */ +char get_choice() { + switch(joy_input_wait()) { + case KEY_UP: return CH1; + case KEY_LT: return CH2; + case KEY_RT: return CH3; + case KEY_DN: return CH4; + case KEY_ST: return CH_Q; + default: return CH_R; + } +} + +/* Draw a room character according to the index and value */ +void render_room_item(short i, short v) { + short topleft_x, topleft_y, + topright_x, bottomleft_y, + text_x, text_y, cheight=7, cwidth=8, left_offset = 9; + + if(i == 0) { + topleft_x = 9; + topleft_y = 3; + } else if(i == 1) { + topleft_x = 2; + topleft_y = 9; + } else if(i == 2) { + topleft_x = 16; + topleft_y = 9; + } else if(i == 3) { + topleft_x = 9; + topleft_y = 15; + } + topright_x = topleft_x + cwidth - 1; + bottomleft_y = topleft_y + cheight - 1; + text_x = topleft_x + 1; + text_y = topleft_y + 2; + if(v == 0) {} /* don't render anything */ + else { + /* draw the rectangle */ + chlinexy(topleft_x + 1, topleft_y, cwidth - 2); + chlinexy(topleft_x + 1, bottomleft_y, cwidth - 2); + cvlinexy(topleft_x, topleft_y + 1, cheight - 2); + cvlinexy(topright_x, topleft_y + 1, cheight - 2); + /* draw text */ + gotoxy(text_x, text_y); + if(v < 14) cputs("ENEMY "); + else if(v < 23) cputs("WEAPON"); + else cputs("POTION"); + gotoxy(text_x, text_y+2); + if(v < 14) cprintf("LVL %02d", v + 1); + else if(v < 23) cprintf("LVL %02d", v - 12); + else cprintf("LVL %02d", v - 21); + } +} + +/* Render main game screen */ +void render_main_scene() { + short i, deck_count = 0, statbase = SCREEN_WIDTH - 7; + short room_start_x = 1, + room_start_y = 1, + room_end_x = statbase - 8, + room_end_y = SCREEN_HEIGHT - 4, + room_mid_x = room_start_x + ((room_end_x - room_start_x)>>1) - 1, + room_mid_y = room_start_y + ((room_end_y - room_start_y)>>1) - 1; + for(i=0;i 1) { + switch(joy_input_wait()) { + case KEY_A: res = 1; break; + case KEY_B: res = 0; break; + default: res = 2; + } + } + 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(); +} + +/* Array shuffle method */ +void shuffle(char *array, size_t n) { + unsigned int i, j, t; + if(n > 1) { + for(i=n-1;i>0;i--) { + j = (unsigned int) (rand()%(i+1)); + t = array[j]; + array[j] = array[i]; + array[i] = t; + } + } +} + +/* Room item handling methods */ + +void handle_monster(int val) { + int healthlost = val; + char monster_msg[(SCREEN_WIDTH-1)]; + if(weapon > 0 && val <= durability) { + int weapondiff = val - weapon; + if(weapondiff < 0) weapondiff = 0; + snprintf(monster_msg, sizeof(status_msg), "%d-level monster: use weapon?", val); + if(confirm(monster_msg)) { + healthlost = weapondiff; + durability = val - 1; + if(durability < 2) { + durability = weapon = 0; + } + } + } else { + snprintf(status_msg, sizeof(status_msg), "You fight a %d-level monster.", val); + } + hp -= healthlost; + if(hp < 0) hp = 0; + snprintf(status_msg, sizeof(status_msg), "You lost %d HP. New HP: %d", healthlost, hp); +} + +void handle_weapon(int val) { + weapon = val; + snprintf(status_msg, sizeof(status_msg), "You equip a %d-level weapon.", weapon); + durability = 14; /* reset durability to max limit */ +} + +void handle_potion(int val) { + if(potion_drank) { + print_msg("Already drank a potion here."); + } else { + potion_drank = 1; + hp += val; + if(hp > 20) hp = 20; + snprintf(status_msg, sizeof(status_msg), "You drink a %d-level potion.", val); + if(deck_idx >= sizeof(deck) || deck[deck_idx] == 0) + last_potion_val = val; + } +} + +/* Main gameplay */ +int game() { + unsigned int i; + int score = 0; /* score storage */ + int item, room_sum; /* true item value */ + int last_room = 0, last_room_clr = 0; /* last room markers */ + char 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!"); + continue; + } else if(ch == CH_R) { /* run mechanics */ + /* Compact and rearrange the deck */ + char temp_deck[sizeof(deck)]; + int 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; + } else { /* fail */ + for(i=0;i 0) { + win_screen(score); + break; + } else { + snprintf(status_msg, sizeof(status_msg), "RIP! Score: %d. Restart?", score); + if(!confirm(status_msg)) break; + } + } + return 0; +}