-
succinctly express simple, discrete probability events
-
build complex events from simple events
-
calculate distributions of outcomes for complex events
-
abstract away the math (math is the compiler's problem)
All happenings in the world of lod are mediated by rolls of dice. The world is comprised of actions and actors. An actor is an assemblage of states. An action is a roll of dice that may alter actor states.
lod is a grammar for describing random events. A lod script describes a single event that yields a single outcome. Every time the executable is called, a random value is emitted.
The simplest use of the lod language is to simulate outcomes for immediate consumption (if you like to eat outcomes). Of course, if you are interested in the statistical distribution of outcomes, you can simulate an enormous number of outcomes and then analyze them with the tools of your choice. You might want to tweak the compiler to simulate many outcomes every time the executable is called, this will be faster than simply calling the executable many times.
I will not attempt to implement a good compiler. Rather I am interested in a merely functional one. My main interest is developing the syntax and semantics of the language itself.
There are several fancy optimizations that can be added to the compiler. Mainly, the distribution of many events can be solved exactly. Since lod deals exclusively with discrete outcomes, an event can be expressed exactly as a probability vector, which will require infinite memory in certain cases. This compiler optimization step will require a decent mathematician.
When exact solutions are not tractable, certain events can be replaced by their simulated probability mass functions. This will be expecially simple for events that take no arguments.
A lod script yields as its outcome the value of the final expression:
'Hello World'
lod makes no distinction between single and double quotes.
Comments in lod will be the same as in C
// This is a comment
/*
This is a
multi-line comment
*/
All of this is pretty standard except that all numbers are integers in lod.
x = 5 + 2
y = x + 5
All division is integer division
5 / 2 // yields 2
All the arithmetic operators can be used with '='
x = 7
x /= 2 // x becomes 3
As the name 'Language of Dice' implies, dice are important in lod and have their own builtin syntax, which follows DnD conventions, e.g.
4d6 // yields the 4 outcomes of 4 rolled 6-sided dice
The rolls are internally a vector. So you can say
sum(4d6) // yields the sum of the 4 dice
min(4d6) // yield the lowest of 4, 6-sided dice
There are two types of variables in lod, random and non-random. A non-random variable is exactly like variables in any other programming language. A random variable, however, yields random results every time it is referenced. A non-random variable may hold the number resulting from rolling a die. A random variable represents the die itself.
A random variable is similar to a generator in python. It may be better to think of it as a function. Indeed, it can take arguments.
x ~ 2d6 // create the random variable 'x' which represents rolling 2d6
a = x // assign 'a' to an outcome 'x'
b = x // assign 'b' to a different outcome
y ~ x + 4 // create the random variable 'y' which calls 'x' and adds 4 to the outome
The '*' operator draws mutiple instances from the generator. So 2 * 1d6 is
equivalent to 2d6. Rather than multiplying the outcome of the event, you run
the event multiple times.
Similarly adding dice simply concatenates the events.
x = [1d6, 1d6, 1d6, 1d6]
This is identical to all of the following
x = 4d6
x = 4 * 1d6
x = 2 * 2d6
x = 1d6 + 1d6 + 2d6
Arrays are indexed from 1
x = [1,2]
x[1] // yield first value
The functions min, max, and sum work on arrays.
min and max both take an optional integer parameter specifying how many
elements to take. For example, max(4d6, 2) returns an array holding the two
highest rolls.
Sets contain mutually exclusive events.
y ~ {'a', 'b'} // y is assigned to a random variable that emits 'a', 'b'
A = y // 'A' is randomly assigned to one of y's values
In the example above, 'a' and 'b' are emitted with equal probability. If you wish one to be more likely than the other, the set can be weighted. E.g.
w ~ {1:'a', 3:'b'} // now 'b' will be emitted 3 or 4 times and 'a' 1 of 4
// Indexing for sets is based on the sum of the weights
w[1] // 'a'
w[2] // 'b'
w[3] // 'b'
w[4] // 'b'
w[5] // ERROR, variable 'w' has onlw 4 states
// multiplying a set results in multiple draws with replacement
B = sum(4 * {1:1d6, 3:1d4})
Here the numbers in brackets are NOT indices, but rather numbers. It is as if a 1d4 is rolled and the numbers 2 and higher yield 'b'. The number in brackets corresponds to the 1d4 roll outcome.
Note, the weighting should not increase memory requirement. Writing y ~ [100:'a', 300:'b'] allocates space equivalent to y ~ [1:'a', 3:'b'].
x = [1,2,3] // x is assigned to tuple of 3 values
x = {1,2,3} // x is randomly assigned to one of 3 values
Sometimes the output of a lod script will not be a single number, but rather a complex state involving many variables. These composite entities can be expressed as below
y ~ {
hp = 5d6
name = 'unset'
atk ~ {19:1d6, 1:2d6}
}
bob = y
bob.name = 'bob'
alice = t
alice.name = 'alice'
The syntactic difference between a set and a structure is that every element of a set must yield an outcome and a structure must not yield an outcome. For example:
s ~ {1, 1d4} // valid set
s ~ {x=1, 1d4} // ERROR - first element returns nothing
s ~ {x=1, y=1d4} // ERROR - if it is meant to be a set, nothing is returned;
// if it is meant to be a structure, it has too many elements
s ~ {x=1 y=1d4} // valid structure
The structure can be thought of as a code block that returns access to the variables within its scope.
// set with parameters
s ~ par(a,b){1d6 + a, 1d8 + b}
// structure with parameters
s ~ par(a,b){
atk = 1d4 + a
con = b * (1d10 / 2) + 10
}
a = s(1,2)
// event with parameters
s ~ par(a,b){a * 1d10 + b}
The general syntax is def ( PAR ) { BLOCK THAT USES PARAMETERS }
Whenever a parameterized generator is called, arguments must be given in parentheses.
fairly standard syntax
i = 0
while
i > 1 // some expression that evaluates to a boolean
i++ // arbitrary block of code, may contain 'break' and 'continue'
done
pretty self-explanatory
if 1d20 < 5
1
elif . < 10
2
else
3
done
Often if-elif-else syntax can be replaced with smart use of weighted sets. For example:
{4:1, 5:2, 11:3}
- Advantage and disadvantage on a 1d20 roll
radv ~ max(2d20)
rdis ~ min(2d20)
Dice rolls are stored as an integer array. When their values are used in a context expecting an integer, their sum is used.
- Simulate sum of 4 d6's after dropping the lowest
x ~ { sum(4d6) - min(.) }
x ~ max(4d6, 3)
'.' stores the result of the last calculated value within the current block and is returned at the end of an event (unless explicitely stated otherwise via the yield keyword). The above code could be rewritten as
x ~ {
a = 4d6
b = sum(a) - min(a)
b
}
- If 1d20 is less than 10, set to 10
x ~ if (1d20 < 10) 10 else .
- a single attack
// executing this will return a single result
dc = 15 // assign a constant value
dmg ~ 1d6 + 3 // assign to a random variable (each usage rolls a die)
crit ~ dmg + dmg // assign to an event
{dc:0, (20-dc-1):dmg, 1:crit}
// every event yields 0 by default
dmg ~ 1d6 + 3
crit ~ dmg + dmg
hp = 100
turns = 0
while hp > 0
turns += 1
hp -= {15:0, 4:dmg, 1:crit}
done
// the last lone expression in a block is yielded
turns
// using vector forms of rolls
top3 ~ max(4d6, 3) // take the three highest
dis ~ min(2d20)
| Level | operators |
|---|---|
| 1 | ( ) { } |
| 2 | + - |
| 3 | / * % |
| 4 | < <= > >= |
| 5 | == != |
| 6 | and or |
| 7 | = := *= += -= /= %= |
- datatypes
digit -> 0 | ... | 9
int -> digit digit | digit
roll -> intdint
string -> '[^']*' | "[^"]*"
array -> [ expr, expr ] | [ expr, expr, expr ] | ...
array -> [ int : expr, int : expr ] | [ int : expr, int : expr , int : expr ] | ...
array -> par(var) array | par(var, var) array
set -> { expr, expr } | { expr, expr, expr } | ...
set -> { int : expr, int : expr } | { int : expr, int : expr, int : expr } | ...
set -> par(var) set | par(var, var) set
struct -> { stmt } | { stmt stmt } | ...
struct -> par(var) struct | par(var, var) struct
struct-access -> struct**.id**
- builtin functions
func -> max ( array [, int] )
func -> min ( array [, int] )
func -> sum ( array )
- expressions
factor -> int | roll | array | set | struct | ( expr ) | ( cond )
term -> term / factor | term * factor | term % factor | factor
expr -> term + factor | term - factor | term
COP -> < | <= | > | >= | == | !=
cond -> cond COP factor
cond -> cond and cond | cond or cond | not cond
cond -> factor
- statements
decl-stmt -> id = expr
gen-stmt -> id ~ expr
if-stmt -> if cond block done
if-stmt -> if cond block else block done
if-stmt -> if cond block [elif cond block]+ done
if-stmt -> if cond block [elif cond block]+ else cond block done
while-stmt -> while cond block done
- non-generator statements
stmt -> decl-stmt | gen-stmt | if-stmt | while-stmt
block -> expr | stmt | block expr | block stmt
non-gen-stmt-if-stmt -> if cond non-gen-block done
non-gen-stmt-if-stmt -> if cond non-gen-block else non-gen-block done
non-gen-stmt-if-stmt -> if cond non-gen-block [elif cond non-gen-block]+ done
non-gen-while-stmt -> while cond non-gen-block done
non-gen-stmt -> decl-stmt | gen-stmt | non-gen-if-stmt | non-gen-while-stmt
non-gen-block -> non-gen-stmt | non-gen_stmt + non-gen-stmt
LAST_RESULT := '.'
L_RPAR := '('
R_RPAR := ')'
L_CPAR := '{'
R_CPAR := '}'
QUOTE := '\''
INT := [1-9][0-9]*
DICE := [1-9][0-9]*d[1-9][0-9]*
BOOL := true | false
INCR := '++'
DECR := '--'
ADD := '+'
SUB := '-'
DIV := '/'
MUL := '*'
MOD := '%'
VAR := [A-Za-z_][A-Za-z0-9_]*
DEC_CON := '='
DEC_RAN := ':='
ASSIGN_ADD := '+='
ASSIGN_SUB := '-='
ASSIGN_DIV := '/='
ASSIGN_MUL := '*='
ASSIGN_MOD := '%='
GT := '>'
LT := '<'
EQ := '=='
NE := '!='
GE := '>='
LE := '<='
WHILE := while
DONE := done
BREAK := break
CONTINUE := continue
AND := and
NOT := not
OR := or
YIELD := yield
MAX := max(*array* [, N])
MIN := min(*array* [, N])
SUM := sum(*array*)