added Fennel port
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user