emacs-rpgdm/docs/fate-rpg.org
2025-11-21 10:14:35 -06:00

164 lines
7.1 KiB
Org Mode

#+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'."
(unless 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: