Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33b640f9ea | ||
|
|
cc15e10652 | ||
|
|
4d7c6aeb2f | ||
|
|
3ef673cf21 | ||
|
|
b7ddaf3c17 | ||
|
|
587801bb59 | ||
|
|
9af0374c93 | ||
|
|
833c57631b | ||
|
|
69c3f46640 | ||
|
|
c9034b3762 | ||
|
|
12a5d39c54 | ||
|
|
7c2a0b24ed | ||
|
|
af08983f56 | ||
|
|
fc6a230af7 | ||
|
|
7f2cab0b5e | ||
|
|
c6a026846a | ||
|
|
e8156e49e5 | ||
|
|
eb3feaf34c | ||
|
|
f1eecc00f1 | ||
|
|
a4f54caaec |
@@ -6,11 +6,16 @@ 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)
|
* [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
|
* [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
|
||||||
|
* [Janet port](janet/scoundrel.janet) - close to C89 port but not fully identical
|
||||||
|
* [Fennel port](fennel/scoundrel.fnl) - functionally identical to the Janet port
|
||||||
|
* [Jim Tcl port](scoundrel-jim.tcl) - functionally close to the Bash port
|
||||||
|
|
||||||
## Ports NOT included in this repo for various reasons
|
## Ports NOT included in this repo for various reasons
|
||||||
|
|
||||||
* [(Circuit)Python port](https://codeberg.org/luxferre/t-deckard/src/branch/master/game/scoundrel.py) (as a part of the T-DeckARD suite)
|
* [(Circuit)Python port](https://codeberg.org/luxferre/t-deckard/src/branch/master/game/scoundrel.py) (as a part of the T-DeckARD suite)
|
||||||
* [HTML+CSS+JS port](https://ironlynx.neocities.org/scoundrel) (hosted as a single HTML file with readable source code)
|
* [HTML+CSS+JS port](https://ironlynx.neocities.org/scoundrel) (hosted as a single HTML file with readable source code)
|
||||||
|
* [Bash shell port](https://codeberg.org/luxferre/bash-goodies/src/branch/master/scoundrel)
|
||||||
|
|
||||||
## Currently planned ports
|
## Currently planned ports
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ Z80BUILD=zcc
|
|||||||
BUILDDIR=build
|
BUILDDIR=build
|
||||||
|
|
||||||
$(PROJNAME): bdir
|
$(PROJNAME): bdir
|
||||||
$(CC) -std=c89 -O2 -o $(BUILDDIR)/scoundrel $(CFILES)
|
$(CC) -std=c89 -O2 -o $(BUILDDIR)/$(PROJNAME) $(CFILES)
|
||||||
|
|
||||||
bdir:
|
bdir:
|
||||||
mkdir -p $(BUILDDIR)
|
mkdir -p $(BUILDDIR)
|
||||||
|
|||||||
+4
-10
@@ -163,7 +163,7 @@ void shuffle(char *array, size_t n) {
|
|||||||
|
|
||||||
/* Room item handling methods */
|
/* Room item handling methods */
|
||||||
|
|
||||||
void handle_monster(int val) {
|
int handle_monster(int val) {
|
||||||
int healthlost = val;
|
int healthlost = val;
|
||||||
printf("You fight a %d-level monster.\n", val);
|
printf("You fight a %d-level monster.\n", val);
|
||||||
if(weapon > 0 && val <= durability) {
|
if(weapon > 0 && val <= durability) {
|
||||||
@@ -184,6 +184,7 @@ void handle_monster(int val) {
|
|||||||
hp -= healthlost;
|
hp -= healthlost;
|
||||||
if(hp < 0) hp = 0;
|
if(hp < 0) hp = 0;
|
||||||
printf("You lost %d HP. New HP: %d.\n", healthlost, hp);
|
printf("You lost %d HP. New HP: %d.\n", healthlost, hp);
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_weapon(int val) {
|
void handle_weapon(int val) {
|
||||||
@@ -208,7 +209,7 @@ void handle_potion(int val) {
|
|||||||
/* Main gameplay */
|
/* Main gameplay */
|
||||||
int game() {
|
int game() {
|
||||||
unsigned int i;
|
unsigned int i;
|
||||||
int score = 0; /* score storage */
|
int score = -208; /* score storage */
|
||||||
int item, room_sum; /* true item value */
|
int item, room_sum; /* true item value */
|
||||||
int last_room = 0, last_room_clr = 0; /* last room markers */
|
int last_room = 0, last_room_clr = 0; /* last room markers */
|
||||||
char ch;
|
char ch;
|
||||||
@@ -306,7 +307,7 @@ int game() {
|
|||||||
/* Note: We do NOT put the item back in deck. It is consumed. */
|
/* Note: We do NOT put the item back in deck. It is consumed. */
|
||||||
|
|
||||||
if(item < 14) { /* monster */
|
if(item < 14) { /* monster */
|
||||||
handle_monster(item + 1);
|
score += handle_monster(item + 1);
|
||||||
} else if(item < 23) { /* weapon */
|
} else if(item < 23) { /* weapon */
|
||||||
handle_weapon(item - 12);
|
handle_weapon(item - 12);
|
||||||
} else { /* potion */
|
} else { /* potion */
|
||||||
@@ -339,13 +340,6 @@ int game() {
|
|||||||
if(hp > 0) { /* win */
|
if(hp > 0) { /* win */
|
||||||
score = hp;
|
score = hp;
|
||||||
if(hp == 20) score += last_potion_val;
|
if(hp == 20) score += last_potion_val;
|
||||||
} else { /* fail */
|
|
||||||
for(i=0;i<sizeof(room);i++)
|
|
||||||
if(room[i] && room[i] < 14)
|
|
||||||
score -= (room[i] + 1);
|
|
||||||
for(i=0;i<sizeof(deck);i++)
|
|
||||||
if(deck[i] && deck[i] < 14)
|
|
||||||
score -= (deck[i] + 1);
|
|
||||||
}
|
}
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env fennel
|
||||||
|
;; Scoundrel roguelike game port to Fennel
|
||||||
|
;; Created by Luxferre in 2026, released into public domain
|
||||||
|
|
||||||
|
; helper method section
|
||||||
|
|
||||||
|
; RNG helpers
|
||||||
|
(fn randint [m] (math.random 0 (- m 1)))
|
||||||
|
|
||||||
|
; Shuffle helper
|
||||||
|
(fn shuffle [vec]
|
||||||
|
(local l (- (length vec) 1)) ; because for is inclusive
|
||||||
|
(var j 0)
|
||||||
|
(var t 0)
|
||||||
|
(for [i 1 l]
|
||||||
|
(set j (+ 1 (randint (+ 1 i))))
|
||||||
|
(set t (. vec (+ 1 i)))
|
||||||
|
(tset vec (+ 1 i) (. vec j))
|
||||||
|
(tset vec j t)))
|
||||||
|
|
||||||
|
; Read a single-letter choice from the stdin
|
||||||
|
(fn get-choice [prompt]
|
||||||
|
(io.write prompt)
|
||||||
|
(let [rawc (io.read)]
|
||||||
|
(if (> (length rawc) 0)
|
||||||
|
(string.lower (rawc:sub 1 1))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
; prin/prinf/printf emulation
|
||||||
|
(fn prin [...] (io.write ...))
|
||||||
|
(fn prinf [fmt ...]
|
||||||
|
(io.write (fmt:format ...)))
|
||||||
|
(fn printf [fmt ...]
|
||||||
|
(io.write (fmt:format ...))
|
||||||
|
(print))
|
||||||
|
|
||||||
|
; Ask user for y/n confirmation (with custom prompt)
|
||||||
|
(fn get-confirm [prompt]
|
||||||
|
(var resp nil)
|
||||||
|
(var valid false)
|
||||||
|
(while (not valid)
|
||||||
|
(set resp (get-choice prompt))
|
||||||
|
(set valid (case resp "y" true "n" true _ false)))
|
||||||
|
(= resp "y"))
|
||||||
|
|
||||||
|
; data section
|
||||||
|
|
||||||
|
; The Deck
|
||||||
|
(local deck (table.move (fcollect [i 1 13] i) 1 13 32 (fcollect [i 1 31] i)))
|
||||||
|
|
||||||
|
; The Room
|
||||||
|
(local room {})
|
||||||
|
|
||||||
|
; The State
|
||||||
|
(local state {
|
||||||
|
:score -208
|
||||||
|
:hp 20
|
||||||
|
:can-run true
|
||||||
|
:engaged false
|
||||||
|
:drank false
|
||||||
|
:weapon 0
|
||||||
|
:durability 0
|
||||||
|
:rooms-cleared 0
|
||||||
|
})
|
||||||
|
|
||||||
|
; last potion value (for special scoring)
|
||||||
|
(var last-potion-val 0)
|
||||||
|
|
||||||
|
; game logic section
|
||||||
|
|
||||||
|
; display the current stats and room
|
||||||
|
(fn game-field-display []
|
||||||
|
(print "--------------------------")
|
||||||
|
(printf "H%02d | W%02d | D%02d | Deck: %02d"
|
||||||
|
(. state :hp)
|
||||||
|
(. state :weapon)
|
||||||
|
(. state :durability)
|
||||||
|
(length deck))
|
||||||
|
(print "--------------------------")
|
||||||
|
(each [_ x (ipairs room)] (if
|
||||||
|
(< x 14) (prinf "M%02d " (+ x 1))
|
||||||
|
(< x 23) (prinf "W%02d " (- x 12))
|
||||||
|
(prinf "P%02d " (- x 21))))
|
||||||
|
(prin "\n 1 2 3 4")
|
||||||
|
(if (and (. state :can-run) (not (. state :engaged))) (prin " r)un->"))
|
||||||
|
(print))
|
||||||
|
|
||||||
|
; get valid user's choice
|
||||||
|
(fn get-valid-choice []
|
||||||
|
(var valid false)
|
||||||
|
(var ch "")
|
||||||
|
(while (not valid)
|
||||||
|
(set ch (get-choice "Your choice: "))
|
||||||
|
(set valid (if
|
||||||
|
(= ch "r")
|
||||||
|
(and (. state :can-run) (not (. state :engaged)))
|
||||||
|
(or (= ch "1") (= ch "2") (= ch "3") (= ch "4"))
|
||||||
|
(<= (tonumber ch) (length room))
|
||||||
|
false)))
|
||||||
|
ch)
|
||||||
|
|
||||||
|
; monster handling logic (adds monster value to the score)
|
||||||
|
(fn handle-monster [val]
|
||||||
|
(var healthlost val)
|
||||||
|
(printf "You encounter a monster of strength %d." val)
|
||||||
|
(if (and (> (. state :weapon) 0) (>= (. state :durability) val))
|
||||||
|
(do ; can use weapon
|
||||||
|
(if (get-confirm "Use weapon? (y/n) ")
|
||||||
|
(do
|
||||||
|
(var weapondiff (- val (. state :weapon)))
|
||||||
|
(if (< weapondiff 0) (set weapondiff 0))
|
||||||
|
(printf "You fight the monster with your %d-level weapon." (. state :weapon))
|
||||||
|
(set healthlost weapondiff)
|
||||||
|
(tset state :durability (- val 1))
|
||||||
|
(if (< (. state :durability) 2) (do
|
||||||
|
(tset state :weapon 0)
|
||||||
|
(tset state :durability 0))))
|
||||||
|
(print "You fight the monster barehanded.")))
|
||||||
|
( ; barehanded encounter
|
||||||
|
print "You fight the monster barehanded."
|
||||||
|
))
|
||||||
|
(tset state :hp (- (. state :hp) healthlost))
|
||||||
|
(if (< (. state :hp) 0) (tset state :hp 0))
|
||||||
|
(printf "Lost %d HP. New HP: %d" healthlost (. state :hp))
|
||||||
|
val)
|
||||||
|
|
||||||
|
; weapon handling logic (adds 0 score)
|
||||||
|
(fn handle-weapon [val]
|
||||||
|
(tset state :weapon val)
|
||||||
|
(tset state :durability 14)
|
||||||
|
(printf "Equipped a %d-level weapon." val)
|
||||||
|
0)
|
||||||
|
|
||||||
|
; potion handling logic (adds 0 score)
|
||||||
|
(fn handle-potion [val]
|
||||||
|
(if (. state :drank)
|
||||||
|
(print "Already drank a potion here! Discarding...")
|
||||||
|
(do
|
||||||
|
(if (and (= (length room) 0) (= (length deck) 0))
|
||||||
|
(set last-potion-val val))
|
||||||
|
(tset state :hp (+ (. state :hp) val))
|
||||||
|
(if (> (. state :hp) 20) (tset state :hp 20))
|
||||||
|
(tset state :drank true)
|
||||||
|
(printf "Drank a %d-level potion. New HP: %d" val (. state :hp))))
|
||||||
|
0)
|
||||||
|
|
||||||
|
; entrypoint
|
||||||
|
|
||||||
|
(fn main [& args]
|
||||||
|
; print the banner
|
||||||
|
(print "----- Scoundrel 2026 -----")
|
||||||
|
(print " Fennel port by Luxferre")
|
||||||
|
(print " Good luck!")
|
||||||
|
; shuffle the deck
|
||||||
|
(shuffle deck)
|
||||||
|
; start the main game loop
|
||||||
|
(var choice nil)
|
||||||
|
(var item nil)
|
||||||
|
(var itemidx nil)
|
||||||
|
(while (and (> (+ (length deck) (length room)) 0) (> (. state :hp) 0))
|
||||||
|
(if (not (. state :engaged)) ; build the room
|
||||||
|
(while (and (< (length room) 4) (> (length deck) 0))
|
||||||
|
(table.insert room (table.remove deck))))
|
||||||
|
(game-field-display)
|
||||||
|
(set choice (get-valid-choice))
|
||||||
|
(if (= choice "r")
|
||||||
|
(do ; running logic
|
||||||
|
(tset state :can-run false) ; cannot run twice in a row
|
||||||
|
(shuffle room) ; shuffle this room
|
||||||
|
(while (> (length room) 0)
|
||||||
|
(table.insert deck 1 (table.remove room)))
|
||||||
|
(print "You run from the room.")
|
||||||
|
)
|
||||||
|
(do ; engagement logic
|
||||||
|
(tset state :engaged true)
|
||||||
|
; get the current item from the choice character
|
||||||
|
(set itemidx (tonumber choice)) ; in Fennel, they are 1-based
|
||||||
|
(set item (. room itemidx))
|
||||||
|
; remove the element under itemidx from the room
|
||||||
|
(table.remove room itemidx)
|
||||||
|
; handling routines may or may not modify your score
|
||||||
|
(tset state :score (+ (. state :score) (if
|
||||||
|
(< item 14) (handle-monster (+ item 1))
|
||||||
|
(< item 23) (handle-weapon (- item 12))
|
||||||
|
(handle-potion (- item 21)))))
|
||||||
|
; now, evaluate whether we can move to the next room
|
||||||
|
(if (and (= (length room) 1) (> (length deck) 0)) (do
|
||||||
|
(tset state :can-run true)
|
||||||
|
(tset state :engaged false)
|
||||||
|
(tset state :drank false)
|
||||||
|
(tset state :rooms-cleared (+ (. state :rooms-cleared) 1)))))))
|
||||||
|
|
||||||
|
; now score and output the game result
|
||||||
|
(if (> (. state :hp) 0)
|
||||||
|
(do ; win condition
|
||||||
|
(tset state :score (. state :hp))
|
||||||
|
(if (= (. state :hp) 20)
|
||||||
|
(tset state :score (+ (. state :score) last-potion-val)))
|
||||||
|
(prin "You win!"))
|
||||||
|
(prin "You lose!"))
|
||||||
|
(printf " Your score: %d. Rooms cleared: %d" (. state :score) (. state :rooms-cleared)))
|
||||||
|
|
||||||
|
(main)
|
||||||
@@ -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
|
||||||
@@ -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 <gb/gb.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#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<dl;++i);
|
||||||
|
play_pulse(0, n2f1, 1);
|
||||||
|
play_pulse(1, n2f2, 1);
|
||||||
|
for(i=0;i<dl;++i);
|
||||||
|
play_pulse(0, n3f1, 1);
|
||||||
|
play_pulse(1, n3f2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic helper methods */
|
||||||
|
|
||||||
|
/* Array shuffle method */
|
||||||
|
void shuffle(s8 *array, u16 n) {
|
||||||
|
u16 i, j, t;
|
||||||
|
if(n > 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<n;++i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CC65 compat */
|
||||||
|
|
||||||
|
#define EZ_HLINE (char) 0x0e
|
||||||
|
#define EZ_VLINE (char) 0x0f
|
||||||
|
#define EZ_CORNER_UL (char) 0x1c
|
||||||
|
#define EZ_CORNER_UR (char) 0x1d
|
||||||
|
#define EZ_CORNER_DL (char) 0x1e
|
||||||
|
#define EZ_CORNER_DR (char) 0x1f
|
||||||
|
|
||||||
|
#define cprintf printf
|
||||||
|
|
||||||
|
void convdigits(char *s, u16 len) {
|
||||||
|
u16 i;
|
||||||
|
for(i=0;i<len;i++) {
|
||||||
|
if(s[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<l;++i) cputc(EZ_HLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cvlinexy(u8 x, u8 y, u8 l) {
|
||||||
|
u8 i;
|
||||||
|
for(i=0;i<l;++i) {
|
||||||
|
gotoxy(x, y+i);
|
||||||
|
cputc(EZ_VLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw a proper rectangle with corners */
|
||||||
|
void rect(u8 x, u8 y, u8 w, u8 h) {
|
||||||
|
u8 i, bl_y = y + h - 1, tr_x = x + w - 1;
|
||||||
|
gotoxy(x, y);
|
||||||
|
cputc(EZ_CORNER_UL);
|
||||||
|
for(i=0;i<w-2;++i) cputc(EZ_HLINE);
|
||||||
|
cputc(EZ_CORNER_UR);
|
||||||
|
for(i=0;i<h-2;++i) {
|
||||||
|
gotoxy(x, y+i+1); cputc(EZ_VLINE);
|
||||||
|
gotoxy(tr_x, y+i+1); cputc(EZ_VLINE);
|
||||||
|
}
|
||||||
|
gotoxy(x, bl_y);
|
||||||
|
cputc(EZ_CORNER_DL);
|
||||||
|
for(i=0;i<w-2;++i) cputc(EZ_HLINE);
|
||||||
|
cputc(EZ_CORNER_DR);
|
||||||
|
}
|
||||||
|
|
||||||
|
void termreset() {
|
||||||
|
clrscr();
|
||||||
|
move_bkg(0, 0);
|
||||||
|
gotoxy(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void gamereset() {
|
||||||
|
#asm
|
||||||
|
jp 0
|
||||||
|
#endasm
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
/**
|
||||||
|
gbcoundrel.c: a Game Boy port of the Scoundrel roguelike card game
|
||||||
|
using the official ruleset only
|
||||||
|
Created by Luxferre in 2026, released into public domain
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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 */
|
||||||
|
static char* ranks[] = {"Survivor", "Master", "Epic", "Legend", "God"};
|
||||||
|
void win_screen(s16 score) {
|
||||||
|
u8 i, j, r;
|
||||||
|
if(score > 29) r = 4;
|
||||||
|
else if(score > 24) r = 3;
|
||||||
|
else if(score > 19) r = 2;
|
||||||
|
else if(score > 9) r = 1;
|
||||||
|
else r = 0;
|
||||||
|
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(1, 11);
|
||||||
|
cprintf("Score: %d", score);
|
||||||
|
gotoxy(SCREEN_WIDTH-3-strlen(ranks[r]), 11);
|
||||||
|
cprintf("|%s|", ranks[r]);
|
||||||
|
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<sizeof(deck);i++)
|
||||||
|
if(deck[i]) deck_count++;
|
||||||
|
termreset();
|
||||||
|
/* render the room */
|
||||||
|
for(i=0;i<4;i++) render_room_item(i, room[i]);
|
||||||
|
/* render deck count */
|
||||||
|
rect(7, 6, 6, 4);
|
||||||
|
cputsxy(8, 7, "DECK");
|
||||||
|
gotoxy(9, 8);
|
||||||
|
printnum(deck_count);
|
||||||
|
/* render stats */
|
||||||
|
gotoxy(1, 2);
|
||||||
|
cprintf("HP "); printnum(hp);
|
||||||
|
gotoxy(1, 3);
|
||||||
|
cprintf("RC "); printnum(rooms_cleared);
|
||||||
|
gotoxy(14, 2);
|
||||||
|
cprintf("WP "); printnum(weapon);
|
||||||
|
gotoxy(14, 3);
|
||||||
|
cprintf("DR "); printnum(durability);
|
||||||
|
/* render controls */
|
||||||
|
cputsxy(1, 12, "Item:");
|
||||||
|
cputsxy(1, 13, "D-Pad");
|
||||||
|
if(can_run) {
|
||||||
|
cputsxy(14, 12, "Run:");
|
||||||
|
cputsxy(14, 13, "A/B/SL");
|
||||||
|
} else {
|
||||||
|
cputsxy(14, 12, "Cannot");
|
||||||
|
cputsxy(15, 13, "run!");
|
||||||
|
}
|
||||||
|
/* render status message */
|
||||||
|
chlinexy(0,SCREEN_HEIGHT-2,SCREEN_WIDTH);
|
||||||
|
gotoxy(0, SCREEN_HEIGHT-1);
|
||||||
|
cprintf("%s", status_msg);
|
||||||
|
wait_vbl_done();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confirmation method */
|
||||||
|
u8 confirm(char *prompt) {
|
||||||
|
u8 res = 2, statbase = SCREEN_WIDTH - 5;
|
||||||
|
memset(status_msg, 0, sizeof(status_msg));
|
||||||
|
snprintf(status_msg, sizeof(status_msg), "%s", prompt);
|
||||||
|
render_main_scene();
|
||||||
|
gotoxy(statbase, SCREEN_HEIGHT-3);
|
||||||
|
cprintf("A=YES");
|
||||||
|
gotoxy(statbase, SCREEN_HEIGHT-2);
|
||||||
|
cprintf("B=NO");
|
||||||
|
while(res > 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<sizeof(room);i++) room[i] = 0; /* reset the room */
|
||||||
|
print_msg("Good luck!"); /* reset the status message */
|
||||||
|
|
||||||
|
while(hp > 0 && !last_room_clr) {
|
||||||
|
/* fill in the new room unless engaged */
|
||||||
|
if(!engaged) {
|
||||||
|
for(i=0;i<sizeof(room);i++) {
|
||||||
|
/* fill only if empty */
|
||||||
|
if(!room[i]) {
|
||||||
|
/* Check if deck is empty or exhausted */
|
||||||
|
if(deck_idx >= 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) {
|
||||||
|
if(confirm("End game?")) {
|
||||||
|
hp = 0;
|
||||||
|
break;
|
||||||
|
} else print_msg("Go on!");
|
||||||
|
} 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<sizeof(deck); i++) {
|
||||||
|
if(deck[i] != 0) temp_deck[t_idx++] = deck[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Shuffle room cards and append to the end */
|
||||||
|
shuffle(room, sizeof(room));
|
||||||
|
for(i=0; i<sizeof(room); i++) {
|
||||||
|
if(room[i] != 0) {
|
||||||
|
temp_deck[t_idx++] = room[i];
|
||||||
|
room[i] = 0; /* Clear room */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Rebuild deck */
|
||||||
|
memset(deck, 0, sizeof(deck));
|
||||||
|
for(i=0; i<t_idx; i++) deck[i] = temp_deck[i];
|
||||||
|
|
||||||
|
/* 4. Reset state */
|
||||||
|
deck_idx = 0; /* Reset index to start of compacted deck */
|
||||||
|
can_run = 0; /* can't run twice in a row */
|
||||||
|
engaged = 0;
|
||||||
|
last_room = 0; /* Since we added cards, it's not the last room anymore */
|
||||||
|
print_msg("Ran from the room!");
|
||||||
|
beep_run();
|
||||||
|
} else { /* engagement mechanics */
|
||||||
|
engaged++; /* increase the amount of engaged cards */
|
||||||
|
can_run = 0; /* Cannot run once engaged */
|
||||||
|
|
||||||
|
item = room[ch]; /* get the item from the room */
|
||||||
|
room[ch] = 0; /* mark the room slot as empty */
|
||||||
|
/* Note: We do NOT put the item back in deck. It is consumed. */
|
||||||
|
|
||||||
|
if(item < 14) { /* monster */
|
||||||
|
score += handle_monster(item + 1);
|
||||||
|
} else if(item < 23) { /* weapon */
|
||||||
|
handle_weapon(item - 12);
|
||||||
|
} else { /* potion */
|
||||||
|
handle_potion(item - 21);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* condition to move to the next room */
|
||||||
|
room_sum = 0;
|
||||||
|
for(i=0;i<sizeof(room);i++) room_sum += room[i];
|
||||||
|
|
||||||
|
if(hp > 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Makefile for Scoundrel Janet port JPM build
|
||||||
|
|
||||||
|
JC := jpm build --build-type=release --ldflags="-s"
|
||||||
|
|
||||||
|
all: clean
|
||||||
|
$(JC)
|
||||||
|
|
||||||
|
static: clean
|
||||||
|
$(JC) --janet-cflags="-static"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
jpm clean
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
(declare-project
|
||||||
|
:name "scoundrel"
|
||||||
|
:description "A Janet port of Scoundrel roguelike solitaire game")
|
||||||
|
|
||||||
|
(declare-source
|
||||||
|
:source ["scoundrel.janet"])
|
||||||
|
|
||||||
|
(declare-executable
|
||||||
|
:name "scoundrel"
|
||||||
|
:entry "scoundrel.janet"
|
||||||
|
:install true)
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env janet
|
||||||
|
# Scoundrel roguelike game port to Janet
|
||||||
|
# Created by Luxferre in 2026, released into public domain
|
||||||
|
|
||||||
|
# helper method section
|
||||||
|
|
||||||
|
# RNG helpers
|
||||||
|
(def rng (math/rng (os/cryptorand 8)))
|
||||||
|
(defn randint [max] (math/rng-int rng max))
|
||||||
|
|
||||||
|
# Shuffle helper
|
||||||
|
(defn shuffle [vec]
|
||||||
|
(def l (length vec))
|
||||||
|
(var j 0)
|
||||||
|
(var t 0)
|
||||||
|
(for i 1 l
|
||||||
|
(set j (randint (+ i 1)))
|
||||||
|
(set t (vec i))
|
||||||
|
(put vec i (vec j))
|
||||||
|
(put vec j t)))
|
||||||
|
|
||||||
|
# Read a single-letter choice from the stdin
|
||||||
|
(defn get-choice [prompt]
|
||||||
|
(let [rawc (getline prompt)]
|
||||||
|
(if (> (length rawc) 0)
|
||||||
|
(string/ascii-lower (string/slice rawc 0 1))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
# Ask user for y/n confirmation (with custom prompt)
|
||||||
|
(defn get-confirm [prompt]
|
||||||
|
(var resp nil)
|
||||||
|
(var valid false)
|
||||||
|
(while (not valid)
|
||||||
|
(set resp (get-choice prompt))
|
||||||
|
(set valid (string/check-set "yn" resp)))
|
||||||
|
(= resp "y"))
|
||||||
|
|
||||||
|
# data section
|
||||||
|
|
||||||
|
# The Deck
|
||||||
|
(def deck (array/concat (range 1 14) (range 1 32)))
|
||||||
|
|
||||||
|
# The Room
|
||||||
|
(def room (array/new 4))
|
||||||
|
|
||||||
|
# The State
|
||||||
|
(def state @{
|
||||||
|
:score -208
|
||||||
|
:hp 20
|
||||||
|
:can-run true
|
||||||
|
:engaged false
|
||||||
|
:drank false
|
||||||
|
:weapon 0
|
||||||
|
:durability 0
|
||||||
|
:rooms-cleared 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# last potion value (for special scoring)
|
||||||
|
(var last-potion-val 0)
|
||||||
|
|
||||||
|
# game logic section
|
||||||
|
|
||||||
|
# display the current stats and room
|
||||||
|
(defn game-field-display []
|
||||||
|
(print "--------------------------")
|
||||||
|
(printf "H%02d | W%02d | D%02d | Deck: %02d"
|
||||||
|
(state :hp)
|
||||||
|
(state :weapon)
|
||||||
|
(state :durability)
|
||||||
|
(length deck))
|
||||||
|
(print "--------------------------")
|
||||||
|
(each x room (cond
|
||||||
|
(< x 14) (prinf "M%02d " (+ x 1))
|
||||||
|
(< x 23) (prinf "W%02d " (- x 12))
|
||||||
|
(prinf "P%02d " (- x 21))))
|
||||||
|
(prin "\n 1 2 3 4")
|
||||||
|
(if (and (state :can-run) (not (state :engaged))) (prin " r)un->"))
|
||||||
|
(print))
|
||||||
|
|
||||||
|
# get valid user's choice
|
||||||
|
(defn get-valid-choice []
|
||||||
|
(var valid false)
|
||||||
|
(var ch "")
|
||||||
|
(while (not valid)
|
||||||
|
(set ch (get-choice "Your choice: "))
|
||||||
|
(set valid (cond
|
||||||
|
(= ch "r")
|
||||||
|
(and (state :can-run) (not (state :engaged)))
|
||||||
|
(string/check-set "1234" ch)
|
||||||
|
(<= (scan-number ch) (length room))
|
||||||
|
false)))
|
||||||
|
ch)
|
||||||
|
|
||||||
|
# monster handling logic (adds monster value to the score)
|
||||||
|
(defn handle-monster [val]
|
||||||
|
(var healthlost val)
|
||||||
|
(printf "You encounter a monster of strength %d." val)
|
||||||
|
(if (and (> (state :weapon) 0) (>= (state :durability) val))
|
||||||
|
(do # can use weapon
|
||||||
|
(if (get-confirm "Use weapon? (y/n) ")
|
||||||
|
(do
|
||||||
|
(var weapondiff (- val (state :weapon)))
|
||||||
|
(if (< weapondiff 0) (set weapondiff 0))
|
||||||
|
(printf "You fight the monster with your %d-level weapon." (state :weapon))
|
||||||
|
(set healthlost weapondiff)
|
||||||
|
(put state :durability (- val 1))
|
||||||
|
(if (< (state :durability) 2) (do
|
||||||
|
(put state :weapon 0)
|
||||||
|
(put state :durability 0))))
|
||||||
|
(print "You fight the monster barehanded.")))
|
||||||
|
( # barehanded encounter
|
||||||
|
print "You fight the monster barehanded."
|
||||||
|
))
|
||||||
|
(-= (state :hp) healthlost)
|
||||||
|
(if (< (state :hp) 0) (put state :hp 0))
|
||||||
|
(printf "Lost %d HP. New HP: %d" healthlost (state :hp))
|
||||||
|
val)
|
||||||
|
|
||||||
|
# weapon handling logic (adds 0 score)
|
||||||
|
(defn handle-weapon [val]
|
||||||
|
(put state :weapon val)
|
||||||
|
(put state :durability 14)
|
||||||
|
(printf "Equipped a %d-level weapon." val)
|
||||||
|
0)
|
||||||
|
|
||||||
|
# potion handling logic (adds 0 score)
|
||||||
|
(defn handle-potion [val]
|
||||||
|
(if (state :drank)
|
||||||
|
(print "Already drank a potion here! Discarding...")
|
||||||
|
(do
|
||||||
|
(if (and (= (length room) 0) (= (length deck) 0))
|
||||||
|
(set last-potion-val val))
|
||||||
|
(+= (state :hp) val)
|
||||||
|
(if (> (state :hp) 20) (put state :hp 20))
|
||||||
|
(put state :drank true)
|
||||||
|
(printf "Drank a %d-level potion. New HP: %d" val (state :hp))))
|
||||||
|
0)
|
||||||
|
|
||||||
|
# entrypoint
|
||||||
|
|
||||||
|
(defn main [& args]
|
||||||
|
# print the banner
|
||||||
|
(print "----- Scoundrel 2026 -----")
|
||||||
|
(print " Janet port by Luxferre")
|
||||||
|
(print " Good luck!")
|
||||||
|
# shuffle the deck
|
||||||
|
(shuffle deck)
|
||||||
|
# start the main game loop
|
||||||
|
(var choice nil)
|
||||||
|
(var item nil)
|
||||||
|
(var itemidx nil)
|
||||||
|
(while (and (> (+ (length deck) (length room)) 0) (> (state :hp) 0))
|
||||||
|
(if (not (state :engaged)) # build the room
|
||||||
|
(while (and (< (length room) 4) (> (length deck) 0))
|
||||||
|
(array/push room (array/pop deck))))
|
||||||
|
(game-field-display)
|
||||||
|
(set choice (get-valid-choice))
|
||||||
|
(if (= choice "r")
|
||||||
|
(do # running logic
|
||||||
|
(put state :can-run false) # cannot run twice in a row
|
||||||
|
(shuffle room) # shuffle this room
|
||||||
|
(while (> (length room) 0)
|
||||||
|
(array/insert deck 0 (array/pop room)))
|
||||||
|
(print "You run from the room.")
|
||||||
|
)
|
||||||
|
(do # engagement logic
|
||||||
|
(put state :engaged true)
|
||||||
|
# get the current item from the choice character
|
||||||
|
(set itemidx (- (scan-number choice) 1))
|
||||||
|
(set item (get room itemidx))
|
||||||
|
# remove the element under itemidx from the room
|
||||||
|
(array/remove room itemidx)
|
||||||
|
# handling routines may or may not modify your score
|
||||||
|
(+= (state :score) (cond
|
||||||
|
(< item 14) (handle-monster (+ item 1))
|
||||||
|
(< item 23) (handle-weapon (- item 12))
|
||||||
|
(handle-potion (- item 21))))
|
||||||
|
# now, evaluate whether we can move to the next room
|
||||||
|
(if (and (= (length room) 1) (> (length deck) 0)) (do
|
||||||
|
(put state :can-run true)
|
||||||
|
(put state :engaged false)
|
||||||
|
(put state :drank false)
|
||||||
|
(++ (state :rooms-cleared)))))))
|
||||||
|
|
||||||
|
# now score and output the game result
|
||||||
|
(if (> (state :hp) 0)
|
||||||
|
(do # win condition
|
||||||
|
(put state :score (state :hp))
|
||||||
|
(if (= (state :hp) 20)
|
||||||
|
(+= (state :score) last-potion-val))
|
||||||
|
(prin "You win!"))
|
||||||
|
(prin "You lose!"))
|
||||||
|
(printf " Your score: %d. Rooms cleared: %d" (state :score) (state :rooms-cleared)))
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#ifndef EZNES_H
|
||||||
|
#define EZNES_H
|
||||||
|
/**
|
||||||
|
EZnes: a set of helpers for easy NES indie development in pure C
|
||||||
|
for CC65 (in text mode)
|
||||||
|
|
||||||
|
Created by Luxferre in 2026, released into public domain
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <nes.h>
|
||||||
|
#include <joystick.h>
|
||||||
|
#include <conio.h>
|
||||||
|
|
||||||
|
#define u8 unsigned char
|
||||||
|
#define u16 unsigned short
|
||||||
|
#define s8 signed char
|
||||||
|
#define s16 signed short
|
||||||
|
|
||||||
|
/* Generic constants */
|
||||||
|
|
||||||
|
#ifdef PAL
|
||||||
|
#define CPU 1662607 /* PAL */
|
||||||
|
#else
|
||||||
|
#define CPU 1789773 /* NTSC */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SCREEN_WIDTH 32
|
||||||
|
#define SCREEN_HEIGHT 28
|
||||||
|
#define SCREEN_CENTER_X (SCREEN_WIDTH >> 1)
|
||||||
|
#define SCREEN_CENTER_Y (SCREEN_HEIGHT >> 1)
|
||||||
|
|
||||||
|
#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
|
||||||
|
#define EZJOY_COMBO 9
|
||||||
|
|
||||||
|
#define EZ_CORNER_UL (char) 20
|
||||||
|
#define EZ_CORNER_UR (char) 18
|
||||||
|
#define EZ_CORNER_DL (char) 17
|
||||||
|
#define EZ_CORNER_DR (char) 8
|
||||||
|
|
||||||
|
#define NES_BLACK 0x0F
|
||||||
|
#define NES_WHITE 0x30
|
||||||
|
#define NES_RED 0x15
|
||||||
|
#define NES_GREEN 0x28
|
||||||
|
#define NES_YELLOW 0x2A
|
||||||
|
|
||||||
|
/* RNG stuff */
|
||||||
|
static u16 _ez_entropy = 0;
|
||||||
|
|
||||||
|
u16 ezrand() {
|
||||||
|
_ez_entropy = 1 + ((_ez_entropy >> 15) | (_ez_entropy << 1));
|
||||||
|
return ((u16) rand()) ^ _ez_entropy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graphics section */
|
||||||
|
|
||||||
|
void ppu_off() {
|
||||||
|
PPU.mask &= 0xE7;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ppu_on() {
|
||||||
|
waitvsync();
|
||||||
|
PPU.scroll = 0;
|
||||||
|
PPU.mask |= 0x1A;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 joy_input_wait() {
|
||||||
|
u8 last_joy = 0, joy, pressed;
|
||||||
|
/* wait until the previous key is released */
|
||||||
|
while(joy_read(JOY_1));
|
||||||
|
/* wait for a new press */
|
||||||
|
while(1) {
|
||||||
|
++_ez_entropy;
|
||||||
|
joy = joy_read(JOY_1);
|
||||||
|
pressed = joy & ~last_joy;
|
||||||
|
if(pressed & JOY_UP_MASK) return EZJOY_UP;
|
||||||
|
if(pressed & JOY_DOWN_MASK) return EZJOY_DN;
|
||||||
|
if(pressed & JOY_LEFT_MASK) return EZJOY_LT;
|
||||||
|
if(pressed & JOY_RIGHT_MASK) return EZJOY_RT;
|
||||||
|
if(pressed & JOY_BTN_1_MASK) return EZJOY_A;
|
||||||
|
if(pressed & JOY_BTN_2_MASK) return EZJOY_B;
|
||||||
|
if(pressed & JOY_BTN_3_MASK) return EZJOY_SL;
|
||||||
|
if(pressed & JOY_BTN_4_MASK) return EZJOY_ST;
|
||||||
|
last_joy = joy;
|
||||||
|
waitvsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* palette loader */
|
||||||
|
void init_palette(u8 bg1, u8 bg2, u8 bg3, u8 bg4, u8 c1, u8 c2, u8 c3, u8 c4) {
|
||||||
|
u8 i, pal[16] = {0};
|
||||||
|
pal[0] = bg1;
|
||||||
|
pal[4] = bg2;
|
||||||
|
pal[8] = bg3;
|
||||||
|
pal[12] = bg4;
|
||||||
|
pal[3] = c1;
|
||||||
|
pal[7] = c2;
|
||||||
|
pal[11] = c3;
|
||||||
|
pal[15] = c4;
|
||||||
|
PPU.vram.address = 0x3F;
|
||||||
|
PPU.vram.address = 0;
|
||||||
|
for(i=0; i<16; i++) PPU.vram.data = pal[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4x4 char block colorization method (palette is 0 to 3) */
|
||||||
|
void colorxy(u8 x, u8 y, u8 palette) {
|
||||||
|
u16 addr;
|
||||||
|
u8 attr_byte;
|
||||||
|
addr = 0x23C0 + ((y >> 2) << 3) + (x >> 2);
|
||||||
|
attr_byte = palette | (palette << 2) | (palette << 4) | (palette << 6);
|
||||||
|
waitvsync();
|
||||||
|
PPU.vram.address = (u8) (addr >> 8);
|
||||||
|
PPU.vram.address = (u8) (addr & 255);
|
||||||
|
PPU.vram.data = attr_byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sound section */
|
||||||
|
|
||||||
|
u16 get_timer_by_freq(u16 freq) {
|
||||||
|
return (u16) ((CPU / 16 / freq) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_pulse(u8 idx, u16 freq, u8 len) {
|
||||||
|
u16 tm = get_timer_by_freq(freq);
|
||||||
|
if(len > 31) len = 31;
|
||||||
|
if(idx > 1) idx = 0;
|
||||||
|
APU.pulse[idx].period_low = tm & 255;
|
||||||
|
APU.pulse[idx].len_period_high = (tm >> 8) | (len << 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_jingle(u16 n1f1,
|
||||||
|
u16 n1f2,
|
||||||
|
u16 n2f1,
|
||||||
|
u16 n2f2,
|
||||||
|
u16 n3f1,
|
||||||
|
u16 n3f2) {
|
||||||
|
u16 i, dl = 2000;
|
||||||
|
play_pulse(0, n1f1, 2);
|
||||||
|
play_pulse(1, n1f2, 2);
|
||||||
|
for(i=0;i<dl;++i);
|
||||||
|
play_pulse(0, n2f1, 2);
|
||||||
|
play_pulse(1, n2f2, 2);
|
||||||
|
for(i=0;i<dl;++i);
|
||||||
|
play_pulse(0, n3f1, 2);
|
||||||
|
play_pulse(1, n3f2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_sound() {
|
||||||
|
APU.status = 3;
|
||||||
|
APU.pulse[0].ramp = 0x8;
|
||||||
|
APU.pulse[1].ramp = 0x8;
|
||||||
|
APU.pulse[0].control = 0x9F;
|
||||||
|
APU.pulse[1].control = 0x9F;
|
||||||
|
waitvsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game initialization section */
|
||||||
|
|
||||||
|
void init_game(u8 bg1, u8 bg2, u8 bg3, u8 bg4, u8 c1, u8 c2, u8 c3, u8 c4) {
|
||||||
|
init_sound();
|
||||||
|
bgcolor(COLOR_BLACK);
|
||||||
|
textcolor(0);
|
||||||
|
init_palette(bg1, bg2, bg3, bg4, c1, c2, c3, c4);
|
||||||
|
joy_install(joy_static_stddrv);
|
||||||
|
clrscr();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wait until Start is pressed */
|
||||||
|
void wait_for_start() {
|
||||||
|
u8 joy;
|
||||||
|
while(1) {
|
||||||
|
++_ez_entropy;
|
||||||
|
joy = joy_read(JOY_1);
|
||||||
|
if(JOY_START(joy)) break;
|
||||||
|
waitvsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* randomize until Start is pressed */
|
||||||
|
void wait_for_start_rand() {
|
||||||
|
u8 joy;
|
||||||
|
#ifdef _randomize
|
||||||
|
_randomize();
|
||||||
|
#endif
|
||||||
|
_ez_entropy = rand();
|
||||||
|
while(1) {
|
||||||
|
++_ez_entropy;
|
||||||
|
joy = joy_read(JOY_1);
|
||||||
|
if(JOY_START(joy)) break;
|
||||||
|
waitvsync();
|
||||||
|
}
|
||||||
|
srand(_ez_entropy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wait for Start or a button combination */
|
||||||
|
u8 wait_for_start_or_combo(u8 combo_mask) {
|
||||||
|
u8 last_joy = 0, joy, pressed;
|
||||||
|
/* wait until the previous key is released */
|
||||||
|
while(joy_read(JOY_1));
|
||||||
|
/* wait for a new press */
|
||||||
|
while(1) {
|
||||||
|
++_ez_entropy;
|
||||||
|
joy = joy_read(JOY_1);
|
||||||
|
pressed = joy & ~last_joy;
|
||||||
|
if(pressed == combo_mask) return EZJOY_COMBO;
|
||||||
|
if(pressed & JOY_BTN_4_MASK) return EZJOY_ST;
|
||||||
|
last_joy = joy;
|
||||||
|
waitvsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Generic helper methods */
|
||||||
|
|
||||||
|
/* Array shuffle method */
|
||||||
|
void shuffle(s8 *array, u16 n) {
|
||||||
|
u16 i, j, t;
|
||||||
|
if(n > 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<n;++i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draw a proper rectangle with corners */
|
||||||
|
void rect(u8 x, u8 y, u8 w, u8 h) {
|
||||||
|
u8 bl_y = y + h - 1, tr_x = x + w - 1;
|
||||||
|
cputcxy(x, y, EZ_CORNER_UL);
|
||||||
|
chlinexy(x + 1, y, w - 2);
|
||||||
|
cputcxy(tr_x, y, EZ_CORNER_UR);
|
||||||
|
chlinexy(x + 1, bl_y, w - 2);
|
||||||
|
cvlinexy(x, y + 1, h - 2);
|
||||||
|
cputcxy(x, bl_y, EZ_CORNER_DL);
|
||||||
|
cvlinexy(tr_x, y + 1, h - 2);
|
||||||
|
cputcxy(tr_x, bl_y, EZ_CORNER_DR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
+74
-211
@@ -4,135 +4,20 @@
|
|||||||
Created by Luxferre in 2026, released into public domain
|
Created by Luxferre in 2026, released into public domain
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <nes.h>
|
|
||||||
#include <joystick.h>
|
|
||||||
#include <conio.h>
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include "eznes.h"
|
||||||
|
|
||||||
/* Constants section */
|
/* Constants section */
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
#define GAME_WHITE 0
|
#define GAME_WHITE 0
|
||||||
#define GAME_RED 1
|
#define GAME_RED 1
|
||||||
#define GAME_YELLOW 2
|
#define GAME_YELLOW 2
|
||||||
#define GAME_GREEN 3
|
#define GAME_GREEN 3
|
||||||
|
|
||||||
/* Graphics section */
|
|
||||||
|
|
||||||
void ppu_off() {
|
|
||||||
PPU.mask &= 0xE7;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ppu_on() {
|
|
||||||
waitvsync();
|
|
||||||
PPU.scroll = 0;
|
|
||||||
PPU.mask |= 0x1A;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* palette loader */
|
|
||||||
void init_palette() {
|
|
||||||
unsigned char i;
|
|
||||||
const unsigned char my_palette[16] = {
|
|
||||||
0x0f, 0, 0, 0x30, /* white */
|
|
||||||
0x0f, 0, 0, 0x15, /* red */
|
|
||||||
0x0f, 0, 0, 0x28, /* yellow */
|
|
||||||
0x0f, 0, 0, 0x2A /* green */
|
|
||||||
};
|
|
||||||
PPU.vram.address = 0x3F;
|
|
||||||
PPU.vram.address = 0;
|
|
||||||
for(i=0; i<16; i++) PPU.vram.data = my_palette[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4x4 char block colorization method (palette is 0 to 3) */
|
|
||||||
void colorxy(unsigned char x, unsigned char y, unsigned char palette) {
|
|
||||||
unsigned int addr;
|
|
||||||
unsigned char attr_byte;
|
|
||||||
addr = 0x23C0 + ((y >> 2) << 3) + (x >> 2);
|
|
||||||
attr_byte = palette | (palette << 2) | (palette << 4) | (palette << 6);
|
|
||||||
waitvsync();
|
|
||||||
PPU.vram.address = (unsigned char) (addr >> 8);
|
|
||||||
PPU.vram.address = (unsigned char) (addr & 255);
|
|
||||||
PPU.vram.data = attr_byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sound section */
|
/* Sound section */
|
||||||
|
|
||||||
#define CPU 1789773 /* NTSC */
|
|
||||||
|
|
||||||
unsigned int get_timer(int freq) {
|
|
||||||
return (unsigned int) ((CPU / 16 / freq) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void play_pulse(unsigned char idx, unsigned int freq, unsigned char len) {
|
|
||||||
unsigned int tm = get_timer(freq);
|
|
||||||
if(len > 31) len = 31;
|
|
||||||
if(idx > 1) idx = 0;
|
|
||||||
APU.pulse[idx].period_low = tm & 255;
|
|
||||||
APU.pulse[idx].len_period_high = (tm >> 8) | (len << 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
void play_jingle(unsigned int n1f1,
|
|
||||||
unsigned int n1f2,
|
|
||||||
unsigned int n2f1,
|
|
||||||
unsigned int n2f2,
|
|
||||||
unsigned int n3f1,
|
|
||||||
unsigned int n3f2) {
|
|
||||||
unsigned int i, dl = 2000;
|
|
||||||
play_pulse(0, n1f1, 2);
|
|
||||||
play_pulse(1, n1f2, 2);
|
|
||||||
for(i=0;i<dl;++i);
|
|
||||||
play_pulse(0, n2f1, 2);
|
|
||||||
play_pulse(1, n2f2, 2);
|
|
||||||
for(i=0;i<dl;++i);
|
|
||||||
play_pulse(0, n3f1, 2);
|
|
||||||
play_pulse(1, n3f2, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void init_sound() {
|
|
||||||
APU.status = 3;
|
|
||||||
APU.pulse[0].ramp = 0x8;
|
|
||||||
APU.pulse[1].ramp = 0x8;
|
|
||||||
APU.pulse[0].control = 0x9F;
|
|
||||||
APU.pulse[1].control = 0x9F;
|
|
||||||
waitvsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
void beep_start() {
|
void beep_start() {
|
||||||
play_jingle(220, 440, 440, 660, 660, 880);
|
play_jingle(220, 440, 440, 660, 660, 880);
|
||||||
}
|
}
|
||||||
@@ -152,17 +37,15 @@ void beep_run() {
|
|||||||
play_jingle(660, 880, 440, 660, 220, 440);
|
play_jingle(660, 880, 440, 660, 220, 440);
|
||||||
}
|
}
|
||||||
void beep_win() {
|
void beep_win() {
|
||||||
unsigned short i;
|
|
||||||
play_jingle(55, 110, 110, 220, 220, 440);
|
play_jingle(55, 110, 110, 220, 220, 440);
|
||||||
for(i=0;i<4000;++i);
|
delay_cycles(4000);
|
||||||
play_jingle(440, 660, 660, 880, 880, 1760);
|
play_jingle(440, 660, 660, 880, 880, 1760);
|
||||||
for(i=0;i<4000;++i);
|
delay_cycles(4000);
|
||||||
play_jingle(880, 660, 660, 880, 880, 1760);
|
play_jingle(880, 660, 660, 880, 880, 1760);
|
||||||
}
|
}
|
||||||
void beep_lose() {
|
void beep_lose() {
|
||||||
unsigned short i;
|
|
||||||
play_jingle(110, 55, 220, 110, 110, 55);
|
play_jingle(110, 55, 220, 110, 110, 55);
|
||||||
for(i=0;i<4000;++i);
|
delay_cycles(4000);
|
||||||
play_jingle(110, 55, 110, 55, 110, 55);
|
play_jingle(110, 55, 110, 55, 110, 55);
|
||||||
}
|
}
|
||||||
void beep_err() {
|
void beep_err() {
|
||||||
@@ -172,21 +55,12 @@ void beep_err() {
|
|||||||
/* Main game logic */
|
/* Main game logic */
|
||||||
|
|
||||||
void game_startup() {
|
void game_startup() {
|
||||||
unsigned int entropy;
|
u8 i, j, x = 0;
|
||||||
unsigned char joy, i, j;
|
init_game(NES_BLACK, NES_BLACK, NES_BLACK, NES_BLACK, NES_WHITE, NES_RED, NES_GREEN, NES_YELLOW);
|
||||||
#ifdef _randomize
|
|
||||||
_randomize();
|
|
||||||
#endif
|
|
||||||
entropy = rand();
|
|
||||||
init_sound();
|
|
||||||
bgcolor(COLOR_BLACK);
|
|
||||||
textcolor(0);
|
|
||||||
init_palette();
|
|
||||||
clrscr();
|
|
||||||
ppu_off();
|
ppu_off();
|
||||||
for(j=0;j<24;j+=4)
|
for(j=0;j<24;j+=4)
|
||||||
for(i=3;i<32;i+=4)
|
for(i=3;i<32;i+=4)
|
||||||
colorxy(i, j, 1 + ((++entropy) % 3));
|
colorxy(i, j, 1 + ((++x) % 3));
|
||||||
gotoxy(0, 2);
|
gotoxy(0, 2);
|
||||||
cputs( \
|
cputs( \
|
||||||
" _______ _______ _______ \r\n" \
|
" _______ _______ _______ \r\n" \
|
||||||
@@ -215,27 +89,30 @@ void game_startup() {
|
|||||||
cputsxy(SCREEN_CENTER_X - 6, SCREEN_HEIGHT - 4, "Press Start");
|
cputsxy(SCREEN_CENTER_X - 6, SCREEN_HEIGHT - 4, "Press Start");
|
||||||
cputsxy(1, SCREEN_HEIGHT - 2, "Luxferre");
|
cputsxy(1, SCREEN_HEIGHT - 2, "Luxferre");
|
||||||
cputsxy(SCREEN_WIDTH-5, SCREEN_HEIGHT - 2, "2026");
|
cputsxy(SCREEN_WIDTH-5, SCREEN_HEIGHT - 2, "2026");
|
||||||
joy_install(joy_static_stddrv);
|
|
||||||
ppu_on();
|
ppu_on();
|
||||||
while(1) {
|
wait_for_start_rand();
|
||||||
++entropy;
|
|
||||||
joy = joy_read(JOY_1);
|
|
||||||
if(JOY_START(joy)) break;
|
|
||||||
waitvsync();
|
|
||||||
}
|
|
||||||
srand(entropy);
|
|
||||||
beep_start();
|
beep_start();
|
||||||
clrscr();
|
clrscr();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Win screen display */
|
/* Win screen display */
|
||||||
void win_screen(int score) {
|
static char* ranks[] = {"Survivor", "Master", "Epic", "Legend", "God"};
|
||||||
unsigned char i, j;
|
static u8 sb1[] = { 6, 0x14, 3, 16, 0x75, 0, 0x1E, 7, 0x14, 0x1C, 0x1B, 0x1C, 0x14, 0x1B, 6, 0};
|
||||||
|
static u8 sb2[] = { 0x13, 7, 0x1A, 0x18, 0x75, 15, 16, 0x16, 1, 0x14, 1, 0x1A, 7, 6, 0x1D, 0x1C, 5, 0 };
|
||||||
|
static u8 sb3[] = { 0xF9, 0xFA, 0xF8, 0xEF, 0xEB, 0xEE, 0x8A, 0xFE, 0xE2, 0xEF, 0x8A, 0xFD,
|
||||||
|
0xE5, 0xF8, 0xEE, 0};
|
||||||
|
void win_screen(s16 score) {
|
||||||
|
u8 i, j, r;
|
||||||
|
if(score > 29) r = 4;
|
||||||
|
else if(score > 24) r = 3;
|
||||||
|
else if(score > 19) r = 2;
|
||||||
|
else if(score > 9) r = 1;
|
||||||
|
else r = 0;
|
||||||
clrscr();
|
clrscr();
|
||||||
ppu_off();
|
ppu_off();
|
||||||
for(j=0;j<12;j+=4)
|
for(j=0;j<12;j+=4)
|
||||||
for(i=3;i<32;i+=4)
|
for(i=3;i<32;i+=4)
|
||||||
colorxy(i, j, 1 + (rand() % 3));
|
colorxy(i, j, 1 + (ezrand() % 3));
|
||||||
cputsxy(0, 3, " ___ ___ ___ ______ \r\n" \
|
cputsxy(0, 3, " ___ ___ ___ ______ \r\n" \
|
||||||
" | Y | | _ \\ \r\n" \
|
" | Y | | _ \\ \r\n" \
|
||||||
" |. | |. |. | |\r\n" \
|
" |. | |. |. | |\r\n" \
|
||||||
@@ -244,7 +121,9 @@ void win_screen(int score) {
|
|||||||
" |::.|:. |::.|::.| |\r\n" \
|
" |::.|:. |::.|::.| |\r\n" \
|
||||||
" `--- ---`---`--- ---'");
|
" `--- ---`---`--- ---'");
|
||||||
cputsxy(0, 12, " Congratulations! You made it\r\n through the dangerous\r\n dungeon alive!\r\n\r\n");
|
cputsxy(0, 12, " 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);
|
cprintf(" Score: %d", score);
|
||||||
|
gotoxy(SCREEN_WIDTH - 8 - strlen(ranks[r]), 16);
|
||||||
|
cprintf("Rank: %s", ranks[r]);
|
||||||
chlinexy(0, 17, SCREEN_WIDTH);
|
chlinexy(0, 17, SCREEN_WIDTH);
|
||||||
cputs("Created by Luxferre in 2026\r\nReleased into public domain\r\n");
|
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");
|
cputs("\r\nOriginal concept by Zach Gage\r\nand Kurt Bieg, 2011");
|
||||||
@@ -253,7 +132,17 @@ void win_screen(int score) {
|
|||||||
ppu_on();
|
ppu_on();
|
||||||
waitvsync();
|
waitvsync();
|
||||||
beep_win();
|
beep_win();
|
||||||
while(joy_input_wait() != KEY_ST);
|
for(i=0;i<15;++i) sb1[i] ^= 0x55;
|
||||||
|
for(i=0;i<17;++i) sb2[i] ^= 0x55;
|
||||||
|
for(i=0;i<15;++i) sb3[i] ^= 0xaa;
|
||||||
|
do {
|
||||||
|
r = wait_for_start_or_combo(JOY_BTN_1_MASK | JOY_BTN_2_MASK);
|
||||||
|
if(r == EZJOY_COMBO) {
|
||||||
|
cputsxy(8, 1, sb1);
|
||||||
|
cputsxy(7, 2, sb2);
|
||||||
|
cputsxy(8, 10, sb3);
|
||||||
|
}
|
||||||
|
} while(r != EZJOY_ST);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* UI choices */
|
/* UI choices */
|
||||||
@@ -280,35 +169,34 @@ Potion level: x - 21
|
|||||||
/* Game stats */
|
/* Game stats */
|
||||||
|
|
||||||
/* main deck and room */
|
/* main deck and room */
|
||||||
static char deck[44], room[] = {0, 0, 0, 0};
|
static s8 deck[44], room[] = {0, 0, 0, 0};
|
||||||
|
|
||||||
/* status message */
|
/* status message */
|
||||||
static char status_msg[(SCREEN_WIDTH-1)];
|
static char status_msg[(SCREEN_WIDTH-1)];
|
||||||
|
|
||||||
/* tracking variables */
|
/* tracking variables */
|
||||||
static short deck_idx = 0, hp = 20, rooms_cleared = 0,
|
static s16 hp = 20;
|
||||||
|
static u8 deck_idx = 0, rooms_cleared = 0,
|
||||||
weapon = 0, durability = 0, can_run = 1,
|
weapon = 0, durability = 0, can_run = 1,
|
||||||
engaged = 0, potion_drank = 0, last_potion_val = 0;
|
engaged = 0, potion_drank = 0, last_potion_val = 0;
|
||||||
|
|
||||||
/* Input method */
|
/* Input method */
|
||||||
/* up 1, left 2, right 3, down 4, a run, start quit */
|
/* up 1, left 2, right 3, down 4, a run, start quit */
|
||||||
char get_choice() {
|
u8 get_choice() {
|
||||||
switch(joy_input_wait()) {
|
switch(joy_input_wait()) {
|
||||||
case KEY_UP: return CH1;
|
case EZJOY_UP: return CH1;
|
||||||
case KEY_LT: return CH2;
|
case EZJOY_LT: return CH2;
|
||||||
case KEY_RT: return CH3;
|
case EZJOY_RT: return CH3;
|
||||||
case KEY_DN: return CH4;
|
case EZJOY_DN: return CH4;
|
||||||
case KEY_ST: return CH_Q;
|
case EZJOY_ST: return CH_Q;
|
||||||
default: return CH_R;
|
default: return CH_R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Draw a room character according to the index and value */
|
/* Draw a room character according to the index and value */
|
||||||
void render_room_item(short i, short v) {
|
void render_room_item(u8 i, u8 v) {
|
||||||
unsigned char color,
|
u8 color, topleft_x, topleft_y, text_x, text_y,
|
||||||
topleft_x, topleft_y,
|
cheight=7, cwidth=8, left_offset = 9;
|
||||||
topright_x, bottomleft_y,
|
|
||||||
text_x, text_y, cheight=7, cwidth=8, left_offset = 9;
|
|
||||||
if(i == 0) {
|
if(i == 0) {
|
||||||
topleft_x = 8;
|
topleft_x = 8;
|
||||||
topleft_y = 0;
|
topleft_y = 0;
|
||||||
@@ -322,8 +210,6 @@ void render_room_item(short i, short v) {
|
|||||||
topleft_x = 8;
|
topleft_x = 8;
|
||||||
topleft_y = 16;
|
topleft_y = 16;
|
||||||
}
|
}
|
||||||
topright_x = topleft_x + cwidth - 1;
|
|
||||||
bottomleft_y = topleft_y + cheight - 1;
|
|
||||||
text_x = topleft_x + 1;
|
text_x = topleft_x + 1;
|
||||||
text_y = topleft_y + 2;
|
text_y = topleft_y + 2;
|
||||||
if(v == 0) {} /* don't render anything */
|
if(v == 0) {} /* don't render anything */
|
||||||
@@ -340,10 +226,7 @@ void render_room_item(short i, short v) {
|
|||||||
colorxy(topleft_x, topleft_y+4, color);
|
colorxy(topleft_x, topleft_y+4, color);
|
||||||
colorxy(topleft_x+4, topleft_y+4, color);
|
colorxy(topleft_x+4, topleft_y+4, color);
|
||||||
/* draw the rectangle */
|
/* draw the rectangle */
|
||||||
chlinexy(topleft_x + 1, topleft_y, cwidth - 2);
|
rect(topleft_x, topleft_y, cwidth, cheight);
|
||||||
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 */
|
/* draw text */
|
||||||
gotoxy(text_x, text_y);
|
gotoxy(text_x, text_y);
|
||||||
if(v < 14) {
|
if(v < 14) {
|
||||||
@@ -362,7 +245,7 @@ void render_room_item(short i, short v) {
|
|||||||
|
|
||||||
/* Render main game screen */
|
/* Render main game screen */
|
||||||
void render_main_scene() {
|
void render_main_scene() {
|
||||||
unsigned char i, deck_count = 0, statbase = 17,
|
u8 i, deck_count = 0, statbase = 17,
|
||||||
room_start_x = 1,
|
room_start_x = 1,
|
||||||
room_start_y = 1,
|
room_start_y = 1,
|
||||||
room_end_x = statbase - 8,
|
room_end_x = statbase - 8,
|
||||||
@@ -378,13 +261,10 @@ void render_main_scene() {
|
|||||||
chlinexy(0,SCREEN_HEIGHT-3,SCREEN_WIDTH);
|
chlinexy(0,SCREEN_HEIGHT-3,SCREEN_WIDTH);
|
||||||
/* render the room */
|
/* render the room */
|
||||||
for(i=0;i<4;i++) render_room_item(i, room[i]);
|
for(i=0;i<4;i++) render_room_item(i, room[i]);
|
||||||
|
rect(9, 9, 6, 5);
|
||||||
cputsxy(10, 10, "DECK");
|
cputsxy(10, 10, "DECK");
|
||||||
gotoxy(11, 12);
|
gotoxy(11, 12);
|
||||||
cprintf("%02d", deck_count);
|
cprintf("%02d", deck_count);
|
||||||
cvlinexy(9, 10, 3);
|
|
||||||
cvlinexy(14, 10, 3);
|
|
||||||
chlinexy(10, 9, 4);
|
|
||||||
chlinexy(10, 13, 4);
|
|
||||||
/* render stats */
|
/* render stats */
|
||||||
gotoxy(statbase, 1);
|
gotoxy(statbase, 1);
|
||||||
cprintf("Health %02d", hp);
|
cprintf("Health %02d", hp);
|
||||||
@@ -404,8 +284,8 @@ void render_main_scene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Confirmation method */
|
/* Confirmation method */
|
||||||
unsigned char confirm(char *prompt) {
|
u8 confirm(char *prompt) {
|
||||||
unsigned char res = 2, statbase = SCREEN_WIDTH - 7;
|
u8 res = 2, statbase = SCREEN_WIDTH - 7;
|
||||||
snprintf(status_msg, sizeof(status_msg), "%s", prompt);
|
snprintf(status_msg, sizeof(status_msg), "%s", prompt);
|
||||||
render_main_scene();
|
render_main_scene();
|
||||||
gotoxy(statbase, SCREEN_HEIGHT-5);
|
gotoxy(statbase, SCREEN_HEIGHT-5);
|
||||||
@@ -414,8 +294,8 @@ unsigned char confirm(char *prompt) {
|
|||||||
cprintf("B=NO");
|
cprintf("B=NO");
|
||||||
while(res > 1) {
|
while(res > 1) {
|
||||||
switch(joy_input_wait()) {
|
switch(joy_input_wait()) {
|
||||||
case KEY_A: res = 1; break;
|
case EZJOY_A: res = 1; break;
|
||||||
case KEY_B: res = 0; break;
|
case EZJOY_B: res = 0; break;
|
||||||
default: res = 2;
|
default: res = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,27 +310,14 @@ void print_msg(char *s) {
|
|||||||
render_main_scene();
|
render_main_scene();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Array shuffle method */
|
|
||||||
void shuffle(char *array, unsigned int 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 */
|
/* Room item handling methods */
|
||||||
|
|
||||||
void handle_monster(int val) {
|
s16 handle_monster(s16 val) {
|
||||||
int healthlost = val;
|
s16 healthlost = val;
|
||||||
char monster_msg[(SCREEN_WIDTH-1)];
|
char monster_msg[(SCREEN_WIDTH-1)];
|
||||||
beep_enemy();
|
beep_enemy();
|
||||||
if(weapon > 0 && val <= durability) {
|
if(weapon > 0 && val <= durability) {
|
||||||
int weapondiff = val - weapon;
|
s16 weapondiff = val - weapon;
|
||||||
if(weapondiff < 0) weapondiff = 0;
|
if(weapondiff < 0) weapondiff = 0;
|
||||||
snprintf(monster_msg, sizeof(status_msg), "%d-level monster: use weapon?", val);
|
snprintf(monster_msg, sizeof(status_msg), "%d-level monster: use weapon?", val);
|
||||||
if(confirm(monster_msg)) {
|
if(confirm(monster_msg)) {
|
||||||
@@ -466,16 +333,17 @@ void handle_monster(int val) {
|
|||||||
hp -= healthlost;
|
hp -= healthlost;
|
||||||
if(hp < 0) hp = 0;
|
if(hp < 0) hp = 0;
|
||||||
snprintf(status_msg, sizeof(status_msg), "You lost %d HP. New HP: %d", healthlost, hp);
|
snprintf(status_msg, sizeof(status_msg), "You lost %d HP. New HP: %d", healthlost, hp);
|
||||||
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_weapon(int val) {
|
void handle_weapon(u8 val) {
|
||||||
weapon = val;
|
weapon = val;
|
||||||
snprintf(status_msg, sizeof(status_msg), "You equip a %d-level weapon.", weapon);
|
snprintf(status_msg, sizeof(status_msg), "You equip a %d-level weapon.", weapon);
|
||||||
durability = 14; /* reset durability to max limit */
|
durability = 14; /* reset durability to max limit */
|
||||||
beep_weapon();
|
beep_weapon();
|
||||||
}
|
}
|
||||||
|
|
||||||
void handle_potion(unsigned char val) {
|
void handle_potion(u8 val) {
|
||||||
if(potion_drank) {
|
if(potion_drank) {
|
||||||
print_msg("Already drank a potion here.");
|
print_msg("Already drank a potion here.");
|
||||||
beep_err();
|
beep_err();
|
||||||
@@ -491,12 +359,12 @@ void handle_potion(unsigned char val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main gameplay */
|
/* Main gameplay */
|
||||||
int game() {
|
s16 game() {
|
||||||
unsigned int i;
|
u16 i;
|
||||||
int score = 0; /* score storage */
|
s16 score = -208; /* score storage */
|
||||||
int item, room_sum; /* true item value */
|
s16 item, room_sum; /* true item value */
|
||||||
int last_room = 0, last_room_clr = 0; /* last room markers */
|
s16 last_room = 0, last_room_clr = 0; /* last room markers */
|
||||||
char ch;
|
u8 ch;
|
||||||
deck_idx = 0; /* reset the deck index */
|
deck_idx = 0; /* reset the deck index */
|
||||||
/* Prefill the deck */
|
/* Prefill the deck */
|
||||||
for(i=0;i<13;i++) deck[i] = deck[i+13] = i+1;
|
for(i=0;i<13;i++) deck[i] = deck[i+13] = i+1;
|
||||||
@@ -547,8 +415,10 @@ int game() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(ch == CH_Q) {
|
if(ch == CH_Q) {
|
||||||
|
if(confirm("End game?")) {
|
||||||
hp = 0;
|
hp = 0;
|
||||||
break;
|
break;
|
||||||
|
} else print_msg("Go on!");
|
||||||
} else if(ch == CH_INVAL) {
|
} else if(ch == CH_INVAL) {
|
||||||
print_msg("Invalid input!");
|
print_msg("Invalid input!");
|
||||||
beep_err();
|
beep_err();
|
||||||
@@ -556,7 +426,7 @@ int game() {
|
|||||||
} else if(ch == CH_R) { /* run mechanics */
|
} else if(ch == CH_R) { /* run mechanics */
|
||||||
/* Compact and rearrange the deck */
|
/* Compact and rearrange the deck */
|
||||||
char temp_deck[sizeof(deck)];
|
char temp_deck[sizeof(deck)];
|
||||||
int t_idx = 0;
|
s16 t_idx = 0;
|
||||||
|
|
||||||
/* 1. Collect all valid remaining cards from deck */
|
/* 1. Collect all valid remaining cards from deck */
|
||||||
for(i=0; i<sizeof(deck); i++) {
|
for(i=0; i<sizeof(deck); i++) {
|
||||||
@@ -587,12 +457,12 @@ int game() {
|
|||||||
engaged++; /* increase the amount of engaged cards */
|
engaged++; /* increase the amount of engaged cards */
|
||||||
can_run = 0; /* Cannot run once engaged */
|
can_run = 0; /* Cannot run once engaged */
|
||||||
|
|
||||||
item = room[(int)ch]; /* get the item from the room */
|
item = room[ch]; /* get the item from the room */
|
||||||
room[(int)ch] = 0; /* mark the room slot as empty */
|
room[ch] = 0; /* mark the room slot as empty */
|
||||||
/* Note: We do NOT put the item back in deck. It is consumed. */
|
/* Note: We do NOT put the item back in deck. It is consumed. */
|
||||||
|
|
||||||
if(item < 14) { /* monster */
|
if(item < 14) { /* monster */
|
||||||
handle_monster(item + 1);
|
score += handle_monster(item + 1);
|
||||||
} else if(item < 23) { /* weapon */
|
} else if(item < 23) { /* weapon */
|
||||||
handle_weapon(item - 12);
|
handle_weapon(item - 12);
|
||||||
} else { /* potion */
|
} else { /* potion */
|
||||||
@@ -625,20 +495,13 @@ int game() {
|
|||||||
if(hp > 0) { /* win */
|
if(hp > 0) { /* win */
|
||||||
score = hp;
|
score = hp;
|
||||||
if(hp == 20) score += last_potion_val;
|
if(hp == 20) score += last_potion_val;
|
||||||
} else { /* fail */
|
|
||||||
for(i=0;i<sizeof(room);i++)
|
|
||||||
if(room[i] && room[i] < 14)
|
|
||||||
score -= (room[i] + 1);
|
|
||||||
for(i=0;i<sizeof(deck);i++)
|
|
||||||
if(deck[i] && deck[i] < 14)
|
|
||||||
score -= (deck[i] + 1);
|
|
||||||
}
|
}
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Entry point */
|
/* Entry point */
|
||||||
int main() {
|
s16 main() {
|
||||||
int score;
|
s16 score;
|
||||||
game_startup();
|
game_startup();
|
||||||
while(1) {
|
while(1) {
|
||||||
score = game();
|
score = game();
|
||||||
|
|||||||
Executable
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env jimsh
|
||||||
|
# Scoundrel roguelike solitaire game port to Jim Tcl
|
||||||
|
# Created by Luxferre in 2026, released into the public domain
|
||||||
|
|
||||||
|
# helpers
|
||||||
|
|
||||||
|
proc shuffle list {
|
||||||
|
set n [llength $list]
|
||||||
|
loop i 1 $n {
|
||||||
|
set j $(int(rand() * $n))
|
||||||
|
set temp [lindex $list $i]
|
||||||
|
lset list $i [lindex $list $j]
|
||||||
|
lset list $j $temp
|
||||||
|
}
|
||||||
|
return $list
|
||||||
|
}
|
||||||
|
|
||||||
|
proc get_valid_choice {prompt pat} {
|
||||||
|
set c --
|
||||||
|
puts -nonewline [format "%s " $prompt]
|
||||||
|
stdout flush
|
||||||
|
stdin tty input raw
|
||||||
|
while {$c ni $pat} {
|
||||||
|
set c [string tolower [stdin read 1]]
|
||||||
|
}
|
||||||
|
stdin tty input cooked
|
||||||
|
return $c
|
||||||
|
}
|
||||||
|
|
||||||
|
# variables and states
|
||||||
|
set score -208
|
||||||
|
set hp 20
|
||||||
|
set can_run 1
|
||||||
|
set engaged 0
|
||||||
|
set drank 0
|
||||||
|
set weapon 0
|
||||||
|
set durability 0
|
||||||
|
set rooms_clrd 0
|
||||||
|
set last_potion_val 0
|
||||||
|
|
||||||
|
# deck and room
|
||||||
|
set deck [shuffle [concat [range 1 14] [range 1 32]]]
|
||||||
|
set room {}
|
||||||
|
|
||||||
|
# game logic functions
|
||||||
|
|
||||||
|
proc game_field_display {} {
|
||||||
|
global hp weapon durability room deck can_run engaged
|
||||||
|
puts "--------------------------"
|
||||||
|
puts [format "H%02d | W%02d | D%02d | Deck: %02d" $hp $weapon $durability [llength $deck]]
|
||||||
|
puts "--------------------------"
|
||||||
|
foreach x $room {
|
||||||
|
if $($x < 14) {
|
||||||
|
puts -nonewline [format "M%02d " $($x + 1)]
|
||||||
|
} elseif $($x < 23) {
|
||||||
|
puts -nonewline [format "W%02d " $($x - 12)]
|
||||||
|
} else {
|
||||||
|
puts -nonewline [format "H%02d " $($x - 21)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
puts -nonewline "\n 1 2 3 4"
|
||||||
|
if {$can_run == 1 && $engaged == 0} {puts -nonewline " r)un->"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# monster handling logic (adds monster value to the score)
|
||||||
|
proc handle_monster {val} {
|
||||||
|
global weapon durability hp score
|
||||||
|
set healthlost $val
|
||||||
|
puts [format "You encounter a monster of strength %d." $val]
|
||||||
|
if {$weapon > 0 && $durability >= $val} {
|
||||||
|
if {[get_valid_choice "Use weapon? (y/n)" "y n"] == "y"} {
|
||||||
|
set healthlost $($val - $weapon)
|
||||||
|
if {$healthlost < 0} {set healthlost 0}
|
||||||
|
puts [format "\nYou fight the monster with your %d-level weapon." $weapon]
|
||||||
|
set durability $($val - 1)
|
||||||
|
if {$durability < 2} {
|
||||||
|
set healthlost 0
|
||||||
|
set weapon 0
|
||||||
|
puts "Your weapon fully breaks!"
|
||||||
|
}
|
||||||
|
} else {puts "\nYou fight the monster barehanded."}
|
||||||
|
} else {puts "You fight the monster barehanded."}
|
||||||
|
set hp $($hp - $healthlost)
|
||||||
|
if {$hp < 0} {set hp 0}
|
||||||
|
puts [format "Lost %d HP. New HP: %d" $healthlost $hp]
|
||||||
|
incr score $val
|
||||||
|
}
|
||||||
|
|
||||||
|
# weapon handling logic
|
||||||
|
proc handle_weapon {val} {
|
||||||
|
global weapon durability
|
||||||
|
set weapon $val
|
||||||
|
set durability 14
|
||||||
|
puts [format "Equipped a %d-level weapon." $weapon]
|
||||||
|
}
|
||||||
|
|
||||||
|
# potion handling logic
|
||||||
|
proc handle_potion {val} {
|
||||||
|
global hp drank deck room
|
||||||
|
if {$drank > 0} {
|
||||||
|
puts "Already drank a potion here! Discarding..."
|
||||||
|
} else {
|
||||||
|
if {[llength $deck] == 0 && [llength $room] == 0} {set last_potion_val $val}
|
||||||
|
incr hp $val
|
||||||
|
if {$hp > 20} {set hp 20}
|
||||||
|
set drank 1
|
||||||
|
puts [format "Drank a %d-level potion. New HP: %d" $val $hp]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# game banner
|
||||||
|
puts "----- Scoundrel 2026 -----"
|
||||||
|
puts " Jim Tcl port by Luxferre"
|
||||||
|
puts " Good luck!"
|
||||||
|
|
||||||
|
# main game loop
|
||||||
|
while {([llength $deck] + [llength $room]) > 0 && $hp > 0} {
|
||||||
|
if {$engaged == 0} {
|
||||||
|
while {[llength $deck] > 0 && [llength $room] < 4} {
|
||||||
|
lappend room [lindex $deck 0]
|
||||||
|
set deck [lreplace $deck 0 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game_field_display
|
||||||
|
set chs {1 2 3 4 q}
|
||||||
|
if {$can_run == 1 && $engaged == 0} {set chs {1 2 3 4 q r}}
|
||||||
|
set ch [get_valid_choice {} $chs]
|
||||||
|
puts {}
|
||||||
|
if {$ch == "q"} {
|
||||||
|
if {[get_valid_choice "Really quit? (y/n)" "y n"] == "y"} {
|
||||||
|
puts "\nQuitting!"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
} elseif {$ch == "r"} {
|
||||||
|
set can_run 0
|
||||||
|
while {[llength $room] > 0} {
|
||||||
|
set idx [rand 0 [llength $room]]
|
||||||
|
lappend deck [lindex $room $idx]
|
||||||
|
set room [lreplace $room $idx $idx]
|
||||||
|
}
|
||||||
|
puts "You run from the room."
|
||||||
|
} else {
|
||||||
|
incr ch -1
|
||||||
|
set item [lindex $room $ch]
|
||||||
|
if {$item > 0} {
|
||||||
|
set engaged 1
|
||||||
|
set room [lreplace $room $ch $ch]
|
||||||
|
if {$item < 14} {
|
||||||
|
handle_monster $($item + 1)
|
||||||
|
} elseif {$item < 23} {
|
||||||
|
handle_weapon $($item - 12)
|
||||||
|
} else {handle_potion $($item - 21)}
|
||||||
|
if {[llength $deck] > 0 && [llength $room] == 1} {
|
||||||
|
set can_run 1
|
||||||
|
set engaged 0
|
||||||
|
set drank 0
|
||||||
|
incr rooms_clrd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# now score and output the game result
|
||||||
|
if {$hp > 0} {
|
||||||
|
set score $hp
|
||||||
|
if {$hp == 20} {incr score $last_potion_val}
|
||||||
|
incr rooms_clrd
|
||||||
|
puts -nonewline "You win!"
|
||||||
|
} else {
|
||||||
|
puts -nonewline "You lose!"
|
||||||
|
}
|
||||||
|
puts [format " Your score: %d. Rooms cleared: %d" $score $rooms_clrd]
|
||||||
Reference in New Issue
Block a user