Created
August 17, 2026 17:02
-
-
Save Kodiologist/e757e5c17d934a58753a34d57d30e676 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env hy | |
| "Randomly generate ideas for a game of the tabletop RPG Kill Sector | |
| using the system described in Gauntlet-O-Matic. | |
| https://www.drivethrurpg.com/en/product/492971/gauntlet-o-matic-gauntlet-generation-tool-for-kill-sector | |
| This program is in the public domain (although the linked book is not)." | |
| (require | |
| hyrule [do-n list-n]) | |
| (import | |
| string | |
| re | |
| json | |
| pathlib [Path] | |
| hyrule [thru]) | |
| (setv _prompts None) | |
| (defn prompts [[path None]] | |
| (global _prompts) | |
| (when (is _prompts None) | |
| (setv _prompts (dfor | |
| d (json.loads (.read-text (Path path))) | |
| (:prompt d) (:options d)))) | |
| _prompts) | |
| (defn random-seed [] | |
| (.join "" (hy.I.random.choices :k 20 | |
| (+ string.ascii-letters string.digits "_")))) | |
| (defn generate-gauntlet [seed] | |
| (setv buffer (hy.I.io.StringIO)) | |
| (defn pr [#* args #** kwargs] (print :file buffer #* args #** kwargs)) | |
| (setv R (hy.I.random.Random seed)) | |
| (pr "Seed:" (hy.repr seed)) | |
| (defn roll-choice [question] | |
| "Roll 1d100 to pick an item for a prompt." | |
| (setv i (R.randint 1 100)) | |
| (for [[i-range value] (.items (get (prompts) question))] | |
| (setv [lo hi] (if (in "-" i-range) | |
| (.split i-range "-") | |
| [i-range i-range])) | |
| (when (<= (int lo) i (int hi)) | |
| (return value))) | |
| (assert False)) | |
| (defn roll-enemy-nature [] | |
| (setv n 1) | |
| (setv parts []) | |
| (while n | |
| (setv x (get (re.match r"[A-Za-z]+" (roll-choice "Enemy nature")) 0)) | |
| (-= n 1) | |
| (when (= x "Combo") | |
| (+= n 2) | |
| (continue)) | |
| (.append parts (if (= x "Guest") | |
| f"Guest ({(roll-choice #[[Where's the arena?]])})" | |
| x))) | |
| (.join ", " (sorted parts))) | |
| (defn roll-enemy-tier [] | |
| "Generate a random enemy type." | |
| (setv good-traits []) | |
| (setv bad-traits []) | |
| (setv tiers { | |
| "Horde Fighter" (dict :n-good 1 :n-bad 1) | |
| "Squad Fighter" (dict :n-good 2 :n-bad 1) | |
| "Warrior" (dict :n-good 4 :n-bad 1) | |
| "Miniboss" (dict :n-good 5 :n-bad 2) | |
| "Wildcard" (fn [] | |
| (.append good-traits (roll-choice "Good traits")) | |
| (.append bad-traits (roll-choice "Bad traits"))) | |
| "Specialist" (fn [] | |
| (.remove good-traits (R.choice good-traits)) | |
| (.append good-traits (R.choice good-traits)))}) | |
| (setv callbacks []) | |
| (setv full-tier-name "") | |
| (while True | |
| (setv tier (roll-choice "Enemy tier")) | |
| (setv [tier-name tier-description] (.split tier ". " 1)) | |
| (when (and (= tier-name "Specialist") (<= (len good-traits) 2)) | |
| ; We don't have enough good traits to apply this modifier. Try again. | |
| (continue)) | |
| (+= full-tier-name " " tier-name) | |
| (setv tier-effect (get tiers tier-name)) | |
| (when (isinstance tier-effect dict) | |
| (.extend good-traits (list-n (:n-good tier-effect) | |
| (roll-choice "Good traits"))) | |
| (.extend bad-traits (list-n (:n-bad tier-effect) | |
| (roll-choice "Bad traits"))) | |
| (break)) | |
| (.append callbacks tier-effect)) | |
| (for [f (reversed callbacks)] | |
| (f)) | |
| (dict | |
| :name (.lstrip full-tier-name) | |
| :desc (re.sub r", \d+ Good Traits?, \d+ Bad Traits?" "" tier-description) | |
| :good-traits (sorted good-traits) | |
| :bad-traits (sorted bad-traits))) | |
| (setv [n-settings] (R.choices | |
| :population [1 2 3] | |
| :weights [12 5 3])) | |
| (setv question None) | |
| (defn prompt [q] | |
| (nonlocal question) | |
| (setv question q) | |
| (pr) | |
| (pr q)) | |
| (defn roll [] | |
| (roll-choice question)) | |
| (prompt "Where's the arena?") | |
| (pr (if (= n-settings 1) | |
| (roll) | |
| (+ "Mash up the following:\n" (.join "\n" (gfor | |
| i (range n-settings) | |
| (+ "- " (roll))))))) | |
| (prompt "Why are we here?") | |
| (pr (roll)) | |
| (prompt "What's the scenario?") | |
| (pr "(Use the first for the whole gauntlet, or use one per wave.)") | |
| (do-n 3 | |
| (pr "-" (roll))) | |
| (prompt "Is there a twist?") | |
| (pr (roll)) | |
| (prompt "What's the halftime?") | |
| (pr (roll)) | |
| (for [wave [1 2]] | |
| (pr) | |
| (pr "Enemies for wave" wave) | |
| (for [enemy [1 2 3]] | |
| (pr "- Enemy type" enemy) | |
| (pr " Nature:" (roll-enemy-nature)) | |
| (setv enemy (roll-enemy-tier)) | |
| (pr " Tier:" (+ (:name enemy) ". " (:desc enemy))) | |
| (pr " Good traits:" (:good-traits enemy)) | |
| (pr " Bad traits:" (:bad-traits enemy)))) | |
| (pr) | |
| (pr "Boss") | |
| (pr "Nature:" (roll-enemy-nature)) | |
| (pr "Good traits (exaggerated):" (list-n 3 (roll-choice "Good traits"))) | |
| (pr "Bad trait (exaggerated):" (roll-choice "Bad traits")) | |
| (prompt "Sector layout") | |
| (setv layout (roll)) | |
| (pr layout) | |
| (for [sector-i (thru 1 (* (int (get layout 0)) (int (get layout 2))))] | |
| (pr f"- Feature for sector {sector-i}:" (roll-choice "Sector features"))) | |
| (.getvalue buffer)) | |
| (when (= __name__ "__main__") | |
| (prompts "kill_sector_gauntlet_o_matic.json") | |
| (print :end "" (generate-gauntlet (random-seed)))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment