Fixed a number of bugs

This also tangles out the unit tests.
This commit is contained in:
Howard Abrams 2022-02-26 10:28:42 -08:00
parent 0f03c237d2
commit 3c43efdbba
4 changed files with 326 additions and 95 deletions

View file

@ -169,7 +169,12 @@ We also need the name of the directory for this project, so that we can load tab
"The root directory to the rpgdm-ironsworn project.")
#+END_SRC
** Dice Roller
In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die). You always three possible values:
In *Ironsworn*, all dice rolls follow a pattern where you set the challenge level for a check by rolling /challenge dice/ (two d10s) and compare that against rolling an /action die/ (a single d6 ... adding all modifiers to that six-sided die).
#+ATTR_HTML: :width 800px
[[file:images/dice-roll-all.png]]
You always three possible values:
- A *strong hit* where your action die + modifiers is larger than both d10 challenge dice.
- A *weak hit* is where your action die + modifiers is larger than /only one/ of the d10 challenge dice.
@ -236,7 +241,7 @@ When we roll, I want one of those three results printed, but in different colors
So the following messages, given various /rolls/ should cover those possibilities with text properties:
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--results-test ()
(should (equal (rpgdm-ironsworn--results 3 2 4 1)
"Strong hit :: 5 (3 + 2) → 4 / 1"))
@ -250,7 +255,7 @@ So the following messages, given various /rolls/ should cover those possibilitie
(should (equal (rpgdm-ironsworn--results 3 2 8 6 7)
"Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Weak hit"))
(should (equal (rpgdm-ironsworn--results 3 2 8 6 9)
"Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit")))))
"Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit")))
#+END_SRC
The basic interface will query for a modifer, roll all three dice, and then display the results using the [[https://gitlab.com/howardabrams/emacs-rpgdm/-/blob/main/rpgdm.el#L76][rpgdm-message]] function:
@ -279,12 +284,12 @@ Assuming we have a new character, let's query the user for all of these stats, a
We assume you have created an org-file, and the /template/ will just append some basic text at the end of the buffer. Note that if you don't give this function a name, it will randomly choose one.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--new-character-template (name)
(defun rpgdm-ironsworn--new-character-template (&optional name)
"Insert basic Ironsworn template at the end of the current buffer.
A header is created with NAME, but if this is an empty string,
a random name is generated for the purposes of the template."
(when (s-blank? name)
(setq name (rpgdm-tables-choose "names-ironlander")))
(when (or (null name) (s-blank? name))
(setq name (rpgdm-tables-choose "name/ironlander")))
(let ((frmt (seq-random-elt '("* The Adventures of %s"
"* The Journeys of %s"
@ -335,8 +340,8 @@ Let's fill in those asset values /lazily/. The first time we call this function,
the `assets' directory, otherwise, we return a cached version."
(unless rpgdm-ironsworn-character-assets
(let ((asset-files (thread-first rpgdm-ironsworn-project
(f-join "assets")
(directory-files-recursively (rx (one-or-more any) ".org") nil))))
(f-join "assets")
(directory-files-recursively (rx (one-or-more any) ".org") nil))))
(setq rpgdm-ironsworn-character-assets
(seq-map (lambda (file) (cons (rpgdm-ironsworn--character-asset-label file) file))
@ -390,7 +395,7 @@ When you start a character, you choose three assets, but what if we choose them
And I can write a little unit test to verify my test cases:
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--good-character-assets-test ()
(should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz")))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo")))
@ -399,7 +404,7 @@ And I can write a little unit test to verify my test cases:
"assets/companions/monkey.org")))
(should-not (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org")
("Paths :: Good Guy" . "assets/paths/good-guy.org")
("Companions :: Monkey" . "assets/companions/monkey.org"))))))
("Companions :: Monkey" . "assets/companions/monkey.org")))))
#+END_SRC
The rules say to start off with three assets, so let's have a function that can give us three random assets:
@ -416,7 +421,7 @@ The rules say to start off with three assets, so let's have a function that can
And again, a little unit test would be nice:
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--some-character-assets-test ()
(should (= 4 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6) 4))))
(should (= 3 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6))))))
@ -459,7 +464,7 @@ Whew. We finally can have a function that queries the user about a new character
(goto-char (point-max))
(insert "\n** Assets\n")
(if (y-or-n-p "Would you like three random assets? ")
(rpgdm-ironsworn-random-character-assets asset-files)
(rpgdm-ironsworn-random-character-assets 3)
(if (y-or-n-p "Would you like to choose your assets? ")
(call-interactively 'rpgdm-ironsworn-insert-character-asset))))
#+END_SRC
@ -474,7 +479,7 @@ This function will query the user for all of the stats and other properties that
`rpgdm-ironsworn-progress-create' functions."
(dolist (stat '(edge heart iron shadow wits))
(rpgdm-ironsworn-store-character-state stat
(read-string (format "What '%s' stat: " stat))))
(read-string (format "What '%s' stat: " stat))))
(dolist (stat '(health spirit supply))
(rpgdm-ironsworn-store-character-state stat 5))
@ -482,10 +487,11 @@ This function will query the user for all of the stats and other properties that
(rpgdm-ironsworn-progress-create (read-string "What title should we give this new character's Epic vow? ") 1)
(rpgdm-ironsworn-progress-create "Bonds" 1)
(rpgdm-ironsworn-progress-mark "Bonds")
(search-forward ":END:")
(end-of-line)
(insert "\n** Bonds\n")
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names"))))
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement/name"))))
#+END_SRC
**** New Character Interface
Do we choose the stats first, or the assets? Do we ask, or have two separate functions?
@ -495,14 +501,12 @@ Perhaps the clearest approach is to do both, create two process functions, and t
(defun rpgdm-ironsworn--new-character-stats-first (&optional name)
"Insert a new character template for character, NAME.
The character stats are first queried, and then assets inserted."
(rpgdm-ironsworn--new-character-template name)
(rpgdm-ironsworn--new-character-stats)
(rpgdm-ironsworn--new-character-assets))
(defun rpgdm-ironsworn--new-character-assets-first (&optional name)
"Insert a new character template for character, NAME.
The assets are inserted first, and then character stats are queried."
(rpgdm-ironsworn--new-character-template name)
;; Saving and restoring point, means the properties should be in the
;; correct, top-level position.
(save-excursion
@ -516,16 +520,18 @@ Perhaps the clearest approach is to do both, create two process functions, and t
This function _appends_ this information to the current buffer,
which should be using the `org-mode' major mode."
(interactive (list
(read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(rpgdm-ironsworn--new-character-template name)
(if (equal order "Assets first")
(rpgdm-ironsworn--new-character-assets-first)
(rpgdm-ironsworn--new-character-assets-first)
(rpgdm-ironsworn--new-character-stats-first))
(message "Alright, the template is complete. Edit away!" name))
#+END_SRC
*** Showing a Character Stats
Sure, you could open up the appropriate drawer to see a character's stats, but we could do better? An updated org table? A separate buffer? For the moment, I am just going to display it in the mini-buffer whenever I want to see it:
Sure, you could open up the appropriate drawer to see a character's stats, but we could do better? An updated org table? A separate buffer? For the moment, I am just going to display it in the mini-buffer whenever I want to see it.
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--display-stat (stat character)
@ -719,13 +725,13 @@ The [[file:moves][moves]] directory contains one org file for each move. These f
And let's verify the format:
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--move-tuple-test ()
(let ((file "moves/fate/ask-the-oracle.org")
(full "~/other/over/here/moves/fate/ask-the-oracle.org"))
(should (equal (list "Fate :: Ask The Oracle" file)
(should (equal (list "Fate :: ask the oracle" file)
(rpgdm-ironsworn--move-tuple file)))
(should (equal (list "Fate :: Ask The Oracle" full)
(should (equal (list "Fate :: ask the oracle" full)
(rpgdm-ironsworn--move-tuple full)))))
#+END_SRC
@ -876,12 +882,26 @@ Given a label, can we get the level as a value:
(alist-get label rpgdm-ironsworn-progress-levels 8 nil 'equal))
#+END_SRC
Since we'll store only the numbers, we might want to get back the label:
Since we'll store only the numbers, we might want to get back the label. Originally, I used an =rassoc=, but since I =propertized= the labels, my new function is a bit more complicated:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-progress-level-label (level)
"Return the label associated with the progress LEVEL, e.g. dangerous."
(car (rassoc level rpgdm-ironsworn-progress-levels)))
(thread-last rpgdm-ironsworn-progress-levels
(--filter (= (second it) level))
(first) ; Assume we only get one match
(first) ; Get the textual label
(s-split " ")
(first))) ; Word before the space
#+END_SRC
Basic tests for sanity:
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn-progress-level-label-test ()
(should (equal (rpgdm-ironsworn-progress-level-label 1) "epic"))
(should (equal (rpgdm-ironsworn-progress-level-label 12) "troublesome"))
(should (equal (rpgdm-ironsworn-progress-level-label 4) "formidable")))
#+END_SRC
A helper function for allowing the user to choose which track to mark progress against:
@ -911,26 +931,27 @@ Adding a progress to a character amounts to an arbitrary name, and the number of
Stored as a property in the org file. Keep in mind that the
NAME should be a short title, not a description."
(interactive (list (read-string "Progress Name: ")
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels)))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels)))
(let* ((track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name))))
(when (called-interactively-p)
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name)))))
(org-set-property track-prop track-val)))
#+END_SRC
@ -948,22 +969,97 @@ Interactively, we can call the =-mark= function multiple times, but we might wan
(rpgdm-ironsworn-mark-progress-track name)))
#+END_SRC
The =rpgdm-ironsworn--progress-amount= function returns the information about a progress, by name. It returns a list of all aspects, allowing us to decide how to display it:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--progress-amount (name)
"Return list of aspects of progress by NAME.
Including:
- track :: The name of the track (same as `name')
- value :: The number complexity level, e.g. how many ticks per marked progress
- level :: The string label of a progress, e.g. Epic or Troublesome
- ticks :: The amount of ticks marked, shnould be <= 40
- boxes :: The number of completed boxes (ticks / 4, rounded down)
- remain :: The number of remaining ticks marked towards the next box."
(let* ((tracks (rpgdm-ironsworn-character-progresses))
(matched (assoc name tracks))
(track (first matched))
(value (second matched))
(level (rpgdm-ironsworn-progress-level-label value))
(ticks (third matched))
(boxes (/ ticks 4))
(remain (% ticks 4)))
(list track value level ticks boxes remain)))
#+END_SRC
Make a progress to visual table of boxes. Not sure if this is very helpful or not, but it sure will look cool:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn--progress-box (boxes leftover)
"Return a org-like table of current completion of an Ironsworn progress.
For instance, with 4 boxes and 2 leftover tick marks, this will return:
| ■ | ■ | ■ | ■ | x | | | | | | "
(defun make-box (boxes leftover-ticks blanks)
(cond
((> boxes 0) (concat " ■ |" (make-box (1- boxes) leftover-ticks blanks)))
((> leftover-ticks 2) (concat " * |" (make-box 0 0 blanks)))
((> leftover-ticks 1) (concat " x |" (make-box 0 0 blanks)))
((> leftover-ticks 0) (concat " - |" (make-box 0 0 blanks)))
((> blanks 0) (concat " |" (make-box 0 0 (1- blanks))))))
(when (> leftover 3)
(setq boxes (+ boxes (/ leftover 4)))
(setq leftover (% leftover 4)))
(when (< boxes 0)
(setq boxes 0))
(when (> boxes 10)
(setq boxes 10))
(setq blanks (if (> leftover 0)
(- 9 boxes)
(- 10 boxes)))
(concat "|" (make-box boxes leftover blanks)))
#+END_SRC
Some tests may make it clear how that function will look:
#+BEGIN_SRC emacs-lisp
(ert-deftest rpgdm-ironsworn--progress-box-test ()
(should (equal (rpgdm-ironsworn--progress-box 0 0) "| | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 1) "| - | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 2) "| x | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 3) "| * | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 1 0) "| ■ | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 2 0) "| ■ | ■ | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 3 0) "| ■ | ■ | ■ | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 0) "| ■ | ■ | ■ | ■ | | | | | | |"
(should (equal (rpgdm-ironsworn--progress-box 4 1) "| ■ | ■ | ■ | ■ | - | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 2) "| ■ | ■ | ■ | ■ | x | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 3) "| ■ | ■ | ■ | ■ | * | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 5 0) "| ■ | ■ | ■ | ■ | ■ | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 10 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |"))
;; Negative test cases
(should (equal (rpgdm-ironsworn--progress-box 11 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |"))
(should (equal (rpgdm-ironsworn--progress-box -1 0) "| | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 8) "| ■ | ■ | ■ | ■ | ■ | ■ | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 6) "| ■ | ■ | ■ | ■ | ■ | x | | | | |")))
#+END_SRC
If we call the =-amount= function /interactively/, we should get the current results displayed, otherwise, we really just want the number of full boxes marked:
#+BEGIN_SRC emacs-lisp
(defun rpgdm-ironsworn-progress-amount (name)
"Display the progress made against a track, NAME."
(interactive (list (rpgdm-ironsworn-progress-track-choose)))
(let* ((tracks (rpgdm-ironsworn-character-progresses))
(matched (--filter (equal name (first it)) tracks))
(track (first matched))
(value (second track))
(level (rpgdm-ironsworn-progress-level-label value))
(ticks (third track))
(boxes (/ ticks 4)))
(if (called-interactively-p 'any)
(rpgdm-message "[%d] Progress on %s: %d (Ticks: %d)" level name boxes ticks)
boxes)))
(cl-destructuring-bind
(track value level ticks boxes leftover)
(rpgdm-ironsworn--progress-amount name)
(if (not (called-interactively-p 'any))
boxes
(rpgdm-message "[%s] Progress on '%s': %d %s" level name boxes
(rpgdm-ironsworn--progress-box boxes leftover)))))
#+END_SRC
Rolling against the progress just means we need to request that value instead of rolling the d6, but in this case, we should either take a currently registered progress track, or simply specify a completed amount:
@ -993,9 +1089,9 @@ When we've finished a track, we can remove it from the hash to not clutter the i
(remhash name tracks))))
#+END_SRC
Let's make sure these function work as we expect:
Let's make sure these function work as we expect (keep in mind, these can only be run in an Org file):
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp
(ert-deftest rpgdm-ironsworn-progress-test ()
(let ((track "Battling a Grue"))
(rpgdm-ironsworn-progress-delete track)
@ -1523,7 +1619,7 @@ We also need a property-symbol to string conversion. I don't know if the string
A test is always explanatory for how this function should behave:
#+BEGIN_SRC emacs-lisp :tangle no
#+BEGIN_SRC emacs-lisp :tangle rpgdm-ironsworn-tests.el
(ert-deftest rpgdm-ironsworn--progress-to-str-test ()
(should (equal (rpgdm-ironsworn--progress-to-str :IRONSWORN-PROGRESS-EPIC)
"ironsworn-progress-epic")))

BIN
images/dice-roll-all.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

65
rpgdm-ironsworn-tests.el Normal file
View file

@ -0,0 +1,65 @@
(ert-deftest rpgdm-ironsworn--results-test ()
(should (equal (rpgdm-ironsworn--results 3 2 4 1)
"Strong hit :: 5 (3 + 2) → 4 / 1"))
(should (equal (rpgdm-ironsworn--results 3 2 8 1)
"Weak hit :: 5 (3 + 2) → 8 / 1"))
(should (equal (rpgdm-ironsworn--results 3 2 8 6)
"Miss :: 5 (3 + 2) → 8 / 6"))
(should (equal (rpgdm-ironsworn--results 3 2 6 6)
"Miss :: 5 (3 + 2) → 6 / 6 ← Create a Twist"))
(should (equal (rpgdm-ironsworn--results 3 2 8 6 7)
"Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Weak hit"))
(should (equal (rpgdm-ironsworn--results 3 2 8 6 9)
"Miss :: 5 (3 + 2) → 8 / 6 -- Burn momentum for a Strong hit")))
(ert-deftest rpgdm-ironsworn--good-character-assets-test ()
(should (rpgdm-ironsworn--good-character-assets '("foo" "bar" "baz")))
(should-not (rpgdm-ironsworn--good-character-assets '("foo" "bar" "foo")))
(should-not (rpgdm-ironsworn--good-character-assets '("assets/companions/dog.org"
"assets/paths/good-guy.org"
"assets/companions/monkey.org")))
(should-not (rpgdm-ironsworn--good-character-assets '(("Companions :: Dog" . "assets/companions/dog.org")
("Paths :: Good Guy" . "assets/paths/good-guy.org")
("Companions :: Monkey" . "assets/companions/monkey.org")))))
(ert-deftest rpgdm-ironsworn--some-character-assets-test ()
(should (= 4 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6) 4))))
(should (= 3 (seq-length (rpgdm-ironsworn--some-character-assets '(1 2 3 4 5 6))))))
(ert-deftest rpgdm-ironsworn--move-tuple-test ()
(let ((file "moves/fate/ask-the-oracle.org")
(full "~/other/over/here/moves/fate/ask-the-oracle.org"))
(should (equal (list "Fate :: ask the oracle" file)
(rpgdm-ironsworn--move-tuple file)))
(should (equal (list "Fate :: ask the oracle" full)
(rpgdm-ironsworn--move-tuple full)))))
(ert-deftest rpgdm-ironsworn-progress-level-label-test ()
(should (equal (rpgdm-ironsworn-progress-level-label 1) "epic"))
(should (equal (rpgdm-ironsworn-progress-level-label 12) "troublesome"))
(should (equal (rpgdm-ironsworn-progress-level-label 4) "formidable")))
(ert-deftest rpgdm-ironsworn-progress-test ()
(let ((track "Battling a Grue"))
(rpgdm-ironsworn-progress-delete track)
(rpgdm-ironsworn-progress-create track "Dangerous")
(should (= (rpgdm-ironsworn-progress-amount track) 0))
(rpgdm-ironsworn-progress-mark track)
(should (= (rpgdm-ironsworn-progress-amount track) 2))
(rpgdm-ironsworn-progress-mark track 2)
(should (= (rpgdm-ironsworn-progress-amount track) 6))))
(ert-deftest rpgdm-ironsworn-progress-test ()
(let ((track "Battling an Extreme Grue"))
(rpgdm-ironsworn-progress-delete track)
(rpgdm-ironsworn-progress-create track "Extreme")
(should (= (rpgdm-ironsworn-progress-amount track) 0))
(rpgdm-ironsworn-progress-mark track)
(should (= (rpgdm-ironsworn-progress-amount track) 0))
(rpgdm-ironsworn-progress-mark track 2)
(should (= (rpgdm-ironsworn-progress-amount track) 1))))
(ert-deftest rpgdm-ironsworn--progress-to-str-test ()
(should (equal (rpgdm-ironsworn--progress-to-str :IRONSWORN-PROGRESS-EPIC)
"ironsworn-progress-epic")))

View file

@ -88,12 +88,12 @@ results."
one-challenge two-challenge
momentum))))
(defun rpgdm-ironsworn--new-character-template (name)
(defun rpgdm-ironsworn--new-character-template (&optional name)
"Insert basic Ironsworn template at the end of the current buffer.
A header is created with NAME, but if this is an empty string,
a random name is generated for the purposes of the template."
(when (s-blank? name)
(setq name (rpgdm-tables-choose "names-ironlander")))
(when (or (null name) (s-blank? name))
(setq name (rpgdm-tables-choose "name/ironlander")))
(let ((frmt (seq-random-elt '("* The Adventures of %s"
"* The Journeys of %s"
@ -132,8 +132,8 @@ that contains the text. The first time we call this, we read from
the `assets' directory, otherwise, we return a cached version."
(unless rpgdm-ironsworn-character-assets
(let ((asset-files (thread-first rpgdm-ironsworn-project
(f-join "assets")
(directory-files-recursively (rx (one-or-more any) ".org") nil))))
(f-join "assets")
(directory-files-recursively (rx (one-or-more any) ".org") nil))))
(setq rpgdm-ironsworn-character-assets
(seq-map (lambda (file) (cons (rpgdm-ironsworn--character-asset-label file) file))
@ -205,7 +205,7 @@ The chosen assets are _good_ in that they won't have duplicates, etc."
(goto-char (point-max))
(insert "\n** Assets\n")
(if (y-or-n-p "Would you like three random assets? ")
(rpgdm-ironsworn-random-character-assets asset-files)
(rpgdm-ironsworn-random-character-assets 3)
(if (y-or-n-p "Would you like to choose your assets? ")
(call-interactively 'rpgdm-ironsworn-insert-character-asset))))
@ -216,7 +216,7 @@ Note: The stats are added as properties using the
`rpgdm-ironsworn-progress-create' functions."
(dolist (stat '(edge heart iron shadow wits))
(rpgdm-ironsworn-store-character-state stat
(read-string (format "What '%s' stat: " stat))))
(read-string (format "What '%s' stat: " stat))))
(dolist (stat '(health spirit supply))
(rpgdm-ironsworn-store-character-state stat 5))
@ -224,22 +224,21 @@ Note: The stats are added as properties using the
(rpgdm-ironsworn-progress-create (read-string "What title should we give this new character's Epic vow? ") 1)
(rpgdm-ironsworn-progress-create "Bonds" 1)
(rpgdm-ironsworn-progress-mark "Bonds")
(search-forward ":END:")
(end-of-line)
(insert "\n** Bonds\n")
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement-names"))))
(insert (format " - Your home settlement of %s\n" (rpgdm-tables-choose "settlement/name"))))
(defun rpgdm-ironsworn--new-character-stats-first (&optional name)
"Insert a new character template for character, NAME.
The character stats are first queried, and then assets inserted."
(rpgdm-ironsworn--new-character-template name)
(rpgdm-ironsworn--new-character-stats)
(rpgdm-ironsworn--new-character-assets))
(defun rpgdm-ironsworn--new-character-assets-first (&optional name)
"Insert a new character template for character, NAME.
The assets are inserted first, and then character stats are queried."
(rpgdm-ironsworn--new-character-template name)
;; Saving and restoring point, means the properties should be in the
;; correct, top-level position.
(save-excursion
@ -253,8 +252,10 @@ template will generate and query the user for the rest of the data.
This function _appends_ this information to the current buffer,
which should be using the `org-mode' major mode."
(interactive (list
(read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(read-string "What is the new character's name? ")
(completing-read "What order should we build this? " '("Statistics first" "Assets first"))))
(rpgdm-ironsworn--new-character-template name)
(if (equal order "Assets first")
(rpgdm-ironsworn--new-character-assets-first)
(rpgdm-ironsworn--new-character-stats-first))
@ -505,7 +506,12 @@ For instance, if LABEL is `Dangerous', this returns `8'."
(defun rpgdm-ironsworn-progress-level-label (level)
"Return the label associated with the progress LEVEL, e.g. dangerous."
(car (rassoc level rpgdm-ironsworn-progress-levels)))
(thread-last rpgdm-ironsworn-progress-levels
(--filter (= (second it) level))
(first) ; Assume we only get one match
(first) ; Get the textual label
(s-split " ")
(first))) ; Word before the space
(defun rpgdm-ironsworn-progress-track-choose (&optional allow-other)
"Query the user to choose a track stored in the org file.
@ -527,26 +533,27 @@ marked against some progress."
Stored as a property in the org file. Keep in mind that the
NAME should be a short title, not a description."
(interactive (list (read-string "Progress Name: ")
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels)))
(completing-read-value "Progress Level: "
rpgdm-ironsworn-progress-levels)))
(let* ((track-id (substring (secure-hash 'md5 name) 0 8))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(track-prop (format "ironsworn-progress-%s" track-id))
(track-val (format "\"%s\" %d %d" name level 0))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(title (org-get-heading))
(option '(("At the same level as a sibling?" same-level)
("As a subheading to this?" subheading)
("No new heading. Re-use this." no))))
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name))))
(when (called-interactively-p)
(cl-case (completing-read-value "Create a new heading? " option)
('same-level (progn
(org-insert-heading-respect-content)
(insert name)))
('subheading (progn
(org-insert-heading-respect-content)
(org-shiftmetaright)
(insert name)))))
(org-set-property track-prop track-val)))
@ -559,19 +566,82 @@ the number of TIMES to mark progress."
(dotimes (idx times)
(rpgdm-ironsworn-mark-progress-track name)))
(defun rpgdm-ironsworn--progress-amount (name)
"Return list of aspects of progress by NAME.
Including:
- track :: The name of the track (same as `name')
- value :: The number complexity level, e.g. how many ticks per marked progress
- level :: The string label of a progress, e.g. Epic or Troublesome
- ticks :: The amount of ticks marked, shnould be <= 40
- boxes :: The number of completed boxes (ticks / 4, rounded down)
- remain :: The number of remaining ticks marked towards the next box."
(let* ((tracks (rpgdm-ironsworn-character-progresses))
(matched (assoc name tracks))
(track (first matched))
(value (second matched))
(level (rpgdm-ironsworn-progress-level-label value))
(ticks (third matched))
(boxes (/ ticks 4))
(remain (% ticks 4)))
(list track value level ticks boxes remain)))
(defun rpgdm-ironsworn--progress-box (boxes leftover)
"Return a org-like table of current completion of an Ironsworn progress.
For instance, with 4 boxes and 2 leftover tick marks, this will return:
| | | | | x | | | | | | "
(defun make-box (boxes leftover-ticks blanks)
(cond
((> boxes 0) (concat " ■ |" (make-box (1- boxes) leftover-ticks blanks)))
((> leftover-ticks 2) (concat " * |" (make-box 0 0 blanks)))
((> leftover-ticks 1) (concat " x |" (make-box 0 0 blanks)))
((> leftover-ticks 0) (concat " - |" (make-box 0 0 blanks)))
((> blanks 0) (concat " |" (make-box 0 0 (1- blanks))))))
(when (> leftover 3)
(setq boxes (+ boxes (/ leftover 4)))
(setq leftover (% leftover 4)))
(when (< boxes 0)
(setq boxes 0))
(when (> boxes 10)
(setq boxes 10))
(setq blanks (if (> leftover 0)
(- 9 boxes)
(- 10 boxes)))
(concat "|" (make-box boxes leftover blanks)))
(ert-deftest rpgdm-ironsworn--progress-box-test ()
(should (equal (rpgdm-ironsworn--progress-box 0 0) "| | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 1) "| - | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 2) "| x | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 0 3) "| * | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 1 0) "| ■ | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 2 0) "| ■ | ■ | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 3 0) "| ■ | ■ | ■ | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 0) "| ■ | ■ | ■ | ■ | | | | | | |"
(should (equal (rpgdm-ironsworn--progress-box 4 1) "| ■ | ■ | ■ | ■ | - | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 2) "| ■ | ■ | ■ | ■ | x | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 3) "| ■ | ■ | ■ | ■ | * | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 5 0) "| ■ | ■ | ■ | ■ | ■ | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 10 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |"))
;; Negative test cases
(should (equal (rpgdm-ironsworn--progress-box 11 0) "| ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ | ■ |"))
(should (equal (rpgdm-ironsworn--progress-box -1 0) "| | | | | | | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 8) "| ■ | ■ | ■ | ■ | ■ | ■ | | | | |"))
(should (equal (rpgdm-ironsworn--progress-box 4 6) "| ■ | ■ | ■ | ■ | ■ | x | | | | |")))
(defun rpgdm-ironsworn-progress-amount (name)
"Display the progress made against a track, NAME."
(interactive (list (rpgdm-ironsworn-progress-track-choose)))
(let* ((tracks (rpgdm-ironsworn-character-progresses))
(matched (--filter (equal name (first it)) tracks))
(track (first matched))
(value (second track))
(level (rpgdm-ironsworn-progress-level-label value))
(ticks (third track))
(boxes (/ ticks 4)))
(if (called-interactively-p 'any)
(rpgdm-message "[%d] Progress on %s: %d (Ticks: %d)" level name boxes ticks)
boxes)))
(cl-destructuring-bind
(track value level ticks boxes leftover)
(rpgdm-ironsworn--progress-amount name)
(if (not (called-interactively-p 'any))
boxes
(rpgdm-message "[%s] Progress on '%s': %d %s" level name boxes
(rpgdm-ironsworn--progress-box boxes leftover)))))
(defun rpgdm-ironsworn-progress-roll (progress-value)
"Display a Hit/Miss message based on the PROGRESS-VALUE.
@ -950,8 +1020,8 @@ Return 0 if not at a heading, or above first headline."
(defun rpgdm-ironsworn--current-character-state (results)
"Recursive helper to insert current header properties in RESULTS.
Calls itself if it is not looking at the top-level header in the
file. If a property is already in the hash table, RESULTS, it is
Calls itself if it is not looking at the top level header in the
file. If a property is already in the hash table, RESULTS, it is
not overwritten, thereby having lower-level subtrees take
precendence over similar settings in higher headers."
(defun key-convert (ironsworn-prop)