Re: Factor

Factor: the language, the theory, and the practice.

Rolling Dice

Thursday, July 1, 2010

#ebnf #math #syntax

Update: I noticed that random is [0,b) (from 0 to n-1). The code has been fixed to make it [1..b] (e.g., return a number from 1 to n).

I was curious about the ability to define new syntax for Factor, and to learn a little about how the lexer works. Inspired by a recent project that I’ve been working on, I thought it would be interesting to define a simple DSL for calculating dice rolls.

We need a way to describe a dice roll. There are various methods used in Python or Ruby. A common example of a common short-hand description is 4d8 – specifying how many times (4) to roll a dice with a number (8) of sides.

Let’s setup a vocabulary and a list of dependencies that will be used to implement this functionality:

USING: fry kernel lexer math math.parser peg.ebnf random
sequences strings ;

IN: dice

We will be using EBNF parsers similar to what was used in building a calculator. First we will implement support for basic (e.g., 4d8) rolls.

EBNF: parse-roll

number = ([0-9])+    => [[ >string string>number ]]
dice   = "d" number  => [[ second '[ _ random ] ]]
roll   = number dice => [[ first2 '[ 0 _ [ @ + 1 + ] times ] ]]
error  = .*          => [[ "unknown dice" throw ]]
total  = roll | error


We can see how this works by trying it out:

IN: scratchpad "4d8" parse-roll .
[ 0 4 [ 8 random + 1 + ] times ]

IN: scratchpad "4d8" parse-roll call .

IN: scratchpad "foo" parse-roll
unknown dice

We can now define a piece of SYNTAX: for dice rolls:

SYNTAX: ROLL: scan parse-roll append ;

And using it:

IN: scratchpad ROLL: 4d8 .

This could be extended to support other things such as “base” numbers (e.g., 4d8+10), negative rolls, ranges, and better handling of parse errors. The code for this is available on my GitHub.