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 aVertexinstance 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 aVertexinstancegetVertex(vertexID)returns theVertexinstance 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 orVertexinstances, optional third argument is anEdgeinstance to use (one being created if none is supplied)removeEdge(vertex0, vertex1)removes the given edge. Arguments are either vertex IDs orVertexinstancesgetEdge(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 orFSMStateinstance.
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, returningtrueon success andnilotherwise.
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 = 1000the 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 ornilon 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:
RulebookMatchAllisnilby default and becomestruewhen ALL of its rules aretrueRulebookMatchAnyisnilby default and becomestruewhen ANY of its rules aretrueRulebookMatchNoneistrueby default and becomesnilwhen 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 returningaVectorcontaining all the permutations of the elements in the given collection.
NOTE the largestCollectionfor which the TADS 3 VM can generate all the permutations is8. Called on largerCollectionsthe 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 elementsiandj, returningtrueon success,nilotherwise.
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)
Evaluatestrueifvalueis an integer betweenminandmax(inclusive)nilToInt(value, default)
Evaluates todefaultifvalueisnil,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)
Setsobjto bevalue, but only ifobjis 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.