Building the Capy Prototype on Sui

Sui Capys, Mysten Labs’ new prototype, serves as a developer preview and demonstrates key capabilities of the Sui ecosystem.

Building the Capy Prototype on Sui

Sui Capys, Mysten Labs’ new prototype, serves as a developer preview and demonstrates key capabilities of the Sui ecosystem. In this decentralized game, players buy, trade, breed, and accessorize Capybaras, the cute South American semi-aquatic rodent. Sui developers can adapt many of the principles and code examples outlined here for their own projects.

As programmable objects on the Sui blockchain, Capys demonstrate principles such as asset ownership, transferability, and dynamic object fields. They appear in a player’s wallet along with accessories, including hats, bicycles, and scarves. To preview Capys now, head to Capy.art.

Developing Sui Capys using Move on Sui required defining the basic modules, creating types, and most importantly, building a registry to record and verify Capys. One unique feature of the prototype is the ability to breed two existing Capys, generating a completely new one based on characteristics of the breeding pair.

To reiterate, this is a developer preview to showcase unique aspects of Sui and for developers to reference when building projects of their own. Capys are not for sale.

Disclaimers

  • This dApp is currently considered an early Alpha, so it may be a little rough around the edges.
  • The dApp runs on Sui's Devnet which does not have the maturity and stability of a Mainnet.
  • There is a known Sui Wallet bug on transaction history when using Capys, and we'll be shipping a Wallet fix next week to address this.
  • We’re planning to update capy.art to the wallet adapter standard in the near future
  • Sui Capys is a demo specifically to inspire our developer community. This is not an airdrop. Please faucet responsibly – do not spam our Devnet faucet.

Capy Architecture

The Capy application (Github) consists of three modules: capy, capy_items, and capy_market. These modules define the Capys, accessories, and trading mechanism.

We began the development process by defining specific principles to make the prototype enjoyable and performant. The following principles helped determine the architecture and implementation choices:

  • Capys should be freely transferable and usable in any on-chain application.
  • Types should contain a minimum amount of data to maintain performance.
  • Events can be used to emit static data to be fetched by the indexer.
  • The prototype should be extendable so new properties can be added later.

Developers looking to build games and applications on Sui should begin by defining core concepts such as these, which will guide the development process.

Capy Core

The capy module defines the core functionality of Sui Capys: it defines a Capy type as well as granting the publisher with a CapyManagerCap, opening admin functions for the bearer. It defines CapyRegistry, the centralized prototype state, and ways in which it can evolve.

Type: Capy

