Sensei MCP

Official
Cairo Framework This section highlights the three core components essential for building dojo apps: Models: Models act like structured database entries, managing and organizing your onchain data. Like a regular ORM. Systems: Implement business logic and state changes, which are just like regular Cairo contracts, but with some syntax sugar. World: The central hub that connects all models and systems. The World contract ensures consistency, manages authorization, and orchestrates interactions between different parts of your application. Simplifying State with Dojo Models Dojo simplifies onchain application development by providing a standardized approach to state management, similar to a regular database. In dojo you write state like this: #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Moves { #[key] pub player: ContractAddress, pub remaining: u8, pub last_direction: Direction, pub can_move: bool, } Breaking it down Think of the model here as an entry into your onchain database and the #[key] is the primary key to reference it. You use this key to query your state onchain within contracts and off-chain in your clients. #[derive(Copy, Drop, Serde)] // This defines the model attribute. Indicating to the compile that // this struct is a Model. #[dojo::model] pub struct Moves { // Define a Key. This acts like a primary key does in a regular ORM #[key] pub player: ContractAddress, // These are the fields you define for your application state pub remaining: u8, pub last_direction: Direction, pub can_move: bool, } Composing Contracts Once you have your state designed, you want it to be effectively mutated via contracts. Dojo simplifies this by exposing an interface to interact with the world state, allowing fast queries and mutations. (It does this by extending the Cairo compiler!) // the dojo decorator indicating to the compiler that this is a dojo // contract. #[dojo::contract] mod actions { use starknet::{ContractAddress, get_caller_address}; // import the model like a normal Cairo Struct use dojo_starter::models::{Moves}; #[abi(embed_v0)] impl ActionsImpl of super::IActions<ContractState> { // You have to pass in the world as the first param, // which then allows you to set the models state. fn spawn(ref self: ContractState) { // Get the default world. let mut world = self.world(@"dojo_starter"); // Get the address of the current caller, possibly the player's address. let player = get_caller_address(); // Retrieve the player's current position from the world. let mut position: Position = world.read_model(player); // Update the world state with the new data. // 1. Move the player's position 10 units in both the x and y direction. let new_position = Position { player, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 } }; // Write the new position to the world. world.write_model(@new_position); // 2. Set the player's remaining moves to 100. let moves = Moves { player, remaining: 100, last_direction: Direction::None(()), can_move: true }; // Write the new moves to the world. world.write_model(@moves); } } } The World Models and contracts are the foundation of your application. We encapsulate them within the World contract, which serves as a container for all models, systems, and authorization management. Takeaways Models: Utilize the #[dojo::model] attribute to define structured data representing your application's state. Models function similarly to database entries, organizing and managing onchain data effectively. Systems: Implement business logic and state transitions using the #[dojo::contract] attribute. Systems handle operations that modify models, ensuring seamless and reliable updates to your application's state. World: Acts as the central orchestrator connecting all models and systems. The World contract maintains consistency, manages authorization, and coordinates interactions between different components, serving as the single source of truth for your application. Events Events play a pivotal role in decoding the dynamics of a Dojo world. Every time there's an update to a model, the world contract emits events. What's even more exciting is that you can craft your own custom events to fit specific needs! Moreover, thanks to model's introspection and Torii, all these events are seamlessly indexed, ensuring easy and efficient querying. Custom Events Within your game, emitting custom events can be highly beneficial. Fortunately, there's a handy emit_event api that lets you release events directly from your world. These events are indexed by Torii. There are two kind of Custom Events with different use-cases. Using dojo::event These events are acting like 'off-chain' storage and behave like models which allows Torii to easily parse them. Since it mimics models behaviour, a Dojo event must have a least a #[key] and any type used inside it must derive Introspect. For example we will declare a PlayerMood struct to keep track of player mood. We don't need this information onchain. We don't want to historize PlayerMood changes, just keep track of the current/latest PlayerMood. #[derive(Copy, Drop, Introspect)] struct Mood { Happy, Angry, } #[derive(Copy, Drop, Serde)] #[dojo::event] struct PlayerMood { #[key] player: ContractAddress, mood: Mood, } Emit the PlayerMood event: world.emit_event(@PlayerMood { player, mood: Mood::Happy }); Each time a PlayerMood event is emitted, the PlayerMood event indexed by Torii will reflect the lasted mood. Example Now a full example using a custom event: fn move(ref self: ContractState, direction: Direction) { let player = get_caller_address(); let mut position: Position = world.read_model(player); let mut moves: Moves = world.read_model(player); moves.remaining -= 1; moves.last_direction = direction; let next = next_position(position, direction); world.write_model(@moves); world.write_model(@next); world.emit_event(@Moved { address, direction }); } Entities Entities are the primary key value within the world, to which models can be attached. In Dojo, entities are treated as a primary key value within the world, to which models can be attached. To illustrate this concept, consider a simple example of a character in a game that has a Moves and a Position model. When defining the models for the character entity, it is important to note that we do not reference the entity directly. Instead, we simply provide two structs that the entity will be related with. #[derive(Drop, Serde)] #[dojo::model] struct Moves { #[key] player: ContractAddress, remaining: u8, } #[derive(Drop, Serde)] #[dojo::model] struct Health { #[key] player: ContractAddress, x: u32, y: u32 } Dojo is based on the ECS principle. Enum What is Enum Enums, short for "enumerations," are a way to define a custom data type that consists of a fixed set of named values, called variants. Enums are useful for representing a collection of related values where each value is distinct and has a specific meaning. Enums are particularly useful in game development for representing game states, player actions, or any other set of related constants that a game might need to track. In this example, we've defined an enum called PlayerCharacter with four variants: Godzilla, Dragon, Fox, and Rhyno. The naming convention is to use PascalCase for enum variants. Each variant represents a distinct value of the PlayerCharacter type. In this particular example, variants don't have any associated value. #[derive(Serde, Drop, Introspect)] enum PlayerCharacter { Godzilla, Dragon, Fox, Rhyno } Now let's imagine that our variants have associated values, We can define a new PlayerCharacter enum: #[derive(Serde, Drop, Introspect)] enum PlayerCharacter { Godzilla: u128, Dragon: u32, Fox: (u8, u8), Rhyno: ByteArray } Trait Implementations for Enums Traits are a way to define shared behavior across types. By defining traits and implementing them for your custom enums, you can encapsulate common behaviors and methods that are relevant to the enum. This approach enhances code reusability and maintainability, especially in complex systems like game development. Consider the GameStatus enum, which represents the various states a game can be in. This enum is a simple yet powerful example of how enums can be used to model game states. # [derive(Serde, Copy, Drop, Introspect, PartialEq, Print)] // Define an enum representing different states of a game enum GameStatus { NotStarted, Lobby, InProgress, Finished } impl GameStatusFelt252 of Into<GameStatus, felt252> { // Converts a GameStatus variant to its corresponding `felt252` value fn into(self: GameStatus) -> felt252 { match self { GameStatus::NotStarted => 0, GameStatus::Lobby => 1, GameStatus::InProgress => 2, GameStatus::Finished => 3, } } } Structuring Game Logic with Traits and enum Building upon the GameStatus enum, we can define a Game struct that includes a GameStatus field. By implementing a custom trait for the Game struct, we can encapsulate game-specific logic and assertions. #[derive(Copy, Drop, Serde)] #[dojo::model] // Define the Game struct to represent a game object struct Game { #[key] id: u64, status: GameStatus, } // Implement the trait for the Game struct #[generate_trait] impl GameImpl of GameTrait { // Asserts that the game is in progress fn assert_in_progress(self: Game) { assert(self.status == GameStatus::InProgress, "Game not started"); } // Asserts that the game is in the lobby fn assert_lobby(self: Game) { assert(self.status == GameStatus::Lobby, "Game not in lobby"); } } Enum Placement Regarding the placement of the enum, it's generally best practice to define enums in the same file or module where they are used, especially if they are closely tied to the functionality of that system. This makes the code easier to understand and maintain, as the enum is defined in context. However, if the enum is used across multiple systems or components, it might make sense to define it in a common module that can be imported wherever needed. This approach helps to avoid duplication and keeps the codebase organized. Given that Eternum has one enum in each system, it suggests a design where each system is self-contained and has its own set of related events or states. This design can help to encapsulate the logic of each system, making the codebase easier to navigate and understand. Important Of Enums Semantic Clarity: Enums provide semantic clarity by giving meaningful names to specific values. Instead of using arbitrary integers or strings, you can use descriptive identifiers. For example, consider an enum representing different player states: Idle, Running, Jumping, and Attacking. These names convey the purpose of each state more effectively than raw numeric values. Avoiding Magic Numbers: Magic numbers (hard-coded numeric values) in your code can be confusing and error-prone. Enums help you avoid this pitfall. Suppose you have an event system where different events trigger specific actions. Instead of using 0, 1, 2, etc., you can define an enum like this: enum Event { PlayerSpawned, EnemyDefeated, ItemCollected, } Now, when handling events, you can use Event::PlayerSpawned instead of an arbitrary number. Type Safety: Enums provide type safety. Each enum variant has a type, preventing accidental mixing of incompatible values. For instance, if you have an enum representing different power-ups. you can’t mistakenly assign a PowerUp value to a variable expecting a different type. enum PowerUp { Health, SpeedBoost, Invincibility, } Pattern Matching: Enums shine when used in pattern matching (also known as switch/case statements). You can handle different cases based on the enum variant, making your code more expressive and concise. Example: fn handle_power_up(power_up: PowerUp) { match power_up { PowerUp::Health => println!("Restored health!"), PowerUp::SpeedBoost => println!("Zooming ahead!"), PowerUp::Invincibility => println!("Invincible!"), } } Extensibility: Enums allow you to add new variants without breaking existing code. Suppose you later introduce a DoubleDamage power-up. You can simply extend the PowerUp enum: enum PowerUp { Health, SpeedBoost, Invincibility, DoubleDamage, } Enums serve as powerful tools for creating expressive, self-documenting code. They enhance readability, prevent errors, and facilitate better software design. Models examples In practice with modularity in mind Consider a tangible analogy: Humans and Goblins. While they possess intrinsic differences, they share common traits, such as having a position and health. However, humans possess an additional model. Furthermore, we introduce a Counter model, a distinct feature that tallies the numbers of humans and goblins. #[derive(Copy, Drop, Serde)] #[dojo::model] struct Potions { #[key] id: u32, quantity: u8, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Health { #[key] id: u32, health: u8, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Position { #[key] id: u32, x: u32, y: u32 } // Special counter model #[derive(Copy, Drop, Serde)] #[dojo::model] struct Counter { #[key] counter: u32, goblin_count: u32, human_count: u32, } Custom Types For user defined types, it's crucial to implement the Introspect trait if you plan to use those types inside a model. Two possible cases: If the user-defined type contains only Cairo built-in types and is defined within your project, simply derive Introspect and the implementation for the type will be handled automatically by Cairo. #[derive(Drop, Serde, Introspect)] struct Stats { atk: u8, def: u8, } If the user-defined type includes a type that is either defined outside of your project or is an unsupported type, you will need to manually implement the Introspect trait. Primitive types: bool u8 u16 u32 u64 u128 u256 i8 i16 i32 i64 i128 felt252 ClassHash ContractAddress EthAddress ByteArray - mostly used for dynamica sized strings Tuples - like (u32, u32) Array - like Array<u32>