How Dice Expressions work in Sophie’s Dice

Dice expressions in Sophie’s Dice are the most complex part of the app, to the point that I wasn’t sure I could implement even the basics in any form – and once I did, things only got more complicated as new features were added. It wasn’t my intention, but it has essentially become a kind of programming language interpreter.

If you don’t know about the expression system at all, you can check the documentation page here!

The systems underneath it have changed a couple of times since I first added them, but the core approach has remained pretty much the same and that’s what I’ll go over now!

Step 1 – Parsing

The dice expression system is fed a string of text to evaluate, it goes through it mostly one character at a time trying to pick out components of an expression. Components are things like dice pools (d8, 3d6, 2d20L, etc), operators (+, -, *, etc), number values (7, 69, 666, etc), strings (“Win”, “Escape!”, “Hello World”, etc), and ‘terms’ (which are a catch-all for a lot of different things like references to user values, variables like “diceCount”, functions like “check”, and even references to other results as part of the expression).

While parsing, most component types are easy to identify by the characters they use and what the parsing state is (eg are we looking at something between curly brackets) to the point that if a component can’t be simply identified – it is assumed to be a dice pool.

Notation for dice pools can get *wild* which is why these components have their own parsing that happens after the overall expression parsing his done.

It begins by looking for the pool’s count (how many dice are in the pool), and what type of die the pool consists of (eg are we rolling d6s? is it a named die from the user’s own dice bags?). From there, the pool’s roll conditions are parsed.

The roll conditions are… numerous, I’ve been adding them for a while now. They are parsed largely the same way however, from left to right, tracking what rules the roll condition has as they are found, and adding the roll condition to the dice pool’s list of roll conditions.

Once all of this is all done, the dice expression exists as a list of components: each component knows what it represents and what rules it must follow.

Step 2 – Beginning the roll

Once we know what we are rolling, we can begin. Dice pool components spawn 3D dice models which they can keep track of, and modify if necessitated by roll conditions (eg, if a roll condition says to make the dice green, they are spawned green. If a roll condition is capping values below 3, then lesser values will be replaced by a 3).

Once the dice are out and rolling, dice pool components will keep track of them and ensure they follow the rules of any roll conditios that have been applied. For example if there is an explosion condition, the dice pool component checks dice as they settle to see if they should explode and then spawns new dice if so.

Step 3 – Math expression

From this point on, the list of expression components can be used to create a math expression. This will look somewhat like the original dice expression, except things like user value names will be replaced by the actual user values, and dice pool components will be replaced by the result of that dice pool’s roll.

Every frame, the produced math expression is parsed to calculate a result. The maths expression parser is a fairly standard one that I found online, then expanded for my purposes to allow a greater variety of functions, variable/constant values, and also to fix bugs like not it supporting some unary operations.

Step 4 – Display expression

The final step is to display the results. Just as expression components can have math expression representations, they can also have display representations. These are usually just like the expression as it was originally entered by the user, but with specific colours, case, and spacing. Each component’s display is collected in sequence and then displayed, followed by the result calculated by parsing the math expression.

Things are of course a little more complicated than that (math expressions and dice pools can sometimes produce string results that are not numbers, exploded dice need to be tracked so that they can be removed if the expression is re-rolled, and other things need to be recorded to highlight tampering if it occurs, not to mention that some expressions have multiple results and sub-expressions), but the above process is the main framework of the system.

The truth is sometimes its complicatedness is overwhelming to me, and I wish I had never implemented it – just leaving it up to users to always roll their dice “manually” instead. That would certainly be a lot less work for me, but it’s one of those things where I remind myself that people appreciate having it, even though most won’t explore just how deep it goes. But to make it work at all it needs to have that depth, so every change is a big amount of work. For example, in the next update (coming soon!) values will be *much* more versatile, allowing more comparisons and ways of referring to numbers or strings – and though most people won’t even notice the change, it took me over two weeks of work.

~

Thanks for reading! I don’t know if this was interesting to anyone, but I know it has been a while since I’ve posted any ‘behind the scenes’ stuff that wasn’t limited to just the $5+ tier patrons and this is the first thing I’ve thought of. It’s frustrating to me because all patron support is an immense help to me no matter the pledge level, but also I often don’t have the energy to post here even when I am doing work like this. I just want you all to know I am incredibly grateful and that even when it doesn’t look like it here, your support is helping me do work! (FYI: You can link in to the discord if you want more frequent and less formal updates! It doesn’t take me as much energy to post there so I will often drop screenshots of whatever I’m doing and you can follow along.)

Thanks again, I hope you’re doing alright! Oh and if you have any Qs about the above or anything else, just ask <3

This content is available exclusively to members of this creator's Patreon at $1 or more.