Capy, the main type of the application, is an owned object with a defined set of attributes: 32 genes and additional utility information required for prototype features. Capy has two abilities, key and store. The former makes it an own-able asset and the latter allows free transfer and wrapping.

  • The gen property marks the Capy’s generation. First Capys have gen 0; newer breeds have their parents’ generation plus one, so gen 1, gen 2, etc.
  • The src property enables explorer display of the image. Capys themselves don’t store their full image as it is dynamic and can be changed when new items are added.
  • The genes property stores the gene sequence, a 32-byte vector which is used to calculate attributes and selecting genes for a newborn during breeding.
  • The item_count is a utility property tracking the number of objects attached to each Capy.
  • The attributes property stores human-readable attributes generated during breeding. For example, { “name": "pattern", "value": "panda" }.

This set of fields is a minimal requirement for Sui Capys functions, including breeding or adding/removing items.

Untitled.png
/// The Capy itself. Every Capy has its unique set of genes,
/// as well as generation and utility information. Ownable, tradeable.
struct Capy has key, store {
    id: UID,
    gen: u64,
    url: Url,
    genes: Genes,
    item_count: u8,
    attributes: vector<Attribute>,
}

Type: Capy Registry

CapyRegistry, a shared object required for breeding, stores the total number of Capys ever born and contains a pseudo-random seed, described below in the Gene science section, used for gene selection during breeding. It contains all attribute definitions assigned to newborns at the breeding stage.

New attributes can be added to the prototype, as described in the admin features section below.

/// Every capybara is registered here. CapyRegistry acts as a source of randomness
/// as well as the storage for the main information about the game state.
struct CapyRegistry has key {
    id: UID,
    capy_born: u64,
    capy_hash: vector<u8>,
    genes: vector<GeneDefinition>
}

Type: CapyManagerCap

The CapyManagerCap is a capability sent to the module publisher (the sender of the publishing transaction) when a module publishes. It authorizes admin actions in all of the modules, including capy_items and capy_market.

Initializer

Capys are a standalone (non-generic) application, so their main logic can be launched in the module initializer. The init function does two things:

  1. Creates a CapyManagerCap and sends it to the module publisher.
  2. Creates and shares a CapyRegistry.

Admin Functions

For the application to become playable and have some meaning, admin has to perform a set of actions:

  • The add_gene function registers a new GeneDefinition in the CapyRegistry. During breeding, all existing attributes in the registry are assigned to a new Capy. If a new GeneDefinition (Attribute) was added to the prototype, Capys born before this addition won’t get it, but their children will. Each gene definition has a name and a set of selectors which are used to choose the value of each attribute.
  • The batch function allows batch creation of Capys with predefined genes. It is used for initialization and in later stages to populate the marketplace with more Capys for new users.

Breeding

The main piece of logic which creates unpredictability and helps prototype evolution is the capy::breed function. Any player with two Capys can perform this function. Logic for this function is the following:

  1. Based on CapyRegistry.capy_hash select parent genes for the new Capy.
  2. Get list of current GeneDefinitions from the CapyRegistry and set attributes.
  3. Emit an event with the new Capy’s data.
  4. Return a new Capy (use breed_and_keep to send to sender).
 public fun breed(
    reg: &mut CapyRegistry, c1: &mut Capy, c2: &mut Capy, ctx: &mut TxContext
): Capy {
    let id = object::new(ctx);

    // Update capy hash in the registry
    vec::append(&mut reg.capy_hash, object::uid_to_bytes(&id));
    reg.capy_hash = hash(reg.capy_hash);

    // compute genes
    let genes = compute_genes(&reg.capy_hash, &c1.genes, &c2.genes, GENES);
    let gen = math::max(c1.gen, c2.gen) + 1;
    let attributes = get_attributes(&reg.genes, &genes);
    let sender = tx_context::sender(ctx);

    emit(CapyBorn { /* ... */ }); 

    // Send newborn to parents.
    Capy {
        url: img_url(&id),
        id,
        gen,
        genes,
        attributes,
        item_count: 0,
    }
}

Gene Science

Before we get to the most interesting part of the application, we must note that this solution does not provide absolute unpredictability, and therefore should not be used for applications with high stakes. But it does create some degree of randomness by turning user input into a pseudo-random modifier.

Untitled (1).png

A really fun and unique feature of this prototype is the ability to breed two existing Capys to generate a third. The new Capy inherits characteristics of its parents.

The breed function takes two parent Capys and computes genes for the newborn. For this action to be fair and random, we need to have a selection algorithm and a seed. CapyRegistry provides the seed (stored as capy_hash) and is updated after each breed. The algorithm is the following:

83C758FA-D971-4CAB-BE6D-346DDA6B9C5F.jpeg
  1. Use hashing function (sha3_256) three times with salt to generate three vectors of 32 bytes (marked as A, B, and C) derived from the capy_hash.
  2. Use the first vector (A) for parent gene selection. If the value of the Nth byte is more than 126, select the first parent’s gene. Otherwise, select the second parent’s gene. As shown in the diagram above, the first gene will be P2, second P1, third P2, and fourth P1 again (up to N=32).
  3. The second vector (B) defines a chance of mutation. If the value in position N is more than 250, use the same position in the third vector (C) to select a value for mutation. In this example, the third gene will mutate and its value will be 42.
/// Computes genes for the newborn based on the 'random' seed r0, and parents' genes.
///
/// The `max` parameter affects how many genes should be changed.
/// For example, if the number of genes is 32 but only 4 Attributes defined in the
/// registry, the `max` should be set to 4. Remainder genes should not mutate.
fun compute_genes(r0: &vector<u8>, g1: &Genes, g2: &Genes, max: u64): Genes {
    let i = 0;

    let s1 = &g1.sequence;
    let s2 = &g2.sequence;
    let s3 = vec::empty();

    let r1 = derive(r0, 1); // for parent gene selection
    let r2 = derive(r0, 2); // chance of random mutation
    let r3 = derive(r0, 3); // value selector for random mutation

    while (i < max) {
        let rng = *vec::borrow(&r1, i);
        let gene = if (lor(rng, 127)) {
            *vec::borrow(s1, i)
        } else {
            *vec::borrow(s2, i)
        };

        // There's a tiny chance that a mutation will happen.
        if (lor(*vec::borrow(&r2, i), MUTATION_CHANCE)) {
            gene = *vec::borrow(&r3, i);
        };

        vec::push_back(&mut s3, gene);
        i = i + 1;
    };

    Genes { sequence: s3 }
}

Capy Items

This drastically simple module defines wearable items which can be added to each Capy, and how we implement the frontend display. Items can only be added by the Capy Admin, as it requires authorization with a CapyManagerCap.

Untitled (2).png
/// Wearable item. Has special display in capy.art application
struct CapyItem has key, store {
  id: UID,
  url: Url,
  type: String,
  name: String,
}

Managing Items

Adding and removing accessories to Capys makes use of dynamic object fields, a more efficient and user-friendly replacement for parent-child objects in Sui. Dynamic Fields allow arbitrary names and can be added and removed on-the-fly.


The following code adds an item to a Capy:

entry fun add_item<T: key + store>(capy: &mut Capy, item: T) {
    emit(ItemAdded<T> {
        capy_id: object::id(capy),
        item_id: object::id(&item)
    });

    dof::add(&mut capy.id, object::id(&item), item);
}

Capy Market

To acquire and sell Capys and CapyItems, we created the Capy Market. This module makes use of dynamic object fields and locks Items to be acquirable after paying their price. In this marketplace architecture, one marketplace object exists per one item type (CapyMarket<Capy> sells Capys while a different object, CapyMarket<CapyItem>, sells Capy accessories), listings are attached to the marketplace as dynamic object fields, and listed objects are attached to Listings.

+--> Listing --> T
CapyMarket<T> +--> Listing --> T
+--> Listing --> T

Marketplace and the List Function

Each Marketplace instance serves only one type. In this application, one instance of Marketplace exists for the Capy type and another for the CapyItem type.

/// A generic marketplace for anything. T marks the type for Listings.
struct CapyMarket<phantom T: key + store> has key { id: UID }

/// A listing for the marketplace. Contains the price and owner of the Listing.
struct Listing<phantom T: key + store> has key, store {
    id: UID,
    price: u64,
    owner: address
}

The List function makes use of Dynamic Fields. It makes a listed Item a field of a Listing and then makes Listing a field of CapyMarket.

/// List a new item on the `CapyMarket`.
public entry fun list<T: key + store>(
    market: &mut CapyMarket<T>,
    item: T,
    price: u64,
    ctx: &mut TxContext
) {
    let id = object::new(ctx);
    let item_id = object::id(&item);
    let owner = tx_context::sender(ctx);

    emit(ItemListed<T> { /* ... */ });

    // First attach Item to the Listing with a boolean `true` value;
    // Then attach listing to the marketplace through `item.id`;
    dynamic_object_field::add(&mut id, true, item);
    dynamic_object_field::add(&mut market.id, item_id, Listing<T> { 
id, price, owner 
});
}

Capy Inspiration

We created the Capy prototype to show off some key Sui features and inspire developers in their own projects. Capys leverage Sui’s object-oriented nature to allow portable accessories that players can trade and buy, and the generation of new Capys based on parental attributes. We designed the prototype to be infinitely extendable so that it will delight users and developers alike.

We hope the examples and code here prove useful to Sui developers. A few implementations that should stand out include the Capy Market, Accessories, and Breeding. The Capy Market serves as a model for any trading or store mechanism. Accessories show a good use ofDynamic Fields. Breeding offers a unique means of automatically generating new objects with infinite applications.