2026-03-31 16:47:55 +03:00
|
|
|
#!/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
|
2026-06-08 21:21:33 +03:00
|
|
|
(if (and (= (length room) 0) (= (length deck) 0))
|
2026-03-31 16:47:55 +03:00
|
|
|
(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)
|