#+TITLE: Fate and Fate Accelerated RPG #+AUTHOR: Howard X. Abrams #+DATE: 2021-08-25 August #+TAGS: rpg fate In my quest for a simple, but generic RPG, I've noticed FATE, and the toolbox for crafting your own stuff. * Fate Dice Obviously, step one for me in looking at a new game system is to render their dice, which I want to do slightly differently than returning a random number. This function returns both the random number and its /visual aspect/ of a =+=, =-=, or blank: #+BEGIN_SRC emacs-lisp :results silent (defun rpgdm--fate-die () "Return a cons of a Fate die and its visual representation." (seq-random-elt '((-1 . "[-]") ( 0 . "[ ]") ( 1 . "[+]")))) #+END_SRC Instead of an even spread (like in many dice ranged from 1-20 or 1-100), we roll FATE dice in groups of four to create a /bell curve/. Sure, the range is -4 to +4, but those edges show up a lot less. The odds are easy enough to calculate, but I like to see the percentage to roll the actual results, but also to see the change of getting the result or lower/higher, as in the final three columns: | | *Odds* | Rolling | Rolling this | Rolling this | | | | Exactly | or Higher | or Lower | |----+-------+---------+--------------+--------------| | +4 | 1/81 | 1.2% | 1.2% | 100.0% | | +3 | 4/81 | 4.9% | 6.2% | 98.8% | | +2 | 10/81 | 12.3% | 18.5% | 93.8% | | +1 | 16/81 | 19.8% | 38.3% | 81.5% | | 0 | 19/81 | 23.5% | 61.7% | 61.7% | | -1 | 16/81 | 19.8% | 81.5% | 38.3% | | -2 | 10/81 | 12.3% | 93.8% | 18.5% | | -3 | 4/81 | 4.9% | 98.8% | 6.2% | | -4 | 1/81 | 1.2% | 100.0% | 1.2% | Let's have a function that can roll four dice and give a list of the tuples. #+BEGIN_SRC emacs-lisp (defun rpgdm--fate-roll-dice (&optional number) "Return a list of Fate roll results. Each element of the list is a cons of its value and its visual representation, see `rpgdm--fate-die'." punless number (setq number 4)) (let (results) (--dotimes number (push (rpgdm--fate-die) results)) results)) #+END_SRC So calling the =rpgdm--fate-role-dice= would return something like: #+begin_src emacs-lisp :tangle no '((0 . "[ ]") (1 . "[+]") (0 . "[ ]") (-1 . "[-]")) #+end_src My UI will collect the roll and figure out how to sum and display the results as a list where the first element is the total and the second is a string that we could display. #+BEGIN_SRC emacs-lisp (defun rpgdm--fate-roll (modifier &optional number) "Simulate a FATE dice roll and return the results. Return a list where the first element is the total results, and the second element is a user-formatted view of the results. " (unless number (setq number 4)) (let* ((roll (rpgdm--fate-roll-dice number)) (vals (-map 'car roll)) (dice (-map 'cdr roll)) (roll (s-join " " dice)) (sum (apply '+ vals)) (total (+ sum modifier)) (results (propertize (number-to-string total) 'face '(:foreground "green")))) (list total (format "Rolled: %s :: %s + %d" results roll modifier)))) #+END_SRC And we make an interactive version: #+BEGIN_SRC emacs-lisp (defun rpgdm-fate-roll (modifier) "Simulate a FATE dice roll and display the results. The total is the difference of the `+' to `-' plus the MODIFIER. Note that the NUMBER of dice defaults to 4." (interactive (list (rpgdm-fate-ladder "effort level"))) (rpgdm-message (second (rpgdm--fate-roll modifier 4)))) #+END_SRC * Skill Checks First step is the Ladder, which is a simple table containing the numeric ranking (to use in calculations), a key-mnemonic, and a descriptive /skill/ label. The final column is a descriptive label for /challenge/ level: #+name: ladder-table | -2 | t | Terrible | Trivial | | -1 | p | Poor | Easy | | 0 | m | Mediocre | Simple | | 1 | a | Average | Average | | 2 | f | Fair | | | 3 | g | Good | Hard | | 4 | r | Great | Daunting | | 5 | s | Superb | Extreme | | 6 | f | Fantastic | Impossible | | 7 | e | Epic | | | 8 | l | Legendary | | And use that table as a global variable: #+BEGIN_SRC emacs-lisp :var rpgdm-fate-ladder-values=ladder-table (defvar rpgdm-fate-ladder rpgdm-fate-ladder-values "The FATE RPG ladder of challenge levels.") #+END_SRC Now we can use it to prompt for the #+BEGIN_SRC emacs-lisp (defun rpgdm-fate-ladder (&optional request-type) "Prompt for a choice on the FATE ladder, and return the numeric value of that level. The REQUEST-TYPE is an optional string inserted into the prompt to describe the request." (interactive) (unless request-type (setq request-type "challenge level")) (let* ((choices (mapconcat 'rpgdm--fate-ladder-prompt rpgdm-fate-ladder " ")) (prompt (format "What is the %s?\n%s" (propertize request-type 'face '(:foreground "yellow")) choices)) (choice (char-to-string (read-char prompt))) (entry (--filter (equal (second it) choice) rpgdm-fate-ladder))) (first (first entry)))) #+END_SRC This assumes that we can create a prompt from one entry in our table: #+BEGIN_SRC emacs-lisp (defun rpgdm--fate-ladder-prompt (entry) (let* ((entry-number (format "[%d]" (first entry))) (render-number (propertize entry-number 'face '(:foreground "#888888"))) (keyed-prompt (propertize (second entry) 'face '(:foreground "green")))) (format "%s) %s %s" keyed-prompt (third entry) render-number)))) #+END_SRC Let's prompt for both the challenge level as well as the current effort: #+BEGIN_SRC emacs-lisp (defun rpgdm-fate-challenge (opposition-level effort-level) "Return a user message for a FATE dice challenge. Given a numeric EFFORT-LEVEL as well as the OPPOSITION-LEVEL, this function rolls the Fate dice and interprets the results." (interactive (list (rpgdm-fate-ladder "opposition level") (rpgdm-fate-ladder "effort level"))) (let* ((die-roll (rpgdm--fate-roll effort-level)) (shifts (- (first die-roll) opposition-level)) (results (cond ((< shifts 0) (propertize "Failed" 'face '(:foregound "red"))) ((= shifts 0) (propertize "Tie" 'face '(:foreground "yellow"))) ((> shifts 3) (propertize "Succeed with Style!" 'face '(:foregound "green"))) (t (propertize "Success" 'face '(:foregound "green")))))) (rpgdm-message "%s ... %s" results (second die-roll)))) #+END_SRC #+PROPERTY: header-args:sh :tangle no #+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-fate.el #+PROPERTY: header-args :results none :eval no-export :comments no #+OPTIONS: num:nil toc:nil todo:nil tasks:nil tags:nil date:nil #+OPTIONS: skip:nil author:nil email:nil creator:nil timestamp:nil #+INFOJS_OPT: view:nil toc:nil ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js # Local Variables: # eval: (add-hook 'after-save-hook #'org-babel-tangle t t) # End: