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) } }