reddit / emacs

Pick a random element in a weighted list

JBCKB · 2022-03-07 11:36
Hello , I'm looking to write a function, for my role-playing games, allowing me to create random tables with elements that have weight. For example, consider the following list: 1 - 20 : 2d6 kobold 21 - 45 : 1 troll 46 - 100 : 3d6 orcs Running the function I want to be able to pick 20% of the time the "2d6 kobold" etc. Ideally, the function would allow me to automatically execute the dice roll. I will have then, for example: 5 kobolds! Not being a programmer, all I can do is create a function to select an item from a "flat" list. Any idea?
EcstaticBill2638 · 2022-03-07 12:01
I'm not a programmer either, but I have had a go at exactly this in the past, here's what I came up with: (defun roll-weighted-list (alist) "Transforms a weighted `alist' of format (item . odds) into a `pcase' sexp and evals it. The total odds will be a sum of all the `odds' values in the `alist'." (eval (cl-loop for (key . value) in alist for running-total = (+ (or running-total 1) value) collect `((pred (> ,running-total)) ,key) into list finally return (append `(pcase (1+ (random ,(1- running-total)))) list)))) I think it works with your list if you enter it like this: (roll-weighted-list '(("2d6 kobolds" . 20) ("1 troll" . 15) ("3d6 orcs" . 65))) If you want it to do your die rolling in there, maybe use a helper function like this: (defun roll-dice (&optional dice num &rest exclude) "Roll a random number from 1 to `dice' (default 6). If `num' is specified, roll `num' times and sum the result. If `exclude' numbers are specified, re-roll any matching results (after summing)." (let* ((dice (or dice 6)) (num (or num 1)) (output (cl-loop repeat num sum (1+ (random dice))))) (if (member output exclude) (apply #'roll-dice dice num exclude) output))) and format the weighted list like this: (roll-weighted-list `((,(format "%s kobolds" (roll-dice 6 2) . 20) ("1 troll" . 15) ("3d6 orcs" . 65)))
bendersteed · 2022-03-07 13:21
(defvar enemy-alist '((20 (2 6) kobolds) (45 1 troll) (100 (3 6) orcs))) (defun dice (times size) (if (= times 1) (1+ (random size)) (+ (1+ (random size)) (dice (1- times) size)))) (defun pick-random-encounter (roll enemy-alist) (if (<= roll (first (car enemy-alist))) (cons (let ((number-or-spec (second (car enemy-alist)))) (if (listp number-or-spec) (apply #'dice number-or-spec) number-or-spec)) (third (car enemy-alist))) (pick-random-encounter roll (cdr enemy-alist)))) (defun random-encounter (enemy-alist) (let ((roll (dice 1 100))) (pick-random-encounter roll enemy-alist))) I think something like this could work, we express the possible partition of enemies as a list of specs (threshold (dice specs or number) enemy-symbol) with dice specs being lists and numbers plain atoms. It's important that the thresholds need to be in ascending order.
[deleted] · 2022-03-07 14:37
[removed]
JBCKB · 2022-03-07 14:51
Thank you kindly ! I rewrote the last snippet of code like this : (defun dd5-aqua-encounter-lvl1-4 () (interactive) (message (roll-weighted-list `( (,(format "%s kobolds" (roll-dice 6 2)) . 20) ("1 troll" . 15) (,(format "%s orcs" (roll-dice 6 3)) . 65)))))
EcstaticBill2638 · 2022-03-07 14:56
Ah yep, that's what I meant. Good luck with it.
JBCKB · 2022-03-07 14:58
thank you very much for your reply ^^ I will use the solution above but study yours to try to understand a little how it works ;)
JBCKB · 2022-03-07 15:00
I don't know if this is the place to start a debate on the different ways to practice ttrpg XD However, I play D&D, but also QuestWorlds or PbtA ;) (among others)
jsled · 2022-03-07 16:55
This has been removed, as it is not very civil; please attack ideas, not people.
nv-elisp · 2022-03-07 18:52
Here's another approach: (defun roll-weighted (items) "Rerturn a random weighted item from ITEMS. ITEMS is an alist of form: ((description . weight)...)." (let* ((rand (random (cl-reduce #'+ items :key #'cdr))) (len (length items)) (index -1) found) (catch 'found (while t (let* ((current (nth (mod (cl-incf index) len) items)) (current-weight (cdr current))) (when (< (setq rand (- rand current-weight)) current-weight) (throw 'found (car current)))))))) Translated from this [SO answer](https://stackoverflow.com/questions/1761626/weighted-random-numbers).