Compare commits

...

10 commits

Author SHA1 Message Date
Howard Abrams
213391a54e Merge branch 'refactoring' into 'main'
♻️ Extract `rpgdm-core` and address deprecations

See merge request howardabrams/emacs-rpgdm!1
2023-12-10 20:15:03 +00:00
Jeremy Friesen
cf24af20b6 ♻️ Extract `rpgdm-core.el'
Prior to this commit, I experienced the following use case:

Given I have the following code:

```emacs-lisp
(require 'rpgdm-dice "~/git/emacs-rpgdm/rpgdm-dice.el")
(require 'rpgdm-tables "~/git/emacs-rpgdm/rpgdm-tables.el")
(require 'rpgdm-tables-dice "~/git/emacs-rpgdm/rpgdm-tables-dice.el")
(require 'rpgdm-tables-freq "~/git/emacs-rpgdm/rpgdm-tables-freq.el")

(setq rpgdm-base "~/git/emacs-rpgdm/")
```

And I call `rpgdm-tables-load`
And load all the tables.

When I then call `rpgdm-tables-choose`

Then I get the following error:

```
let*: Symbol’s function definition is void: rpgdm-message
```

With this commit I'm able to skip requiring `rpgdm` and thus only use a
segment of the `rpgdm` package ecosystem.
2023-12-10 10:52:34 -05:00
Jeremy Friesen
25c282c094 ♻️ Favor cl- functions
When requiring `rpgdm.el` and it's "child" packages, I encounter the
following warnings:

> Warning: ‘destructuring-bind’ is an obsolete alias (as of 27.1); use
> ‘cl-destructuring-bind’ instead.
>
> Warning: ‘incf’ is an obsolete alias (as of 27.1); use ‘cl-incf’
> instead.
>
> Warning: ‘decf’ is an obsolete alias (as of 27.1); use ‘cl-decf’
> instead.
>
> Warning: ‘defstruct’ is an obsolete alias (as of 27.1); use
> ‘cl-defstruct’ instead.

Since we've already required the `cl` package, this should be a noop change.
2023-12-10 09:56:04 -05:00
Howard Abrams
5c5f17e7a6 Reformatting and cleanup 2023-11-18 15:57:04 -08:00
Howard Abrams
14f6524eec Add any message to the standard kill-ring too.
You know, the clipboard!
2023-09-19 20:55:07 -07:00
Howard Abrams
1a99ec6539 Better description for a function. 2022-05-28 19:57:55 -07:00
Howard Abrams
b0c3601e4c Tables refer to other tables
If an entry in a table references <<another/table>>, that reference is
replaced with a call to that other table. This allows building complex responses.
2022-05-06 21:13:41 -07:00
Howard Abrams
769fcc27d4 Move the Ironsworn code to its own repo 2022-02-24 21:36:16 -08:00
Howard Abrams
71777f5792 Converting from f12 to f6
As the F12 adjusts the volume on a Mac. Sheesh.
2022-02-18 15:31:37 -08:00
Howard Abrams
8b89bf665b Add support for functions in the tables
This is still programmatic, as opposed to dynamically loading source code.
2022-02-18 15:26:48 -08:00
12 changed files with 135 additions and 433 deletions

View file

@ -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 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 #+attr_html: :width 800px
[[file:images/screenshot-of-hydra.png]] [[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. 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 * Code
What do I have here: 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: - [[file:rpgdm.el][rpgdm]] :: Primary interface offering:
- =rpgdm-mode=, plus a Hydra interface for easily calling the rest of these functions. - =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. - =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/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/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.

View file

@ -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. 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 #+BEGIN_SRC emacs-lisp :results silent
(defstruct dice-table dice rows) (cl-defstruct dice-table dice rows)
#+END_SRC #+END_SRC
How is this used to render the example table above? How is this used to render the example table above?

View file

@ -226,7 +226,7 @@ decrement the ROLL value."
;; (message "Comparing %d <= %d for %s" roll num-elems tag) ;; (message "Comparing %d <= %d for %s" roll num-elems tag)
(if (<= roll num-elems) (if (<= roll num-elems)
(return tag) (return tag)
(decf roll num-elems)))) (cl-decf roll num-elems))))
(ert-deftest rpgdm-tables--find-tag-test () (ert-deftest rpgdm-tables--find-tag-test ()
(let ((weighted-tags (let ((weighted-tags

80
rpgdm-core.el Normal file
View file

@ -0,0 +1,80 @@
;;; rpgdm-core --- Core functionality for rpgdm -*- lexical-binding: t -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Jeremy Friesen <jeremy@jeremyfriesen.com>
;; 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

View file

@ -81,7 +81,7 @@ This really tests the `rpgdm--test-rolls' function."
(8 1 8) (8 1 8)
(20 1 20) (20 1 20)
(100 1 100))) (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)))) (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) ((3 6 4) 7 22)
((4 6 4 "-") 0 20)))) ((4 6 4 "-") 0 20))))
(dolist (test-seq test-data) (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))))) (rpgdm--test-roll-series 'rpgdm--roll dice-args lowest highest)))))
@ -240,7 +240,7 @@ the following:
("2d12" 2 24) ("2d12" 2 24)
("3d6+2" 5 20)))) ("3d6+2" 5 20))))
(dolist (test-data test-cases) (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))))) (rpgdm--test-roll-series 'rpgdm--roll-expression (list dice-expression) lowest highest)))))

View file

@ -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") ("<f12>" nil))
(provide 'rpgdm-ironsworn)
;;; rpgdm-ironsworn.el ends here

View file

@ -28,9 +28,9 @@
;;; ;;;
;;; CCode: ;;; 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-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 () (defun rpgdm-npc-gender-name ()
"Return nil or non-nil for male or female names." "Return nil or non-nil for male or female names."

View file

@ -21,8 +21,7 @@
(require 'org) (require 'org)
(require 'org-element) (require 'org-element)
(require 's) (require 's)
(require 'rpgdm-core (expand-file-name "rpgdm-core.el" rpgdm-base) t)
(defvar rpgdm-base ".")
(defvar rpgdm-screen-directory (defvar rpgdm-screen-directory
(expand-file-name "dnd-5e" rpgdm-base) (expand-file-name "dnd-5e" rpgdm-base)

View file

@ -17,7 +17,7 @@
;;; Code: ;;; Code:
(defstruct dice-table dice rows) (cl-defstruct dice-table dice rows)
(defun rpgdm-tables--choose-dice-table (table) (defun rpgdm-tables--choose-dice-table (table)
"Choose a string from a random dice table." "Choose a string from a random dice table."

View file

@ -142,7 +142,7 @@ decrement the ROLL value."
;; (message "Comparing %d <= %d for %s" roll num-elems tag) ;; (message "Comparing %d <= %d for %s" roll num-elems tag)
(if (<= roll num-elems) (if (<= roll num-elems)
(return tag) (return tag)
(decf roll num-elems)))) (cl-decf roll num-elems))))
(ert-deftest rpgdm-tables--find-tag-test () (ert-deftest rpgdm-tables--find-tag-test ()
(let ((weighted-tags (let ((weighted-tags

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2021 Howard X. Abrams ;; Copyright (C) 2021 Howard X. Abrams
;; ;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams> ;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams ;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Created: January 8, 2021 ;; Created: January 8, 2021
;; ;;
;; This file is not part of GNU Emacs. ;; This file is not part of GNU Emacs.
@ -25,11 +25,10 @@
;; ;;
;;; Code: ;;; Code:
(require 'ert) (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) (require 'rpgdm-tables-freq (expand-file-name "rpgdm-tables-freq.el" rpgdm-base) t)
(require 'rpgdm-tables-freq) (require 'rpgdm-tables-dice (expand-file-name "rpgdm-tables-dice.el" rpgdm-base) t)
(require 'rpgdm-tables-dice)
(defvar rpgdm-tables-directory (defvar rpgdm-tables-directory
@ -37,7 +36,10 @@
"Directory path containing the tables to load and create functions.") "Directory path containing the tables to load and create functions.")
(defvar rpgdm-tables (make-hash-table :test 'equal) (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 () (defun rpgdm-tables-clear ()
"Clear previously loaded tables." "Clear previously loaded tables."
@ -88,7 +90,6 @@ Store it by NAME in the `rpgdm-tables' hash table."
(message "Read: %s" name)) (message "Read: %s" name))
(puthash name contents rpgdm-tables))) (puthash name contents rpgdm-tables)))
(defun rpgdm-tables-choose (table-name) (defun rpgdm-tables-choose (table-name)
"Return random item from a table of a given TABLE-NAME string. "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-let ((table (gethash table-name rpgdm-tables)))
(when (stringp table) (when (stringp table)
(setq table (rpgdm-tables-load-file table table-name))) (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)) ((hash-table-p table) (rpgdm-tables--choose-freq-table table))
((functionp table) (call-interactively table))
((listp table) (rpgdm-tables--choose-list table)) ((listp table) (rpgdm-tables--choose-list table))
(t "Error: Could choose anything from %s (internal bug?)" table-name))) (t "Error: Could not choose anything from %s (internal bug?)" table-name)))
;; Replace any dice expression in the message with an roll:
;; Function to return dice expression as a sum (and string):
(dice-sum (lambda (dice-exp) (number-to-string (rpgdm-roll-sum dice-exp)))) (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))) (results (thread-last initial
(kill-new no-alt-words) ;; Replace dice expression in the message with an roll:
(rpgdm-message "%s" no-alt-words)))) (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) (defun rpgdm-tables--choose-list (lst)
"Randomly choose (equal chance for any) element in 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]' For instance, the string: 'You found a [chair/box/statue]'
would be converted randomly to something like: 'You found a box.'" would be converted randomly to something like: 'You found a box.'"
(let ((regexp (rx "[" (+? any) "/" (+? any) "]")) (let ((regexp (rx "[" (+? any) "/" (+? any) "]"))
(subbed (lambda (str) (--> str (subbed (lambda (str) (--> str
(substring it 1 -1) (substring it 1 -1)
(s-split (rx "/") it) (s-split (rx (*? space) "/" (*? space)) it)
(seq-random-elt it) (seq-random-elt it)))))
(s-trim it)))))
(replace-regexp-in-string regexp subbed str))) (replace-regexp-in-string regexp subbed str)))
(ert-deftest rpgdm-tables--choose-string-list () (defun rpgdm-tables--choose-string-from-table (str)
(let ((empty-string "") "Replace <<table-name>> sequence in STR with call to `rpgdm-tables-choose'."
(no-op-string "This is just a phrase.") (let ((regexp (rx "<<" (+? any) ">>"))
(two-choices "We can have [this/that]") (subbed (lambda (s) (rpgdm-tables-choose (substring s 2 -2)))))
(two-choices1 "We can have this") (replace-regexp-in-string regexp subbed str nil nil 0)))
(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))))))
;; I originally thought that I could have a single regular expression that ;; 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 ;; matched all possible tables, but that is a bit too complicated. The following

View file

@ -25,20 +25,16 @@
(require 'ert) (require 'ert)
(require 'rpgdm-core)
(require 'rpgdm-dice) (require 'rpgdm-dice)
(require 'rpgdm-screen) (require 'rpgdm-screen)
(require 'rpgdm-tables) (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 (define-minor-mode rpgdm-mode
"Minor mode for layering role-playing game master functions over your notes." "Minor mode for layering role-playing game master functions over your notes."
:lighter " D&D" :lighter " D&D"
:keymap (let ((map (make-sparse-keymap))) :keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "<f12>") 'hydra-rpgdm/body) (define-key map (kbd "<f6>") 'hydra-rpgdm/body)
map)) map))
(defhydra hydra-rpgdm (:color pink :hint nil) (defhydra hydra-rpgdm (:color pink :hint nil)
@ -73,64 +69,6 @@
("q" nil "quit") ("<f12>" nil)) ("q" nil "quit") ("<f12>" 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.") (defvar rpgdm-oracle-mod 0 "Cummulative skew to create more tension.")
(defun rpgdm-oracle () (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 Players Handbook in Dungeons and Dragons, we have this table
to determine difficulty skill check levels: to determine difficulty skill check levels:
- Very easy 5 - Very easy 5
- Easy 10 - Easy 10
- Medium 15 - Medium 15
- Hard 20 - Hard 20
- Very hard 25 - Very hard 25
- Nearly impossible 30 - Nearly impossible 30
But I read somewhere that you could roll some 6 sided die to help 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 add a bit of randomness to the leve setting. Essentially, roll