npx claudepluginhub dojoengine/bookThis skill uses the workspace's default tool permissions.
Integrate Cartridge's Verifiable Random Function (VRF) for provably fair, atomic randomness in Dojo games.
Generates Cairo Dojo system contracts for implementing game logic, modifying model state, handling player actions, and emitting events in Starknet games.
Build Starknet dApps with starknet.js v9 SDK: contract interactions, account management, transactions, fee estimation, wallet and paymaster integration.
Provides core Ethereum onchain concepts: state machines, 'nothing automatic,' incentives, CROPS. For new devs, system design, mechanism questions.
Share bugs, ideas, or general feedback.
Integrate Cartridge's Verifiable Random Function (VRF) for provably fair, atomic randomness in Dojo games.
Cartridge VRF provides cheap, atomic verifiable randomness for fully onchain games. The VRF request and response are processed within the same transaction, enabling synchronous and immediate randomness.
| Network | Contract Address |
|---|---|
| Mainnet | 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f |
| Sepolia | 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f |
Add to your Scarb.toml:
[dependencies]
cartridge_vrf = { git = "https://github.com/cartridge-gg/vrf" }
use cartridge_vrf::IVrfProviderDispatcher;
use cartridge_vrf::IVrfProviderDispatcherTrait;
use cartridge_vrf::Source;
// Use the deployed VRF provider address
const VRF_PROVIDER_ADDRESS: felt252 = 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f;
#[dojo::contract]
mod game_system {
use starknet::{ContractAddress, get_caller_address};
use cartridge_vrf::{IVrfProviderDispatcher, IVrfProviderDispatcherTrait, Source};
const VRF_PROVIDER_ADDRESS: felt252 = 0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f;
#[abi(embed_v0)]
impl GameImpl of IGame<ContractState> {
fn roll_dice(ref self: ContractState) -> u8 {
let vrf_provider = IVrfProviderDispatcher {
contract_address: VRF_PROVIDER_ADDRESS.try_into().unwrap()
};
let player = get_caller_address();
// Consume random value using player's nonce
let random_value: felt252 = vrf_provider.consume_random(Source::Nonce(player));
// Convert to dice roll (1-6)
let random_u256: u256 = random_value.into();
let dice_roll: u8 = (random_u256 % 6 + 1).try_into().unwrap();
dice_roll
}
}
}
Two source types for randomness:
Source::Nonce(ContractAddress)let random = vrf_provider.consume_random(Source::Nonce(player_address));
Source::Salt(felt252)let random = vrf_provider.consume_random(Source::Salt(game_id));
When executing transactions that use VRF, prefix with request_random:
import { Account, CallData } from "starknet";
const VRF_PROVIDER = "0x051fea4450da9d6aee758bdeba88b2f665bcbf549d2c61421aa724e9ac0ced8f";
async function rollDice(account: Account, gameContract: string) {
const call = await account.execute([
// First: request_random
{
contractAddress: VRF_PROVIDER,
entrypoint: "request_random",
calldata: CallData.compile({
caller: gameContract,
source: [0, account.address], // Source::Nonce(address)
}),
},
// Then: your game action
{
contractAddress: gameContract,
entrypoint: "roll_dice",
calldata: [],
},
]);
return call;
}
import { buildVrfCalls } from "@cartridge/vrf";
const calls = await buildVrfCalls({
account,
call: {
contractAddress: GAME_CONTRACT,
entrypoint: "roll_dice",
calldata: [],
},
vrfProviderAddress: VRF_PROVIDER,
});
await account.execute(calls);
request_random(caller, source) as first call in multicallconsume_random(source) internallysubmit_random and assert_consumedassert_consumed ensures consume_random was called and resets storageFor local development/testing, use the mock provider:
#[dojo::contract]
mod vrf_provider_mock {
use cartridge_vrf::PublicKey;
use cartridge_vrf::vrf_provider::vrf_provider_component::VrfProviderComponent;
use openzeppelin::access::ownable::OwnableComponent;
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);
component!(path: VrfProviderComponent, storage: vrf_provider, event: VrfProviderEvent);
#[abi(embed_v0)]
impl VrfProviderImpl = VrfProviderComponent::VrfProviderImpl<ContractState>;
#[storage]
pub struct Storage {
#[substorage(v0)]
ownable: OwnableComponent::Storage,
#[substorage(v0)]
vrf_provider: VrfProviderComponent::Storage,
}
fn dojo_init(ref self: ContractState, pubkey_x: felt252, pubkey_y: felt252) {
self.ownable.initializer(starknet::get_caller_address());
self.vrf_provider.initializer(PublicKey { x: pubkey_x, y: pubkey_y });
}
}
fn random_in_range(random: felt252, min: u32, max: u32) -> u32 {
let random_u256: u256 = random.into();
let range = max - min + 1;
let result: u32 = (random_u256 % range.into()).try_into().unwrap();
result + min
}
fn weighted_random(random: felt252, weights: Span<u32>) -> u32 {
let total_weight: u32 = weights.iter().sum();
let random_u256: u256 = random.into();
let threshold: u32 = (random_u256 % total_weight.into()).try_into().unwrap();
let mut cumulative: u32 = 0;
let mut i: u32 = 0;
loop {
cumulative += *weights.at(i);
if cumulative > threshold {
break i;
}
i += 1;
}
}
fn shuffle<T, impl TCopy: Copy<T>, impl TDrop: Drop<T>>(
ref arr: Array<T>,
vrf_provider: IVrfProviderDispatcher,
player: ContractAddress,
) {
let mut i = arr.len();
loop {
if i <= 1 {
break;
}
i -= 1;
let random = vrf_provider.consume_random(Source::Nonce(player));
let j: u32 = (random.into() % (i + 1).into()).try_into().unwrap();
// Swap arr[i] and arr[j]
let temp = *arr.at(i);
arr.set(i, *arr.at(j));
arr.set(j, temp);
};
}
Source in request_random and consume_randomconsume_random must be called within the same transaction