diff --git a/README.org b/README.org index 9be0256..cd09a86 100644 --- a/README.org +++ b/README.org @@ -5,7 +5,7 @@ The overlap between Emacs and running a Dungeons & Dragons campaign is... expected? Jotting notes and plans in an org-mode file has been great, but what if, during a game session, my notes became /more interactive/? I started creating some helper functions, which has now become a minor mode I use as a sort of layer on top of Org. Let's blame this insanity on the pandemic, but it has been fun. -The primary interface is =f12= which calls up a /sticky/ Hydra to call my functions, but still allowing full cursor movement (mostly): +The primary interface is =f6= which calls up a /sticky/ Hydra to call my functions, but still allowing full cursor movement (mostly): #+attr_html: :width 800px [[file:images/screenshot-of-hydra.png]] @@ -99,6 +99,7 @@ The second thing I realized is that Org's links can call Emacs functions. This a My initial ideas for listing a bunch of random NPC names and having a link that displayed one of them, got supplanted for the ideas I described above. * Code What do I have here: + - [[file:rpgdm-core.el][rpgdm-core]] :: Package that provides common functionality; namely in messaging and the =rpgdm-last-results= ring. - [[file:rpgdm.el][rpgdm]] :: Primary interface offering: - =rpgdm-mode=, plus a Hydra interface for easily calling the rest of these functions. - =rpgdm-yes-and-50/50=, flip a coin and make a give a result with or without complications or bonuses. @@ -127,4 +128,4 @@ I'm also intrigued with rulesets that are unique, for instance: - [[file:docs/fate-rpg.org][FATE]] :: Easy character creation and a nice bell-curve dice roll, but it requires [[https://fudgerpg.com/products/fudge-dice.html][special Fudge dice]], that are easy enough to recreate in Emacs. See [[https://fate-srd.com/][fate-srd.com]] for details about this game. - [[file:docs/mythic-rpg.org][Mythic RPG]] :: A nice RPG for solo play as it has a GM-less option that I wanted to capture, see [[https://www.wordmillgames.com/mythic-rpg.html][Wordmill Games]] for details. - - [[file:docs/ironsworn-rpg.org][Ironsworn]] :: Another good solo RPG, I wanted to capture its quick check resolution. See [[https://www.ironswornrpg.com/][ironswornrpg.com]] for the free rules. + - [[https://gitlab.com/howardabrams/emacs-ironsworn][Ironsworn]] :: Another good solo RPG, I wanted to capture its quick check resolution. See [[https://www.ironswornrpg.com/][ironswornrpg.com]] for the free rules. diff --git a/docs/rpgdm-tables-dice.org b/docs/rpgdm-tables-dice.org index 6cd062f..766a12c 100644 --- a/docs/rpgdm-tables-dice.org +++ b/docs/rpgdm-tables-dice.org @@ -46,7 +46,7 @@ For instance, Xanathar's Guide to Everything, a Dungeons and Dragons supplement To represent these types of tables, we create a special type, called a =dice-table=. Where the first "slot" is the dice expression (or the number of sides of a dice to roll), and an associative list of result values and the choice. #+BEGIN_SRC emacs-lisp :results silent -(defstruct dice-table dice rows) +(cl-defstruct dice-table dice rows) #+END_SRC How is this used to render the example table above? diff --git a/docs/rpgdm-tables-freq.org b/docs/rpgdm-tables-freq.org index f511fb2..c09d072 100644 --- a/docs/rpgdm-tables-freq.org +++ b/docs/rpgdm-tables-freq.org @@ -226,7 +226,7 @@ decrement the ROLL value." ;; (message "Comparing %d <= %d for %s" roll num-elems tag) (if (<= roll num-elems) (return tag) - (decf roll num-elems)))) + (cl-decf roll num-elems)))) (ert-deftest rpgdm-tables--find-tag-test () (let ((weighted-tags diff --git a/rpgdm-core.el b/rpgdm-core.el new file mode 100644 index 0000000..531b16d --- /dev/null +++ b/rpgdm-core.el @@ -0,0 +1,80 @@ +;;; rpgdm-core --- Core functionality for rpgdm -*- lexical-binding: t -*- +;; +;; Copyright (C) 2021 Howard X. Abrams +;; +;; Author: Howard X. Abrams +;; Jeremy Friesen +;; Maintainer: Howard X. Abrams +;; Created: December 10, 2023 +;; +;; This file is NOT part of GNU Emacs. +;;; Commentary: +;; +;; There are functions shared across different `rpgdm' packages. These are +;; considered "core" functionality. +;; +;;; Code: +(defvar rpgdm-base + (seq-find (lambda (elt) (string-match "rpgdm" elt)) load-path (getenv "HOME")) + "Default directory to look for supporting data, like tables and charts.") + +(defvar rpgdm-last-results (make-ring 10) + "The results from calls to `rpgdm-screen-' functions are stored here.") + +(defvar rpgdm-last-results-ptr 0 + "Keeps track of where we are in the message display ring. +Each call to `rpgdm-last-results' resets this to 0.") + +(defun rpgdm-message (format-string &rest args) + "Replace `message' function allowing it to be re-displayed. +The FORMAT-STRING is a standard string for the `format' function, +and ARGS are substitued values." + (let ((message (apply 'format format-string args))) + (ring-insert rpgdm-last-results message) + (kill-new message) + (rpgdm-last-results))) + +(defun rpgdm-last-results () + "Display results from the last call to a `rpgdm-message' function." + (interactive) + (setq rpgdm-last-results-ptr 0) + (message (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) + +(defun rpgdm-last-results-previous () + "Display results from an earlier call to `rpgdm-message'." + (interactive) + (cl-incf rpgdm-last-results-ptr) + (when (>= rpgdm-last-results-ptr (ring-length rpgdm-last-results)) + (setq rpgdm-last-results-ptr 0)) + (message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) + +(defun rpgdm-last-results-next () + "Display results from an later call to `rpgdm-message'. +Meant to be used with `rpgdm-last-results-previous'." + (interactive) + (when (> rpgdm-last-results-ptr 0) + (cl-decf rpgdm-last-results-ptr)) + (message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) + +(defun rpgdm-paste-last-message () + "Yank, e.g. paste, the last displayed message." + (interactive) + (insert (rpgdm-last-results))) + +(ert-deftest rpgdm-last-results-test () + (progn + (setq rpgdm-last-results (make-ring 10)) + (rpgdm-message "First in, so this is the oldest") + (rpgdm-message "Something or other") + (rpgdm-message "Almost the newest") + (rpgdm-message "Newest")) + + (should (equal "Newest" (rpgdm-last-results))) + (should (equal "1> Almost the newest" (rpgdm-last-results-previous))) + (should (equal "2> Something other" (rpgdm-last-results-previous))) + (should (equal "1> Almost the newest" (rpgdm-last-results-next))) + (should (equal "0> Almost the newest" (rpgdm-last-results-next))) + (should (equal "0> Almost the newest" (rpgdm-last-results-next)))) + +(provide 'rpgdm-core) +;;; rpgdm-core.el ends here diff --git a/rpgdm-dice.el b/rpgdm-dice.el index 02e7859..03f2daa 100644 --- a/rpgdm-dice.el +++ b/rpgdm-dice.el @@ -81,7 +81,7 @@ This really tests the `rpgdm--test-rolls' function." (8 1 8) (20 1 20) (100 1 100))) - (destructuring-bind (die lowest highest) test-data + (cl-destructuring-bind (die lowest highest) test-data (rpgdm--test-rolls #'rpgdm--roll-die (list die) lowest highest)))) ;; ---------------------------------------------------------------------- @@ -155,7 +155,7 @@ average value of AVG, if given." ((3 6 4) 7 22) ((4 6 4 "-") 0 20)))) (dolist (test-seq test-data) - (destructuring-bind (dice-args lowest highest) test-seq + (cl-destructuring-bind (dice-args lowest highest) test-seq (rpgdm--test-roll-series 'rpgdm--roll dice-args lowest highest))))) @@ -240,7 +240,7 @@ the following: ("2d12" 2 24) ("3d6+2" 5 20)))) (dolist (test-data test-cases) - (destructuring-bind (dice-expression lowest highest) test-data + (cl-destructuring-bind (dice-expression lowest highest) test-data (rpgdm--test-roll-series 'rpgdm--roll-expression (list dice-expression) lowest highest))))) diff --git a/rpgdm-ironsworn.el b/rpgdm-ironsworn.el deleted file mode 100644 index 51da487..0000000 --- a/rpgdm-ironsworn.el +++ /dev/null @@ -1,313 +0,0 @@ -(require 'rpgdm) - -(defun rpgdm-ironsworn--results (action modifier one-challenge two-challenge) - (let* ((action-results (+ action modifier)) - (str-results (cond - ((and (> action-results one-challenge) (> action-results two-challenge)) - (propertize "Strong hit" 'face '(:foreground "green"))) - ((or (> action-results one-challenge) (> action-results two-challenge)) - (propertize "Weak hit" 'face '(:foreground "yellow"))) - (t (propertize "Miss" 'face '(:foreground "red"))))) - (matched-msg (if (= one-challenge two-challenge) - (propertize " ← Create a Twist" 'face '(:foreground "orange")) - ""))) - (format "%s %s %d %s %d %s %d %s %d %s" str-results - (propertize "::" 'face '(:foreground "#888")) - action - (propertize "+" 'face '(:foreground "#888")) - modifier - (propertize "→" 'face '(:foreground "#888")) - one-challenge - (propertize "/" 'face '(:foreground "#888")) - two-challenge matched-msg))) - -(defun rpgdm-ironsworn-roll (modifier) - "Display a Hit/Miss message based on comparing a d6 action -roll (added to MODIFIER) vs. two d10 challenge dice." - (interactive "nModifier: ") - (let ((one-challenge (rpgdm--roll-die 10)) - (two-challenge (rpgdm--roll-die 10)) - (action-roll (rpgdm--roll-die 6))) - (rpgdm-message (rpgdm-ironsworn--results action-roll modifier - one-challenge two-challenge)))) - -(defun rpgdm-ironsworn-progress-roll (progress-value) - "Display a Hit/Miss message based on comparing the PROGRESS-VALUE -to rolling two d10 challenge dice." - (interactive "nCurrent Progress Value: ") - (let ((one-challenge (rpgdm--roll-die 10)) - (two-challenge (rpgdm--roll-die 10))) - (rpgdm-message (rpgdm-ironsworn--results progress-value 0 - one-challenge two-challenge)))) - -(define-hash-table-test 'str-or-keys - (lambda (a b) - (string-equal - (downcase - (if (symbolp a) (symbol-name a) a)) - (downcase - (if (symbolp b) (symbol-name b) b)))) - - (lambda (s) (sxhash-equal (downcase - (if (symbolp s) (symbol-name s) s))))) - -(defvar rpgdm-ironsworn-character (make-hash-table :test 'str-or-keys) - "Stats and attributes for the currently loaded character") - -(cl-defun rpgdm-ironsworn-character (&key edge heart iron shadow wits - (health 5) (spirit 5) (supply 5) - (momentum 2)) - "Store the player character's stats, as well as set up the defaults for the others." - (clrhash rpgdm-ironsworn-character) - ;; (setq rpgdm-ironsworn-character (make-hash-table :test 'str-or-keys)) - (puthash 'edge edge rpgdm-ironsworn-character) - (puthash 'heart heart rpgdm-ironsworn-character) - (puthash 'iron iron rpgdm-ironsworn-character) - (puthash 'shadow shadow rpgdm-ironsworn-character) - (puthash 'wits wits rpgdm-ironsworn-character) - - (puthash 'health health rpgdm-ironsworn-character) - (puthash 'spirit spirit rpgdm-ironsworn-character) - (puthash 'supply supply rpgdm-ironsworn-character) - (puthash 'momentum momentum rpgdm-ironsworn-character)) - -(defun rpgdm-ironsworn-adjust-health (health-adj) - "Increase or decrease the current character's health by HEALTH-ADJ." - (interactive "nHealth Adjustment: ") - (puthash 'health - (+ (gethash 'health rpgdm-ironsworn-character 5) health-adj) - rpgdm-ironsworn-character)) - -(defun rpgdm-ironsworn-adjust-spirit (spirit-adj) - "Increase or decrease the current character's spirit by SPIRIT-ADJ." - (interactive "nSpirit Adjustment: ") - (puthash 'spirit - (+ (gethash 'spirit rpgdm-ironsworn-character 5) spirit-adj) - rpgdm-ironsworn-character)) - -(defun rpgdm-ironsworn-adjust-supply (supply-adj) - "Increase or decrease the current character's supply by SUPPLY-ADJ." - (interactive "nSupply Adjustment: ") - (puthash 'supply - (+ (gethash 'supply rpgdm-ironsworn-character 5) supply-adj) - rpgdm-ironsworn-character)) - -(defun rpgdm-ironsworn-adjust-momentum (momentum-adj) - "Increase or decrease the current character's momentum by MOMENTUM-ADJ." - (interactive "nMomentum Adjustment: ") - (puthash 'momentum - (+ (gethash 'momentum rpgdm-ironsworn-character 5) momentum-adj) - rpgdm-ironsworn-character)) - -(defun rpgdm-ironsworn--display-stat (stat) - (let* ((value (gethash stat rpgdm-ironsworn-character 5)) - (s-val (number-to-string value)) - (color (cond - ((< value 1) "red") - ((< value 3) "orange") - ((< value 4) "yellow") - (t "green")))) - (propertize s-val 'face `(:foreground ,color)))) - -(defun rpgdm-ironsworn-character-display () - "Easily display the character's stats and other things." - (interactive) - (rpgdm-message "Edge: %d Heart: %d Iron: %d Shadow: %d Wits: %d -Health: %s Spirit: %s Supply: %s Momentum: %d" - (gethash 'edge rpgdm-ironsworn-character 0) - (gethash 'heart rpgdm-ironsworn-character 0) - (gethash 'iron rpgdm-ironsworn-character 0) - (gethash 'shadow rpgdm-ironsworn-character 0) - (gethash 'wits rpgdm-ironsworn-character 0) - - (rpgdm-ironsworn--display-stat 'health) - (rpgdm-ironsworn--display-stat 'spirit) - (rpgdm-ironsworn--display-stat 'supply) - - (gethash 'momentum rpgdm-ironsworn-character 5))) - -(defun rpgdm-ironsworn-roll-stat (stat modifier) - "Roll an action based on a loaded character's STAT with a MODIFIER." - (interactive (list (completing-read "Stat Modifier: " '(Edge Heart Iron Shadow Wits)) - (read-string "Other Modifier: "))) - (let ((all-mods (+ (gethash stat rpgdm-ironsworn-character) - (string-to-number modifier)))) - (rpgdm-ironsworn-roll all-mods))) - -(defun rpgdm-ironsworn-roll-edge (modifier) - "Roll an action based on a loaded character's Edge stat with a MODIFIER." - (interactive (list (read-string "Edge + Modifier: "))) - (rpgdm-ironsworn-roll-stat 'edge modifier)) - -(defun rpgdm-ironsworn-roll-heart (modifier) - "Roll an action based on a loaded character's Heart stat with a MODIFIER." - (interactive (list (read-string "Heart + Modifier: "))) - (rpgdm-ironsworn-roll-stat 'heart modifier)) - -(defun rpgdm-ironsworn-roll-iron (modifier) - "Roll an action based on a loaded character's Iron stat with a MODIFIER." - (interactive (list (read-string "Iron + Modifier: "))) - (rpgdm-ironsworn-roll-stat 'iron modifier)) - -(defun rpgdm-ironsworn-roll-shadow (modifier) - "Roll an action based on a loaded character's Shadow stat with a MODIFIER." - (interactive (list (read-string "Shadow + Modifier: "))) - (rpgdm-ironsworn-roll-stat 'shadow modifier)) - -(defun rpgdm-ironsworn-roll-wits (modifier) - "Roll an action based on a loaded character's Wits stat with a MODIFIER." - (interactive (list (read-string "Wits + Modifier: "))) - (rpgdm-ironsworn-roll-stat 'wits modifier)) - -(defun rpgdm-ironsworn-oracle-action-theme () - "Rolls on two tables at one time." - (interactive) - (let ((action (rpgdm-tables-choose "actions")) - (theme (rpgdm-tables-choose "themes"))) - (rpgdm-message "%s / %s" action theme))) - -(defun rpgdm-ironsworn-oracle-npc () - (interactive) - (let ((name (rpgdm-tables-choose "names-ironlander")) - (goal (rpgdm-tables-choose "character-goal")) - (role (rpgdm-tables-choose "character-role")) - (activity (rpgdm-tables-choose "character-activity")) - (description (rpgdm-tables-choose "character-descriptor")) - (disposition (rpgdm-tables-choose "character-disposition"))) - (rpgdm-message "%s, %s %s (Activity: %s Disposition: %s Goal: %s)" - name description role activity disposition goal))) - -(defun rpgdm-ironsworn-oracle-combat () - (interactive) - (let ((action (rpgdm-tables-choose "combat-action")) - (method (rpgdm-tables-choose "combat-event-method")) - (target (rpgdm-tables-choose "combat-event-target"))) - (rpgdm-message "%s %s or %s" method target action))) - -(defun rpgdm-ironsworn-oracle-feature () - "Rolls on two tables at one time for a Site's feature." - (interactive) - (let ((aspect (rpgdm-tables-choose "feature-aspect")) - (focus (rpgdm-tables-choose "feature-focus"))) - (rpgdm-message "%s / %s" aspect focus))) - -(defun rpgdm-ironsworn-oracle-site-nature () - "Rolls on two tables at one time for a random Site." - (interactive) - (let* ((theme (rpgdm-tables-choose "site-theme")) - (domain (rpgdm-tables-choose "site-domain")) - (place (downcase domain)) - (name (rpgdm-ironsworn-oracle-site-name place))) - (rpgdm-message "%s %s :: %s" theme domain name))) - -(defun rpgdm-ironsworn-oracle-site-name (&optional place-type) - "Rolling on multiple tables to return a random site name." - (interactive (list (completing-read "Place type: " - '(barrow cavern icereach mine pass ruin - sea-cave shadowfen stronghold - tanglewood underkeep)))) - (unless place-type - (setq place-type "unknown")) - (let ((description (rpgdm-tables-choose "site-name-description")) - (detail (rpgdm-tables-choose "site-name-detail")) - (namesake (rpgdm-tables-choose "site-name-namesake")) - (place (rpgdm-tables-choose (format "site-name-place-%s" place-type))) - (roll (rpgdm--roll-die 100))) - (rpgdm-message - (cond - ((<= roll 25) (format "%s %s" description place)) - ((<= roll 50) (format "%s of %s" place detail)) - ((<= roll 70) (format "%s of %s %s" place description detail)) - ((<= roll 80) (format "%s of %s's %s" place namesake detail)) - ((<= roll 85) (format "%s's %s" namesake place)) - ((<= roll 95) (format "%s %s of %s" description place namesake)) - (t (format "%s of %s" place namesake)))))) - -(defvar rpgdm-ironsworn-oracle-threats '("Burgeoning Conflict" "Ravaging Horde" - "Cursed Site" "Malignant Plague" - "Scheming Leader" "Zealous Cult" - "Environmental Calamity" "Power-Hungry Mystic" - "Rampaging Creature") - "A list of threats that correspond to tables") - -(defun rpgdm-ironsworn-oracle-threat-goal (&optional category) - "Given a CATEGORY, display a threat goal." - (interactive (list (completing-read "Threat: " rpgdm-ironsworn-oracle-threats))) - (unless category - (setq category (seq-random-elt rpgdm-ironsworn-oracle-threats))) - (let ((table-name (format "threat-%s" (downcase (string-replace " " "-" category))))) - (rpgdm-message "%s: %s" category (rpgdm-tables-choose table-name)))) - -(rpgdm-ironsworn-oracle-threat-goal) - -(defun rpgdm-ironsworn-oracle () - "Given a LIKLIHOOD as a single character, return weighted coin flip." - (interactive) - (let* ((prompt "What are the odds? - c) Almost Certain l) Likely h) 50/50 u) Unlikely n) Small Chance ") - (odds (read-char prompt)) - (roll (rpgdm--roll-die 100)) - (yes! (when (or (and (= roll 11) (eq odds ?c)) - (and (= roll 26) (eq odds ?l)) - (and (= roll 51) (eq odds ?h)) - (and (= roll 76) (eq odds ?u)) - (and (= roll 91) (eq odds ?n))) - t)) - (yes (when (or (and (> roll 11) (eq odds ?c)) - (and (> roll 26) (eq odds ?l)) - (and (> roll 51) (eq odds ?h)) - (and (> roll 76) (eq odds ?u)) - (and (> roll 91) (eq odds ?n))) - t))) - (rpgdm-message "%s %s %s" - (if yes! "Extreme" "") - (if yes "Yes" "No") - (if yes! "or a twist." "")))) - -(defhydra hydra-rpgdm (:color blue :hint nil) - " - ^Dice^ ^Adjust^ ^Oracles/Tables^ ^Moving^ ^Messages^ - ---------------------------------------------------------------------------------------------------------------------------------------------------- - _d_: Roll Dice _D_: Progress Dice _H_: Health _z_: Yes/No Oracle _a_: Action/Theme _n_: NPC _o_: Links ⌘-‿: Show Stats - _e_: Roll Edge _s_: Roll Shadow _S_: Spirit _c_: Show Oracle _c_: Combat Action _f_: Feature _J_/_K_: Page up/dn ⌘-l: Last Results - _h_: Roll Heart _w_: Roll Wits _G_: Supply _O_: Other Oracle _p_: Place Name _P_: Place _N_/_W_: Narrow/Widen ⌘-k: ↑ Previous - _i_: Roll Iron _x_: Roll Stat _M_: Momentum _T_: Load Oracles _t_: Threat Goal ⌘-j: ↓ Next " - ("d" rpgdm-ironsworn-roll) ("D" rpgdm-ironsworn-progress-roll) - ("z" rpgdm-ironsworn-oracle) ("O" rpgdm-oracle) - - ("a" rpgdm-ironsworn-oracle-action-theme) - ("n" rpgdm-ironsworn-oracle-npc) - ("c" rpgdm-ironsworn-oracle-combat) - ("f" rpgdm-ironsworn-oracle-feature) - ("P" rpgdm-ironsworn-oracle-site-nature) - ("p" rpgdm-ironsworn-oracle-site-name) - ("t" rpgdm-ironsworn-oracle-threat-goal) - - ("e" rpgdm-ironsworn-roll-edge) - ("h" rpgdm-ironsworn-roll-heart) - ("i" rpgdm-ironsworn-roll-iron) - ("s" rpgdm-ironsworn-roll-shadow) - ("w" rpgdm-ironsworn-roll-wits) - ("x" rpgdm-ironsworn-roll-stat :color pink) - - ("H" rpgdm-ironsworn-adjust-health :color pink) - ("S" rpgdm-ironsworn-adjust-spirit :color pink) - ("G" rpgdm-ironsworn-adjust-supply :color pink) - ("M" rpgdm-ironsworn-adjust-momentum :color pink) - - ("T" rpgdm-tables-load) ("c" rpgdm-tables-choose) ("C" rpgdm-tables-choose :color pink) - ("o" ace-link) ("N" org-narrow-to-subtree) ("W" widen) - ("K" scroll-down :color pink) ("J" scroll-up :color pink) - - ("s-SPC" rpgdm-ironsworn-character-display) - ("C-m" rpgdm-last-results :color pink) - ("C-n" rpgdm-last-results-next :color pink) - ("C-p" rpgdm-last-results-previous :color pink) - ("s-l" rpgdm-last-results :color pink) - ("s-j" rpgdm-last-results-next :color pink) - ("s-k" rpgdm-last-results-previous :color pink) - - ("q" nil "quit") ("" nil)) - -(provide 'rpgdm-ironsworn) -;;; rpgdm-ironsworn.el ends here diff --git a/rpgdm-npc.el b/rpgdm-npc.el index 680734b..5c424eb 100644 --- a/rpgdm-npc.el +++ b/rpgdm-npc.el @@ -28,9 +28,9 @@ ;;; ;;; CCode: -(defvar rpgdm-base ".") +(require 'rpgdm-core (expand-file-name "rpgdm-core.el" rpgdm-base) t) (require 'rpgdm-dice (expand-file-name "rpgdm-dice.el" rpgdm-base) t) -(require 'rpgdm-dice (expand-file-name "rpgdm-tables.el" rpgdm-base) t) +(require 'rpgdm-tables (expand-file-name "rpgdm-tables.el" rpgdm-base) t) (defun rpgdm-npc-gender-name () "Return nil or non-nil for male or female names." diff --git a/rpgdm-screen.el b/rpgdm-screen.el index 24f8571..12e9ea5 100644 --- a/rpgdm-screen.el +++ b/rpgdm-screen.el @@ -21,8 +21,7 @@ (require 'org) (require 'org-element) (require 's) - -(defvar rpgdm-base ".") +(require 'rpgdm-core (expand-file-name "rpgdm-core.el" rpgdm-base) t) (defvar rpgdm-screen-directory (expand-file-name "dnd-5e" rpgdm-base) diff --git a/rpgdm-tables-dice.el b/rpgdm-tables-dice.el index 6cd28a7..32b42f8 100644 --- a/rpgdm-tables-dice.el +++ b/rpgdm-tables-dice.el @@ -17,7 +17,7 @@ ;;; Code: -(defstruct dice-table dice rows) +(cl-defstruct dice-table dice rows) (defun rpgdm-tables--choose-dice-table (table) "Choose a string from a random dice table." diff --git a/rpgdm-tables-freq.el b/rpgdm-tables-freq.el index 8a7f807..b257b5d 100644 --- a/rpgdm-tables-freq.el +++ b/rpgdm-tables-freq.el @@ -142,7 +142,7 @@ decrement the ROLL value." ;; (message "Comparing %d <= %d for %s" roll num-elems tag) (if (<= roll num-elems) (return tag) - (decf roll num-elems)))) + (cl-decf roll num-elems)))) (ert-deftest rpgdm-tables--find-tag-test () (let ((weighted-tags diff --git a/rpgdm-tables.el b/rpgdm-tables.el index 94d0fd4..0db6942 100644 --- a/rpgdm-tables.el +++ b/rpgdm-tables.el @@ -3,7 +3,7 @@ ;; Copyright (C) 2021 Howard X. Abrams ;; ;; Author: Howard X. Abrams -;; Maintainer: Howard X. Abrams +;; Maintainer: Howard X. Abrams ;; Created: January 8, 2021 ;; ;; This file is not part of GNU Emacs. @@ -25,11 +25,10 @@ ;; ;;; Code: -(require 'ert) - -(require 'rpgdm-dice) -(require 'rpgdm-tables-freq) -(require 'rpgdm-tables-dice) +(require 'rpgdm-core (expand-file-name "rpgdm-core.el" rpgdm-base) t) +(require 'rpgdm-dice (expand-file-name "rpgdm-dice.el" rpgdm-base) t) +(require 'rpgdm-tables-freq (expand-file-name "rpgdm-tables-freq.el" rpgdm-base) t) +(require 'rpgdm-tables-dice (expand-file-name "rpgdm-tables-dice.el" rpgdm-base) t) (defvar rpgdm-tables-directory @@ -37,7 +36,10 @@ "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.") + "Collection of tables and lists for the Dungeon Master. +When a table directory is first loaded, the _values_ are the +filenames. After a call to `rpgdm-tables-choose', the value +is replaced by the data (in the form of a data structure).") (defun rpgdm-tables-clear () "Clear previously loaded tables." @@ -88,7 +90,6 @@ Store it by NAME in the `rpgdm-tables' hash table." (message "Read: %s" name)) (puthash name contents rpgdm-tables))) - (defun rpgdm-tables-choose (table-name) "Return random item from a table of a given TABLE-NAME string. @@ -102,16 +103,25 @@ dice table (see `rpgdm-tables--choose-dice-table')." (when-let ((table (gethash table-name rpgdm-tables))) (when (stringp table) (setq table (rpgdm-tables-load-file table table-name))) - (let* ((result (cond ((dice-table-p table) (rpgdm-tables--choose-dice-table table)) + (let* ((initial (cond ((dice-table-p table) (rpgdm-tables--choose-dice-table table)) ((hash-table-p table) (rpgdm-tables--choose-freq-table table)) + ((functionp table) (call-interactively table)) ((listp table) (rpgdm-tables--choose-list table)) - (t "Error: Could choose anything from %s (internal bug?)" table-name))) - ;; Replace any dice expression in the message with an roll: + (t "Error: Could not choose anything from %s (internal bug?)" table-name))) + + ;; Function to return dice expression as a sum (and string): (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))) - (kill-new no-alt-words) - (rpgdm-message "%s" no-alt-words)))) + + (results (thread-last initial + ;; Replace dice expression in the message with an roll: + (replace-regexp-in-string rpgdm-roll-regexp dice-sum ) + ;; Replace [[table-name]] with results from table: + (rpgdm-tables--choose-string-from-table) + ;; Replace [one/two/three] with one of those words: + (rpgdm-tables--choose-string-list)))) + + (kill-new results) + (rpgdm-message "%s" results)))) (defun rpgdm-tables--choose-list (lst) "Randomly choose (equal chance for any) element in LST." @@ -123,30 +133,17 @@ dice table (see `rpgdm-tables--choose-dice-table')." 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 "/") it) - (seq-random-elt it) - (s-trim it))))) + (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))) -(ert-deftest rpgdm-tables--choose-string-list () - (let ((empty-string "") - (no-op-string "This is just a phrase.") - (two-choices "We can have [this/that]") - (two-choices1 "We can have this") - (two-choices2 "We can have that") - (tri-choices "We can have [this / that / the other].") - (tri-choices1 "We can have this.") - (tri-choices2 "We can have that.") - (tri-choices3 "We can have the other.")) - - (should (equal (rpgdm-tables--choose-string-list empty-string) empty-string)) - (should (equal (rpgdm-tables--choose-string-list no-op-string) no-op-string)) - (let ((chosen (rpgdm-tables--choose-string-list two-choices))) - (should (or (equal chosen two-choices1) (equal chosen two-choices2)))) - (let ((chosen (rpgdm-tables--choose-string-list tri-choices))) - (should (or (equal chosen tri-choices1) (equal chosen tri-choices2) (equal chosen tri-choices3)))))) +(defun rpgdm-tables--choose-string-from-table (str) + "Replace <> sequence in STR with call to `rpgdm-tables-choose'." + (let ((regexp (rx "<<" (+? any) ">>")) + (subbed (lambda (s) (rpgdm-tables-choose (substring s 2 -2))))) + (replace-regexp-in-string regexp subbed str nil nil 0))) ;; 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 diff --git a/rpgdm.el b/rpgdm.el index dcedb2f..3d1ba66 100644 --- a/rpgdm.el +++ b/rpgdm.el @@ -25,20 +25,16 @@ (require 'ert) +(require 'rpgdm-core) (require 'rpgdm-dice) (require 'rpgdm-screen) (require 'rpgdm-tables) - -(defvar rpgdm-base - (seq-find (lambda (elt) (string-match "rpgdm" elt)) load-path (getenv "HOME")) - "Default directory to look for supporting data, like tables and charts.") - (define-minor-mode rpgdm-mode "Minor mode for layering role-playing game master functions over your notes." :lighter " D&D" :keymap (let ((map (make-sparse-keymap))) - (define-key map (kbd "") 'hydra-rpgdm/body) + (define-key map (kbd "") 'hydra-rpgdm/body) map)) (defhydra hydra-rpgdm (:color pink :hint nil) @@ -73,64 +69,6 @@ ("q" nil "quit") ("" nil)) - -(defvar rpgdm-last-results (make-ring 10) - "The results from calls to `rpgdm-screen-' functions are stored here.") - -(defvar rpgdm-last-results-ptr 0 - "Keeps track of where we are in the message display ring. -Each call to `rpgdm-last-results' resets this to 0.") - -(defun rpgdm-message (format-string &rest args) - "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." - (let ((message (apply 'format format-string args))) - (ring-insert rpgdm-last-results message) - (rpgdm-last-results))) - -(defun rpgdm-last-results () - "Display results from the last call to a `rpgdm-message' function." - (interactive) - (setq rpgdm-last-results-ptr 0) - (message (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) - -(defun rpgdm-last-results-previous () - "Display results from an earlier call to `rpgdm-message'." - (interactive) - (incf rpgdm-last-results-ptr) - (when (>= rpgdm-last-results-ptr (ring-length rpgdm-last-results)) - (setq rpgdm-last-results-ptr 0)) - (message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) - -(defun rpgdm-last-results-next () - "Display results from an later call to `rpgdm-message'. -Meant to be used with `rpgdm-last-results-previous'." - (interactive) - (when (> rpgdm-last-results-ptr 0) - (decf rpgdm-last-results-ptr)) - (message "%d> %s" rpgdm-last-results-ptr (ring-ref rpgdm-last-results rpgdm-last-results-ptr))) - -(defun rpgdm-paste-last-message () - "Yank, e.g. paste, the last displayed message." - (interactive) - (insert (rpgdm-last-results))) - -(ert-deftest rpgdm-last-results-test () - (progn - (setq rpgdm-last-results (make-ring 10)) - (rpgdm-message "First in, so this is the oldest") - (rpgdm-message "Something or other") - (rpgdm-message "Almost the newest") - (rpgdm-message "Newest")) - - (should (equal "Newest" (rpgdm-last-results))) - (should (equal "1> Almost the newest" (rpgdm-last-results-previous))) - (should (equal "2> Something other" (rpgdm-last-results-previous))) - (should (equal "1> Almost the newest" (rpgdm-last-results-next))) - (should (equal "0> Almost the newest" (rpgdm-last-results-next))) - (should (equal "0> Almost the newest" (rpgdm-last-results-next)))) - (defvar rpgdm-oracle-mod 0 "Cummulative skew to create more tension.") (defun rpgdm-oracle () @@ -195,12 +133,12 @@ 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 + - 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