emacs-rpgdm/rpgdm.el
Howard Abrams 9411e512ce After a night or two of late hacking...
I have a semblance of code giving me functions to help me run my D&D
games. The README needs a lot more love, but the code is generally tidy.

Time to add some tables...
2021-01-29 23:18:36 -08:00

183 lines
8 KiB
EmacsLisp

;;; rpgdm.el --- Support utilities for the RPG Game Master -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Created: January 4, 2021
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This package includes all the help and support I can think for the Game
;; Master running a role-playing game.
;;
;; This include a minor mode, `rpgdm' that adds a few keybindings useful
;; with either org-mode or markdown-formatted files.
;;
;;; Code:
(defconst rpgdm-base (file-name-directory load-file-name))
(load-file (expand-file-name "rpgdm-dice.el" rpgdm-base))
(load-file (expand-file-name "rpgdm-screen.el" rpgdm-base))
(load-file (expand-file-name "rpgdm-tables.el" rpgdm-base))
(defgroup rpgdm nil
"Customization for the Dungeon Master support package."
:prefix "rpgdm-"
:group 'applications
:link '(url-link :tag "Github" "https://gitlab.com/howardabrams/emacs-rpgdm"))
(defun rpgdm-yes-and-50/50 ()
"Add spice to your 50/50 events (luck) with Yes/No+complications.
The Freeform Universal RPG has the idea that you could succeed or
fail, but have extra complications or extra bonuses. This returns
one of six answers with equal frequency:
- No, and ... in other words, no luck, plus a minor complication.
- No. ... Nope, no luck at this time.
- No, but ... in other words, no luck, but something else good.
- Yes, but ... you got what you wanted, but with a complication.
- Yes. ... Yup, luck is on your side.
- Yes, and ... Yes, plus you get a little something-something.
https://www.drivethrurpg.com/product/89534/FU-The-Freeform-Universal-RPG-Classic-rules"
(interactive)
(let (rolled (rpgdm--roll-die 6))
(cond ((= rolled 1) "No, and... (fails badly that you add a complication)")
((= rolled 2) "No.")
((= rolled 3) "No, but... (fails, but add a little bonus or consolation prize)")
((= rolled 4) "Yes, but... (succeeds, but add a complication or caveat)")
((= rolled 5) "Yes.")
(t "Yes, and... (succeeds, plus add a litle extra something-something)"))))
;; ----------------------------------------------------------------------
;; SKILL CHECKS
;; ----------------------------------------------------------------------
;; I would like to have a function that
(defun rpgdm--skill-level-dice (number-of-dice)
"Return a random skill challenge level.
The formula is based on the NUMBER-OF-DICE. According to the
Players Handbook in Dungeons and Dragons, we have this table
to determine difficulty skill check levels:
- Very easy 5
- Easy 10
- Medium 15
- Hard 20
- Very hard 25
- Nearly impossible 30
But I read somewhere that you could roll some 6 sided die to help
add a bit of randomness to the leve setting. Essentially, roll
the 6d and add 7.
Easy -- Die: 1 Lowest: 8 Highest: 13 Average: 10
Medium -- Die: 2 Lowest: 9 Highest: 19 Average: 14
Hard -- Die: 3 Lowest: 10 Highest: 25 Average: 17
Very hard -- Die: 4 Lowest: 11 Highest: 30 Average: 20
Nearly Impossible -- Die: 5 Lowest: 14 Highest: 35 Average: 24"
(rpgdm--sum
(rpgdm--roll number-of-dice 6 7)))
;; Let's verify my assumptions:
;; (dolist (die '(0 1 2 3 4 5))
;; (let* ((rolls (repeatedly 'rpgdm--skill-level-dice (list die) 1000))
;; (highest (apply 'max rolls))
;; (lowest (apply 'min rolls))
;; (average (/ (-sum rolls) 1000)))
;; (message
;; (format "Die: %d Lowest: %d Highest: %d Average: %d\n" die lowest highest average))))
(defun rpgdm--skill-level (target)
"Return a skill challenge level by Interpreting TARGET.
This parameter can be a symbol or string for 'easy', 'hard', etc.
Or it can be an actual number."
(when (symbolp target)
(setq target (symbol-name target)))
(cond ((string-prefix-p target "trivial" t) 5)
((string-prefix-p target "easy" t) (rpgdm--skill-level-dice 1))
((string-prefix-p target "moderate" t) (rpgdm--skill-level-dice 2))
((string-prefix-p target "medium" t) (rpgdm--skill-level-dice 2))
((string-prefix-p target "hard" t) (rpgdm--skill-level-dice 3))
((string-prefix-p target "difficult" t) (rpgdm--skill-level-dice 4))
((string-prefix-p target "very hard" t) (rpgdm--skill-level-dice 4))
((string-prefix-p target "impossible" t) (rpgdm--skill-level-dice 5))
((numberp target) target)
(t (max (string-to-number target) 12))))
(defun rpgdm--yes-and (target rolled-results)
"Instead of returning a pass/fail, return 'Yes, but' strings.
The Freeform Universal RPG has the idea that you could succeed or
fail, but have extra complications or extra bonuses based on how
high/low you pass/fail. I have expanded the idea with a d20, so
given a TARGET number, like '12', and the ROLLED-RESULTS from the
player, this returns a string based on a table.
https://www.drivethrurpg.com/product/89534/FU-The-Freeform-Universal-RPG-Classic-rules"
(cond ((< rolled-results (- target 7)) "No, and... !!")
((< rolled-results (- target 3)) "No.")
((< rolled-results target) "No, but...")
((< rolled-results (+ target 3)) "Yes, but...")
((< rolled-results (+ target 7)) "Yes.")
(t "Yes, and... !!")))
(ert-deftest rpgdm--yes-and-test ()
(should (equal (rpgdm--yes-and 10 1) "No, and..."))
(should (equal (rpgdm--yes-and 10 2) "No, and..."))
(should (equal (rpgdm--yes-and 10 3) "No."))
(should (equal (rpgdm--yes-and 10 4) "No."))
(should (equal (rpgdm--yes-and 10 5) "No."))
(should (equal (rpgdm--yes-and 10 6) "No."))
(should (equal (rpgdm--yes-and 10 7) "No, but..."))
(should (equal (rpgdm--yes-and 10 8) "No, but..."))
(should (equal (rpgdm--yes-and 10 9) "No, but..."))
(should (equal (rpgdm--yes-and 10 10) "Yes, but..."))
(should (equal (rpgdm--yes-and 10 11) "Yes, but..."))
(should (equal (rpgdm--yes-and 10 12) "Yes, but..."))
(should (equal (rpgdm--yes-and 10 13) "Yes."))
(should (equal (rpgdm--yes-and 10 14) "Yes."))
(should (equal (rpgdm--yes-and 10 15) "Yes."))
(should (equal (rpgdm--yes-and 10 16) "Yes."))
(should (equal (rpgdm--yes-and 10 17) "Yes, and...")))
(defun rpgdm-skill-check (target rolled-results)
"Given a TARGET skill check, and ROLLED-RESULTS, return pass/fail.
The string can return a bit of complications, from `rpgdm--yes-and'."
(interactive (list (completing-read "Target Level: "
'(Trivial Easy Moderate Hard Difficult Impossible))
(read-number "Rolled Results: ")))
(message (rpgdm--yes-and target rolled-results)))
(defun rpgdm-skill-check-easy (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for an easy skill check."
(interactive "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'easy) rolled-results))
(defun rpgdm-skill-check-moderate (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for a moderately-difficult skill check."
(interactive "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'medium) rolled-results))
(defun rpgdm-skill-check-hard (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for a hard skill check."
(interactive "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'hard) rolled-results))
(defun rpgdm-skill-check-difficult (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for a difficult skill check."
(interactive "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'difficult) rolled-results))
(defun rpgdm-skill-check-impossible (rolled-results)
"Return an embellished pass/fail from ROLLED-RESULTS for an almost impossible skill check."
(interactive "nRolled Results: ")
(rpgdm-skill-check (rpgdm--skill-level 'impossible) rolled-results))
(provide 'rpgdm)
;;; rpgdm.el ends here