Skip to content

Dialects

tilttek/dice

A virtual tabletop reads some constructs differently from the library's notation. A Dialect writes a pool its way, and reports what it cannot write rather than writing it wrong.

$translation = $dice->translate($dice->parse('4d6ro1, 6d10>=7'), 'roll20');
$translation->isComplete();   // true
$translation->text();         // "/r 4d6ro1\n/r 6d10>7"

A Translation holds one expression per expression of the pool, since a table rolls each with its own total — but for DiceParser, which rolls them in one command; commands() puts the table's roll command before each, text() joins them by lines. When a construct cannot be written, unsupported() lists it — in the library's notation, with the reason — and commands() and text() raise UntranslatableExpression.

ConstructRoll20Foundry VTTAvrae
Command/r /r !r
Reroll once, ro1, ro<3ro1, ro<2r=1, r<3ro1, ro<3
Repeated reroll, r1, r<3r1, r<2rr=1, rr<3rr1, rr<3
Explosion, !, !>=5!, !>5x, x>=5e6, e>4
Compound, penetrating, !!, !p!!, !preportedreported
Explosion once, !(1)reportedxoreported
Floor, ceiling, min3, max5reportedmin3, max5mi3, ma5
Keep, dropkh kl dh dlkh kl dh dlkh kl ph pl
Successes, >=7>7cs>=7reported
Failures, f1f1df=1reported
Critical marks, cs>19, cfcs>20, cf1reportedreported
Sort, sa, sdsa, sdreportedreported
A list of values, [10,25]kh1{10,25}kh1{10,25}kh1(10, 25)kh1
Fudge dicedFdFreported
Divisionfloor(a/b)floor(a/b)a//b
ceil(), round()ceil(a/b), round(a/b)ceil(a/b), round(a/b)the argument alone, its divisions reported
floor()floor(x)floor(x)the argument alone
abs()abs(x)abs(x)reported
min(), max()reportedmin(a, b), max(a, b)reported
Remainder, a%ba%ba%ba%b
Power, a^b(a**b)(a**b)reported

Roll20's comparison points include their threshold (>5 is 5 or more), so a strict comparison is written one step further; Avrae's selectors exclude theirs. A critical mark without a face is written on the highest or the lowest face.

The arithmetic of a table is not always the library's, and a dialect writes only what gives the same result:

  • A division is written rounded down, as a roll does by default. translate($pool, $rounding) takes the rounding of the roll: when it is another one, the division is still written rounded down, and Translation::$notices says so — 1d6/2 is written rounded down; the roll rounds it PositiveInfinity. —, the translation staying complete. Inside ceil() and round(), a division is written rounded their way.
  • Roll20 and Foundry round halves up, the library away from zero: a division inside round() is written only when neither operand can be negative, as far as the expression shows.
  • Roll20 and Foundry's % keeps the sign of the dividend, the library's the sign of the divisor: a remainder is written plainly when neither operand can be negative, through a second remainder — (a%b+b)%b — when the divisor is a constant, and reported otherwise. Avrae's % has the sign of the divisor already.
  • A power is written when its exponent cannot be negative, since the table does not round 2**-1.

Every other construct of the library — repeated unique dice, dice all the same, transformations, filters, validators other than a plain comparison, scopes, occurrences, groups, look tokens, dice on a range, weighted Fudge dice, a count or sides rolled first, the dice before, limits of explosions beyond what a table writes — is reported by Roll20, Foundry VTT and Avrae, and so are the constructs of instructions: references, the dice of instructions, groups, text lists, sentences, branches and repeat(). The dialects follow each table's public dice notation; a table that changes its notation changes its dialect.

Rolisteam's DiceParser

The dialect diceparser writes the notation read by readRolisteam(). DiceParser rolls every instruction of a command together, so a translation holds one expression, the instructions separated by ;, after the command !; references, the dice of one instruction, groups, text lists, sentences, branches and repeat() are written.

$translation = $dice->translate($dice->parse('4d6dl1, $1+2, 3d10!!10(1)'), 'diceparser');
$translation->text();   // "!4d6k3;$1+2;3d10a[10]"
ConstructDiceParser
Reroll once, repeatedr[1], R[<3]
Explosion, limited, on the highest facee[>8], e(3)[10], e[10]
Compound explosion oncea[10]
Unique dice, dice all the sameu, t
TransformationsT[>5]{15}[<=2]{-1}
Filter, successesf[*>1], c[>=7]
Keepk3, kl1
Drop, for a known number of dice4d6dl1 is 4d6k3
Occurrences from a heighto3,7
Groups, look tokens, sortsg10, p[2:accent], s, sl
Fudge dice, dice on a ranged[-1..1], d[0..9]
Scope of a condition* all, . any, : the total
Divisionfloor(a/b), ceil(a/b), round(a/b)
Power(a^b)

It reports a unique reroll once, a compound explosion more than once, a penetrating explosion, an explosion of sides rolled first without its face, floors and ceilings, failures, critical marks, a condition on each die as it fell, occurrences judged otherwise than from a height, dropped dice whose number the expression does not show, the dice of several instructions, weighted Fudge dice, custom dice, a remainder that may be negative, a power whose exponent may be negative, and round() of a division that may be negative. What it writes reads back to the same text.

Adding a dialect

Extend Dialect: give it a name() and its command(), write the modifiers() of a set of dice and the division() rounded as a function says; override fudge(), rangeDie(), valueList(), call(), modulo(), power(), group(), reference(), instructionDice(), textList(), sentence(), conditional() or choice() when the table writes them, and call unsupported() — or unwritten() for several modifiers at once — for what it cannot write. The helpers plain(), plainSuccess(), bound(), nonNegative(), function() and rounded() cover what the built-in dialects share. A table that rolls every instruction of a command together returns its separator from separator(); the translation then holds one expression. Add it to Dialects, where a dialect of the same name replaces the built-in one.