Skip to main content

๐ŸŽฎ 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 named hero in the move_slayers package
  • use std::signer - Imports the signer type for account authentication
  • use std::vector - For dynamic arrays (inventory system)
  • use std::string - For human-readable item names
  • use 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 items
  • has 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 item
  • name: string::String - Human-readable item name
  • item_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 address
  • has key - Can be stored as a top-level resource in global storage
  • level: u8 - Player level (1-255, sufficient for most games)
  • exp: u64 - Experience points (64-bit allows for large numbers)
  • health/mana - Current values
  • max_health/max_mana - Maximum values (increase with level)
  • inventory: vector<Item> - Dynamic array of items
  • equipped_*: 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 modules
  • account: &signer - Reference to the account that will own the player
  • move_to(account, Player {...}) - Creates and stores the Player resource at the account's address
  • vector::empty<Item>() - Creates an empty vector to hold items
  • option::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 resource
  • borrow_global_mut<Player>(signer::address_of(account)) - Gets mutable reference to Player resource
  • signer::address_of(account) - Converts signer to account address
  • vector::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 logic
  • TYPE_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 (no public) - Private helper function
  • &mut vector<Item> - Mutable reference to inventory
  • while (i < len) - Manual loop through inventory
  • vector::borrow(inventory, i) - Gets reference to item at index i
  • vector::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 equipped
  • option::borrow(&player.equipped_sword) - Gets reference to equipped sword
  • enemies::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 once
  • player.exp = player.exp - required_exp - Subtracts used EXP
  • player.max_health = player.max_health + 20 - Increases max stats
  • player.health = player.max_health - Full heal on level up
  • required_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 stored
  • name - Human-readable enemy name
  • health - Current health points
  • attack - Damage dealt when counterattacking
  • exp_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 array
  • b"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 code
  • use 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 syntax
  • account::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:โ€‹

  1. More Enemy Types - Different stats and behaviors
  2. Item Effects - Shields reduce damage, armor provides defense
  3. Spell System - Mana-based abilities
  4. Quest System - Objectives and rewards
  5. Multiplayer - Player vs player combat
  6. 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