https://github.com/howardabrams/emacs-rpgtk
The Solo RPG Toolkit for Emacs
https://github.com/howardabrams/emacs-rpgtk
Last synced: 6 months ago
JSON representation
The Solo RPG Toolkit for Emacs
- Host: GitHub
- URL: https://github.com/howardabrams/emacs-rpgtk
- Owner: howardabrams
- Created: 2024-06-28T04:43:33.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2024-06-28T21:53:24.000Z (almost 2 years ago)
- Last Synced: 2025-01-19T08:15:27.394Z (over 1 year ago)
- Language: Emacs Lisp
- Size: 336 KB
- Stars: 6
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.org
Awesome Lists containing this project
README
#+title: Emacs RPG Toolkit
#+author: Howard Abrams
#+email: howard@sting
#+date: 2023-10-01 October
#+tags: emacs rpg solottrpg
#+startup: inlineimages
This project supplies a collection of functions in Emacs Lisp to allow
developers to craft RPG-inspired programs, with a focus on Solo RPGs
played in Org-formatted files.
Sure, if you are a non-Lisper Emacs user, you may get some benefit,
but this project is a /toolkit/ for making (or at least, emulating) some
typical TTRPGs (with or without the table of friends).
Note that we store different /features/ in different files, described in
each of the following sections. Your project would =require= the
individual files you need.
* Dice Rollers
The basis of most games are random numbers, and RPGs have depended on
dice. The [[file:rpgtk-dice.el][rpgtk-dice]] file contains functions for rolling dice, and
displaying the results.
The function, =rpgtk-roll=, is the primary user interface, as it rolls a
dice based on a /dice expression/, e.g. =3d8+2=, either at the point
location or from a prompt from the user.
[[file:images/screenshot-rpgtk-roll.png]]
Notice in the example, the code displays the expression used on the right
side, and all the dice and calculations are also displayed, as if you
did the math.
Other interesting functions are =rpgtk-roll-again= (to re-roll previous
rolls), and =rpgtk-forward-roll= that moves the point to the next dice
expression in the current buffer.
Game often /craft/ the game’s specialty dice mechanic. For instance, in
playing /Year Zero/-based games, like Vaesen, you roll a pool of
six-sided dice, choose the highest, and see if you rolled a six.
A dice mechanic involves three steps:
1. Rolling one or more dice, using =rpgtk-dice-roll=
2. Modifying the results through a series of steps using =rpgtk-dice-roll-mod=
3. Displaying the results using =rpgtk-dice-format-roll=
For instance:
#+begin_src emacs-lisp
(defun year-zero-roll (dice-pool)
"Roll a DICE-POOL number of six sided dice.
Displays the highest value. Shows 6s in green."
(interactive "nDice Pool Size: ")
(let* ((roll (rpgtk-dice-roll num-dice 6))
(high (rpgtk-dice-roll-mod roll :max)))
(rpgtk-message (rpgtk-dice-format-roll high "YZ" 6))))
#+end_src
To display:
[[file:images/screenshot-rpgtk-roll-yz.png]]
Games based on /Blades in the Dark/ have a similar mechanic for rolling
a pool of six-side dice, choosing the highest value, but instead of a
single six being success, a dice of =4= or =5= is a partial success. We
could make a complete function to simulate this dice mechanic:
#+begin_src emacs-lisp
(defun blades-in-the-dark-roll (dice-pool)
"Display formatted dice expression for Blades in the Dark.
Where NUM-DICE are the number six-sided dice to roll."
(interactive "nNumber of Dice: ")
(thread-first dice-pool
(rpgtk-dice-roll 6) ; Roll a pool of d6s
(rpgtk-dice-roll-mod :max) ; Select the highest
(rpgtk-dice-format-roll
(format "%dd" dice-pool) ; Create a dice expression (opt)
6 ; Set the success level
4) ; Set partial success level
(rpgtk-message)))
#+end_src
If the highest roll was a 6, the function shows it in a green color
(actually based on the [[file:rpgtk-dice.el::(defface rpgtk-successful-roll][rpgtk-successful-roll]] face), and for a
/partial success/, we use the [[file:rpgtk-dice.el::(defface rpgtk-middlin-roll][rpgtk-middlin-roll]] face:
[[file:images/screenshot-rpgtk-roll-bitd1.png]]
Otherwise, we show that value with a [[file:rpgtk-dice.el::(defface rpgtk-failed-roll][rpgtk-failed-roll]] face:
[[file:images/screenshot-rpgtk-roll-bitd2.png]]
As a final example, in the 5th Edition of /Dungeons and Dragons/, a
player can roll /with advantage/ by rolling two twenty-sided dice,
choosing the higher of the two, and then adding their =modifier= bonus.
The =rpgtk-dice-roll-mod= function call would look something like:
#+begin_src emacs-lisp
(rpgtk-dice-roll-mod (rpgtk-dice-roll 2 20)
:max ; Choose larger roll
:add (or modifier 0)
:sum)
#+end_src
See the function documentation for =rpgtk-dice-roll-mod= for a list of
modifications.
* Deck Draws
The [[file:rpgtk-deck.el][rpgtk-deck]] contains functions for drawing cards from a deck.
*Note:* A deck is simply a list of strings, but normally built from a small list of numbers (called /orders/) and /suits/. These may or may not be followed by a list of /trumps/, e.g. The Jokers in a standard deck of playing cards.
Drawing a card and putting it back randomly into the deck is simulated with the function: =rpgtk-deck-random= (which, if the order and suits are not given, defaults to a standard deck of playing cards). Instead of this function, you might as well make a table (see below).
Drawing one or more cards from a deck (and having a discard pile, so those cards are not redrawn), first, make a /shuffled/ deck with =rpgtk-deck-shuffle= (or =rpgtk-deck-shuffle-standard=), and draw using =rpgtk-deck-draw= (or =rpgtk-deck-deck-draw-cards= for more than one).
For instance, an RPG that tells you to use a standard deck of playing cards, but only use Ace through 6. The RPG uses the /suits/ for different aspects of a character’s life, and the order (number) be the intensity of the situation:
#+begin_src emacs-lisp :results silent
(rpgtk-deck-shuffle
;; Instead of numbers, we use some T-shirt sizes:
'("XS" "S" "M" "L" "XL" "XXL")
;; We use Hearts Diamonds Clubs and Spades for:
'(" Relationship" " at Work" " Project" " in School"))
;; Notice the spaces above.
#+end_src
This could results in drawing three cards at a complicated point in the character’s life:
#+begin_example
Cards: ⸢XL at Work⸥ ⸢S Relationship⸥ ⸢XL in School⸥
#+end_example
Hrm. Perhaps asking out the crush caused our character to get fired from working at the diner as well as a failing grade in Chemistry. /shrug/
* Random Tables
From Random Encounters, to Treasure Tables, to answering, “What’s the
weather like?” RPGs have relied on randomly choosing entries from
tables. This toolkit offers a function, =rpgtk-tables-load=, that parses
a directory of text files (recursively). Then call =rpgtk-tables-choose=
and select a random table, e.g.
#+attr_html: :width 600px
[[file:images/screenshot-rpgtk-choose-1.png]]
Using fuzzy enhancers to =completing-read=, allows you to trim down the
options:
#+attr_html: :width 600px
[[file:images/screenshot-rpgtk-choose-2.png]]
And using something like [[https://github.com/oantolin/orderless][Orderless]], limits the choices even more:
#+attr_html: :width 600px
[[file:images/screenshot-rpgtk-choose-3.png]]
Until you have what you want:
#+attr_html: :width 600px
[[file:images/screenshot-rpgtk-choose-4.png]]
The random entry from the table is both displayed and copied to the
clipboard, er, kill-ring.
The table parsing function accepts three /types/ of formats for these
text files:
- lists
- frequency tables
- dice tables
** List Tables
Most text files for these files contain a list of items. The file
could contain items where each line is one entry, e.g.
#+begin_example
Grughuc Coinhelm
Lobatin Flaskhide
Koghurum Longgut
Emgus Barbedpike
Belbek Bronzehand
Lasris Blazingblade
Emthrun Stronghammer
Thurthrum Norsk
Gwynmura Rejuhkak
Jintin Glowdust
Gergrom Frosthorn
Nysdille Heavybeard
#+end_example
Unlike published RPG material that relies on dice combinations, the
beauty of these random selection tables is you can have any number of
items. For instance, a list may have seven items, and you wouldn’t
have to add either an item or an entry that says, “Roll twice on this
table”.
The entries in the files can begin with /list characters/, i.e. ~-~, ~+~,
~*~, and ~|~. This allows the file to mimic an org-mode list or table. The
code ignores lines beginning with ~#~ as comments, which allows a table
writer to specify meta information, e.g.
#+begin_example
#+name: Elf Names
#+property: source-url=https://www.fantasynamegenerators.com/elf-names.php
- Rydel Helegwyn
- Merellien Reywynn
- Ivasaar Theric
- Naertho Inanorin
- Folen Zumnorin
- Inchel Craroris
- Simimar Yesdove
- Cyran Qimaer
- Naeryndam Thelynn
- Eriladar Carsatra
#+end_example
Each entry can specify a random numerical value, e.g. for a Random
Encounter Table,
#+begin_example
- A group of 1d4+2 goblins gambling at dice
- A bugbear dangles 2d20+10' above the characters, ready to drop.
- A hobgoblin carries 1d4 bags of loot.
…
#+end_example
Which could return:
#+begin_example
A bugbear dangles 31' above the characters, ready to drop.
#+end_example
Entries can also specify textual choices, e.g.
#+begin_example
- A group of 1d4+2 [stealthy/drunk/sleeping/angry] goblins
#+end_example
Which could return:
#+begin_example
A group of 5 sleeping goblins
#+end_example
Text that matches a pattern between double angle brackets are replaced by a recursive call to another table. For instance, you could have a table of monsters in =monsters.txt=:
#+begin_example
- ogres
- goblins
- trolls
- orcs
...
#+end_example
And another table, =monster-activity.txt= that has stuff like:
#+begin_example
- sleeping
- playing [dice/cards/stones]
- drinking
- arguing
...
#+end_example
And now, in your =random-dungeon-encounters.txt= table, you can have:
#+begin_example
...
- mannacles attached to the wall
- 2d4+1 <> <>
...
#+end_example
And now, you might get a response, like:
#+begin_example
3 goblins playing cards
#+end_example
This feature can also be used instead of rolling on multiple tables. For instance, you could have an =npc= table that has a single entry, like:
#+begin_example
- <>, who appears to be a <> is <> ...
#+end_example
** Dice Tables
A /dice table/ is a table that is easy to manipulate with dice, and is
pretty typical in published supplements. The general idea is to roll
one or more specific dice, and compare the number in the first column.
For instance, /Xanathar's Guide to Everything/, a Dungeons &
Dragons supplement from Wizards of the Coast, allows you to
choose a random alignment for a character with the following table:
| 3d6 | Alignment |
|--------|---------------------------------------------|
| 3 | Chaotic evil (50%) or chaotic neutral (50%) |
| 4--5 | Lawful evil |
| 6--8 | Neutral evil |
| 9--12 | Neutral |
| 13--15 | Neutral good |
| 16--17 | Lawful good (50%) or lawful neutral (50%) |
| 18 | Chaotic good (50%) or chaotic neutral (50%) |
This would be render as a table with a range in the first column,
and equally weighted choices in the rest of the columns. For instance:
#+begin_example
Roll on Table: 3d6
| 3 | Chaotic evil | chaotic neutral |
| 4--5 | Lawful evil | |
| 6--8 | Neutral evil | |
| 9--12 | Neutral | |
| 13--15 | Neutral good | |
| 16--17 | Lawful good | lawful neutral |
| 18 | Chaotic good | chaotic neutral |
#+end_example
Notice that we need to have a /dice expression/ to explain how to arrive
at a number for selecting a row. To do this, add the text,
=Roll on Table= and a standard /dice expression/.
These types of tables are good when rendering published material, but
are obnoxious to create.
** Frequency Tables
While the a table could be simple lists to choose a random element,
some lists could return /some elements/ more often than /other elements/.
While that sounds great in a sentence, this code in this section
describes this concept of /frequency tables/. For instance, here is a
Faction Encounter table:
#+begin_example
| Church of Talos :: Worshipers of the god of storms/destruction | scarcely |
| City Watch :: Members of the Waterdeep constabulary | often |
| Cult of the Dragon :: Cultists who venerate evil dragons | seldom |
| Emerald Enclave :: Alliance of druids/rangers to defend the wilds | seldom |
| Enclave of Red Magic :: Thayan mages who smuggle slaves | sometimes |
| Force Grey :: League of heroes sworn to protect Waterdeep | often |
| Halaster’s Heirs :: Dark arcanists trained at a hidden academy | rarely |
| The Kraken Society :: Shadowy group of thieves and mages | rarely |
…
#+end_example
While Waterdeep could have over 50 factions running around, we
would assume players would run into the City Watch more often than
the delusional members of the /Kraken Society/.
Unlike a normal list, these text files have two columns, where the
first is the item and the second determines the frequency, which can
either be from this group:
- =rarely=
- =seldom= or =sometimes=, which is twice as likely as =rarely=
- =scarcely= , =scarce= or =hardly ever=, which is three-times more
likely than =rarely=
- =often=, which is four-times more likely than =rarely=
Or this group:
- =legendary=
- =veryrare=, =very-rare=, or =very rare=, which is /twice/ =legendary=
- =rare=, which is /four-times/ the occurrence of =legendary=
- =uncommon=, is /seven-times/ the occurrence of =legendary=
- =common=, is /twelve-times/ more likely to be selected
As you can tell, the current implementation, while useful, is quite
awful for a /toolkit/, and we need to change the code to allow a
table-writer to specify the frequency levels and grouping, and not
rely on English semantics.
* Result Messages
Unlike regular Emacs call to =message=, strings sent to =rpgtk-message=
are available to be /re-seen/.
- =rpgtk-last-results= : shows the last results
- =rpgtk-last-results-previous= : shows earlier results, called
multiple times after calling =rpgtk-last-results=.
- =rpgtk-last-results-next= : shows a later result, and
called after a call to =rpgtk-last-results-previous=.
While you can bind each of these functions to keys, it might be easier
to bind a key to =rpgtk-last-message= to show the latest message shown
(for instance, a dice roll or an entry from a table), and assuming a
user has installed Hydra, allows the user to iterate over previous
messages.
For instance, after walking through a forest, in some acclimate
weather, our hero befriends a dwarf, rolling a pretty good score.
“What did you say her name was again?” they say … no problem, you call
=rpgtk-last-message= to see:
[[file:images/screenshot-rpgtk-last-message-1.png]]
Pressing ~j~ (since you’ve installed Evil, otherwise, it would be ~p~),
you see the name of the dwarf:
[[file:images/screenshot-rpgtk-last-message-2.png]]
Press it again to remind you of the random weather your table reported:
[[file:images/screenshot-rpgtk-last-message-3.png]]
*Note:* The user may insert the last message seen using any of these
commands, with a standard call to =yank=.
For RPG designers, you should call =rpgtk-message2=, as it takes a
message shown to the user, as well as a second string for the user to
paste. For instance, when viewing a dice roll, the message is verbose,
but when yanking the roll into the buffer, the total is all that is in
the kill-ring.