Add dice tables

A dice table is a direct translation of a gaming table from a book,
where the table has a stated dice expression, and each entry has a
numeric range. Most tables can now be taken as is from published
sources.

Also, if a message from a randomly rolled table contains a dice
expression, like Found 2d8 scrolls, the expression is automatically
substituted.

Finally if a message contains something like:
  You found a [wolf/fox/badger].
The displayed message will only include on of those entries.
This commit is contained in:
Howard Abrams 2021-02-11 22:08:06 -08:00
parent 106ed2e6dd
commit 656c168a52
8 changed files with 155 additions and 59 deletions

View file

@ -3,7 +3,7 @@
#+email: howard.abrams@gmail.com
#+FILETAGS: :org-mode:emacs:rpgdm:
#+STARTUP: inlineimages yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-dice.el :comments yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-dice.el :comments no
#+PROPERTY: header-args :eval no-export
#+PROPERTY: header-args :results silent
#+PROPERTY: header-args :exports both
@ -21,6 +21,10 @@
;;
;;
;;; Commentary:
;;
;; This file contains the source code, but the concept behind what I'm
;; calling random dice tables is a bit complex, so I recommend looking
;; at the original file in `docs/rpgdm-tables-dice.org'.
#+END_SRC

View file

@ -3,7 +3,7 @@
#+email: howard.abrams@gmail.com
#+FILETAGS: :org-mode:emacs:rpgdm:
#+STARTUP: inlineimages yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-freq.el :comments yes
#+PROPERTY: header-args:emacs-lisp :tangle ../rpgdm-tables-freq.el :comments no
#+PROPERTY: header-args :eval no-export
#+PROPERTY: header-args :results silent
#+PROPERTY: header-args :exports both
@ -20,13 +20,15 @@
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; This file contains the source code, but the concept behind what I'm
;; calling random frequency tables is a bit complex, so I recommend looking
;; at the original file in `docs/rpgdm-tables-freq.org'.
#+END_SRC
* Introduction
While the majority of my tables are simple lists to choose a random element, some lists should return /some elements/ more often than /other elements/. While that sounds great in a sentence, I need to actually code what I mean. I call them /frequency tables/ and they look something like my Faction Encounter table:
| Church of Talos :: Worshipers of the god of storms and destruction. | scarcely |
| City Watch :: Members of the Waterdeep constabulary. | often |
| Cult of the Dragon :: Cultists who venerate evil dragons. | seldom |

View file

@ -210,6 +210,11 @@ the following:
(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)

View file

