emacs-rpgdm/rpgdm-tables.el
Howard Abrams 7dab533415 Working with both Dice and Frequency tables
Some of these tables are getting complicated, so I have created three
different tables, and this should be sufficient.

Describing it, however, seems to be a lot for source code, and I thought
I would describe it using a literate programming style. We'll see.
2021-02-08 15:26:16 -08:00

131 lines
5.7 KiB
EmacsLisp

;;; rpgdm-tables.el --- Choose table from Tables -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2021 Howard X. Abrams
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
;; Maintainer: Howard X. Abrams <howard.abrams@workday.com>
;; Created: January 8, 2021
;;
;; This file is not part of GNU Emacs.
;;
;;; Commentary:
;;
;; By storing tables in files, we can preparse them, allowing us to randomly
;; choose entries. The primary interface are:
;;
;; - `rpgdm-tables-load' :: Which when pointed to a directory, stores the data
;; in all text files read.
;;
;; - `rpgdm-tables-choose' :: Which, when a table is choosen, returns a random
;; element.
;;
;; The files read can be simply formatted as lists, or as an org-mode table. If
;; a table is found, the second column should be a _frequency_ label, where some
;; items in that table will be chosen more often than others.
;;
;;; Code:
(defvar rpgdm-base ".")
(require 'rpgdm-dice (expand-file-name "rpgdm-dice.el" rpgdm-base) t)
(require 'rpgdm-tables-freq (expand-file-name "rpgdm-tables-freq.el" rpgdm-base) t)
(require 'rpgdm-tables-dice (expand-file-name "rpgdm-tables-dice.el" rpgdm-base) t)
(defvar rpgdm-tables-directory
(expand-file-name "tables" rpgdm-base)
"Directory path containing the tables to load and create functions.")
(defvar rpgdm-tables (make-hash-table :test 'equal)
"Collection of tables and lists for the Dungeon Master.")
(defun rpgdm-tables-load (&optional filepath)
"Create functions from table files located in FILEPATH directory."
(interactive (list (read-directory-name "DM Tables Directory: " rpgdm-tables-directory)))
(dolist (table-file (directory-files filepath t))
(let ((name (file-name-base table-file))
(contents (rpgdm-tables--read-table-file table-file)))
;; (message "Read: %s" table-file)
(puthash name contents rpgdm-tables)))
(message "Read: %s" (s-join ", " (hash-table-keys rpgdm-tables))))
(defun rpgdm-tables-choose (table-name)
"Return random item from a table of a given TABLE-NAME string.
The name is searched in the `rpgdm-tables' hash-table, and the
value returned is a _table_ of sorts. It could be a list, which
would be easy (see `rpgdm-tables--choose-list'), or it is either
a freq-uency table (see `rpgdm-tables--choose-freq-table') or a
dice table (see `rpgdm-tables--choose-dice-table')."
(interactive (list (completing-read "Choose from Table: " (hash-table-keys rpgdm-tables))))
(let* ((table (gethash table-name rpgdm-tables))
(response (cond ((dice-table-p table) (rpgdm-tables--choose-dice-table table))
((hash-table-p table) (rpgdm-tables--choose-freq-table table))
((listp table) (rpgdm-tables--choose-list table))
(t "Error: Could choose anything from %s (internal bug?)" table-name))))
(rpgdm-message "%s" response)))
(defun rpgdm-tables--choose-list (lst)
"Randomly choose (equal chance for any) element in LST."
(seq-random-elt lst))
;; 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
;; regular expression, however, will parse a list or a frequency table.
(defvar rpgdm-tables--line-parse
(rx bol (zero-or-more space)
(optional
(any "+" "-" "*" "|")
(one-or-more space))
(group (+? any))
(optional (zero-or-more space) (or ":" "|") (zero-or-more space)
(group (one-or-more alphanumeric))
(zero-or-more space) (optional (or ":" "|")) (zero-or-more space))
eol)
"A regular expression for locating parsable lines.")
(ert-deftest rpgdm-tables--line-parse-test ()
"Why yes, we should test our regular expression."
(dolist (data '((" - normal list" "normal list" nil)
(" + normal list" "normal list" nil)
(" * normal list" "normal list" nil)
("normal list" "normal list" nil)
(" - list with tag :tag:" "list with tag" "tag")
(" - list with tag :tag" "list with tag" "tag")
(" - list with tag : tag" "list with tag" "tag")
(" | table cell | freq |" "table cell" "freq")))
(cl-destructuring-bind (line contents tag) data
(should (string-match rpgdm-tables--line-parse line))
(should (equal (match-string 1 line) contents))
(when tag
(should (equal (match-string 2 line) tag))))))
(defun rpgdm-tables--read-table-file (table-file)
"Read and parse TABLE-FILE as data. Whatever that means."
(when (and (file-regular-p table-file) (file-readable-p table-file))
(with-temp-buffer
(insert-file-contents table-file)
(goto-char (point-min))
(flush-lines (rx bol (zero-or-more space) "#"))
;; The following predicates are not /pure functions/, as they scan the
;; current buffer, leaving the initial match in the 'hopper', so the parsing
;; function called makes that assumption, and will immediately grab that
;; `match-string' and then parse the rest of the buffer.
(cond
((rpgdm-tables-dice-table?) (rpgdm-tables--parse-as-dice-table))
((rpgdm-tables-freq-table?) (rpgdm-tables--parse-as-freq-table))
(t (rpgdm-tables--parse-as-list))))))
(defun rpgdm-tables--parse-as-list ()
"Return list of lines matching `rpgdm-tables--line-parse'."
(let ((results (list (match-string-no-properties 1))))
(while (re-search-forward rpgdm-tables--line-parse nil t)
(let ((entry (match-string-no-properties 1)))
(setq results (cons entry results))))
results))
(provide 'rpgdm-tables)
;;; rpgdm-tables.el ends here