341 lines
14 KiB
EmacsLisp
341 lines
14 KiB
EmacsLisp
;;; RPG-DM-HELPER --- Help for a Dungeon Master -*- lexical-binding: t; -*-
|
|
;;
|
|
;; Author: Howard Abrams <howard@howardabrams.com>
|
|
;; Copyright © 2018, Howard Abrams, all rights reserved.
|
|
;; Created: 15 August 2018
|
|
;;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;
|
|
;;; Commentary:
|
|
;;
|
|
;; Functions to help a DM use an org-mode file as the basis of
|
|
;; notes for an adventure.
|
|
;;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;
|
|
;; This program is free software; you can redistribute it and/or
|
|
;; modify it under the terms of the GNU General Public License as
|
|
;; published by the Free Software Foundation; either version 3, or
|
|
;; (at your option) any later version.
|
|
;;
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
;; General Public License for more details.
|
|
;;
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program; see the file COPYING. If not, write to
|
|
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
|
|
;; Floor, Boston, MA 02110-1301, USA.
|
|
;;
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;;
|
|
;;; Code:
|
|
|
|
(defvar rpgdm-previous-roll-expression nil
|
|
"Whenever we roll a dice from an expression, we remember it
|
|
here, so that we can re-roll it again.")
|
|
|
|
;; The basics of a dice roll is a random number from a given range. Note that if
|
|
;; we give a 6-sided die to the random number, we will end up with a range of 0
|
|
;; to 5, so we need to increase this value by 1.
|
|
|
|
(defun rpgdm--roll-die (sides)
|
|
"Rolls a die of with SIDES."
|
|
(1+ (random sides)))
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; TESTING SUPPORT
|
|
;;
|
|
;; Unit Testing for random numbers is tricky, so what I will want to do is call
|
|
;; a function a number of times. A little function that `repeatedly' calls a
|
|
;; function with arguments and returns a list of the results should be helpful.
|
|
|
|
(defun repeatedly (fn args times)
|
|
"Call a function, FN, with a list of arguments, ARGS, a number of TIMES.
|
|
Return a list of results."
|
|
(let (value)
|
|
(dotimes (number times value)
|
|
(setq value (cons (apply fn args) value)))))
|
|
|
|
;; This function will run a large number of runs and verify that all dice rolls
|
|
;; fall between two ranges. This is completely accurate, as our dice rolls could
|
|
;; be in a smaller subset, so we also check to make sure that at least one roll
|
|
;; was at each end. This should be _good enough_.
|
|
|
|
(defun rpgdm--test-rolls (fn args min max)
|
|
"Run function FN with ARGS to validate all results.
|
|
The numbers returned should be between MIN and MAX, with an
|
|
average value of AVG, if given."
|
|
(let ((rolls (repeatedly fn args 1000)))
|
|
(should (--some? (= it min) rolls))
|
|
(should (--some? (= it max) rolls))
|
|
(should (--every? (>= it min) rolls))
|
|
(should (--every? (<= it max) rolls))))
|
|
|
|
(ert-deftest rpgdm--roll-expression-test ()
|
|
"Simple test of my random number generator.
|
|
This really tests the `rpgdm--test-rolls' function."
|
|
(dolist (test-data '((4 1 4)
|
|
(6 1 6)
|
|
(8 1 8)
|
|
(20 1 20)
|
|
(100 1 100)))
|
|
(cl-destructuring-bind (die lowest highest) test-data
|
|
(rpgdm--test-rolls #'rpgdm--roll-die (list die) lowest highest))))
|
|
|
|
;; ----------------------------------------------------------------------
|
|
|
|
;; Now that we have a `rpgdm--roll-die' function that rolls a single die. How do
|
|
;; we want multiple dice rolls. Perhaps the results should be a list, so that we
|
|
;; can easily sum them, but still have the original results of each die roll.
|
|
|
|
(defun rpgdm--roll-dice (count sides)
|
|
"Return a list of COUNT dice rolls where each die has SIDES."
|
|
(let (value)
|
|
(dotimes (_ count value)
|
|
(setq value (cons (rpgdm--roll-die sides) value)))))
|
|
|
|
(ert-deftest rpgdm--roll-dice-test ()
|
|
"Validate the `rpgdm--roll-dice' by making sure we get a list
|
|
of all die rolls, and that each number is within the range.
|
|
We can assume that `rpgdm--roll-dice' works."
|
|
(let ((results (rpgdm--roll-dice 4 6)))
|
|
(should (= (length results) 4))
|
|
(should (--every? (>= it 1) results))
|
|
(should (--every? (<= it 6) results))))
|
|
|
|
;; An RPG has checks that have multiple dice, plus a modifier (positive or
|
|
;; negative). When displaying the results, I want all the dice rolls displayed
|
|
;; differently from the modifier. Should we just assume the modifier is the
|
|
;; first or last number is the returned list?
|
|
;;
|
|
;; What if we have this function return a cons'd with the `car' be a list of the
|
|
;; rolls, and the `cdr' the modifier amount?
|
|
|
|
(defun rpgdm--roll (num-dice dice-type &optional modifier plus-minus)
|
|
"Generate a random dice roll. Return tuple where `car' is a list of rolled
|
|
results, and the `cdr' is the modifier, see `rpgdm--sum'.
|
|
|
|
The NUM-DICE is the number of DICE-TYPE to roll. The PLUS-MINUS
|
|
is a string of either '+' or '-' to affect the results with the
|
|
MODIFIER amount. If PLUS-MINUS is nil, assume MODIFIER should
|
|
be added."
|
|
(let* ((base-rolls (rpgdm--roll-dice num-dice dice-type)))
|
|
(cond ((string= "-" plus-minus) (cons base-rolls (- modifier)))
|
|
((numberp modifier) (cons base-rolls modifier))
|
|
(t (cons base-rolls 0)))))
|
|
|
|
(defun rpgdm--sum (roll-combo)
|
|
"Return a summation of the dice rolls in ROLL-COMBO tuple.
|
|
The tuple is a `cons'd structure where the `car' is a list of rolls,
|
|
and the `cdr' is a modifier, e.g. '((5 3 2 1 6) . 3)"
|
|
(let ((rolls (car roll-combo))
|
|
(modifier (cdr roll-combo)))
|
|
(+ (-sum rolls) modifier)))
|
|
|
|
(ert-deftest rpgdm--sum-test ()
|
|
(should (= (rpgdm--sum '((1 6) . 3)) 10))
|
|
(should (= (rpgdm--sum '((6) . -3)) 3))
|
|
(should (= (rpgdm--sum '(() . 0)) 0)))
|
|
|
|
(defun rpgdm--test-roll-series (fn args min max)
|
|
"Run function FN with ARGS to validate all results.
|
|
The numbers returned should be between MIN and MAX, with an
|
|
average value of AVG, if given."
|
|
(let ((roll-sums (->> (repeatedly fn args 1000)
|
|
(-map 'rpgdm--sum))))
|
|
;; (should (--some? (= it min) roll-sums))
|
|
;; (should (--some? (= it max) roll-sums))
|
|
(should (--every? (>= it min) roll-sums))
|
|
(should (--every? (<= it max) roll-sums))))
|
|
|
|
(ert-deftest rpgdm--roll-test ()
|
|
(let ((test-data '(((1 6) 1 6)
|
|
((3 6 4) 7 22)
|
|
((4 6 4 "-") 0 20))))
|
|
(dolist (test-seq test-data)
|
|
(cl-destructuring-bind (dice-args lowest highest) test-seq
|
|
(rpgdm--test-roll-series 'rpgdm--roll dice-args lowest highest)))))
|
|
|
|
|
|
;; For programmatic reasons, we need a quick way to roll dice and get a
|
|
;; numeric value.
|
|
|
|
(defun rpgdm-roll-sum (first &optional dice-type modifier)
|
|
"Return a number value from rolling some dice.
|
|
The FIRST can be one of the following values:
|
|
- A dice expression as a string, e.g. 2d4+2
|
|
- A roll-combo tuple list
|
|
- A single number of dice to roll (but this requires more values)
|
|
|
|
If FIRST is an integer, then DICE-TYPE is the number of dice sides.
|
|
MODIFIER, if given, is added to roll."
|
|
(cond
|
|
((stringp first) (rpgdm--sum (rpgdm--roll-expression first)))
|
|
((listp first) (rpgdm--sum first))
|
|
(t (rpgdm--sum (rpgdm--roll first dice-type modifier)))))
|
|
|
|
|
|
;; Now that we can roll a die with distinct numbers, let's now deal with dice
|
|
;; strings, e.g. 2d10+4. Can we have a regular expression that could identify
|
|
;; as well as pull apart the individual numbers?
|
|
|
|
(defvar rpgdm-roll-regexp
|
|
(rx word-start
|
|
(optional (group (one-or-more digit)))
|
|
"d"
|
|
(group (one-or-more digit))
|
|
(optional
|
|
(group (or "+" "-"))
|
|
(group (one-or-more digit)))
|
|
(optional ":"
|
|
(group (one-or-more digit)))
|
|
word-end)
|
|
"A regular expression that matches a dice roll.")
|
|
|
|
;; See: ace-jump-search-candidate (re-query-string visual-area-list)
|
|
|
|
(defun rpgdm-forward-roll (count)
|
|
"Move the point to the next COUNT of a dice roll.
|
|
|
|
Note: This moves the point to the _beginning_ of what is
|
|
considered the dice roll description, which could include any of
|
|
the following:
|
|
|
|
- d8
|
|
- 2d6
|
|
- 1d12+5
|
|
- d20-4"
|
|
(interactive "p")
|
|
(when (looking-at-p rpgdm-roll-regexp)
|
|
(re-search-forward rpgdm-roll-regexp))
|
|
(dotimes (repeat count)
|
|
(re-search-forward rpgdm-roll-regexp))
|
|
(goto-char (match-beginning 0)))
|
|
|
|
(defun rpgdm-dice-format-string (str)
|
|
"Replace all dice expressions in STR with a dice roll results."
|
|
(while (string-match rpgdm-roll-regexp str)
|
|
(replace-regexp-in-string (concat rpgdm-roll-regexp "'") 'rpgdm-roll-sum str)))
|
|
|
|
;; Practice: somed8 d4 2d8 3d6+2
|
|
|
|
(defun rpgdm--roll-expression (expression)
|
|
"Return dice roll of EXPRESSION as a string, e.g. 2d6+3."
|
|
(if (string-match rpgdm-roll-regexp expression)
|
|
(let* ((num-dice-s (or (match-string 1 expression) "1"))
|
|
(num-dice (string-to-number num-dice-s))
|
|
(dice-type-s (or (match-string 2 expression) "20"))
|
|
(dice-type (string-to-number dice-type-s))
|
|
(plus-minus (or (match-string 3 expression) "+"))
|
|
(modifier-s (or (match-string 4 expression) "0"))
|
|
(modifier (string-to-number modifier-s)))
|
|
(rpgdm--roll num-dice dice-type modifier plus-minus))
|
|
(rpgdm--roll 1 20)))
|
|
|
|
(ert-deftest rpgdm--roll-expression-test ()
|
|
(let ((test-cases '(("d6" 1 6)
|
|
("DC" 1 20)
|
|
("2d12" 2 24)
|
|
("3d6+2" 5 20))))
|
|
(dolist (test-data test-cases)
|
|
(cl-destructuring-bind (dice-expression lowest highest) test-data
|
|
(rpgdm--test-roll-series 'rpgdm--roll-expression (list dice-expression) lowest highest)))))
|
|
|
|
|
|
(defun rpgdm--display-roll (roll-combo &optional expression)
|
|
"Convert a ROLL-COMBO.results into a suitable string.
|
|
The format for a roll combo is described with `rpgdm--sum' function.
|
|
The EXPRESSION is a string that may have generated the roll combo."
|
|
(let ((answer (rpgdm--sum roll-combo))
|
|
(die-rolls (car roll-combo))
|
|
(modifier (cdr roll-combo)))
|
|
(rpgdm--display-roll-parts answer die-rolls modifier expression)))
|
|
|
|
(defun rpgdm--display-roll-parts (answer die-rolls modifier &optional expression)
|
|
"Render parameters into a suitable string.
|
|
The ANSWER is probably the sum expression of our dice rolls,
|
|
rendered brightly. And DIE-ROLLS is a list of the die rolls.
|
|
MODIFIER is positive or negative number. The EXPRESSION is a
|
|
string that may have generated the roll combo."
|
|
(let* ((sum-str (propertize (number-to-string answer) 'face 'alert-moderate-face))
|
|
(die-str (cond ((and (= (length die-rolls) 1)
|
|
(= modifier 0)) "")
|
|
((= (length die-rolls) 1) (format " ... %d" (car die-rolls)))
|
|
(t (format " ... %s" die-rolls))))
|
|
(mod-str (cond ((> modifier 0) (format " +%d" modifier))
|
|
((< modifier 0) (format " %d" modifier))
|
|
(t "")))
|
|
(exp-str (if expression
|
|
(format " | %s" expression)
|
|
"")))
|
|
(format "%s%s%s%s" sum-str die-str mod-str exp-str)))
|
|
|
|
(ert-deftest rpgdm--display-roll-test ()
|
|
(should (equal (rpgdm--display-roll '((1 2 3) . 0)) "6 ... (1 2 3)"))
|
|
(should (equal (rpgdm--display-roll '((1 2 3) . 4)) "10 ... (1 2 3) +4"))
|
|
(should (equal (rpgdm--display-roll '((1 2 3) . -4)) "2 ... (1 2 3) -4"))
|
|
(should (equal (rpgdm--display-roll '((2) . 4)) "6 ... 2 +4"))
|
|
(should (equal (rpgdm--display-roll '((2) . 0)) "2"))
|
|
(should (equal (rpgdm--display-roll '((1 2 3) . 4) "3d6+4") "10 ... (1 2 3) +4 | 3d6+4")))
|
|
|
|
(defun rpgdm-roll (expression)
|
|
"Generate a random number based on a given dice roll EXPRESSION.
|
|
Unless the point is on a dice roll description, e.g 2d12+3."
|
|
(interactive (list (if (looking-at rpgdm-roll-regexp)
|
|
(match-string-no-properties 0)
|
|
(read-string "Dice Expression: "))))
|
|
(setq rpgdm-previous-roll-expression expression)
|
|
(let ((roll-results (rpgdm--roll-expression expression)))
|
|
(rpgdm-message "Rolled: %s" (rpgdm--display-roll roll-results expression))))
|
|
|
|
(defun rpgdm-roll-again ()
|
|
"Roll the previous expression ... again.
|
|
Never rolled before? No problem, we'll query for the expression
|
|
if we need too."
|
|
(interactive)
|
|
(if rpgdm-previous-roll-expression
|
|
(rpgdm-roll rpgdm-previous-roll-expression)
|
|
(call-interactively 'rpgdm-roll)))
|
|
|
|
;; ----------------------------------------------------------------------
|
|
;; ADVANTAGE and DISADVANTAGE ROLLS
|
|
;; ----------------------------------------------------------------------
|
|
|
|
(defun rpgdm--roll-with-choice (choose-fn modifier &optional plus-minus)
|
|
"Roll a d20 but choose the results based on the CHOOSE-FN function.
|
|
This is really a helper function for rolling with advantage or
|
|
disadvantage. The results are added to the MODIFIER and PLUS-MINUS,
|
|
and the entire thing is formatted with `rpgdm--display-roll-parts'."
|
|
(let* ((rolls (rpgdm--roll 2 20 modifier plus-minus))
|
|
(die-rolls (car rolls))
|
|
(modifier (cdr rolls))
|
|
(answer (+ (apply choose-fn die-rolls) modifier)))
|
|
(rpgdm--display-roll-parts answer die-rolls modifier)))
|
|
|
|
(defun rpgdm-roll-advantage (modifier &optional plus-minus)
|
|
"Roll a d20 with advantage (rolling twice taking the higher).
|
|
If looking at a dice expression, use it for MODIFIER (the
|
|
PLUS-MINUS string from the regular expression,`rpgdm-roll-regexp'),
|
|
otherwise, prompt for the modifier. Results are displayed."
|
|
(interactive (list (if (looking-at rpgdm-roll-regexp)
|
|
(match-string-no-properties 0)
|
|
(read-number "Advantage roll with modifier: "))))
|
|
(rpgdm-message "Rolled with Advantage: %s"
|
|
(rpgdm--roll-with-choice 'max modifier plus-minus)))
|
|
|
|
(defun rpgdm-roll-disadvantage (modifier &optional plus-minus)
|
|
"Roll a d20 with disadvantage (rolling twice taking the lower).
|
|
If looking at a dice expression, use it for MODIFIER (the
|
|
PLUS-MINUS string from the regular expression,`rpgdm-roll-regexp'),
|
|
otherwise, prompt for the modifier. Results are displayed."
|
|
(interactive (list (if (looking-at rpgdm-roll-regexp)
|
|
(match-string-no-properties 0)
|
|
(read-number "Disadvantage roll with modifier: "))))
|
|
(rpgdm-message "Rolled with Disadvantage: %s"
|
|
(rpgdm--roll-with-choice 'min modifier plus-minus)))
|
|
|
|
|
|
(provide 'rpgdm-dice)
|
|
;;; rpgdm-dice.el ends here
|