@ -22,6 +22,12 @@
(require 'org-element)
(require 's)
(defvar rpgdm-base ".")
(defvar rpgdm-screen-directory
(expand-file-name "dnd-5e" rpgdm-base)
"Directory path containing the tables to load and create functions.")
(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
@ -47,14 +53,17 @@ The contents of the item is displayed in the mini-buffer."
(rpgdm-screen-choose-list)
(widen)))
(defun rpgdm-screen ()
(interactive)
(defun rpgdm-screen (&optional filepath)
"Hard-coded (currently) approach to displaying files from FILEPATH."
(interactive (list (read-directory-name "DM Screen Directory: "
rpgdm-screen-directory)))
(delete-other-windows)
(let ((default-directory filepath))
;; Start the Right Side with DIRED
(split-window-right)
(other-window 1)
(dired "dnd-5e")
(dired ".")
(dired-hide-details-mode)
(split-window-right)
@ -81,8 +90,7 @@ The contents of the item is displayed in the mini-buffer."
(split-window-below)
(other-window 1)
(find-file "conditions.org")
(other-window 1)
)
(other-window 1)))
(provide 'rpgdm-screen)
;;; rpgdm-screen.el ends here

View file

@ -1,4 +1,3 @@
;; [[file:docs/rpgdm-tables-dice.org::+BEGIN_SRC emacs-lisp][No heading:1]]
;;; rpgdm-tables-dice.el --- Rolling dice for choosing items from Tables -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
@ -11,4 +10,77 @@
;;
;;
;;; Commentary:
;; No heading:1 ends here
;;
;; This file contains the source code, but the concept behind what I'm
;; calling random dice tables is a bit complex, so I recommend looking
;; at the original file in `docs/rpgdm-tables-dice.org'.
;;; Code:
(defstruct dice-table dice rows)
(defun rpgdm-tables--choose-dice-table (table)
"Choose a string from a random dice table."
(let* ((roll (rpgdm-roll-sum (dice-table-dice table)))
(rows (dice-table-rows table))
(results (rpgdm-tables-dice--choose roll rows)))
(if (stringp results)
results
(seq-random-elt results))))
(defun rpgdm-tables-dice--choose (roll rows)
"Given a numeric ROLL, return row that matches.
This assumes ROWS is a sorted list where the first element (the
`car') is a numeric level that if ROLL is less than or equal, we
return the `rest' of the row. Otherwise, we recursively call this
function with the `rest' of the rows."
(let* ((row (first rows))
(level (car row))
(answer (rest row)))
(if (<= roll level)
answer
(rpgdm-tables-dice--choose roll (rest rows)))))
(setq rpgdm-tables-dice-table-regexp (rx "Roll"
(one-or-more space)
(optional (or "on" "for"))
(zero-or-more space)
"Table:"
(zero-or-more space)
(group
(regexp rpgdm-roll-regexp))))
(defun rpgdm-tables-dice-table? ()
"Return non-nil if current buffer contains a dice-table"
(goto-char (point-min))
(re-search-forward rpgdm-tables-dice-table-regexp nil t))
(defun rpgdm-tables--parse-as-dice-table ()
"Return `dice-table' of lines matching `rpgdm-tables-dice-table-rows'."
(let ((dice (match-string-no-properties 1)) ; Grab expression before moving on
(rows ()) ; Modify this with add-to-list
(row-splitter (rx (* space) "|" (* space)))) ; Split rest of table row
(while (re-search-forward rgpdm-tables-dice-table-rows nil t)
(let* ((levelstr (match-string-no-properties 1))
(level (string-to-number levelstr))
(row (match-string-no-properties 2))
(choices (split-string row row-splitter t)))
(add-to-list 'rows (cons level choices))))
(make-dice-table :dice dice
:rows (sort rows (lambda (a b) (< (first a) (first b)))))))
(setq rgpdm-tables-dice-table-rows (rx bol
(zero-or-more space) "|" (zero-or-more space)
(optional (one-or-more digit)
(one-or-more "-"))
(group
(one-or-more digit))
(zero-or-more space) "|" (zero-or-more space)
(group (+? any))
(zero-or-more space) "|" (zero-or-more space)
eol))
(provide 'rpgdm-tables-dice)
;;; rpgdm-tables-dice.el ends here

View file

@ -1,4 +1,3 @@
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::+BEGIN_SRC emacs-lisp][No heading:1]]
;;; rpgdm-tables-freq.el --- Rolling dice for choosing items from Tables -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
@ -10,17 +9,17 @@
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;; No heading:1 ends here
;;
;; This file contains the source code, but the concept behind what I'm
;; calling random frequency tables is a bit complex, so I recommend looking
;; at the original file in `docs/rpgdm-tables-freq.org'.
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Parsing a Frequency Table][Parsing a Frequency Table:1]]
(defun rpgdm-tables-freq-table? ()
"Return non-nil if current buffer contains a frequency table"
(goto-char (point-min))
(re-search-forward rpgdm-tables--line-parse nil nil)
(match-string 2))
;; Parsing a Frequency Table:1 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Parsing a Frequency Table][Parsing a Frequency Table:2]]
(defun rpgdm-tables--parse-as-freq-table ()
"Return hashtable of lines matching `rpgdm-tables--line-parse'.
The keys in the hashtable are the tags from the file, and the
@ -51,9 +50,7 @@ Would return a hashtable containing:
;; Combine the sublists of equivalent tags:
(rpgdm-tables--merge-frequencies results)))
;; Parsing a Frequency Table:2 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Parsing a Frequency Table][Parsing a Frequency Table:3]]
(defun rpgdm-tables--merge-frequencies (table)
"Combine the values of equivalent table-tags in TABLE.
A table, read as a hash table, may have similar, but equal tags.
@ -68,9 +65,7 @@ For instance, `veryrare' and `very-rare' are the same."
(gethash tag table)) table)
(remhash tag table)))))
table))
;; Parsing a Frequency Table:3 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Frequencies as Weights][Frequencies as Weights:3]]
(defconst rpgdm-tables-tag-groups
'(((12 "common")
(7 "uncommon")
@ -82,9 +77,7 @@ For instance, `veryrare' and `very-rare' are the same."
(3 "seldom" "sometimes")
(2 "scarcely" "scarce" "hardly ever")
(1 "rarely"))))
;; Frequencies as Weights:3 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Choosing an Item][Choosing an Item:1]]
(defun rpgdm-tables--choose-freq-table (table)
"Select item from a hash TABLE.
Note that tables stored in a hash table have weight keys and a list
@ -171,9 +164,7 @@ Uses helper function, `rpgdm-tables--find-tag'."
(roll (rpgdm--roll-die upper-limit)))
;; (message "Rolled %d on %d" roll upper-limit)
(rpgdm-tables--find-tag roll tags)))
;; Choosing an Item:1 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Match Table with Tag Group][Match Table with Tag Group:1]]
(defun rpgdm-tables--which-tag-group (table)
"Return the tag table-tags associated with TABLE."
(let (results
@ -184,9 +175,7 @@ Uses helper function, `rpgdm-tables--find-tag'."
(-flatten))))
(when (-contains? tag-list tag)
(setq results table-tags))))))
;; Match Table with Tag Group:1 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Validating my Assumptions][Validating my Assumptions:1]]
(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)."
@ -209,9 +198,6 @@ Calls `rpgdm-tables-choose' a number of ITERATIONS (defaults to 500)."
(item-name (first (s-split " :: " item))))
(incf (gethash item-name accumulator 0))))
accumulator))
;; Validating my Assumptions:1 ends here
;; [[file:../../../../../Volumes/Personal/dropbox/org/rpg-dm/docs/rpgdm-tables-freq.org::*Validating my Assumptions][Validating my Assumptions:3]]
(provide 'rpgdm-tables-freq)
;;; rpgdm-tables-freq.el ends here
;; Validating my Assumptions:3 ends here

View file

@ -58,17 +58,34 @@ would be easy (see `rpgdm-tables--choose-list'), or it is either
a freq-uency table (see `rpgdm-tables--choose-freq-table') or a
dice table (see `rpgdm-tables--choose-dice-table')."
(interactive (list (completing-read "Choose from Table: " (hash-table-keys rpgdm-tables))))
(let* ((table (gethash table-name rpgdm-tables))
(response (cond ((dice-table-p table) (rpgdm-tables--choose-dice-table table))
(result (cond ((dice-table-p table) (rpgdm-tables--choose-dice-table table))
((hash-table-p table) (rpgdm-tables--choose-freq-table table))
((listp table) (rpgdm-tables--choose-list table))
(t "Error: Could choose anything from %s (internal bug?)" table-name))))
(rpgdm-message "%s" response)))
(t "Error: Could choose anything from %s (internal bug?)" table-name)))
;; Replace any dice expression in the message with an roll:
(dice-sum (lambda (dice-exp) (number-to-string (rpgdm-roll-sum dice-exp))))
(no-dice-nums (replace-regexp-in-string rpgdm-roll-regexp dice-sum result))
(no-alt-words (rpgdm-tables--choose-string-list no-dice-nums)))
(rpgdm-message "%s" no-alt-words)))
(defun rpgdm-tables--choose-list (lst)
"Randomly choose (equal chance for any) element in LST."
(seq-random-elt lst))
(defun rpgdm-tables--choose-string-list (str)
"Return a substituted item from _string-list_ in STR.
For instance, the string: 'You found a [chair/box/statue]'
would be converted randomly to something like: 'You found a box.'"
(let ((regexp (rx "[" (+? any) "/" (+? any) "]"))
(subbed (lambda (str) (--> str
(substring it 1 -1)
(s-split (rx (*? space) "/" (*? space)) it)
(seq-random-elt it)))))
(replace-regexp-in-string regexp subbed str)))
;; I originally thought that I could have a single regular expression that
;; matched all possible tables, but that is a bit too complicated. The following
;; regular expression, however, will parse a list or a frequency table.

View file

@ -71,11 +71,13 @@
"Replace `messasge' function allowing it to be re-displayed.
The FORMAT-STRING is a standard string for the `format' function,
and ARGS are substitued values."
;; TODO Push this onto a ring instead of reset this string variable:
(setq rpgdm-last-results (apply 'format format-string args))
(rpgdm-last-results))
(defun rpgdm-last-results ()
"Display results from the last call to a `rpgdm-screen-' function."
;; TODO Need to add a prefix and display a numeric version with last as a ring.
(interactive)
(message rpgdm-last-results))