๐ฎ Building an On-Chain RPG with Move: A Complete Educational Guide
This guide walks through every line of code in the move_slayers
game, explaining how to build an on-chain RPG using the Move language on Movement.
๐ What You'll Learnโ
- Move Language Fundamentals: Structs, functions, resource management
- On-Chain Game Design: State management, player progression, combat systems
- Best Practices: Resource safety, error handling, modular design
- Real-World Patterns: How to structure complex on-chain applications
๐๏ธ Project Structureโ
move_slayers/
โโโ sources/
โ โโโ hero.move # Player logic and game mechanics
โ โโโ enemies.move # Enemy definitions and combat
โโโ tests/
โ โโโ hero_tests.move # Comprehensive test suite
โโโ Move.toml # Project configuration
๐ฆ Module 1: Hero System (sources/hero.move
)โ
๐ง Module Declaration and Importsโ
module move_slayers::hero {
use std::signer;
use std::vector;
use std::string;
use std::option;
use move_slayers::enemies;
Explanation:
module move_slayers::hero
- Declares this as a Move module namedhero
in themove_slayers
packageuse std::signer
- Imports the signer type for account authenticationuse std::vector
- For dynamic arrays (inventory system)use std::string
- For human-readable item namesuse std::option
- For optional values (equipment slots can be empty)use move_slayers::enemies
- Imports our enemy module for combat
๐ท๏ธ Constants and Type Definitionsโ
const TYPE_SWORD: u8 = 0;
const TYPE_SHIELD: u8 = 1;
const TYPE_ARMOR: u8 = 2;
const TYPE_POTION: u8 = 3;
Explanation:
- These constants define item types using
u8
(8-bit unsigned integers) - Using constants instead of magic numbers makes code more readable
u8
is memory-efficient for simple enums
๐ฆ Item Structureโ
struct Item has copy, drop, store {
id: u64,
name: string::String,
item_type: u8,
power: u64,
}
Explanation:
struct Item
- Defines a custom data type for game itemshas copy, drop, store
- Move capabilities:copy
: Can be duplicated (useful for testing)drop
: Can be discarded (when items are consumed)store
: Can be stored in global storage (inventory)
id: u64
- Unique identifier for each itemname: string::String
- Human-readable item nameitem_type: u8
- References our constants (0=sword, 1=shield, etc.)power: u64
- Effect strength (damage for weapons, heal for potions)
๐ค Player Structureโ
struct Player has key {
level: u8,
exp: u64,
health: u64,
mana: u64,
max_health: u64,
max_mana: u64,
inventory: vector<Item>,
equipped_sword: option::Option<Item>,
equipped_shield: option::Option<Item>,
equipped_armor: option::Option<Item>,
}
Explanation:
struct Player has key
- Player is a resource stored at an account addresshas key
- Can be stored as a top-level resource in global storagelevel: u8
- Player level (1-255, sufficient for most games)exp: u64
- Experience points (64-bit allows for large numbers)health/mana
- Current valuesmax_health/max_mana
- Maximum values (increase with level)inventory: vector<Item>
- Dynamic array of itemsequipped_*: option::Option<Item>
- Optional equipment slots (can be empty)
๐ฎ Player Initializationโ
/// Initializes a new player with base stats and empty inventory
public fun init_player(account: &signer) {
move_to(account, Player {
level: 1,
exp: 0,
health: 100,
mana: 50,
max_health: 100,
max_mana: 50,
inventory: vector::empty<Item>(),
equipped_sword: option::none<Item>(),
equipped_shield: option::none<Item>(),
equipped_armor: option::none<Item>(),
});
}
Explanation:
public fun
- Function accessible from other modulesaccount: &signer
- Reference to the account that will own the playermove_to(account, Player {...})
- Creates and stores the Player resource at the account's addressvector::empty<Item>()
- Creates an empty vector to hold itemsoption::none<Item>()
- Creates empty optional values for equipment slots
๐ Inventory Managementโ
/// Adds an item to the player's inventory
public fun add_item(account: &signer, item: Item) acquires Player {
let player = borrow_global_mut<Player>(signer::address_of(account));
vector::push_back(&mut player.inventory, item);
}
Explanation:
acquires Player
- Tells Move this function will access the Player resourceborrow_global_mut<Player>(signer::address_of(account))
- Gets mutable reference to Player resourcesigner::address_of(account)
- Converts signer to account addressvector::push_back(&mut player.inventory, item)
- Adds item to end of inventory
โ๏ธ Equipment Systemโ
/// Equips a sword from inventory
public fun equip_sword(account: &signer, item_id: u64): bool acquires Player {
let player = borrow_global_mut<Player>(signer::address_of(account));
equip_specific(&mut player.inventory, item_id, TYPE_SWORD, &mut player.equipped_sword)
}
Explanation:
- Returns
bool
- Success/failure of equipping equip_specific()
- Helper function that handles the actual equipping logicTYPE_SWORD
- Ensures only swords can be equipped in this slot
๐ง Helper Function: Equipment Logicโ
/// Helper: equips item of specific type if found in inventory
fun equip_specific(
inventory: &mut vector<Item>,
item_id: u64,
required_type: u8,
equip_slot: &mut option::Option<Item>
): bool {
let i = 0;
let len = vector::length(inventory);
while (i < len) {
let item_ref = vector::borrow(inventory, i);
if (item_ref.id == item_id && item_ref.item_type == required_type) {
let item = vector::remove(inventory, i);
*equip_slot = option::some(item);
return true;
};
i = i + 1;
};
false
}
Explanation:
fun
(nopublic
) - Private helper function&mut vector<Item>
- Mutable reference to inventorywhile (i < len)
- Manual loop through inventoryvector::borrow(inventory, i)
- Gets reference to item at index ivector::remove(inventory, i)
- Removes and returns item at index i*equip_slot = option::some(item)
- Stores item in equipment slot- Returns
false
if item not found
๐งช Potion Systemโ
/// Uses a potion to restore health
public fun use_potion(account: &signer, item_id: u64): bool acquires Player {
let player = borrow_global_mut<Player>(signer::address_of(account));
let i = 0;
let len = vector::length(&player.inventory);
while (i < len) {
let item_ref = vector::borrow(&player.inventory, i);
if (item_ref.id == item_id && item_ref.item_type == TYPE_POTION) {
let potion = vector::remove(&mut player.inventory, i);
player.health = min(player.health + potion.power, player.max_health);
return true;
};
i = i + 1;
};
false
}
Explanation:
- Searches inventory for potion with matching ID
min(player.health + potion.power, player.max_health)
- Heals but doesn't exceed max health- Consumes potion by removing it from inventory
- Returns
true
if potion used,false
if not found
โ๏ธ Combat Systemโ
/// Player attacks an enemy. If enemy survives, it counterattacks. If enemy dies, player gains EXP.
public fun attack_enemy(account: &signer, enemy: &mut enemies::Enemy): bool acquires Player {
let player = borrow_global_mut<Player>(signer::address_of(account));
let damage = if (option::is_some(&player.equipped_sword)) {
let sword_ref = option::borrow(&player.equipped_sword);
sword_ref.power
} else {
5 // base attack
};
let killed = enemies::take_damage(enemy, damage);
if (killed) {
slay_enemy(account, enemies::get_exp_reward(enemy));
true
} else {
// Enemy counterattacks
let attack = enemies::get_attack(enemy);
if (player.health <= attack) {
player.health = 0; // player dies
} else {
player.health = player.health - attack;
};
false
}
}
Explanation:
enemy: &mut enemies::Enemy
- Mutable reference to enemy (can modify its health)option::is_some(&player.equipped_sword)
- Checks if sword is equippedoption::borrow(&player.equipped_sword)
- Gets reference to equipped swordenemies::take_damage(enemy, damage)
- Calls enemy module to apply damage- If enemy killed: calls
slay_enemy()
to award EXP - If enemy survives: enemy counterattacks and damages player
โญ Leveling Systemโ
/// Awards EXP for slaying an enemy and handles leveling
public fun slay_enemy(account: &signer, exp_reward: u64) acquires Player {
let player = borrow_global_mut<Player>(signer::address_of(account));
player.exp = player.exp + exp_reward;
let required_exp = exp_required(player.level);
while (player.exp >= required_exp) {
player.exp = player.exp - required_exp;
player.level = player.level + 1;
player.max_health = player.max_health + 20;
player.max_mana = player.max_mana + 10;
player.health = player.max_health;
player.mana = player.max_mana;
required_exp = exp_required(player.level);
};
}
Explanation:
while (player.exp >= required_exp)
- Handles multiple level-ups at onceplayer.exp = player.exp - required_exp
- Subtracts used EXPplayer.max_health = player.max_health + 20
- Increases max statsplayer.health = player.max_health
- Full heal on level uprequired_exp = exp_required(player.level)
- Recalculates for next level
๐ EXP Calculationโ
/// Returns EXP required to level up (exponential)
fun exp_required(level: u8): u64 {
100 * (1u64 << (level - 1))
}
Explanation:
1u64 << (level - 1)
- Bit shift for exponential growth- Level 1โ2: 100 * 2^0 = 100 EXP
- Level 2โ3: 100 * 2^1 = 200 EXP
- Level 3โ4: 100 * 2^2 = 400 EXP
๐ ๏ธ Utility Functionsโ
/// Helper: finds minimum of two values
fun min(a: u64, b: u64): u64 {
if (a < b) a else b
}
/// Helper: finds maximum of two values
fun max(a: u64, b: u64): u64 {
if (a > b) a else b
}
Explanation:
- Simple utility functions for common operations
- Used in potion healing to prevent exceeding max health
๐ Public Gettersโ
/// Public getters for Player fields
public fun get_level(player: &Player): u8 { player.level }
public fun get_exp(player: &Player): u64 { player.exp }
public fun get_health(player: &Player): u64 { player.health }
public fun get_mana(player: &Player): u64 { player.mana }
public fun get_max_health(player: &Player): u64 { player.max_health }
public fun get_max_mana(player: &Player): u64 { player.max_mana }
Explanation:
- Read-only access to player stats
- Useful for UI/display purposes
- Follows encapsulation principles
๐ Module 2: Enemy System (sources/enemies.move
)โ
๐ง Module Declarationโ
module move_slayers::enemies {
use std::string;
Explanation:
- Simple module with just string import for enemy names
- Focused on enemy creation and combat interaction
๐น Enemy Structureโ
/// Enemy definition
struct Enemy has copy, drop, store {
name: string::String,
health: u64,
attack: u64,
exp_reward: u64,
}
Explanation:
has copy, drop, store
- Can be duplicated, discarded, and storedname
- Human-readable enemy namehealth
- Current health pointsattack
- Damage dealt when counterattackingexp_reward
- Experience points awarded when killed
๐ Enemy Creationโ
/// Spawns a weak early-game boar
public fun spawn_boar(): Enemy {
Enemy {
name: string::utf8(b"Boar"),
health: 30,
attack: 5,
exp_reward: 50,
}
}
Explanation:
string::utf8(b"Boar")
- Creates string from byte arrayb"Boar"
- Byte literal syntax- Balanced stats for early-game progression
โ๏ธ Combat Mechanicsโ
/// Applies damage to the enemy. Returns true if enemy is killed.
public fun take_damage(enemy: &mut Enemy, amount: u64): bool {
if (enemy.health <= amount) {
enemy.health = 0;
true
} else {
enemy.health = enemy.health - amount;
false
}
}
Explanation:
&mut Enemy
- Mutable reference to modify enemy health- Returns
bool
- Whether enemy was killed - Sets health to 0 if damage exceeds current health
- Otherwise subtracts damage from health
๐ Public Gettersโ
/// Public getters for Enemy fields
public fun get_health(enemy: &Enemy): u64 { enemy.health }
public fun get_attack(enemy: &Enemy): u64 { enemy.attack }
public fun get_exp_reward(enemy: &Enemy): u64 { enemy.exp_reward }
public fun get_name(enemy: &Enemy): &string::String { &enemy.name }
Explanation:
- Read-only access to enemy properties
&string::String
- Returns reference to avoid copying
๐ญ Factory Functionโ
/// Public factory for Enemy (for testing)
public fun make_enemy(name: string::String, health: u64, attack: u64, exp_reward: u64): Enemy {
Enemy { name, health, attack, exp_reward }
}
Explanation:
- Factory pattern for creating custom enemies
- Useful for testing and future expansion
- Allows creation of different enemy types
๐งช Testing (tests/hero_tests.move
)โ
๐ง Test Module Setupโ
#[test_only]
module move_slayers::hero_tests {
use std::signer;
use std::string;
use std::vector;
use std::option;
use std::account;
use move_slayers::hero;
use move_slayers::enemies;
Explanation:
#[test_only]
- Compile-time attribute for test-only codeuse std::account
- For creating test accounts- Imports all necessary modules for testing
๐ฏ Test Helperโ
// Test account addresses
const PLAYER_ADDR: address = @0x1;
// Test helper: creates a test signer
fun create_test_signer(): signer {
account::create_account_for_test(PLAYER_ADDR)
}
Explanation:
@0x1
- Address literal syntaxaccount::create_account_for_test()
- Creates test account- Reusable helper for all tests
โ Sample Testโ
#[test]
fun test_hero_creation() {
let player = create_test_signer();
hero::init_player(&player);
// Test that player was created successfully by trying to add an item
let test_item = hero::make_item(1, string::utf8(b"Test Item"), hero::type_sword(), 10);
hero::add_item(&player, test_item);
}
Explanation:
#[test]
- Marks function as a test- Creates player and adds item to verify initialization worked
- Tests basic functionality without explicit assertions
๐ Key Learning Pointsโ
1. Resource Safetyโ
- Move's type system prevents common bugs
- Resources can't be accidentally duplicated or lost
- Global storage is type-safe
2. Modular Designโ
- Separate modules for different concerns
- Clear interfaces between modules
- Easy to extend and maintain
3. On-Chain State Managementโ
- Player data stored as resources
- Immutable references for reading
- Mutable references for writing
4. Error Handlingโ
- Return
bool
for success/failure - Use
option::Option<T>
for optional values - Graceful handling of edge cases
5. Testing Best Practicesโ
- Comprehensive test coverage
- Test-only modules
- Helper functions for common test setup
๐ฏ Next Stepsโ
Ideas for Extension:โ
- More Enemy Types - Different stats and behaviors
- Item Effects - Shields reduce damage, armor provides defense
- Spell System - Mana-based abilities
- Quest System - Objectives and rewards
- Multiplayer - Player vs player combat
- NFT Integration - Unique items as NFTs
Advanced Concepts to Explore:โ
- Events - Emit game events for frontend
- Capabilities - Fine-grained access control
- Generics - Reusable code patterns
- Abort Codes - Custom error handling