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...
This commit is contained in:
commit
9411e512ce
5 changed files with 977 additions and 0 deletions
29
README.org
Normal file
29
README.org
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#+TITLE: Dungeon Master Support in Emacs
|
||||
#+AUTHOR: Howard X. Abrams
|
||||
#+EMAIL: howard.abrams@workday.com
|
||||
#+DATE: 2021-01-27 January
|
||||
#+TAGS: rpg
|
||||
|
||||
The overlap between Emacs and running a Dungeons and Dragon campaign
|
||||
|
||||
* Themes
|
||||
** Yes/No Complications
|
||||
According to [[https://www.hipstersanddragons.com/difficulty-classes-for-ability-checks-5e/][this essay]], the standard DC 15 skill check is actually /too hard/ for most situations,
|
||||
The [[https://www.drivethrurpg.com/product/89534/FU-The-Freeform-Universal-RPG-Classic-rules][FU Rules]] think a attempt with randomness (what D&D calls an ability check) shouldn't be just a yes/no, but could have some /complications/, like "yes, but..." or "no, and...". I want to be able to wrap these ideas into a single interface.
|
||||
** Random Items
|
||||
As a DM, we
|
||||
** DM Screen and Roll from my Notes
|
||||
* Code
|
||||
What do I have here:
|
||||
- [[file:rpgdm.el][rpgdm.el]] :: Primary interface offering:
|
||||
- rpgdm-yes-and-50/50
|
||||
- rpgdm-skill-check given a target and a d20 dice result, returns yes/no, but possibly with complications
|
||||
- rpgdm-skill-check-easy queries a rolled results, and returns a complicated yes/no for an /easy/ skill challenge
|
||||
- rpgdm-skill-check-moderate
|
||||
- rpgdm-skill-check-hard
|
||||
- rpgdm-skill-check-difficult
|
||||
- rpgdm-skill-check-impossible
|
||||
- [[file:rpgdm-dice.el][rpgdm-dice]] :: All the random number generators, plus:
|
||||
- =rpgdm-forward-roll= to move point to the next dice expression
|
||||
- =rpgdm-roll= randomly evaluates dice expression at point, or queries for one
|
||||
- =rpgdm-roll-advantage= / =rpgdm-roll-disadvantage= rolls a d20 with a modifier
|
||||
305
rpgdm-dice.el
Normal file
305
rpgdm-dice.el
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
;;; RPG-DM-HELPER --- Help for a Dungeon Master
|
||||
;;
|
||||
;; 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:
|
||||
|
||||
;; 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)))
|
||||
(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)
|
||||
(destructuring-bind (dice-args lowest highest) test-seq
|
||||
(rpgdm--test-roll-series 'rpgdm--roll dice-args lowest highest)))))
|
||||
|
||||
|
||||
;; 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)))
|
||||
|
||||
;; 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)
|
||||
(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: "))))
|
||||
(let ((roll-results (rpgdm--roll-expression expression)))
|
||||
(message "Rolled: %s" (rpgdm--display-roll roll-results expression))))
|
||||
|
||||
|
||||
;; ----------------------------------------------------------------------
|
||||
;; 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: "))))
|
||||
(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: "))))
|
||||
(message "Rolled with Disadvantage: %s"
|
||||
(rpgdm--roll-with-choice 'min modifier plus-minus)))
|
||||
|
||||
|
||||
(provide 'rpgdm-dice)
|
||||
;;; rpgdm-dice.el ends here
|
||||
97
rpgdm-screen.el
Normal file
97
rpgdm-screen.el
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
;;; rpgdm-screen.el --- Support for viewing/creating a Dungeon Master's Screen -*- lexical-binding: t; -*-
|
||||
;;
|
||||
;; Copyright (C) 2020 Howard X. Abrams
|
||||
;;
|
||||
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
||||
;; Maintainer: Howard X. Abrams <howard.abrams@gmail.com>
|
||||
;; Created: December 15, 2020
|
||||
;;
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; A collection of functions to help make a Dungeon Master Screen in Emacs.
|
||||
;; This assumes functions are called in an org-formatted file.
|
||||
;;
|
||||
;; For instance, let's suppose most of the file is a list, and you want to
|
||||
;; choose one, you can have:
|
||||
;;
|
||||
;; [[elisp:(rpgdm-screen-choose-list)][Choose one]]:
|
||||
;;
|
||||
;;; Code:
|
||||
|
||||
(require 'org)
|
||||
(require 'org-element)
|
||||
(require 's)
|
||||
|
||||
(defvar rpgdm-screen-last-results ""
|
||||
"The results from calls to `rpgdm-screen-' functions are stored here.")
|
||||
|
||||
(defun rpgdm-screen-last-results ()
|
||||
"Display results from the last call to a `rpgdm-screen-' function."
|
||||
(interactive)
|
||||
(message rpgdm-screen-last-results))
|
||||
|
||||
(defun rpgdm-screen--get-list-items ()
|
||||
"Return a list of all the list items in the org document."
|
||||
(org-element-map (org-element-parse-buffer) 'item
|
||||
(lambda (item)
|
||||
(buffer-substring-no-properties
|
||||
(org-element-property :contents-begin item)
|
||||
(org-element-property :contents-end item)))))
|
||||
|
||||
(defun rpgdm-screen-choose-list ()
|
||||
"Randomly choose an elemeent from all lists in the current file.
|
||||
The contents of the item is displayed in the mini-buffer."
|
||||
(interactive)
|
||||
(let* ((items (rpgdm-screen--get-list-items))
|
||||
(item (nth (random (length items)) items)))
|
||||
(setq rpgdm-screen-last-results (s-trim item))
|
||||
(message rpgdm-screen-last-results)))
|
||||
|
||||
(defun rpgdm-screen-choose-sublist ()
|
||||
"Randomly choose an elemeent from the lists in the subtree.
|
||||
The contents of the item is displayed in the mini-buffer."
|
||||
(interactive)
|
||||
(save-excursion
|
||||
(org-narrow-to-subtree)
|
||||
(rpgdm-screen-choose-list)
|
||||
(widen)))
|
||||
|
||||
(defun rpgdm-screen ()
|
||||
(interactive)
|
||||
(delete-other-windows)
|
||||
|
||||
;; Start the Right Side with DIRED
|
||||
(split-window-right)
|
||||
(other-window 1)
|
||||
(dired "dnd-5e")
|
||||
(dired-hide-details-mode)
|
||||
|
||||
(split-window-right)
|
||||
(find-file "skill-checks.org")
|
||||
|
||||
(split-window-below)
|
||||
(split-window-below)
|
||||
(other-window 1)
|
||||
(find-file "names.org")
|
||||
(other-window 1)
|
||||
(find-file "magic-schools.org")
|
||||
(split-window-below)
|
||||
(other-window 1)
|
||||
(find-file "costs.org")
|
||||
(other-window 1)
|
||||
|
||||
;; On the far right window:
|
||||
(split-window-below)
|
||||
(split-window-below)
|
||||
(other-window 1)
|
||||
(find-file "gear.org")
|
||||
(other-window 1)
|
||||
(find-file "trinkets.org")
|
||||
(split-window-below)
|
||||
(other-window 1)
|
||||
(find-file "conditions.org")
|
||||
(other-window 1)
|
||||
)
|
||||
|
||||
(provide 'rpgdm-screen)
|
||||
;;; rpgdm-screen.el ends here
|
||||
363
rpgdm-tables.el
Normal file
363
rpgdm-tables.el
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
;;; rpgdm-tables.el --- Choose table from Tables -*- 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 8, 2021
|
||||
;;
|
||||
;; This file is not part of GNU Emacs.
|
||||
;;
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; By storing tables in files, we can preparse them, allowing us to randomly
|
||||
;; choose entries. The primary interface are:
|
||||
;;
|
||||
;; - `rpgdm-tables-load' :: Which when pointed to a directory, stores the data
|
||||
;; in all text files read.
|
||||
;;
|
||||
;; - `rpgdm-tables-choose' :: Which, when a table is choosen, returns a random
|
||||
;; element.
|
||||
;;
|
||||
;; The files read can be simply formatted as lists, or as an org-mode table. If
|
||||
;; a table is found, the second column should be a _frequency_ label, where some
|
||||
;; items in that table will be chosen more often than others.
|
||||
;;
|
||||
;;; Code:
|
||||
|
||||
(defvar rpgdm-base ".")
|
||||
(require 'rpgdm-dice (expand-file-name "rpgdm-dice.el" rpgdm-base) t)
|
||||
|
||||
(defvar rpgdm-tables-directory
|
||||
(expand-file-name "tables" rpgdm-base)
|
||||
"Directory path containing the tables to load and create functions.")
|
||||
|
||||
(defvar rpgdm-tables (make-hash-table :test 'equal)
|
||||
"Collection of tables and lists for the Dungeon Master.")
|
||||
|
||||
|
||||
(defun rpgdm-tables-load (&optional filepath)
|
||||
"Create functions from table files located in FILEPATH directory."
|
||||
(interactive (list (read-directory-name "DM Tables Directory: " rpgdm-tables-directory)))
|
||||
(dolist (table-file (directory-files filepath t))
|
||||
(let ((name (file-name-base table-file))
|
||||
(contents (rpgdm-tables--read-table-file table-file)))
|
||||
;; (message "Read: %s" table-file)
|
||||
(puthash name contents rpgdm-tables)))
|
||||
(message "Read: %s" (s-join ", " (hash-table-keys rpgdm-tables))))
|
||||
|
||||
(defun rpgdm-tables-choose (table-name)
|
||||
"Given a TABLE-NAME string, pick a random item from that table."
|
||||
(interactive (list (completing-read "Choose from Table: " (hash-table-keys rpgdm-tables))))
|
||||
(let ((table (gethash table-name rpgdm-tables)))
|
||||
(cond ((listp table) (rpgdm-tables--choose-list table))
|
||||
((hash-table-p table) (rpgdm-tables--choose-hash table))
|
||||
(t "Error: Could choose anything from %s (internal bug?)" table-name))))
|
||||
|
||||
|
||||
(defvar rpgdm-tables--line-parse
|
||||
(rx bol (zero-or-more space)
|
||||
(optional
|
||||
(any "+" "-" "*" "|")
|
||||
(one-or-more space))
|
||||
(group (+? any))
|
||||
(optional (zero-or-more space) (or ":" "|") (zero-or-more space)
|
||||
(group (one-or-more alphanumeric))
|
||||
(zero-or-more space) (optional (or ":" "|")) (zero-or-more space))
|
||||
eol)
|
||||
"A regular expression for locating parsable lines.")
|
||||
|
||||
(ert-deftest rpgdm-tables--line-parse-test ()
|
||||
"Why yes, we should test our regular expression."
|
||||
(dolist (data '((" - normal list" "normal list" nil)
|
||||
(" + normal list" "normal list" nil)
|
||||
(" * normal list" "normal list" nil)
|
||||
("normal list" "normal list" nil)
|
||||
(" - list with tag :tag:" "list with tag" "tag")
|
||||
(" - list with tag :tag" "list with tag" "tag")
|
||||
(" - list with tag : tag" "list with tag" "tag")
|
||||
(" | table cell | freq |" "table cell" "freq")))
|
||||
(cl-destructuring-bind (line contents tag) data
|
||||
(should (string-match rpgdm-tables--line-parse line))
|
||||
(should (equal (match-string 1 line) contents))
|
||||
(when tag
|
||||
(should (equal (match-string 2 line) tag))))))
|
||||
|
||||
|
||||
(defun rpgdm-tables--read-table-file (table-file)
|
||||
"Read and parse TABLE-FILE as data. Whatever that means."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents table-file)
|
||||
(goto-char (point-min))
|
||||
(flush-lines (rx bol (zero-or-more space) "#"))
|
||||
(when (re-search-forward rpgdm-tables--line-parse nil nil)
|
||||
(if (match-string 2)
|
||||
(rpgdm-tables--parse-as-hash)
|
||||
(rpgdm-tables--parse-as-list)))))
|
||||
|
||||
(defun rpgdm-tables--parse-as-list ()
|
||||
"Return list of lines matching `rpgdm-tables--line-parse'."
|
||||
(let ((results (list (match-string-no-properties 1))))
|
||||
(while (re-search-forward rpgdm-tables--line-parse nil t)
|
||||
(let ((entry (match-string-no-properties 1)))
|
||||
(setq results (cons entry results))))
|
||||
results))
|
||||
|
||||
(defun rpgdm-tables--parse-as-hash ()
|
||||
"Return hashtable of lines matching `rpgdm-tables--line-parse'.
|
||||
The keys in the hashtable are the tags from the file, and the
|
||||
values will be a list of matching entries of that tag.
|
||||
|
||||
For instance, the file:
|
||||
- salt :common:
|
||||
- silver :rare:
|
||||
- gold :rare:
|
||||
- pepper :common:
|
||||
|
||||
Would return a hashtable containing:
|
||||
|
||||
rare: [gold silver]
|
||||
common [salt peper]"
|
||||
;; Create a hash, populated it, and return it:
|
||||
(let ((results (make-hash-table :test 'equal))
|
||||
(entry (match-string-no-properties 1))
|
||||
(tag (match-string-no-properties 2)))
|
||||
;; Store initial match from parent call:
|
||||
(puthash tag (list entry) results)
|
||||
|
||||
(while (re-search-forward rpgdm-tables--line-parse nil t)
|
||||
(let* ((entry (match-string-no-properties 1))
|
||||
(tag (match-string-no-properties 2))
|
||||
(prev-list (gethash tag results)))
|
||||
(puthash tag (cons entry prev-list) results)))
|
||||
|
||||
;; Combine the sublists of equivalent tags:
|
||||
(rpgdm-tables--flatten-hash results)))
|
||||
|
||||
(defun rpgdm-tables--flatten-hash (table)
|
||||
"Combine the values of equivalent table-tags in TABLE.
|
||||
A table, read as a hash table, may have similar, but equal tags.
|
||||
For instance, `veryrare' and `very-rare' are the same."
|
||||
(let* ((table-tags (rpgdm-tables--which-tag-group table)) ; Ignore all the numbers
|
||||
(tags (-map 'rest table-tags)))
|
||||
(dolist (subtag-list tags)
|
||||
(unless (= 1 (length subtag-list))
|
||||
(let ((keeper (first subtag-list)))
|
||||
(dolist (tag (rest subtag-list))
|
||||
(puthash keeper (append (gethash keeper table)
|
||||
(gethash tag table)) table)
|
||||
(remhash tag table)))))
|
||||
table))
|
||||
|
||||
;; ----------------------------------------------------------------------
|
||||
;;
|
||||
;; At this point, we have a hash table of tables, where the key is the name, and
|
||||
;; the value is either:
|
||||
;; - A list of things to choose
|
||||
;; - A hash table where the key is a tag, and the value is a list of items
|
||||
;; that match that tag.
|
||||
;;
|
||||
;; Obviously, selecting an element from a list is easy:
|
||||
|
||||
(defun rpgdm-tables--choose-list (lst)
|
||||
"Randomly choose (equal chance for any) element in LST."
|
||||
(let ((item (seq-random-elt lst)))
|
||||
(message "%s" item)))
|
||||
|
||||
;; However, choosing an element in a hash of tags seems ... challenging. This is
|
||||
;; because I want the tags to somehow add a particular weight to the randomness.
|
||||
;; Not a complete standard distribution (bell curve), but a little more favor to
|
||||
;; some items. For instance, labeling something 'common' should show more often
|
||||
;; than 'uncommon'.
|
||||
;;
|
||||
;; Choosing an item from a hash table is a complicated algorithm that may not be
|
||||
;; as obvious by reading the code, so let's describe this with an example.
|
||||
;; Assume we have the following frequency table with a /relative weight/ for each tag:
|
||||
;;
|
||||
;; - often :: 4
|
||||
;; - seldom :: 3
|
||||
;; - scarely :: 2
|
||||
;; - rarely :: 1
|
||||
;;
|
||||
;; Is coded with the following list of lists:
|
||||
;;
|
||||
;; ((4 "often")
|
||||
;; (3 "seldom" "sometimes")
|
||||
;; (2 "scarcely" "scarce" "hardly ever")
|
||||
;; (1 "rarely"))
|
||||
;;
|
||||
;; Read this as: we should have 4 times as many items labeled "often" as "rarely".
|
||||
;;
|
||||
;; So we use the function, `rpgdm-tables--table-distribution' to make a
|
||||
;; table-specific tag list, usually called `table-tags', where:
|
||||
;;
|
||||
;; each weight = the number of items * relative weight
|
||||
;;
|
||||
;; So if we had 11 items in the table tagged as "often", and 8 rare
|
||||
;; items, we would have a tag table as:
|
||||
;;
|
||||
;; ((44 "often") (27 "seldom") (22 "scarcely") (8 "rarely"))
|
||||
|
||||
;; TODO English tags and their relative weights are hard-coded at the moment.
|
||||
;; This really should just be part of the file, perhaps as a buffer-local
|
||||
;; variable?
|
||||
|
||||
(defconst rpgdm-tables-tag-groups
|
||||
'(((10 "common")
|
||||
(6 "uncommon")
|
||||
(4 "rare")
|
||||
(2 "veryrare" "very-rare" "very rare")
|
||||
(1 "legendary"))
|
||||
|
||||
((4 "often")
|
||||
(3 "seldom" "sometimes")
|
||||
(2 "scarcely" "scarce" "hardly ever")
|
||||
(1 "rarely"))))
|
||||
|
||||
(defun rpgdm-tables--choose-hash (table)
|
||||
"Select item from a hash TABLE.
|
||||
Note that tables stored in a hash table have weight keys and a list
|
||||
of items associated with that weight."
|
||||
(let* ((table-tags (rpgdm-tables--table-distribution table))
|
||||
(tag (rpgdm-tables--choose-tag table-tags)))
|
||||
(rpgdm-tables--choose-list (gethash tag table))))
|
||||
|
||||
|
||||
(defun rpgdm-tables--relevel-table (table tag)
|
||||
"Given a TAG of a hash TABLE, return new relative level.
|
||||
The new value is based on the original weight, e.g. 4 and the
|
||||
number of items of that particular tag.
|
||||
|
||||
Note: This is a helper function for `rpgdm-tables--table-distribution'."
|
||||
(let* ((name (second tag))
|
||||
(items (gethash name table))
|
||||
(weight (first tag))
|
||||
(new-weight (* weight (length items))))
|
||||
(list new-weight name)))
|
||||
|
||||
(ert-deftest rpgdm-tables-relevel-table-test ()
|
||||
;; Need to make a fake table, so we will just have a single entry in this
|
||||
;; table, with a tag of "often". We'll specify that the weight for this should
|
||||
;; be 4, and we'll store 10 items under that tag:
|
||||
(let* ((table (make-hash-table :test 'equal))
|
||||
(tag "often")
|
||||
(tag-weight-tuple (list 4 tag)))
|
||||
(puthash tag (number-sequence 1 10) table)
|
||||
(should (equal (list 40 tag)
|
||||
(rpgdm-tables--relevel-table table tag-weight-tuple)))))
|
||||
|
||||
|
||||
(defun rpgdm-tables--table-distribution (table)
|
||||
"Return a relative frequency tag group for a given TABLE.
|
||||
Works by running map over the table's tags through the
|
||||
`rpgdm-tables--relevel-table' helper function."
|
||||
(let ((table-tags (rpgdm-tables--which-tag-group table)))
|
||||
(--map (rpgdm-tables--relevel-table table it) table-tags)))
|
||||
|
||||
|
||||
(defun rpgdm-tables--sum-tag-weights (tags)
|
||||
"The TAGS is a list of lists where the first element is a numeric weight.
|
||||
Using `-reduce' allows us to sum these, but we need to make sure that the
|
||||
first element of our list is our initial condition, so we `cons' a 0 onto
|
||||
the start."
|
||||
(--reduce (+ acc (first it)) (cons 0 tags)))
|
||||
|
||||
(ert-deftest rpgdm-tables--sum--tag-weights-test ()
|
||||
(let ((weighted-tags
|
||||
'((44 "often") (27 "seldom") (22 "scarcely") (7 "rarely"))))
|
||||
(should (= 100 (rpgdm-tables--sum-tag-weights weighted-tags)))))
|
||||
|
||||
|
||||
(defun rpgdm-tables--find-tag (roll tag-list)
|
||||
"Given a ROLL as a level in TAG-LIST, return matching tag.
|
||||
The matching is based on the weight. A million ways to do this,
|
||||
but stepping through the list of tags to see roll is in that
|
||||
*window*, and if not, both move to the next tag, as well as
|
||||
decrement the ROLL value."
|
||||
(cl-loop for (num-elems tag) in tag-list do
|
||||
;; (message "Comparing %d <= %d for %s" roll num-elems tag)
|
||||
(if (<= roll num-elems)
|
||||
(return tag)
|
||||
(decf roll num-elems))))
|
||||
|
||||
(ert-deftest rpgdm-tables--find-tag-test ()
|
||||
(let ((weighted-tags
|
||||
'((44 "often") (27 "seldom") (22 "scarcely") (7 "rarely"))))
|
||||
(should (equal "often" (rpgdm-tables--find-tag 1 weighted-tags)))
|
||||
(should (equal "often" (rpgdm-tables--find-tag 44 weighted-tags)))
|
||||
(should (equal "seldom" (rpgdm-tables--find-tag 45 weighted-tags)))
|
||||
(should (equal "seldom" (rpgdm-tables--find-tag 71 weighted-tags)))
|
||||
(should (equal "scarcely" (rpgdm-tables--find-tag 72 weighted-tags)))
|
||||
(should (equal "scarcely" (rpgdm-tables--find-tag 93 weighted-tags)))
|
||||
(should (equal "rarely" (rpgdm-tables--find-tag 94 weighted-tags)))
|
||||
(should (equal "rarely" (rpgdm-tables--find-tag 100 weighted-tags)))))
|
||||
|
||||
|
||||
(defun rpgdm-tables--choose-tag (tags)
|
||||
"Select random tag from TAGS in `rpgdm-tables-tag-groups'.
|
||||
Uses helper function, `rpgdm-tables--find-tag'."
|
||||
(let* ((upper-limit (rpgdm-tables--sum-tag-weights tags))
|
||||
(roll (rpgdm--roll-die upper-limit)))
|
||||
;; (message "Rolled %d on %d" roll upper-limit)
|
||||
(rpgdm-tables--find-tag roll tags)))
|
||||
|
||||
(defun rpgdm-tables--which-tag-group (table)
|
||||
"Return the tag table-tags associated with TABLE."
|
||||
(let (results
|
||||
(tag (first (hash-table-keys table))))
|
||||
(dolist (table-tags rpgdm-tables-tag-groups results)
|
||||
(let ((tag-list (->> table-tags
|
||||
(-map 'rest) ; Drop the numeric weight from each sublist
|
||||
(-flatten))))
|
||||
(when (-contains? tag-list tag)
|
||||
(setq results table-tags))))))
|
||||
|
||||
;; ----------------------------------------------------------------------
|
||||
;; Let's attempt to test our code and its theories.
|
||||
;;
|
||||
;; The function repeatedly selects items from a table randomly,
|
||||
;; and returns a hash of the number of times each element was
|
||||
;; selected ...
|
||||
|
||||
(defun rpgdm-tables-validate (&optional table-name iterations)
|
||||
"Return results randomly choosing many items from TABLE-NAME.
|
||||
Calls `rpgdm-tables-choose' a number of ITERATIONS (defaults to 500)."
|
||||
(unless iterations (setq iterations 500))
|
||||
(unless table-name
|
||||
(setq table-name "test-subject")
|
||||
(puthash table-name (make-hash-table :test 'equal) rpgdm-tables)
|
||||
(setf (gethash "often" (gethash table-name rpgdm-tables))
|
||||
'(o1 o2 o3 o4 o5 o6 o7 o8 o9 o0))
|
||||
(setf (gethash "seldom" (gethash table-name rpgdm-tables))
|
||||
'(s1 s2 s3 s4 s5 s6 s7 s8 s9 s0))
|
||||
(setf (gethash "scarcely" (gethash table-name rpgdm-tables))
|
||||
'(l1 l2 l3 l4 l5 l6 l7 l8 l9 l0))
|
||||
(setf (gethash "rarely" (gethash table-name rpgdm-tables))
|
||||
'(r1 r2 r3 r4 r5 r6 r7 r8 r9 r0)))
|
||||
|
||||
(let ((accumulator (make-hash-table :test 'equal)))
|
||||
(dotimes (i iterations accumulator)
|
||||
(let* ((item (rpgdm-tables-choose table-name))
|
||||
(item-name (first (s-split " :: " item))))
|
||||
(incf (gethash item-name accumulator 0))))
|
||||
accumulator))
|
||||
|
||||
;; Since we are randomly selecting items, even over large iterations, one can
|
||||
;; see _scarcely_ appear almost as much as items labeled _often_. However, if we
|
||||
;; first sort the data:
|
||||
;;
|
||||
;; "o1" 35 "o2" 31 "o3" 38 "o4" 44 "o5" 43 ...
|
||||
;; "s1" 35 "s2" 38 "s3" 29 "s4" 28 "s5" 26 ...
|
||||
;; "l1" 26 "l2" 20 "l3" 19 "l4" 19 "l5" 26 ...
|
||||
;; "r1" 10 "r2" 7 "r3" 8 "r4" 5 "r5" 13 ...
|
||||
;;
|
||||
;; And then calculate the average of each _level_, we see that the items occur
|
||||
;; as we would expect:
|
||||
;;
|
||||
;; (/ (+ 35 31 38 44 43) 5) -> 38
|
||||
;; (/ (+ 35 38 29 28 26) 5) -> 31
|
||||
;; (/ (+ 26 20 19 19 26) 5) -> 22
|
||||
;; (/ (+ 10 7 8 5 13) 5) -> 8
|
||||
|
||||
(provide 'rpgdm-tables)
|
||||
;;; rpgdm-tables.el ends here
|
||||
183
rpgdm.el
Normal file
183
rpgdm.el
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
;;; 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue