Introduction
This library provides the capacity to use generative grammars within bevy.
At the moment, it provides two things:
- an set of traits that an be used to build your own grammars (in
bevy_generative_grammars::generator::*
) - an implementation of the tracery generative grammar - in both a stateful generator and a stateless one (in
bevy_generative_grammars::tracery::*
). These are able to act as bevy resources or components, and use the bevy hashmap implementation internally.
We also have API Docs
Installation
To install, add the following to your dependencies in the Cargo.toml
:
bevy-generative-grammars = { version = "0.0.2", features = ["bevy"]}
Available features
-
default - this only provides the basic functionality, and relies on
std::collections::HashMap
internally. -
bevy - this implements
Resource
andComponent
for grammars & stateful generators, as well as switching tobevy::utils::HashMap
-
serde - this provides a serialization/deserialization
-
asset - you don't need to use this directly, but it's used as the backbone for the various asset plugin options.
-
json - provides a JSON asset plugin
-
ron - provides a RON asset plugin
-
msgpack - provides a MessagePack asset plugin
-
toml - provides a TOML asset plugin
-
yaml - provides a YAML asset plugin
Introduction to Tracery
For a quick tutorial on Tracery, look at the Tracery interactive tutorial - courtesy of the wayback machine.
Once you understand it, there are a few modifications to the basic syntax that apply in this project:
- First, you are not limited to storing your grammar in JSON - look here for more info
- Second, the library currently doesn't support any of the modifiers supported by the original JS implementation.
- Lastly, in addition to being able to save data that get's worked out in the moment, like so
[variable:some text to process]
, you can save data in way that will be processed at a later point - like so[variable|some text to process]
. Essentially, this allows you to create re-directions that go do different rules based on remembered context. This is particularly useful since you can use a stateful generator to continue generation from a pre-existing state. I recommend looking at the example asset in/assets/story.json
to see a complex version supporting all the syntax we support.
Tracery Asset Format
When using the serde
feature - Tracery Grammar implements the serde serialize/deserialize traits. This allows you to use many different formats to store the grammars.
In addition, with the asset
feature, we use Bevy Common Assets to implement a multi-file-type asset plugin (found under bevy_generative_grammars::tracery::tracery_asset::TraceryAssetPlugin
) for bevy. You can enable any of the formats supported by Bevy Common Assets using their matching trait - for example the json
trait for JSON files.
When serializing/deserializing formats, we assume the following structure:
{
"rules": {
[key: string]: string[]
},
"starting_point"?: string
}
The rules
structure matches the structure of a tracery grammar by default, and the optional starting_point
provides an alternative default starting point (otherwise, we use origin
).
Simple Tracery Example
In this case, we're going to implement an example that processes the following tracery grammar:
{
"origin": ["Hello #name#!", "#name#, you made it!"],
"name": ["Jane", "Alice", "Bob", "Roberto"]
}
First off, we need to set up the rules - in this case we'll make them a constant:
#![allow(unused)] fn main() { const RULES: &[(&str, &[&str])] = &[ ( "origin", &["Hello #name#!", "#name#, you made it!"], ), ( "name", &["Jane", "Alice", "Bob", "Roberto"], ), ] }
Next, in our main function:
fn main() { // We need to load in our grammar - this stores an immutable copy of our ruleset let grammar = TraceryGrammar::new(RULES, None); // Next, we need to setup our random generation function. // Normally, you'll want to use rand or bevy_turborand for this. // But we're just going to hard code it, so we have consisten results. let mut rng = 0; // Now we generate our story - it should print out: // "Hello Jane" match StringGenerator:generate(&grammar, &mut rng) { Some(result) => { println!("{result}"); }, None => { eprintln!("There was an error..."); } } }
If you want more information on random number generation - take a look at our Random Number Generation docs.
Now, what if we just want a name? In that case, we can adjust the string generation line from:
#![allow(unused)] fn main() { match StringGenerator:generate(&grammar, &mut rng) { }
to:
#![allow(unused)] fn main() { match StringGenerator:generate_at(&"name".to_string(), &grammar, &mut rng) { }
Which will print out Jane
Or - we might want to provide it with some arbitrary content to expand. In this case we'd replace the entire match statement with something like this:
#![allow(unused)] fn main() { let story = "Agnus greeted me, saying \"#origin#\"".to_string(); let result = StringGenerator::expand_from(&story, &grammar, &mut rng); println!("{result}"); }
Notice that using "expand_from" doesn't require an option, since it will always at least return the initial input, if it can't expand it further.
Simple Tracery Example
Now we want to actually remember who we're talking to... so let's adjust our grammar.
{
// all we did was set up a friend variable and start using it instead of name
"origin": ["[friend:#name#]Hello #friend#!", "[friend:#name#]#friend#, you made it!"],
// and we added a second sentence that uses it too.
"second_sentence": ["I missed you #friend#", "#friend, it's been too long"],
"name": ["Jane", "Alice", "Bob", "Roberto"]
}
And we update our rules.
#![allow(unused)] fn main() { const RULES: &[(&str, &[&str])] = &[ ( "origin", &["[friend:#name#]Hello #friend#!", "[friend:#name#]#friend#, you made it!"], ), ( "second_sentence", &["I missed you #friend#", "#friend, it's been too long"], ) ( "name", &["Jane", "Alice", "Bob", "Roberto"], ), ] }
Now, the StringGenerator
we used can't retain state. That means we can't use this kind of more complex grammar with it, since it won't remember our new rules. For that, we can use the StatefulStringGenerator
instead:
fn main() { // We can use an existing grammar if we have one, but in this case we are just creating the generator directly. let mut generator = StatefulStringGenerator::new(RULE, None); // Then we create the rng, like before let mut rng = |_| { 0 }; // Now we generate our story - it should print out: // "Hello Jane", just like before. // Note that we don't need to provide the grammar. match generator.generate(&mut rng) { Some(result) => { println!("{result}"); }, None => { eprintln!("There was an error..."); } } // But then we can go to the second sentence, which prints out: // "I missed you Jane" match generator.generate_at(&mut rng, "second_sentence") { Some(result) => { println!("{result}"); }, None => { eprintln!("There was an error..."); } } // Or even use it in an expandable prompt like this: let story = "Agnus called me \"#friend#\"... we don't even look that similar!".to_string(); let result = generator.expand_from(&story, &mut rng); // Which prints out: // "Agnus called me "Jane"... we don't even look that similar!" println!("{result}"); }
Random Generation Traits
This crate exposes a trait for the random generation process it expects - the GrammarRandomNumberGenerator
trait:
#![allow(unused)] fn main() { pub trait GrammarRandomNumberGenerator { /// This function provides a random number between 0 and len fn get_number(&mut self, len: usize) -> usize; } }
We provide a few built in implementations for it:
usize
- we have set it up so usize implements the trait, always returning it's value.FnMut(usize) -> usize
- if the length is greater then zero, it will call the closure/FnMut and return it's value
In addition, we provide wrapper components for use with 2 different random number generation crates - rand
and bevy_turborand
- hidden behind feature flags.
If you wish to use the rand
crate, enable the rand
feature and wrap any type implementing rand::Rng
using either:
Rand::new(&mut rng)
- this provides a wrapper using the existing reference, and bound to it's lifetime.RandOwned::new(rng)
- this provides a wrapper that takes over the existing type, and ownes it from this point forward.
If you wish to use the bevy_turborand
crate, enable the turborand
feature and wrap any type implementing bevy_turborand::TurboRand
using either:
TurboRand::new(&mut rng)
- this provides a wrapper using the existing reference, and bound to it's lifetime.TurboRandOwned::new(rng)
- this provides a wrapper that takes over the existing type, and ownes it from this point forward.