The module: dataTypes github repo.
This is the result of combining and refactoring a bunch of previous modules. The overall goal was to improve/regularize things like class names and method usages, removing code duplicated across multiple modules, and to as much as possible separate out stuff that’s just related to the underlying data structures and the stuff that was in there because of what I happened to want to do with it.
The module provides a number of basic classes. I won’t try to cover everything here but can field any questions on the off chance there’s anyone out there other than me planning on using this stuff.
Graphs
This is “graph” in the graph theory sense of the word. There’s the base Graph
class, as well as Vertex
and Edge
classes. The base Graph
class is undirected (so Graph.addEdge('foo', 'bar')
is equivalent to Graph.addEdge('bar', 'foo')
), and there’s a DirectedGraph
subclass for directed graphs.
Examples
Here’s a simple three-vertex directed graph in which all three vertices are connected to each other declared in several different ways. Note: it would be easier to declare this as an undirected graph, but I’m using a directed graph because I think the syntax is slightly clearer because there are no “implicit” edges.
First, the “long form” declaration, using normal TADS3 lexical inheritence rules. In this example the Edge
declarations refer to vertices by object reference:
// "Long form" graph declaration.
graph: DirectedGraph;
+foo: Vertex 'foo';
++Edge ->bar;
++Edge ->baz;
+bar: Vertex 'bar';
++Edge ->foo;
++Edge ->baz;
+baz: Vertex 'baz';
++Edge ->foo;
++Edge ->bar;
This declares an identical graph using vertex IDs instead of object references:
// Same as above, using IDs instead of obj references in edge declarations
graph: DirectedGraph;
+Vertex 'foo';
++Edge 'bar';
++Edge 'baz';
+Vertex 'bar';
++Edge 'foo';
++Edge 'baz';
+Vertex 'baz';
++Edge 'foo';
++Edge 'bar';
You can also declare graphs using arrays (this syntax is nearly the same as the one from the old markovChain
module):
// "Short form" graph declaration
graph: DirectedGraph
[ 'foo', 'bar', 'baz' ]
[
0, 1, 1, // edges from "foo"...
1, 0, 1, // edges from "bar"...
1, 1, 0 // edges from "baz"...
]
;
In the above example the integer values are the edge lengths, with zero indicating no edge.
Finally, here’s the same graph generated at runtime:
// Create the graph
g = new DirectedGraph();
// Add vertices.
g.addVertex('foo');
g.addVertex('bar');
g.addVertex('baz');
// Add the edges.
g.addEdge('foo', 'bar');
g.addEdge('foo', 'baz');
g.addEdge('bar', 'foo');
g.addEdge('bar', 'baz');
g.addEdge('baz', 'foo');
g.addEdge('baz', 'bar');
And for completeness, since our example graph is complete (every vertex is connected to every other vertex) we could probably just use an undirected graph (unless we were expecting to twiddle the graph later and wanted directional edges then):
// Declare a simple three vertex undirected complete graph.
g = new Graph();
g.addVertex('foo');
g.addVertex('bar');
g.addVertex('baz');
g.addEdge('foo', 'bar');
g.addEdge('foo', 'baz');
g.addEdge('bar', 'baz');
The examples above illustrate the basic methods for interacting with graphs:
addVertex(vertexID, vertexInstance?)
to add a vertex to the graph. If aVertex
instance is passed as the second arg it will be used, otherwise a new instance will be created with the given ID.removeVertex(vertex)
to remove a vertex from the graph. The argument can either be a vertex ID or aVertex
instancegetVertex(vertexID)
returns theVertex
instance with the given ID, assuming it exists
There are equivalent methods for interacting with edges with similar usage:
addEdge(vertex0, vertex1, edgeInstance?)
adds an edge to the graph. First two args are either vertex IDs orVertex
instances, optional third argument is anEdge
instance to use (one being created if none is supplied)removeEdge(vertex0, vertex1)
removes the given edge. Arguments are either vertex IDs orVertex
instancesgetEdge(vertex0, vertex1)
returns the given edge, where the args are either vertex IDs or instances.
Finite State Machines
A finite state machine in this context is a set of states in which exactly one is the current state, and a set of allowed transitions between states.
In the module a FiniteStateMachine
(or just FSM
or StateMachine
) is a subclass of DirectedGraph
in which the states are the vertices and allowed transitions are the edges.
In addition to working like a basic DirectedGraph
, FiniteStateMachine
also has:
setFSMState(state)
sets the current state. Arg is either a state ID orFSMState
instance.
This method does no validation, or bookkeeping, it just sets the current state bypassing the state transition logic.toFSMState(state)
is similar to the above but verifies that the requested state transition is valid, returningtrue
on success andnil
otherwise.
Markov Chains
A Markov chain is a state machine with probabilistic state transitions.
The module treats edge lengths as transition probabilities. Edge lengths can be declared either as integer weights or floating point probabilities.
If floating point numbers are used they will be interpreted as decimal percentages: 0.25 is a 25% chance of picking that transition, 0.33 is a 33% chance, and so on. In this case all edges leading away from a state should sum to 1.0.
If integers are used they’re interpreted as weights, with the transition probability being the ratio of the individual edge weight to the sum of the weights of all edges leading away from the state. For example if there are two edges on a vertex with weights 250 and 750 then the first will be selected 250 / (250 + 750) or 25% of the time, and the second will be selected 750 / (250 + 750) or 75% of the time. The same probabilities could be given with weights of 1 and 3 respectively.
Floating point probabilities are converted to integer weights during preinit (to avoid the overhead of floating point math at runtime).
Main additions to MarkovChain
over the base StateMachine
are:
markovBaseWeight = 1000
the base weight used for converting decimal probabilities into integer weightspickTransition(fromState?, prng?)
picks a random state transition. With no args the current state is used, with a state ID or instance as the first arg a transition from the given state will be used instead. Optional second arg is a PRNG instance to use for picking the state—this is for integrating the instancable PRNG module but the module doesn’t provide any PRNGs itself.
This method will attempt to change the current state, returning the ID of the new state ornil
on error.
Example
The format is mostly the same as for Graph
. The addition is an optional intitialization vector which defines the probabilities used for assigning the initial state. Without an IV a random unweighted state will be picked (that is, in an n
state chain each state will have a 1 in n
chance of being the initial state):
chain: MarkovChain
[ 'foo', 'bar', 'baz' ]
[
0, 0.75, 0.25,
0.67, 0, 0.33,
0.5, 0.5, 0
]
[ 0.34, 0.33, 0.33 ]
;
That defines a Markov chain with:
- Three states: “foo”, “bar”, and “baz” (defined in the first array)
- The state “foo” becomes “bar” 75% of the time and “baz” 25% of the time (first row of the second array)
- The state “bar” becomes “foo” 67% of the time and “baz” 33% of the time (second row of the second array)
- The state “baz” becomes “foo” or “bar” with equal probability (third row of the second array)
- The initial state is “foo” 34% of the time, “bar” 33% of the time, and “baz” 33% of the time.
Rules and Rulebooks
In this module a rulebook is a container for one or more rules and a rule is a wrapper around a method that returns true
or nil
based on some arbitrary check implemented by the Rule
instance. The rule has a boolean value based on the return value of the check method, and a rulebook has a boolean value based on the value of its rules.
The Rulebook
class has subclasses for different basic rulebook behaviors:
RulebookMatchAll
isnil
by default and becomestrue
when ALL of its rules aretrue
RulebookMatchAny
isnil
by default and becomestrue
when ANY of its rules aretrue
RulebookMatchNone
istrue
by default and becomesnil
when ANY of its rules aretrue
Note: Unlike the old RuleEngine
module, this module provides only the datatype(s). There is no mechanism by which rules and rulebooks are automatically evaluated—that happens in the companion asyncEvent
module that’ll come next (as soon as I get around to typing up a post like this for it).
Tuple
A Tuple
is a data structure for holding information about a turn and it’s action.
Full documentation is in the module, but this is basically a container for holding a “source” object and actor, a “destination” object and actor, an action, and a location. All of which are optional.
The class also provides a number of “match” methods (e.g. matchAction(obj)
and matchSrcActor(obj)
) which will return true
either if the supplied object matches the corresponding value on the tuple or if the corresponding value isn’t set on the tuple.
That is, a Tuple
declared with only an action, for example TakeAction
will match any take action (regardless of the actor doing the action or the object being taken), but if you add Pebble
as the “destination” object (that is, the object being targetted by the action) then that’ll only match pebbles being taken (by any actor) but not rocks. And so on.
The intent of the class is to make it easier to write rule-based action matching.
Trigger
A Trigger
is a Rule
that is also a Tuple
.
Tweaks to Stock Classes
Collection
permutation()
a method returningaVector
containing all the permutations of the elements in the given collection.
NOTE the largestCollection
for which the TADS 3 VM can generate all the permutations is8
. Called on largerCollections
the method will returnnil
(instead of throwing an exception)
TadsObject
copyProps(obj)
copies all the properties directly defined on the passed object (which correspond to properties defined on the calling object).
That is, given:
Thenobj0: object { foo = 1 bar = 2 } obj1: object { foo = nil }
obj1.copyProps(obj0)
will produce:
Or alternately if (instead) you didobj1: object { foo = 1 }
obj0.copyProps(obj1)
:obj0: object { foo = nil bar = 2 }
Vector
swap(i, j)
swaps elementsi
andj
, returningtrue
on success,nil
otherwise.
Macros
The module provides a base isType(obj, cls)
macro that evaluates true
if the given obj
is non-nil
and is an instance of the given class cls
.
This is used to implement a bunch of tests for both base TADS3 types as well as classes provided by the module, for example:
isAction()
isActor()
isCollection()
isInteger()
isList()
isLocation()
isObject()
isRoom()
isString()
isThing()
isVector()
And module-specific classes:
isEdge()
isFSM()
isGraph()
isMarkovChain()
isMarkovTransition()
isMarkovState()
isRule()
isRulebook()
isTuple()
isTrigger()
isVertex()
General Utility Macros
inRange(value, min, max)
Evaluatestrue
ifvalue
is an integer betweenmin
andmax
(inclusive)nilToInt(value, default)
Evaluates todefault
ifvalue
isnil
,value
(cast as an integer) otherwise.
Example:n = nil; m = nilToInt(n, 0)
will result inm = 0
;n = '5'; m = nilToInt(n, 0)
will yieldm = 5
.noClobber(obj, value)
Setsobj
to bevalue
, but only ifobj
is currentlynil
.
Example:n = nil; noClobber(n, 2)
results inn = 2
;n = 5; noClobber(n, 2)
resutls inn = 5
.
As alluded to above I’ve also got an updated module for all of the “automatic” logic associated with rules, rulebooks, and state machines. There’s also an updated pathfinding module. Rolling this one out first because if I don’t start somewhere I’ll never get around it it.
Everything covered above is covered in documentation in the module; there’s a README.md
that covers the above plus a more complete list of all the properties and methods on the various classes.