Crates.io | tyche |
lib.rs | tyche |
version | 0.2.0 |
source | src |
created_at | 2024-04-24 19:59:07.230432 |
updated_at | 2024-04-29 02:44:13.426866 |
description | Dice rolling and dice expression (with a syntax similar to FoundryVTT) parsing library |
homepage | |
repository | https://github.com/Gawdl3y/tyche-rs |
max_upload_size | |
id | 1219459 |
size | 125,015 |
Tyche is a library for parsing, rolling, and explaining the results of tabletop dice.
It also has a simple CLI app binary that evaluates a given expression.
The eventual goal is full compatibility with FoundryVTT's dice syntax with some convenient extensions.
2d6 + 2
2d6 - 2
2d6 * 2
2d6 / 2
2d6 \ 2
(2d6 + 2) * 2
2d20kh
, 2d20k
3d20kh2
2d20kl
3d20kl2
4d6r
4d6r>4
, 4d6r>=5
, 4d6r<3
, 4d6r<=2
, 4d6r4
4d6rr
4d6rr>4
, 4d6rr>=5
, 4d6rr<3
, 4d6rr<=2
, 4d6rr4
4d6x
4d6x>4
, 4d6x>=5
, 4d6x<3
, 4d6x<=2
, 4d6x4
4d6xo
4d6xo>4
, 4d6xo>=5
, 4d6xo<3
, 4d6xo<=2
, 4d6xo4
4d8min3
4d8max6
4d6rr1x>4
, 8d8min2kh4xo
Run cargo add tyche
or add the following to your project's Cargo.toml file:
[dependencies]
tyche = "0.2.0"
Run cargo install tyche --features build-binary
.
Assuming Cargo's bin directory is in your $PATH
, use the app with tyche
or tyche <dice expression>
.
There are three main types that you'll start with while using Tyche:
Dice
: a struct containing a dice count, number of sides for each die, and modifiers that should be applied to any
resulting rolls, representing a set of dice, e.g. 4d8x
.Expr
: an AST-like tree structure enum of individual components of a dice expression, capable of representing
complex sets of mathematical operations including dice, e.g. (2d6 + 2) * 2
.Roller
: a trait for rolling individual die values and entire sets of Dice
. Sometimes referred to as simply "RNG"
for the sake of brevity. There are a few Roller
implementations available out of the box:
All parsing requires the parse
feature of the crate to be enabled (which it is by default).
Tyche uses the chumsky parser generator to parse all strings in a nearly zero-copy and wicked-fast fashion.
Most conveniently, parsing can be done by utilizing the standard FromStr
implementations for the relevant types
(Dice
, Expr
, Modifier
, and Condition
):
use tyche::{
dice::modifier::{Condition, Modifier},
Dice, Expr,
};
let expr: Expr = "4d6rr<3 + 2d8 - 4".parse()?;
let dice: Dice = "4d6rr<3".parse()?;
let modifier: Modifier = "rr<3".parse()?;
let cond: Condition = "<3".parse()?;
Alternatively, you can directly use the parsers for each type via its associated GenParser
implementation
or the functions in the tyche::parse
module.
Programmatically constructing Dice
to roll is painless, even with lots of chained modifiers,
thanks to its use of the builder pattern.
use tyche::{dice::modifier::Condition, Dice};
// Simple set of dice, no modifiers: 2d20
let d2d20 = Dice::new(2, 20);
// Exploding dice: 4d6x
let d4d6x = Dice::builder()
.count(4)
.sides(6)
.explode(None, true)
.build();
// Chained modifiers: 6d6rr1x
let d6d6rr1x = Dice::builder()
.count(6)
.sides(6)
.reroll(Condition::Eq(1), true)
.explode(None, true)
.build();
All rolling of dice is performed by a Roller
implementation.
The most suitable "default" roller implementation is FastRand
, which generates random numbers for die values using a
fastrand::Rng
instance.
use tyche::dice::roller::FastRand as FastRandRoller;
// Create a FastRand roller with the default thread-local fastrand::Rng
let mut roller = FastRandRoller::default();
// Create a FastRand roller with a custom-seeded fastrand::Rng
let rng = fastrand::Rng::with_seed(0x750c38d574400);
let mut roller = FastRandRoller::new(rng);
Once you have a roller, you can roll a single die at a time or a whole set of Dice
with it:
use tyche::dice::{roller::FastRand as FastRandRoller, Dice, Roller};
let mut roller = FastRandRoller::default();
// Roll a single 20-sided die
let die = roller.roll_die(20);
// Roll two six-sided dice
let dice = Dice::new(2, 6);
let rolled = roller.roll(&dice, true)?;
Rolling a single die results in a DieRoll
instance, whereas rolling a set of Dice
returns a Rolled
instance.
Rolled
is a struct that ties multiple DieRoll
s together with the Dice
that were rolled to generate them so
it can accurately describe what happened during the roll and application of any modifiers that were on the dice.
Using a Rolled
result, you can easily total the results of all rolled dice and/or build a string that contains the
original dice set along with a list of each individual die roll.
use tyche::{
dice::{roller::FastRand as FastRandRoller, Dice, Roller},
expr::Describe,
};
// Build and roll 4d6kh2
let mut roller = FastRandRoller::default();
let dice = Dice::builder()
.count(4)
.sides(6)
.keep_high(2)
.build();
let rolled = roller.roll(&dice, true)?;
// Total the results
let total = rolled.total()?;
assert!((2..=12).contains(&total));
// Build a detailed string about the rolls
// The resulting string will look like "4d6kh2[2 (d), 5, 6, 4 (d)]"
let described = rolled.to_string();
// This is the same as the to_string() call above, except only two die rolls will be listed.
// The resulting string will look like "4d6kh2[2 (d), 5, 2 more...]"
let limited = rolled.describe(Some(2));
A DieRoll
contains the final value of the die alongside information about any changes that were made to it and the
source of said changes.
When a modifier (rather than the original dice set) causes the addition of a new DieRoll
to a Rolled
set,
the roll's added_by
field is set to Some(source_modifier)
. An added roll will be marked as such in strings.
Modifiers that can result in additional die rolls are:
When a modifier causes the removal of a DieRoll
from a Rolled
set,
the roll's dropped_by
field is set to Some(source_modifier)
. A dropped roll will not be affected by any further
modifiers, is not included when totaling the rolled set, and will be marked as dropped in strings.
Modifiers that can result in dropped die rolls are:
When a modifier directly manipulates the value of a DieRoll
in a Rolled
set, the roll's changes
field has a
ValChange
item added to it that describes the change made and the modifier that caused it.
Modifiers that can result in changed die rolls are:
Expr
trees are essentially always obtained from parsing an expression string, as manually creating them would be
quite cumbersome.
Once you have an Expr
variant, it can be evaluated to produce an Evaled
expression tree.
Evaled
expression trees are nearly identical to their originating Expr
tree, except any Dice variants have their
dice rolled. This separation allows for describing an expression in a detailed way both before and after rolling dice it
contains, in addition to a few other utilities.
use tyche::{
dice::roller::FastRand as FastRandRoller,
expr::{Describe, Expr},
};
// Parse a nice dice expression
let expr: Expr = "4d6 + 2d8 - 2".parse()?;
// This expression is definitely not deterministic (it contains dice sets)
assert!(!expr.is_deterministic());
// Build a string for the expression - in this case, it'll be identical to the original parsed string
assert_eq!(expr.to_string(), "4d6 + 2d8 - 2");
// Evaluate the expression, rolling any dice sets it contains
let mut roller = FastRandRoller::default();
let evaled = expr.eval(&mut roller)?;
// Build a detailed string about the evaluated expression
// The resulting string will look like "4d6[3, 2, 6, 2] + 2d8[5, 4] - 2"
let described = evaled.to_string();
// This is the same as the to_string() call above, except only two die rolls in each set will be listed.
// The resulting string will look like "4d6[3, 2, 2 more...] + 2d8[5, 4] - 2"
let limited = evaled.describe(Some(2));
// Calculate the final result of the expression
let total = evaled.calc()?;
assert!((4..=38).contains(&total));
All contributions are welcome! Try to keep PRs relatively small in scope (single feature/fix/refactor at a time) and word your commits descriptively.
Tyche is licensed under the LGPLv3 license.