diff --git a/README.md b/README.md index 2a3aa2f..b2cd83a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is a collection of my own ports of the [Scoundrel](http://stfj.net/art/2011 * [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 ## Ports NOT included in this repo for various reasons diff --git a/fennel/scoundrel.fnl b/fennel/scoundrel.fnl new file mode 100644 index 0000000..3c19ae1 --- /dev/null +++ b/fennel/scoundrel.fnl @@ -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) 1) (= (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)