Packages

A minimal, bulletproof dice rolling library for Gleam following the Unix philosophy

Current section

Files

Jump to
dice_trio src dice_trio.erl
Raw

src/dice_trio.erl

-module(dice_trio).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dice_trio.gleam").
-export([parse_count/1, parse_sides_and_modifier/1, parse/1, roll/2, detailed_roll/2]).
-export_type([basic_roll/0, detailed_roll/0, dice_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A minimal, bulletproof dice rolling library following the Unix philosophy.\n"
"\n"
" `dice_trio` does one thing exceptionally well: parse and roll standard dice expressions.\n"
" It provides a simple, reliable foundation for building game systems.\n"
"\n"
" ## Quick Start\n"
"\n"
" ```gleam\n"
" import dice_trio\n"
"\n"
" // Your RNG function (MUST return 1-to-max inclusive)\n"
" let rng = fn(max) { your_random_implementation(max) }\n"
"\n"
" // Roll some dice\n"
" dice_trio.roll(\"d6\", rng) // Ok(4)\n"
" dice_trio.roll(\"2d6+3\", rng) // Ok(11)\n"
" dice_trio.roll(\"d20-1\", rng) // Ok(18)\n"
" ```\n"
"\n"
" ## RNG Contract Requirements\n"
"\n"
" **CRITICAL**: Your RNG function MUST return values between 1 and max (inclusive).\n"
" The dice library does not validate RNG output for performance reasons.\n"
" Invalid RNG output will produce incorrect dice results.\n"
"\n"
" ## Supported Notation\n"
"\n"
" - `\"d6\"` - Single six-sided die\n"
" - `\"2d6\"` - Two six-sided dice\n"
" - `\"d6+2\"` - Six-sided die plus 2\n"
" - `\"d20-1\"` - Twenty-sided die minus 1\n"
" - `\"3d6+5\"` - Three dice with modifier\n"
"\n"
" ## Design Philosophy\n"
"\n"
" - **Unix Philosophy**: Do one thing exceptionally well\n"
" - **Maximum Approachability**: Game systems should be simple to build\n"
" - **RNG Injection**: Bring your own randomness for testing/determinism\n"
" - **Comprehensive Validation**: Clear errors for invalid input\n"
" - **Performance Tested**: Handles extreme loads (1000d6, 100d100+50)\n"
).
-type basic_roll() :: {basic_roll, integer(), integer(), integer()}.
-type detailed_roll() :: {detailed_roll,
basic_roll(),
list(integer()),
integer()}.
-type dice_error() :: missing_separator |
{invalid_count, binary()} |
{invalid_sides, binary()} |
{invalid_modifier, binary()} |
malformed_input.
-file("src/dice_trio.gleam", 136).
-spec safe_parse_int(binary(), fun((binary()) -> dice_error())) -> {ok,
integer()} |
{error, dice_error()}.
safe_parse_int(Value, Error_fn) ->
_pipe = gleam_stdlib:parse_int(Value),
gleam@result:map_error(_pipe, fun(_) -> Error_fn(Value) end).
-file("src/dice_trio.gleam", 123).
?DOC(
" Parses the dice count portion of a dice expression.\n"
"\n"
" Empty strings default to 1 (for expressions like \"d6\").\n"
" Validates that counts are positive integers.\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" parse_count(\"\") // Ok(1) - defaults to 1\n"
" parse_count(\"2\") // Ok(2) - explicit count\n"
" parse_count(\"0\") // Error(InvalidCount(\"0\"))\n"
" parse_count(\"-1\") // Error(InvalidCount(\"-1\"))\n"
" ```\n"
).
-spec parse_count(binary()) -> {ok, integer()} | {error, dice_error()}.
parse_count(C) ->
case C =:= <<""/utf8>> of
true ->
{ok, 1};
false ->
gleam@result:'try'(
safe_parse_int(C, fun(Field@0) -> {invalid_count, Field@0} end),
fun(Parsed_count) -> case Parsed_count < 1 of
true ->
{error, {invalid_count, C}};
false ->
{ok, Parsed_count}
end end
)
end.
-file("src/dice_trio.gleam", 143).
-spec parse_sides_mod_pair(binary(), binary(), boolean(), binary()) -> {ok,
{integer(), integer()}} |
{error, dice_error()}.
parse_sides_mod_pair(Sides, Modifier, Negate, Original) ->
case Sides =:= <<""/utf8>> of
true ->
{error, {invalid_sides, Original}};
false ->
gleam@result:'try'(
safe_parse_int(
Sides,
fun(Field@0) -> {invalid_sides, Field@0} end
),
fun(S) ->
gleam@result:'try'(
safe_parse_int(
Modifier,
fun(Field@0) -> {invalid_modifier, Field@0} end
),
fun(M) ->
Final_mod = case Negate of
true ->
M * -1;
false ->
M
end,
{ok, {S, Final_mod}}
end
)
end
)
end.
-file("src/dice_trio.gleam", 176).
?DOC(
" Parses the sides and modifier portion of a dice expression.\n"
"\n"
" Handles the part after \"d\" in expressions like \"6+3\", \"20-1\", or just \"6\".\n"
" Returns a tuple of (sides, modifier).\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" parse_sides_and_modifier(\"6\") // Ok(#(6, 0)) - no modifier\n"
" parse_sides_and_modifier(\"6+3\") // Ok(#(6, 3)) - positive modifier\n"
" parse_sides_and_modifier(\"20-1\") // Ok(#(20, -1)) - negative modifier\n"
" parse_sides_and_modifier(\"6++1\") // Error(MalformedInput)\n"
" ```\n"
).
-spec parse_sides_and_modifier(binary()) -> {ok, {integer(), integer()}} |
{error, dice_error()}.
parse_sides_and_modifier(Snm) ->
case {gleam_stdlib:contains_string(Snm, <<"+"/utf8>>),
gleam_stdlib:contains_string(Snm, <<"-"/utf8>>)} of
{true, false} ->
case gleam@string:split(Snm, <<"+"/utf8>>) of
[Sides, Modifier] ->
parse_sides_mod_pair(Sides, Modifier, false, Snm);
_ ->
{error, malformed_input}
end;
{false, true} ->
case gleam@string:split(Snm, <<"-"/utf8>>) of
[Sides@1, Modifier@1] ->
parse_sides_mod_pair(Sides@1, Modifier@1, true, Snm);
_ ->
{error, malformed_input}
end;
{false, false} ->
gleam@result:'try'(
safe_parse_int(
Snm,
fun(Field@0) -> {invalid_sides, Field@0} end
),
fun(S) -> {ok, {S, 0}} end
);
{true, true} ->
{error, malformed_input}
end.
-file("src/dice_trio.gleam", 99).
?DOC(
" Parses a dice expression string into structured components.\n"
"\n"
" Supports standard dice notation: `XdY+Z` where:\n"
" - X is dice count (optional, defaults to 1)\n"
" - Y is die sides (required)\n"
" - Z is modifier (optional, defaults to 0)\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" parse(\"d6\")\n"
" // Ok(BasicRoll(roll_count: 1, side_count: 6, modifier: 0))\n"
"\n"
" parse(\"2d6+3\")\n"
" // Ok(BasicRoll(roll_count: 2, side_count: 6, modifier: 3))\n"
"\n"
" parse(\"d20-1\")\n"
" // Ok(BasicRoll(roll_count: 1, side_count: 20, modifier: -1))\n"
"\n"
" parse(\"invalid\")\n"
" // Error(MissingSeparator)\n"
" ```\n"
).
-spec parse(binary()) -> {ok, basic_roll()} | {error, dice_error()}.
parse(Input) ->
case begin
_pipe = gleam@string:trim(Input),
gleam@string:split_once(_pipe, <<"d"/utf8>>)
end of
{ok, {Count, Right_of_d}} ->
gleam@result:'try'(
parse_count(Count),
fun(C) ->
gleam@result:'try'(
parse_sides_and_modifier(Right_of_d),
fun(Snm) ->
{ok,
{basic_roll,
C,
erlang:element(1, Snm),
erlang:element(2, Snm)}}
end
)
end
);
{error, nil} ->
{error, missing_separator}
end.
-file("src/dice_trio.gleam", 228).
?DOC(
" Parses and rolls a dice expression, returning the total result.\n"
"\n"
" This is the main function for dice rolling. It parses the expression and\n"
" then uses your provided RNG function to generate random numbers for each die.\n"
"\n"
" ## Parameters\n"
"\n"
" - `dice_expression`: Standard dice notation string (`\"d6\"`, `\"2d6+3\"`, etc.)\n"
" - `rng_fn`: Function that takes a max value and returns a random 1-to-max number\n"
"\n"
" ## RNG Function Contract\n"
"\n"
" **CRITICAL**: Your RNG function must:\n"
" - Take an `Int` parameter (the die size)\n"
" - Return a value between 1 and that parameter (inclusive)\n"
" - For a d6, return 1, 2, 3, 4, 5, or 6\n"
" - Validate its own output ranges (dice library does not validate for performance)\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Simple roll with fixed RNG for testing\n"
" let test_rng = fn(_) { 3 }\n"
" roll(\"d6\", test_rng) // Ok(3)\n"
" roll(\"2d6+5\", test_rng) // Ok(11) // 3 + 3 + 5\n"
"\n"
" // Error handling\n"
" roll(\"invalid\", test_rng) // Error(MissingSeparator)\n"
" roll(\"0d6\", test_rng) // Error(InvalidCount(\"0\"))\n"
" ```\n"
).
-spec roll(binary(), fun((integer()) -> integer())) -> {ok, integer()} |
{error, dice_error()}.
roll(Dice_expression, Rng_fn) ->
gleam@result:'try'(
parse(Dice_expression),
fun(Roll_result) ->
_pipe = gleam@list:fold(
gleam@list:range(1, erlang:element(2, Roll_result)),
erlang:element(4, Roll_result),
fun(Acc, _) -> Acc + Rng_fn(erlang:element(3, Roll_result)) end
),
{ok, _pipe}
end
).
-file("src/dice_trio.gleam", 274).
?DOC(
" Parses and rolls a dice expression, returning detailed results with individual die values.\n"
"\n"
" This function provides comprehensive roll information including each individual die result,\n"
" the parsed dice components (count, sides, modifier), and the calculated total.\n"
"\n"
" ## Parameters\n"
"\n"
" - `dice_expression`: Standard dice notation string (`\"d6\"`, `\"2d6+3\"`, etc.)\n"
" - `rng_fn`: Function that takes a max value and returns a random 1-to-max number\n"
"\n"
" ## RNG Function Contract\n"
"\n"
" **CRITICAL**: Your RNG function must:\n"
" - Take an `Int` parameter (the die size)\n"
" - Return a value between 1 and that parameter (inclusive)\n"
" - For a d6, return 1, 2, 3, 4, 5, or 6\n"
" - Validate its own output ranges (dice library does not validate for performance)\n"
"\n"
" ## Examples\n"
"\n"
" ```gleam\n"
" // Simple detailed roll with fixed RNG for testing\n"
" let test_rng = fn(_) { 3 }\n"
" detailed_roll(\"2d6+5\", test_rng)\n"
" // Ok(DetailedRoll(\n"
" // basic_roll: BasicRoll(roll_count: 2, side_count: 6, modifier: 5),\n"
" // individual_rolls: [3, 3]\n"
" // ))\n"
"\n"
" // Error handling\n"
" detailed_roll(\"invalid\", test_rng) // Error(MissingSeparator)\n"
" detailed_roll(\"0d6\", test_rng) // Error(InvalidCount(\"0\"))\n"
" ```\n"
).
-spec detailed_roll(binary(), fun((integer()) -> integer())) -> {ok,
detailed_roll()} |
{error, dice_error()}.
detailed_roll(Dice_expression, Rng_fn) ->
gleam@result:'try'(
parse(Dice_expression),
fun(Basic_roll) ->
Individual_rolls = gleam@list:map(
gleam@list:range(1, erlang:element(2, Basic_roll)),
fun(_) -> Rng_fn(erlang:element(3, Basic_roll)) end
),
{ok,
{detailed_roll,
Basic_roll,
Individual_rolls,
gleam@list:fold(
Individual_rolls,
erlang:element(4, Basic_roll),
fun(Acc, Li) -> Acc + Li end
)}}
end
).