2013-05-08 Distributing XP With Emacs

This topic ties together two topics that probably don’t see too much overlap.

1. I play role-playing games of the D&D old school variety.

2. I use Emacs to help me do simple stuff on a daily basis.

The problem: the party of characters my players run is *huge*. Even if there are usually only around ten characters that are part of a single session, there are more than thirty primary and secondary characters on the status page. Given the wiki table for the status page, how can I quickly add up the correct XP and gold values? Any XP gained is shared equally amongst the characters that took part in the session but any gold gained is distributed according to each characters share. Primary characters get a full share, secondary characters get a third of a share.

on the status page

the wiki table

I used Emacs widget mode to create a page like this:

XP total:   805
Gold total: 7191
[X] Schalk
[ ] Uluf
[ ] Witschik
[X] Schachtmann
[ ] Sirius
[X] Logard
[X] Arnd
[X] Tinaya
[ ] Pyrula
[ ] Pijo
[ ] Garo
[X] Zeta
[ ] Pipo
[X] Fusstritt
[ ] Thor
[ ] Jack
[ ] Gloria
[ ] Hermann
[ ] Urs
[ ] Alpha
[ ] Beta
[ ] Gamma
[ ] Boden
[ ] Basel
[ ] Bern
[X] Nuschka
[ ] Moranor
[ ] Axirios Hectaxius

[Go!]

And here’s the code to do it:

(defconst fünf-winde-regexp "^\\(|\\[\\[\\(.*?\\)\\]\\][ \t]*|[ \t]*\\(1\\|1/3\\)[ \t]*\\)|\\([ \t]*[0-9]+[ \t]*\\)|\\([ \t]*[0-9]+[ \t]*\\)"
  "Regular expression to parse the Status page.
\(let ((str (match-string 1))
      (name (match-string 2))
      (share (match-string 3))
      (xp (match-string 4))
      (gold (match-string 5)))
    ...\)")

(defvar fünf-winde-buf nil
  "Source buffer.")

(defvar fünf-winde-xp nil
  "XP share.")

(defvar fünf-winde-gold nil
  "Gold share.")

(defvar fünf-winde-party nil
  "Charakters in the party.")

(defun fünf-winde-xp-and-gold ()
  "Hand out Gold and XP."
  (interactive)
  (let ((buf (current-buffer))
	(names))
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward fünf-winde-regexp nil t)
	(setq names (cons (match-string 2) names))))
    (switch-to-buffer "*Fünf Winde*")
    (kill-all-local-variables)
    (set (make-local-variable 'fünf-winde-buf) buf)
    (make-local-variable 'fünf-winde-xp)
    (make-local-variable 'fünf-winde-gold)
    (make-local-variable 'fünf-winde-party)
    (let ((inhibit-read-only t))
      (erase-buffer))
    (remove-overlays)
    (setq fünf-winde-xp
	  (widget-create 'integer
			 :size 13
			 :format "XP total:   %v\n"
			 0))
    (setq fünf-winde-gold
	  (widget-create 'integer
			 :size 13
			 :format "Gold total: %v\n"
			 0))
    (setq fünf-winde-party
	  (apply 'widget-create 'checklist
		 (mapcar (lambda (name)
			   `(item ,name))
			 (nreverse names))))
    (widget-insert "\n")
    (widget-create 'push-button
		   :notify (lambda (&rest ignore)
			     (fünf-winde-process
			      fünf-winde-buf
			      (widget-value fünf-winde-xp)
			      (widget-value fünf-winde-gold)
			      (widget-value fünf-winde-party)))
		   "Go!")
    (widget-insert "\n")
    (use-local-map widget-keymap)
    (local-set-key (kbd "q") 'bury-buffer)
    (local-set-key (kbd "SPC") 'widget-button-press)
    (local-set-key (kbd "<left>") 'widget-backward)
    (local-set-key (kbd "<up>") 'widget-backward)
    (local-set-key (kbd "<right>") 'widget-forward)
    (local-set-key (kbd "<down>") 'widget-forward)
    (widget-setup)
    (goto-char (point-min))
    (widget-forward 1)))

(defun fünf-winde-process (buf total-xp total-gold party)
  (message "(fünf-winde-process (get-buffer \"%s\") %d %d '%S)"
	   buf total-xp total-gold party)
  (switch-to-buffer buf)
  (save-excursion
    (let ((xp-shares 0)
	  (xp-share nil)
	  (gold-shares 0)
	  (gold-share nil))
      (goto-char (point-min))
      (while (re-search-forward fünf-winde-regexp nil t)
	(let ((name (match-string 2))
	      (share (match-string 3)))
	  (when (member name party)
	    (setq gold-shares (+ gold-shares
				 (cond ((string= share "1/2") 0.5)
				       ((string= share "1/3") (/ 1.0 3))
				       (t (string-to-number share))))
		  xp-shares (1+ xp-shares)))))
      (setq gold-share (/ total-gold gold-shares)
	    xp-share (/ total-xp xp-shares))
      (goto-char (point-min))
      (while (re-search-forward fünf-winde-regexp nil t)
	(let ((str (match-string 1))
	      (name (match-string 2))
	      (share (match-string 3))
	      (xp (match-string 4))
	      (gold (match-string 5)))
	  (when (member name party)
	    (setq gold (format (concat "%9d")
			       (+  (string-to-number gold)
				   (* gold-share (cond ((string= share "1/2") 0.5)
						       ((string= share "1/3") (/ 1.0 3))
						       (t (string-to-number share))))))
		  xp (format (concat "%9d")
			     (+  (string-to-number xp)
				 xp-share)))
	    (replace-match (concat str
				   "|" xp
				   "|" gold))))))))

I’m not sure I’m spending my time wisely, but there you go. I used to have a simpler piece of code that helped me distribute XP and gold separately. The drawback was that it would ask me for every person in the table “was this character in the party? (y/n)” and that’s a lot of yes and no replies if you go through the list *twice*.

It’s also a stark reminder that simpler old rules doesn’t automatically mean less work for the referee. With D&D 3.5, I had a spreadsheet to compute the XP gained based on challenge rating and character level. It wasn’t something to do quickly without a book in front of me. Now the complexity of the task has been reduced, but the number of characters has exploded to compensate!

​#Emacs ​#RPG ​#Old School