From 656c168a52d39cb789eeddd309d89f4c1cb71262 Mon Sep 17 00:00:00 2001 From: Howard Abrams Date: Thu, 11 Feb 2021 22:08:06 -0800 Subject: [PATCH] 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. --- docs/rpgdm-tables-dice.org | 6 ++- docs/rpgdm-tables-freq.org | 8 ++-- rpgdm-dice.el | 5 +++ rpgdm-screen.el | 70 +++++++++++++++++++---------------- rpgdm-tables-dice.el | 76 +++++++++++++++++++++++++++++++++++++- rpgdm-tables-freq.el | 22 ++--------- rpgdm-tables.el | 25 +++++++++++-- rpgdm.el | 2 + 8 files changed, 155 insertions(+), 59 deletions(-) diff --git a/docs/rpgdm-tables-dice.org b/docs/rpgdm-tables-dice.org index 95735c6..74e47f1 100644 --- a/docs/rpgdm-tables-dice.org +++ b/docs/rpgdm-tables-dice.org @@ -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 diff --git a/docs/rpgdm-tables-freq.org b/docs/rpgdm-tables-freq.org index abc7412..54ea894 100644 --- a/docs/rpgdm-tables-freq.org +++ b/docs/rpgdm-tables-freq.org @@ -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 | diff --git a/rpgdm-dice.el b/rpgdm-dice.el index a170c0c..a1c9a56 100644 --- a/rpgdm-dice.el +++ b/rpgdm-dice.el @@ -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) diff --git a/rpgdm-screen.el b/rpgdm-screen.el index 86d5d1f..853f961 100644 --- a/rpgdm-screen.el +++ b/rpgdm-screen.el @@ -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,42 +53,44 @@ 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) - ;; Start the Right Side with DIRED - (split-window-right) - (other-window 1) - (dired "dnd-5e") - (dired-hide-details-mode) + (let ((default-directory filepath)) + ;; Start the Right Side with DIRED + (split-window-right) + (other-window 1) + (dired ".") + (dired-hide-details-mode) - (split-window-right) - (find-file "actions.org") + (split-window-right) + (find-file "actions.org") - (split-window-below) - (split-window-below) - (other-window 1) - (find-file "weather-effects.org") - (other-window 1) - (find-file "magic-schools.org") - (split-window-below) - (other-window 1) - (find-file "costs.org") - (other-window 1) + (split-window-below) + (split-window-below) + (other-window 1) + (find-file "weather-effects.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 "armor.org") - (split-window-below) - (other-window 1) - (find-file "conditions.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 "armor.org") + (split-window-below) + (other-window 1) + (find-file "conditions.org") + (other-window 1))) (provide 'rpgdm-screen) ;;; rpgdm-screen.el ends here diff --git a/rpgdm-tables-dice.el b/rpgdm-tables-dice.el index bf81094..6cd28a7 100644 --- a/rpgdm-tables-dice.el +++ b/rpgdm-tables-dice.el @@ -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 diff --git a/rpgdm-tables-freq.el b/rpgdm-tables-freq.el index abc8ef6..65092a3 100644 --- a/rpgdm-tables-freq.el +++ b/rpgdm-tables-freq.el @@ -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 diff --git a/rpgdm-tables.el b/rpgdm-tables.el index a4d56c7..c3980cc 100644 --- a/rpgdm-tables.el +++ b/rpgdm-tables.el @@ -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)) + + (let* ((table (gethash table-name rpgdm-tables)) + (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. diff --git a/rpgdm.el b/rpgdm.el index 0e156b9..81464b0 100644 --- a/rpgdm.el +++ b/rpgdm.el @@ -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))