7 Commits
6 changed files with 596 additions and 0 deletions
+4
View File
@@ -7,11 +7,15 @@ 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
* [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
* [(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)
* [Bash shell port](https://codeberg.org/luxferre/bash-goodies/src/branch/master/scoundrel)
## Currently planned ports
+203
View File
@@ -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)
+13
View File
@@ -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
+11
View File
@@ -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)
+193
View File
@@ -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)))
+172
View File
@@ -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]