Building a Guessing Game Template
Covered in this guide
Section titled “Covered in this guide”In this guide, you will:
- Create a new Tari Ootle template crate using
cargo-generate. - Design and implement the state and logic for a decentralized guessing game.
- Understand how to create and manage resources.
- Learn how to use
VaultsandBucketsfor holding and transferring assets. - Implement custom
ComponentAccessRules. - Learn about random number generation in a deterministic environment.
- Have a working guessing game template
Create an template crate
Section titled “Create an template crate”First you’ll need to create a new Rust crate for your template. You can do this manually with cargo new,
but we recommend using the cargo-generate template, which sets up the necessary structure and dependencies for you.
# First, install cargo-generate if you haven't already:cargo install cargo-generate
# Find somewhere to put your new template crate, and navigate there in your terminal. For example:cd ~/code
# Then, generate a new template crate:cargo generate https://github.com/tari-project/wasm-template wasm_templates/empty -din_cargo_workspace=false --name guessing_game
# Output:⚠️ Favorite `https://github.com/tari-project/wasm-template` not found in config, using it as a git repository: https://github.com/tari-project/wasm-template🔧 in_cargo_workspace: "false" (value from CLI)🤷 Project Name: guessing_game🔧 Destination: /home/me/code/guessing_game ...🔧 project-name: guessing_game ...🔧 Generating template ...🔧 Initializing a fresh Git repository✨ Done! New project created /home/me/code/guessing_gameThis generates a new Rust library crate with the bare-bones structure and dependencies to start building your template.
You should have a new directory called guessing_game with the following structure:
guessing_game├── Cargo.toml <-- dependencies└── src └── lib.rs <-- source code for your template goes here└── tests └── test.rs <-- unit tests for your template go hereTo check that everything works, you can run the tests with cargo test:
(snip .. a lot of compilation output)
running 1 testtest it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 31.76sWhat we did:
- We used
cargo-generateto create a new starter Rust crate for our template. - Ran a test that invokes your (empty) template function
This template doesn’t do anything yet. So let’s change that!
Implementing the Guessing Game Logic
Section titled “Implementing the Guessing Game Logic”Let’s define some rules for our guessing game:
- It should be a simple guessing game where players can guess a number between 1 and 10.
- Since there are only 10 possible numbers, we limit the number of players to <10 per round (let’s say 5).
- Each player can only make one guess per round.
- No one should know the correct number ahead of time.
- The game should give out a super-special limited-edition NFT prize to the winner.
- There is only one winner per game, any one of the players who guessed the correct number can be awarded the prize.
- Only the component creator can start a new round of the game.
- If there are no winners, the prize is destroyed.
Let’s think about what state we’ll need in the component to accomplish this. First the prize; we’ll need a resource to represent the prize, and a vault to hold it. Then we’ll need to keep track of the guesses that players have made. Finally, we’ll want to keep track of the round number so that we can “engrave” that round number into the prize for each round.
pub struct GuessingGame { /// A vault containing the prize to be won for the round, /// or is empty if no round is active prize_vault: Vault, /// A map between a player's public key and their guess for the current round guesses: HashMap<RistrettoPublicKeyBytes, Guess>, /// The current round number, starting at 0 and /// incrementing by 1 for each new round round_number: u32,}We’ll also need a struct to represent a player’s guess, which includes the player’s payout component (typically an account) and their guess for the current round.
Add this struct inside the template module block:
pub struct Guess { /// The component where the player wants to receive /// their payout if they win. /// We use a ComponentManager for convenience; it wraps /// ComponentAddress and provides some helper methods. pub payout_to: ComponentManager, /// The player's guess for this round, a number between 0 and 10. pub guess: u8,}Implement a constructor
Section titled “Implement a constructor”Next, we need to create an instance of our game. Let’s implement a constructor function called new inside the impl block for our GuessingGame struct.:
impl GuessingGame { pub fn new() -> Component<Self> { let prize_vault = Vault::new_empty(prize_resource); // ↑ what is `prize_resource`?? we'll get to that in a moment
Component::new(Self { // Keep the vault in the component state so we can use it // to hold the prize for each round. prize_vault, guesses: HashMap::new(), round_number: 0, }) .create() }}What we did:
- We created a new empty vault that will hold the prize for our game.
- We created a component that holds the vault and other state for our game
But what is the prize exactly? Let’s create that next.
Create a Resource
Section titled “Create a Resource”We said we want to give out a unique NFT for each round. But how do we create an NFT?
First, you’ll need to create a “resource”. A resource (sometimes called a “token” or an “asset”) is native to Tari Ootle and behaves like an object or currency. That is, it can be minted, transferred, but not copied or unintentionally destroyed.
There are 3 types of resource:
- fungible stealth resources are private and enable “blinded” UTXO substates which hide the amount and the owner on-chain.
- public fungible resources are like ERC-20 tokens, where each token is identical and divisible. Visible on-chain with transparent amounts and addresses.
- non-fungible resources are like ERC-721 tokens, where each token is unique and indivisible. Visible on-chain with transparent metadata and ownership.
For our game, we’ll need a new non-fungible resource that uniquely represents prizes for our game. First we’ll create a new resource and later, when the game starts, we’ll use this resource to mint an NFT prize for each round.
We can do this using the ResourceBuilder in the constructor:
impl GuessingGame { pub fn new() -> Component<Self> { // ADD THIS let prize_resource = ResourceBuilder::non_fungible() // Optionally give it a name .metadata("name", "Guessing Game Prize") // Optionally, provide a token symbol that exchanges and explorers will // display for the new resource. This is usually 3-5 letters, but since // this is a fun game let's make it 🎲 .with_token_symbol("🎲") // Create the resource with no supply // (use `.initial_supply(...)` to mint new NFTs in the builder) .build();
// Pass the resource address to the vault to specify that this vault will // hold this, and only this, resource let prize_vault = Vault::new_empty(prize_resource);
Component::new(Self { prize_vault, guesses: HashMap::new(), round_number: 0, }) .create() }}What we did:
- We created a new non-fungible resource that uniquely identifies the prize for our game.
- We gave it some metadata, including a name and token symbol.
- We passed the resource address to the vault to specify that it will hold this resource.
Starting a new round
Section titled “Starting a new round”Next, we need a function to start a new round of the game.
pub fn start_game(&mut self, prize: NonFungibleId) { self.round_number += 1; // To mint the prize, we need a ResourceManager. For convenience, // a Vault provides `get_resource_manager` for the resource it holds. let manager = self.prize_vault.get_resource_manager(); // Mint the prize. Each NFT has immutable (cannot be changed) data // and mutable (holder can change) data. // We pass in the round number as the immutable data to mint this // round in NFT history, and empty (unit) for the mutable data. let prize = manager.mint_non_fungible( prize, &metadata!["round" => self.round_number.to_string()], &(), ); // Deposit the prize in the vault to hold it until it's awarded // to a winner or destroyed if there are no winners. self.prize_vault.deposit(prize);}What we did:
- We created a
start_gamemethod that takes aNonFungibleIdas an argument. This allows the component creator to specify the prize NFT for this round - We increment the round number to keep track of which round we’re on.
- We used the
ResourceManagerto mint a new NFT for the prize, with the round number as immutable metadata. - We deposited the newly minted prize NFT into the vault to hold it until we determine the winner at the end of the round.
Submitting a guess
Section titled “Submitting a guess”Next, we need a function that allows players to submit their guesses.
pub fn guess(&mut self, guess: u8, payout_to: ComponentAddress) { // We'll get the signer of the transaction to use as the // player identifier for the guess. let player = CallerContext::transaction_signer_public_key(); // Create a ComponentManager. This is a wrapper around a // component address that allows us to call methods on it. // We'll use this in end_game_and_payout. let payout_to = ComponentManager::get(payout_to); // Insert the guess into the hashmap, asserting that the // player hasn't already made a guess this round. let maybe_previous_guess = self.guesses .insert(player, Guess { payout_to, guess }); assert!( maybe_previous_guess.is_none(), "You already guessed in this round", );}What we did:
- We created a
guessmethod that takes a player’s guess and the address of the component where they want to receive their payout if they win. - We used the
CallerContext::transaction_signer_public_key()to identify the player making the guess. - We stored the player’s guess in a hashmap, ensuring that they can only guess once per round.
Looks great! But there’s a problem… We said that anyone can submit a guess.
By default, only the component creator can call methods on the component. How do we change that to allow
anyone to call the guess method?
The answer is access rules! Access rules is a larger subject that is covered in more detail here, but for now, all you need to know is that access rules are a way to control who can call methods on your component.
We’ll need to make the guess method publicly callable by setting the allow_all access rule.
Let’s go back to the component constructor to do this.
let access_rules = ComponentAccessRules::new() // ADDED: Allow anyone to call the "guess" method. .method("guess", rule![allow_all]);
// Construct the componentComponent::new(Self { // We create an empty vault that will hold our prize NFT prize_vault: Vault::new_empty(prize_resource), guesses: HashMap::new(), round_number: 0,})// ADD THIS.with_access_rules(access_rules).create()What we did:
- We created a
ComponentAccessRulesstruct that defines who can call methods on our component. - We set the access rule for the
guessmethod toallow_all, which means that anyone can call it without restriction.
Ending the game and awarding the prize
Section titled “Ending the game and awarding the prize”Finally, we need a function to end the game, determine if there are any winners, and either award the prize or destroy it.
pub fn end_game_and_payout(&mut self) { // Since we only ever have one prize in the vault, we can // withdraw it by amount. Use `vault.withdraw_non_fungible(id)` // if you want to withdraw a specific NFT by id instead. let mut prize = self.prize_vault.withdraw(1u64);
// Generate a (pseudo) random number to determine the winner. // We'll implement generate_number() in a moment let number = generate_number();
// Take the guesses and clear the guesses for the next round. let guesses = mem::take(&mut self.guesses); let num_participants = guesses.len();
for (player, guess) in guesses { if guess.guess == number { // We have a winner! Payout the prize to the component // specified in the guess. This is a cross-component call // that invokes `deposit` with the prize bucket. guess.payout_to.invoke("deposit", args![prize]); return; } }
// No winner, bye bye prize! prize.burn();}What we did:
- We withdrew the prize from the vault into a bucket
- We generated a pseudo-random number to compare against the players’ guesses.
- We iterated through the players’ guesses to see if any of them matched the generated number.
- If there was a winner, we used the
ComponentManagerto invoke thedepositmethod on the winner’s account with the prize bucket as an argument. - If there wasn’t a winner, we burned the prize to permanently remove it from circulation.
What about generating our random number between 0 and 10?
For that we’ll create a normal rust helper function. Place this outside of the impl block for your component.
fn generate_number() -> u8 { use tari_template_lib::rand::random_bytes; // random_bytes calls into the execution engine to generate random bytes // Like all other engine calls, this incurs a small fee. let num = random_bytes(1)[0]; // Naively squish it to between 0 and 10 num % 11}Testing
Section titled “Testing”You’ll want to write some unit tests to make sure your game logic works as expected.
You can use the tari_template_test_tooling crate as a dev-dependency. This contains
a harness that compiles your template to WASM and allows you to create transactions that call into
your template/component using the same execution engine that runs on the Tari L2 network.
Writing tests is out of scope for this guide, but the full annotated code example includes a test that creates a new game, simulates some players making guesses, and then ends the game to check that the prize is awarded correctly.
To run your tests, simply run cargo test in your template crate directory.
The test harness will compile your template to WASM and run the tests against it.
Full annotated code
Section titled “Full annotated code”Use cargo-generate to download the full template crate:
cargo generate https://github.com/tari-project/wasm-template examples/guessing_game/templateNext Steps
Section titled “Next Steps”- Learn how to publish your template to the Tari Ootle.