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

7.1 KiB

Fate and Fate Accelerated RPG

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:

(defun rpgdm--fate-die ()
  "Return a cons of a Fate die and its visual representation."
  (seq-random-elt '((-1 . "[-]")
                    ( 0 . "[ ]")
                    ( 1 . "[+]"))))

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.

(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))

So calling the rpgdm--fate-role-dice would return something like:

'((0 . "[ ]")
  (1 . "[+]")
  (0 . "[ ]")
  (-1 . "[-]"))

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.

(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))))

And we make an interactive version:

(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))))

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:

-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:

(defvar rpgdm-fate-ladder rpgdm-fate-ladder-values "The FATE RPG ladder of challenge levels.")

Now we can use it to prompt for the

(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))))

This assumes that we can create a prompt from one entry in our table:

(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))))

Let's prompt for both the challenge level as well as the current effort:

(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))))