Packages

Simple algebraic dice notation parser for Gleam

Current section

Files

Jump to
diced src diced.gleam
Raw

src/diced.gleam

import gleam/int
import gleam/list
import gleam/result
import splitter
pub type Modifier {
KeepHighest(Int)
KeepLowest(Int)
DropHighest(Int)
DropLowest(Int)
Reroll(Int)
RecursiveReroll(Int)
Explode(Int)
Minimum(Int)
Maximum(Int)
}
const modifier_strings = [
// KeepHighest
"kh",
// KeepLowest
"kl",
// DropHighest
"dh",
// DropLowest
"dl",
// Reroll
"r",
// RecursiveReroll
"R",
// Explode
"!",
// Minimum
"m",
// Maximum
"M",
]
pub type Dice {
/// A "normal" dice roll with an amount, value and a list of modifiers to apply
Basic(amount: Int, value: Int, modifiers: List(Modifier))
/// A fate dice roll with an amount, and a list of modifiers to apply. Fate dice are dice with 3 results, all with an
/// equal number of faces. [Learn more](https://en.wikipedia.org/wiki/Fudge_%28role-playing_game_system%29)
Fate(amount: Int, modifiers: List(Modifier))
/// A basic number and some modifiers that could be applied.
Number(value: Int, modifiers: List(Modifier))
}
pub type DiceError {
InvalidAmount
InvalidString
InvalidValue
InvalidModifier
}
/// Convert a string containing algebraic dice notation into a type describing how the dice should behave when rolled.
///
/// ```gleam
/// diced.parse("2d20kh")
/// // Ok(diced.Basic(2, 20, [KeepHighest(1)]))
/// ```
pub fn parse(string string: String) -> Result(Dice, DiceError) {
let dice_splitter = splitter.new(["d", "dF"])
case splitter.split(dice_splitter, string) {
#(value, "", mods) -> {
use value <- result.try(
int.parse(value) |> result.replace_error(InvalidValue),
)
use modifiers <- result.try(parse_modifiers(mods))
Ok(Number(value:, modifiers:))
}
#(amount, "d", "") -> {
use amount <- result.try(parse_amount(amount))
Ok(Basic(amount:, value: 6, modifiers: []))
}
#("", "d", mods) -> {
use #(value, rest) <- result.try(parse_value(mods))
use modifiers <- result.try(parse_modifiers(rest))
Ok(Basic(1, value:, modifiers:))
}
#(amount, "d", mods) -> {
use amount <- result.try(parse_amount(amount))
use #(value, rest) <- result.try(parse_value(mods))
use modifiers <- result.try(parse_modifiers(rest))
Ok(Basic(amount:, value:, modifiers:))
}
#(amount, "dF", mods) -> {
use amount <- result.try(parse_amount(amount))
use modifiers <- result.try(parse_modifiers(mods))
Ok(Fate(amount:, modifiers:))
}
#(_, _, _) -> Error(InvalidString)
}
}
fn parse_amount(amount amount: String) -> Result(Int, DiceError) {
int.parse(amount)
|> result.replace_error(InvalidAmount)
}
fn parse_value(value value: String) -> Result(#(Int, String), DiceError) {
let modifier_splitter = splitter.new(modifier_strings)
let #(value, a, b) = splitter.split(modifier_splitter, value)
use value <- result.try(result.replace_error(int.parse(value), InvalidValue))
Ok(#(value, a <> b))
}
fn parse_modifiers(modifiers: String) -> Result(List(Modifier), DiceError) {
do_parse_modifiers(modifiers, Ok([]))
}
fn do_parse_modifiers(
modifiers: String,
found: Result(List(Modifier), DiceError),
) -> Result(List(Modifier), DiceError) {
use found <- result.try(found)
let splitter = splitter.new(modifier_strings)
case splitter.split(splitter, modifiers) {
#("", "", "") -> Ok(list.reverse(found))
#("", mod, rest) -> {
let #(rest, value) = with_modifier_value(rest)
let modifier = case mod {
"kh" -> KeepHighest(value)
"kl" -> KeepLowest(value)
"dh" -> DropHighest(value)
"dl" -> DropLowest(value)
"r" -> Reroll(value)
"R" -> RecursiveReroll(value)
"!" -> Explode(value)
"m" -> Minimum(value)
"M" -> Maximum(value)
_ -> panic as "unreachable"
}
do_parse_modifiers(rest, Ok([modifier, ..found]))
}
_ -> Error(InvalidModifier)
}
}
fn with_modifier_value(string: String) -> #(String, Int) {
case do_with_modifier_value(string, 0) {
#(a, v) if v == 0 -> #(a, 1)
#(a, v) -> #(a, v)
}
}
fn do_with_modifier_value(string: String, value: Int) -> #(String, Int) {
case string {
"0" <> rest -> do_with_modifier_value(rest, value * 10)
"1" <> rest -> do_with_modifier_value(rest, value * 10 + 1)
"2" <> rest -> do_with_modifier_value(rest, value * 10 + 2)
"3" <> rest -> do_with_modifier_value(rest, value * 10 + 3)
"4" <> rest -> do_with_modifier_value(rest, value * 10 + 4)
"5" <> rest -> do_with_modifier_value(rest, value * 10 + 5)
"6" <> rest -> do_with_modifier_value(rest, value * 10 + 6)
"7" <> rest -> do_with_modifier_value(rest, value * 10 + 7)
"8" <> rest -> do_with_modifier_value(rest, value * 10 + 8)
"9" <> rest -> do_with_modifier_value(rest, value * 10 + 9)
_ -> #(string, value)
}
}