Overview

PTables

When writing games, we often want the ability to randomly choose an item or value from a set of possible items. We often want to associate each possible item with a probability, so that some items will occur commonly and others will be rare. We often want some parts of the game to be more dangerous and rewarding than others, and we want some items to become more common and others to become more rare as this danger level increases.

We would like for sets to be able to nest -- for example, one of the items in the set "contents of a treasure chest" might be the set "expensive jewelry", which in turn might contain rings, bracelets and so forth.

Finally, it would be convenient, when declaring new classes of game items, monsters and so on, to be able to declare their membership in a particular random set, rather than have to place them into that set manually.

Probability tables ("ptables") provide all of the functionality described above. Each ptable contains a number of entries. Each entry contains some lisp value, associated with a rarity, and optionally with information about how the rarity changes as the game "level" changes.

Ptables were originally inspired by the "levelled lists" used in the Elder Scrolls games.

Defining ptables

Ptables can be created using the "define-ptable" toplevel form, or can be built up piecewise. A "ptabled-class" metaclass is also provided which allows classes to insert entries for themselves into ptables (see later for more details).

(define-ptable :tbl-foo
  (:bar :common)
  (:baz :rare))

:::lisp
(defclass foo ()
  ((...slots...))
  (:metaclass ptabled-class)
  (:ptable-entries ((:tbl-foo :uncommon) (:tbl-bar :rare))))

Format of a ptable entry

(VALUE :rarity RARITY
       :min-level MIN-LEVEL
       :peaks (PEAK [PEAK...]))

Where:

  • RARITY is a positive integer or one of the keywords :common, :uncommon, :rare or :very-rare.
  • MIN-LEVEL is an integer.
  • PEAKS is a list of one or more integers.

Rarity

Each entry in a ptable has a positive integer that represents its rarity. The lower the number, the more common the item, with 1 being maximally common.

The probability of an item being selected from its ptable is equal to its frequency (1/rarity) divided by the sum of the frequencies of all the table's entries.

For example, here is a table containing 5 monsters. Rats are very common (rarity 1). Dragons are very rare (rarity 25). The other monsters have rarities between these extremes.

| Monster | Rarity | Frequency | Probability | | | | 1/rarity | freq/1.89*100 |


| rat | 1 | 1.00 | 53% | | goblin | 2 | 0.50 | 27% | | orc | 4 | 0.25 | 13% | | spectre | 10 | 0.10 | 5% | | dragon | 25 | 0.04 | 2% |


| | | 1.89 | 100% |

When randomly choosing a monster from the table, a rat will be chosen 53% of the time, a goblin 27% of the time, and so on.

The levelled lists used in the Elder Scrolls games do not specify probabilities or rarities for their contents. The only fact that is specified for each item in a levelled list is the minimum player level at which it may appear. Groups of items can be made relatively rare by putting them into a levelled list, then using that list as an item in another list.

Levels and ptables

Each entry can also contain information about how its rarity should vary as a "level" parameter increases. This information includes: a minimum level below which the value will never appear a series of 1 or more peak levels. When these are present, the entry's rarity will be scaled so that it is equal to the given rarity when the level is equal to one of the peaks, and becomes more rare as the level moves away from a peak.

The mixin class ptabled-class

If classes are defined whose metaclass is ptabled-class, those classes may specify two class options:

  • (:ptable-entries ( ENTRY* )) -- where each ENTRY follows the format of a ptable-entry (see above), except that instead of VALUE, a PTABLE-SYMBOL is substituted.
  • (:ptable-entries-inherited? BOOLEAN) -- where BOOLEAN is t (default) or nil.

Each such class will, upon class definition or redefinition, have its name included as a value in each of the ptables listed in :ptable-entries.

The class will also inherit the :ptable-entries of its ancestor classes, except where its own entries contradict earlier entries. To override this behaviour and force the class to only be included in those ptables that are explicitly listed in its own :ptable-entries slot, set :ptable-entries-inherited? to NIL.