Extending Emacs to Play Mythic Bastionland
Summary: Swapping out bookmarks while playing #MythicBastionland then restoring when finished. Also opening PDFs to random pages to simulate rolling on #RandomTables.
For playing Mythic Bastionland 📖 , I’ve been using or building out tooling. First, I’m leaning on my random-tables package. Next, while playing, I manually swapped out my baseline Emacs 📖 bookmarks for game specific bookmarks. Last, I began thinking about flipping to random PDF pages for inspiration.
Swapping Out Bookmarks
What I posted in Forged from the Worst: Session 1 worked, but I started thinking
about how I might alter Emacs
while running/playing the game. At first, this
felt akin to turning on a minor mode. But the more I thought about it, it was
more equivalent to using org-clock.
A quick brainstorm, and I realized that while playing:
- I wanted different bookmarks.
- Additional snippets (for my knight and squires name).
- An indicator that I was playing the game.
- And depending on how I organize my campaign world notes, maybe I’d start a clock on the headline associated with my world notes.
I haven’t yet implemented the world notes, but I have made adjustments for the others. Here’s what I have:
First, I establish a variable to track the state of “playing/not playing.”
(defvar playing-forged-from-the-worst nil
"When non-nil, indicates that I'm playing Forged from the Worst.")
Then I created a command to toggle that on and off:
(defun toggle-forged-from-the-worst ()
"Begin or end playing Forged from the Worst."
(interactive)
(load "jf-mythic-bastionland.el")
(setq playing-forged-from-the-worst
(not playing-forged-from-the-worst))
(bookmark-load
(if playing-forged-from-the-worst
"~/SyncThings/source/forged-from-the-worst/forged=from=the=worst--bookmarks.el"
"~/emacs-bookmarks.el")
t nil t))
The command loads my random tables for the campaign. Toggles state. The loads the correct bookmarks based on state.
To indicate that I’m “playing”, I then added a variable that I could use with my modeline:
(defvar-local jf/mode-line-format/playing-fftw
'(:eval
(when (and (boundp playing-forged-from-the-worst)
playing-forged-from-the-worst
(mode-line-window-selected-p))
(concat
(propertize " 🎲 " 'face 'mode-line-highlight) " "))))
I add the variable into my mode-line-format:
(setq-default mode-line-format
'("%e" " "
jf/mode-line-format/timeclock
jf/mode-line-format/org-clock
jf/mode-line-format/vterm
jf/mode-line-format/kbd-macro
jf/mode-line-format/narrow
jf/mode-line-format/playing-fftw
jf/mode-line-format/buffer-name-and-status " "
jf/mode-line-format/major-mode " "
jf/mode-line-format/project " "
jf/mode-line-format/vc-branch " "
jf/mode-line-format/flymake " "
jf/mode-line-format/eglot
jf/mode-line-format/which-function
))
And ensure that I mark that variable as a risky-local-variable:
(dolist (construct '(
jf/mode-line-format/buffer-name-and-status
jf/mode-line-format/eglot
jf/mode-line-format/flymake
jf/mode-line-format/kbd-macro
jf/mode-line-format/playing-fftw
jf/mode-line-format/major-mode
jf/mode-line-format/misc-info
jf/mode-line-format/narrow
jf/mode-line-format/org-clock
jf/mode-line-format/timeclock
jf/mode-line-format/project
jf/mode-line-format/vc-branch
jf/mode-line-format/vterm
jf/mode-line-format/which-function
))
(put construct 'risky-local-variable t))
With that, when I’m playing the game, I see a little dice in my mode-line and have access to game specific bookmarks. That clock part is going to gnaw at me, so I assume I’ll work through that once I’ve published this post.
Flipping to Random PDF Page in Emacs
In Mythic Bastionland Session Reflection, I thought about the fact that I now had the PDF bookmarked and could quickly, I assume, access the oracular information at the bottom of the Knight/Seer and Myths pages.
My first pass was “what was the minimum viable command to open a random page in
a PDF.” This involved reading the pdf-view-bookmark-jump-handler code and then
setting about making it happen.
What I’m presenting is not the first nor second pass, but instead a third iteration that introduces a bit more utility. But I digress.
The algorithm I wanted was:
- Prompt for whether I wanted a Seer/Knight or a Myth page.
- Open the PDF in a dedicated window.
- Go to a random page based on selection.
There are 72 Seer/Knight pages and 72 Myth pages. On a spread, the left page is a Seer/Knight and the right page is a Myth. The Seer/Knight starts on page 28.
The random function started as:
(+ (if seer-knight 28 29)
(* (random 72) 2))
That is pick a number between 0 and 71, multiple that by 2, then add 28 or 29 depending on Seer/Knight or Myth.
I would then use find-file and in that buffer call pdf-view-goto-page. It was
inelegant but was quick to verify general behavior.
Then I set about creating a better user experience. Below is the random-pages
to choose from, and their relevant information of what file and how to pick a
page.
(defvar random-pages
'(("Knights/Seers" .
(:file
"~/Documents/RPGs/Mythic Bastionland/mythic=bastionland--core-rules__rules_systems.pdf"
:callback
(lambda () (pdf-view-goto-page (+ 28 (* (random 72) 2))))))
("Myths" .
(:file
"~/Documents/RPGs/Mythic Bastionland/mythic=bastionland--core-rules__rules_systems.pdf"
:callback
(lambda ()
(pdf-view-goto-page (+ 29 (* (random 72) 2)))))))
"An alist where `car' is the label and `cdr' is a plist with :file and
optional :callback.
We'll open the :file, then if a :callback is present, we'll run that
callback on the newly opened file.")
Next up is the function to open the random page in a dedicated window; with the happy little “bind g to pick a new random page.”
(defun random-page (&optional label set)
"Open the file from SET with given LABEL.
SET is assumed to be an alist with `car' as the label and `cdr' a plist
with :file and :callback. See `random-pages' for more information."
(interactive)
(let* ((set
(or set random-pages))
(label
(or label
(completing-read "Source: " set nil t)))
(source
(alist-get label set nil nil #'string=))
(file
(plist-get source :file))
(display-buffer-mark-dedicated
t)
(buffer (or
(find-buffer-visiting file)
(find-file-noselect file))))
;; We'll pop open a dedicated side window with ample space for
;; viewing a new file.
(pop-to-buffer buffer '((display-buffer-in-side-window)
(side . right)
(window-width 72)
(window-parameters
(tab-line-format . none)
(mode-line-format . none)
(no-delete-other-windows . t))))
(with-current-buffer buffer
;; As a courtesy let's bind "g" to refresh re-invoke the
;; random-page using the same label.
(local-set-key (kbd "g")
(lambda () (interactive)
(random-page label)))
;; I envision that not every random-page would have a callback.
;; Which highlights that perhaps the function name 'random-page'
;; is a misnomer based on my nascent understanding of what this
;; could be.
(when-let ((callback
(plist-get source :callback)))
(funcall callback)))))
What the above does is pop open a window on the right, with plenty of space to view the whole page. That window gets focus and I can close it q or re-roll with g. It also does the work to re-use a buffer if it already exists.
An animated GIF demontsrating the functions along with a list of commands called.
M-x consult-bookmarkto show starting bookmarks.M-x jf/mode-line-format/playing-fftwto start playing “Forged from the Worst.”M-x consult-bookmarkshow a list of the game specific bookmarks.M-x random-page REG Seer/Knight RETto pop open a random Knight/Seer page from the Mythic Bastionland rule book.- Then g a few times to pick a new random Knight/Seer page each time.
Conclusion
I love the virtuous cycle of playing a game, having a tool to support that game-play, and knowing that I can extend the tool to facilitate play. The result tends towards a generative feedback loop.
And both my during play moments of reflection as well as after play write-ups helped me consider what might be interesting to add to my tool chain. Which fed into exploring existing functionality and implementation to craft something just a bit new.
Now to think about my next session of Forged from the Worst. And attending to how I write up campaign notes while running. See what’s missing, maybe work and clocking time there. That would mean I’d have access to capture content to that clock, and could leverage more native Org-Mode 📖 functionality.