Cómo desarrollar un juego “Play To Earn” en NEAR Protocol

19 min read
To Share and +4 nLEARNs

Revisión profunda del prototipo actual – BerryClub

Esta guía está destinada a permitirle comprender la dinámica básica que le permitirá crear un juego “Play To Earn” en Near Protocol.

No queremos hacer millonarios ni a ti ni a tus jugadores, pero la mecánica de Near funciona de tal manera que las transacciones pequeñas son muy útiles en una interfaz de usuario HTML5 simple y crean un juego de farming simple y divertido.

Analizaremos un contrato inteligente y los archivos js/rust detrás de un juego de farming existente, berryclub.  Si aplicas la misma lógica a tus ideas de juego, ¡Puedes obtener resultados aún más divertidos!

Para poder continuar por el lado técnico de este tutorial, recomendamos encarecidamente verificar el figment Near Pathway para construir una transacción y construir su primer contrato inteligente en Near, los conceptos presentes en esas guías no están incluidos en este tutorial.  Será mucho más fácil para usted seguir este tutorial si mantiene una pestaña abierta con el archivo fuente vinculado mientras lee la explicación, porque a gran parte del código mencionado se le hace referencia con números de línea pero no se muestra en esta guía .  El repositorio original de BerryClub se bifurcó para este tutorial con el fin de mantener este esfuerzo válido en la medida en que el código todavía funcione en la cadena de bloques;  Como verá en la siguiente sección, el juego en sí mismo ha evolucionado durante todo el tiempo y volverá a evolucionar, quisimos dejar su estado en este momento.

La interfaz de Berryclub

Berryclub está construido con React, por lo que a lo primero que entraremos es el archivo app.js ubicado en la carpeta src del repositorio de github, esto nos ahorrará tiempo al analizar el contrato, permitiéndonos enfocarnos en lo que necesitamos extrapolar, la lógica fuera del juego real (qué, por cierto, es divertido y jugado por una gran comunidad).

Después de importar react, la primera biblioteca necesaria es bn.js, una herramienta simple para administrar números enteros o no decimales, muchas características pueden llegar a su interfaz con esta biblioteca, en primer lugar aquí se usa para administrar las transacciones:

const PixelPrice = new BN(“10000000000000000000000”);

El juego berryclub se basa en la economía inmobiliaria, hay un tablero administrado por una parte del contrato llamado board.rs, está subdividido en píxeles y cada píxel tiene un precio que hay que pagar para poder dibujar en él.  La mecánica de la acción de “dibujar” es el núcleo de las habilidades de Farming y de autoabastecimiento del juego y se analizará en profundidad cuando lleguemos al contrato inteligente.

Como puede ver aquí, el precio de un solo píxel se declara como una constante al comienzo de nuestra aplicación, y puede modificarse utilizando las herramientas frontend y la biblioteca bn.j.  La segunda importación es el near sdk que nos permite interactuar con la cadena de bloques de Near cómo se explica en la ruta de Figment near.  El primer uso de la near api es para declarar las variables de contrato utilizadas y asegurarse de que se utilice la red principal cuando se ejecute el código desde la url de berryclub:

const IsMainnet = window.location.hostname === "berryclub.io";
const TestNearConfig = {
  networkId: "testnet",
  nodeUrl: "https://rpc.testnet.near.org",
  contractName: "berryclub.testnet",
  walletUrl: "https://wallet.testnet.near.org",
};
const MainNearConfig = {
  networkId: "mainnet",
  nodeUrl: "https://rpc.mainnet.near.org",
  contractName: "berryclub.ek.near",
  walletUrl: "https://wallet.near.org",
};
const NearConfig = IsMainnet ? MainNearConfig : TestNearConfig;

Luego importamos las utilidades react UI para construir nuestra interfaz y permitir que la gente dibuje, react-color, react-switch y react-compound-timer.  La primera de estas utilidades que se usa es el temporizador, se usa para establecer un tiempo de espera para actualizar el tablero en la línea 62.

La “actualización” del tablero la realiza la interfaz para mostrar el estado actualizado de la placa mediante una llamada RPC al contrato inteligente.

const BatchTimeout = 500;
const RefreshBoardTimeout = 1000;
const MaxWorkTime = 10 * 60 * 1000;
const OneDayMs = 24 * 60 * 60 * 1000;

Lo que vemos aquí son dos constantes más de las necesarias para actualizar, las dos últimas de hecho se utilizan para farmear los píxeles después de dibujarlos, y establecen un intervalo de tiempo de un día para calcular las recompensas.  También se declaran otras constantes para gestionar el tablero de acuerdo con el contrato inteligente, y aquí, por primera vez, cruzamos el concepto de líneas, que será muy importante para comprender la gestión del tablero y es el elemento más reutilizable de toda la interfaz:

const BoardHeight = 50;
const BoardWidth = 50;
const NumLinesPerFetch = 50;
const ExpectedLineLength = 4 + 8 * BoardWidth;

Como puede ver, después de subdividir la placa 50 × 50, le decimos a nuestra interfaz que busque solo las líneas que siguen la instrucción RefreshBoardTimeout y que considere la longitud de cada línea como BoardWidth multiplicado por 12, el tamaño de una sola celda.

const CellWidth = 12;
const CellHeight = 12;
const MaxNumColors = 31;
const BatchOfPixels = 100;

Los píxeles se consideran por lotes, no de forma independiente, tanto cuando se llama a la acción de dibujo como cuando se actualiza la interfaz.

Por último, pero no menos importante, nuestras importaciones incluyen un componente de UI personalizado, llamado Weapons.js: este componente se ha desarrollado más tarde en la historia de la comunidad de berryclub, para que cada usuario pueda cargar y dibujar una imagen completa en el tablero, y generarlo en el mercado NFT de berrycards.

Mecánicas DeFi

Las líneas entre 27 y 51 son una referencia útil de cómo este Dapp construye sus habilidades de Farming sobre algunas mecánicas básicas de DeFi que se analizarán en las últimas partes de este tutorial.  A estas alturas, solo mencionamos brevemente que para dibujar / comprar un píxel, berryclub lo canaliza a través de un par de operaciones DeFi en ref.finance utilizando sus propios Tokens específicos, aguacates para comprar píxeles y plátanos obtenidos de los píxeles que compró.

Hay un clon simple de uniswap creado para intercambiar bananas / aguacates que funcionó en el mismo contrato inteligente construido para los otros tokens de este juego / prototipo. También hay un token de farming creado para el juego, llamado pepino, que permite a las personas ganar una parte de los tokens que toda la comunidad de jugadores paga por gas para pintar en el tablero.

La cuenta o cómo los usuarios ganan dinero

Este es el primer paso que damos en el código rust del contrato inteligente, pero sentí la necesidad de recordarte que la mecánica DeFi no es la única forma en que Berryclub te permite ganar tokens. La cuenta tiene un archivo particular en el contrato inteligente de berryclub, pero no necesitamos entrar en eso de inmediato, lo que necesitamos saber es que se recopila cierta información en el objeto de la cuenta que es crucial para la mecánica farming y de ingresos:

  • accountID
  • accountIndex (para la lista (vector) de cuentas que tocaron el tablero de píxeles por última vez)
  • balance(vector para varios tokens propios)
  • number of pixels
  • claimed timestamp (nanosegundos, de cuando la cuenta dada reclamó recompensas por última vez)
  • farming preferences (plátanos o pepinos)

Los dos últimos datos son para calcular las recompensas en un momento dado, por ejemplo, si posee 5 píxeles durante un día, adquiere 5 plátanos. Si le compras a otra persona sus ganancias disminuyen porque la cantidad de píxeles que poseen disminuye, por lo que se calcula la cantidad de ganancias y se renueva la marca de tiempo que relaciona la nueva cantidad de píxeles que posee. Como veremos, las recompensas se calculan en función de estas dos variables. La operación que se aplica a la cuenta del propietario anterior cuando se dibuja un píxel se llama “touch (toque)” y puede encontrarlo en el archivo rust account.rs. La propiedad de la unidad de un solo píxel es la base para ganar en Berryclub y, de esta manera, esta mecánica es prácticamente la misma que podría usar una interfaz de bloqueo (staking)  NFT, recompensando la propiedad de NFT.

pub fn touch(&mut self) -> (Berry, Balance) {
        let block_timestamp = env::block_timestamp();
        let time_diff = block_timestamp - self.claim_timestamp;
        let farm_bonus = if self.farming_preference == Berry::Avocado {
            1
        } else {
            0
        };
        let farmed = Balance::from(self.num_pixels + farm_bonus)
            * Balance::from(time_diff)
            * REWARD_PER_PIXEL_PER_NANOSEC;
        self.claim_timestamp = block_timestamp;
        self.balances[self.farming_preference as usize] += farmed;
        (self.farming_preference, farmed)
    }

Para despejar cualquier duda, el propietario inicial de la placa es 0, el contrato en sí mismo, y si no es posible encontrar un propietario anterior, el contrato se utiliza como el propietario anterior. Finalmente para iniciar el juego se han almacenado algunos tokens en la cuenta del contrato y siempre se incrementan utilizando el precio del gas establecido para que las personas compren aguacates y plátanos, de manera que la “bóveda” del juego siempre se llene de algunos tokens para que los usuarios ganen. Ahora volvamos a nuestra interfaz.

Números a colores y de vuelta

Las líneas entre 67 y 82 en app.js se utilizan para decodificar números en colores y viceversa, para que los elementos de la interfaz de usuario interactúen con el tablero, se definen dos variables constantes, intToColor y rgbaToInt. Lo que podemos notar aquí es que para transformar un número entero en una cadena de colores se utilizan métodos para dividir los 3 números para rojo, verde y azul:

const intToColor = (c) => `#${c.toString(16).padStart(6, "0")}`;
const intToColorWithAlpha = (c, a) =>
  `#${c.toString(16).padStart(6, "0")}${Math.round(255 * a)
    .toString(16)
    .padStart(2, "0")}`;

Para invertir la cadena de color en un entero, simplemente aplicamos una función math.round() y usamos el entero resultante.

const rgbaToInt = (cr, cg, cb, ca, bgColor) => {
  const bb = bgColor & 255;
  const bg = (bgColor >> 8) & 255;
  const br = (bgColor >> 16) & 255;  const r = Math.round(cr * ca + br * (1 - ca));
  const g = Math.round(cg * ca + bg * (1 - ca));
  const b = Math.round(cb * ca + bb * (1 - ca));
  return (r << 16) + (g << 8) + b;
};

Las líneas debajo de estas se refieren a cargar e imprimir imágenes en el tablero usando el componente weapon y no nos ocuparemos de ellas en profundidad: imgColorToInt e int2hsv transforman los números en dos tipos diferentes de escalas de color, luego se define transparentColor y una gamma para la imagen que se imprimirá con generateGamma. En decodeLine transformamos el búfer en una matriz de píxeles para ser impresos en el tablero usando los colores anteriores, iterando a través de ellos con for.

Primer constructor de React

En las siguientes líneas de app.js definimos un constructor que definirá los estados que usaremos más adelante en nuestra interfaz de usuario para interactuar con la blockchain.

class App extends React.Component {
  constructor(props) {
    super(props);

Usar constructor y super nos permitirá usar esto en el constructor.  Los estados definidos aquí son el color y la paleta de colores seleccionados por defecto:

const colors = [
      "#000000",
      "#666666",
      "#aaaaaa",
      "#FFFFFF",
      "#F44E3B",
      "#D33115",
      "#9F0500",
      "#FE9200",
      "#E27300",
      "#C45100",
      "#FCDC00",
      "#FCC400",
      "#FB9E00",
      "#DBDF00",
      "#B0BC00",
      "#808900",
      "#A4DD00",
      "#68BC00",
      "#194D33",
      "#68CCCA",
      "#16A5A5",
      "#0C797D",
      "#73D8FF",
      "#009CE0",
      "#0062B1",
      "#AEA1FF",
      "#7B64FF",
      "#653294",
      "#FDA1FF",
      "#FA28FF",
      "#AB149E",
    ].map((c) => c.toLowerCase());
    // const currentColor = parseInt(colors[Math.floor(Math.random() * colors.length)].substring(1), 16);
    const currentColor = parseInt(colors[0].substring(1), 16);
    const defaultAlpha = 0.25;

Y para el temporizador que refresca el tablero:

const timeMs = new Date().getTime();
    const freeDrawingStartMsEstimated =
      timeMs -
      ((timeMs - new Date("2021-05-09")) % (7 * OneDayMs)) +
      OneDayMs * 6;

Luego, se definen los estados de la cuenta de usuario en uso, lo más importante es si el usuario ha iniciado sesión, si hay transacciones pendientes (definidas como pendingPixels), el estado boardLoaded cargará el tablero para dibujar, los estados selectedCell alpha y pickerColor definen los estados de los componentes interactivos para agregar colores al tablero, junto con pickingColor para elegir el color del tablero y gammaColors es útil para imprimir imágenes en el tablero junto con los estados armsOn y armsCodePosition.

Estos otros estados son útiles para que la cuenta gane en el juego, basado en píxeles, y, basado en DeFi.

owners: [],
      accounts: {},
      highlightedAccountIndex: -1,
      selectedOwnerIndex: false,
      farmingBanana: false,

Mientras que los últimos tres estados configuran el temporizador para su uso posterior:

freeDrawingStart: new Date(freeDrawingStartMsEstimated),
      freeDrawingEnd: new Date(freeDrawingStartMsEstimated + OneDayMs),
      watchMode: false,

La siguiente lista (líneas 203-215) define objetos y acciones que interactuarán con los estados, haciendo referencia a un elemento DOM por primera vez, el tablero de lienzo.

this._buttonDown = false;
    this._oldCounts = {};
    this._numFailedTxs = 0;
    this._balanceRefreshTimer = null;
    this.canvasRef = React.createRef();
    this._context = false;
    this._lines = false;
    this._queue = [];
    this._pendingPixels = [];
    this._refreshBoardTimer = null;
    this._sendQueueTimer = null;
    this._stopRefreshTime = new Date().getTime() + MaxWorkTime;
    this._accounts = {};

Por último, definimos algunos de los estados después de que se realiza el inicio de sesión:

this._initNear().then(() => {
      this.setState(
        {
          connected: true,
          signedIn: !!this._accountId,
          accountId: this._accountId,
          ircAccountId: this._accountId.replace(".", "_"),
          freeDrawingStart: this._freeDrawingStart,
          freeDrawingEnd: this._freeDrawingEnd,
        },
        () => {
          if (window.location.hash.indexOf("watch") >= 0) {
            setTimeout(() => this.enableWatchMode(), 500);
          }
        }
      );
    });

Interacciones básicas

Ahora comenzamos a describir las interacciones en el tablero / lienzo conectándolas a los estados previamente definidos.  Para estas interacciones usamos funciones.  El primero usará nuestra referencia previa al elemento canvas para crearlo e instruirlo con detalles sobre el tipo de movimiento del mouse que permitimos a nuestros usuarios.  En el primer clic habilitamos el modo de reloj para que se inicie nuestro temporizador:

const click = async () => {
      if (this.state.watchMode) {
        return;
      }

Y modo de renderizado de imágenes si el usuario quiere imprimir una imagen en el tablero:

if (this.state.rendering) {
        await this.drawImg(this.state.selectedCell);
      } else if (this.state.pickingColor) {
        this.pickColor(this.state.selectedCell);
      } else {
        this.saveColor();
        await this.drawPixel(this.state.selectedCell);
      }

La siguiente es la parte importante, definimos cómo la interfaz lee el movimiento del mouse y del tacto sobre el tablero:

if ("touches" in e) {
        if (e.touches.length > 1) {
          return true;
        } else {
          const rect = e.target.getBoundingClientRect();
          x = e.targetTouches[0].clientX - rect.left;
          y = e.targetTouches[0].clientY - rect.top;
        }
      } else {
        x = e.offsetX;
        y = e.offsetY;
      }

El código utilizado toma cuidadosamente en consideración a los usuarios móviles, construyendo una función ad-hoc para calcular la posición y agregando un oyente al lienzo / tablero para eventos táctiles: canvas.addEventListener (“touchmove”, mouseMove);  Luego, estas interacciones se utilizan para establecer el estado selectedCell y rastrear el inicio y el final de la acción del mouse / toque en el lienzo junto con su movimiento en cada celda:

const mouseDown = async (e) => {
      this._buttonDown = true;
      if (this.state.selectedCell !== null) {
        await click();
      }
    };    canvas.addEventListener("mousedown", mouseDown);
    canvas.addEventListener("touchstart", mouseDown);    const unselectCell = () => {
      this.setState(
        {
          selectedCell: null,
        },
        () => this.renderCanvas()
      );
    };    const mouseUp = async (e) => {
      this._buttonDown = false;
      if ("touches" in e) {
        unselectCell();
      }
    };    canvas.addEventListener("mouseup", mouseUp);
    canvas.addEventListener("touchend", mouseUp);    canvas.addEventListener("mouseleave", unselectCell);    canvas.addEventListener("mouseenter", (e) => {
      if (this._buttonDown) {
        if (!("touches" in e) && !(e.buttons & 1)) {
          this._buttonDown = false;
        }
      }
    });

La interacción aquí funciona en los estados previamente definidos, como por ejemplo, el selector de color nos permite elegir colores del tablero y usarlos para dibujar.  La tecla que usa el selector de color es la tecla alt y podemos cargar e imprimir imágenes en el tablero solo si el selector de color está deshabilitado, porque entonces activaremos la función generateGamma.  De esta manera, la función pickColor(), referenciada a la celda, se podrá utilizar para establecer un solo píxel o, en su lugar, todo el tablero para representar una imagen:

pickColor(cell) {
    if (!this.state.signedIn || !this._lines || !this._lines[cell.y]) {
      return;
    }
    const color = this._lines[cell.y][cell.x].color;    this.setState(
      {
        currentColor: color,
        alpha: 1,
        pickerColor: intToColorWithAlpha(color, 1),
        gammaColors: generateGamma(int2hsv(color)[0]),
        pickingColor: false,
      },
      () => {
        this.renderCanvas();
      }
    );
  }

Ahora, llegamos al núcleo, así que prepárense para comenzar a bucear en el contrato inteligente. Sabemos cómo dibujar el píxel en la interfaz, pero necesitamos adjuntar las transacciones para que nuestra interfaz sea realmente un juego Play To Earn.  Por lo tanto, preste mucha atención a lo que voy a decir, porque incluso si su juego se ve completamente diferente a éste en términos de interfaz de usuario, la mecánica de ganancias puede ser adecuada para cualquier otro tipo de juego y te lo explicaré aquí de la manera más simple que pueda.

El contrato inteligente de Berryclub

Líneas

Hemos encontrado líneas por primera vez al comienzo de este artículo, mientras consideramos las definiciones de estados de la interfaz de usuario.  Las líneas son un concepto importante de la interfaz de Berryclub, son las filas por las que se subdivide el tablero / lienzo y cada píxel en ellas es una pieza de metadatos.  Son parte de la interfaz de usuario que interactúa con el contrato inteligente y son el objeto más reutilizable del juego (por ejemplo, para crear niveles en un juego más articulado), por lo que dedicaremos un poco de tiempo a analizar cómo son utilizadas para almacenar datos del tablero y evaluadas mientras los usuarios juegan el juego.

En primer lugar, en el archivo board.rs, encontramos una definición de PixelLine justo después de la definición de Pixel:

pub struct PixelLine(pub Vec<Pixel>);impl Default for PixelLine {
    fn default() -> Self {
        Self(vec![Pixel::default(); BOARD_WIDTH as usize])
    }
}

Un vector (arreglo/array) de datos de cadena subdivididos por el ancho del tablero.

Y luego definimos en PixelBoard como un vector de PixelLines de esta manera:

pub struct PixelBoard {
    pub lines: Vector<PixelLine>,
    pub line_versions: Vec<u32>,
}

Por lo tanto, cada línea se almacena en el tablero como un solo registro con un campo de metadatos llamado line_versions que se incrementa cada vez que se modifica una línea.  Entonces, cada vez que nuestra interfaz busca el tablero, obtiene 50 líneas, pero también metadatos para cada línea que representan cuántas veces se actualizó la línea, y al obtener estos metadatos, la interfaz sabe cuál es la cantidad de veces que se cambió la línea, si la línea se ha cambiado desde la recuperación anterior, entonces recupera los datos para cada píxel, si no, simplemente no lo hace.

impl Place {
    pub fn get_lines(&self, lines: Vec<u32>) -> Vec<Base64VecU8> {
        lines
            .into_iter()
            .map(|i| {
                let line = self.board.get_line(i);
                line.try_to_vec().unwrap().into()
            })
            .collect()
    }    pub fn get_line_versions(&self) -> Vec<u32> {
        self.board.line_versions.clone()
    }
}

Esta es una forma inteligente de almacenar y obtener datos de la interfaz que pueden ser útiles para usar en su próximo juego “play to earn” en Near.

Transacciones

Regresemos a nuestra interfaz de usuario en app.js por un momento para asegurarnos de que entendemos cómo se administran las transacciones desde la interfaz. Primero necesitamos una función para verificar la cuenta por si algo sale mal y esta es:

async refreshAllowance() {
    alert(
      "You're out of access key allowance. Need sign in again to refresh it"
    );
    await this.logOut();
    await this.requestSignIn();
  }

Entonces, ¿Recuerdas los arreglos/arrays _queue y _pendingPixels que definimos en nuestro constructor? Definitivamente es hora de usarlos, ya que las transacciones se administran según los píxeles que hayas dibujado en el tablero:

async _sendQueue() {
    const pixels = this._queue.slice(0, BatchOfPixels);
    this._queue = this._queue.slice(BatchOfPixels);
    this._pendingPixels = pixels;    try {
      await this._contract.draw(
        {
          pixels,
        },
        new BN("75000000000000")
      );
      this._numFailedTxs = 0;
    } catch (error) {
      const msg = error.toString();
      if (msg.indexOf("does not have enough balance") !== -1) {
        await this.refreshAllowance();
        return;
 }
      console.log("Failed to send a transaction", error);
      this._numFailedTxs += 1;
      if (this._numFailedTxs < 3) {
        this._queue = this._queue.concat(this._pendingPixels);
        this._pendingPixels = [];
      } else {
        this._pendingPixels = [];
        this._queue = [];
      }
    }
    try {
      await Promise.all([this.refreshBoard(true), this.refreshAccountStats()]);
    } catch (e) {
      // ignore
    }
    this._pendingPixels.forEach((p) => {
      if (this._pending[p.y][p.x] === p.color) {
        this._pending[p.y][p.x] = -1;
      }
    });
    this._pendingPixels = [];
  }

Espera, no estaba listo para este montón de código… ¡Sí, lo estás! Pero veámoslo detenidamente, creamos un objeto de píxeles (vector), modificamos nuestro objeto _queue para que se ajuste a los píxeles y asignamos su valor al objeto _pendingPixel en una función asíncrona.

¿Y entonces qué? Simplemente dibujamos en un objeto de contrato que se llama desde el near sdk, y la acción para dibujar (una parte de las acciones que definimos para el usuario) se define en el archivo rust lib.rs.

pub fn draw(&mut self, pixels: Vec<SetPixelRequest>) {
        if pixels.is_empty() {
            return;
        }
        let mut account = self.get_mut_account(env::predecessor_account_id());
        let new_pixels = pixels.len() as u32;
        if ms_time() < self.get_free_drawing_timestamp() {
            let cost = account.charge(Berry::Avocado, new_pixels);
            self.burned_balances[Berry::Avocado as usize] += cost;
        }
    let mut old_owners = self.board.set_pixels(account.account_index, &pixels);
        let replaced_pixels = old_owners.remove(&account.account_index).unwrap_or(0);
        account.num_pixels += new_pixels - replaced_pixels;
        self.save_account(account);        for (account_index, num_pixels) in old_owners {
            let mut account = self.get_internal_account_by_index(account_index).unwrap();
            self.touch(&mut account);
            account.num_pixels -= num_pixels;
            self.save_account(account);
        }        self.maybe_send_reward();
    }

Para el contrato inteligente, los píxeles son un color y una id de cuenta (el propietario místico), y es un juego basado en bienes raíces: por lo que tenemos un antiguo propietario que dibujó el píxel antes y un nuevo propietario que quiere dibujarlo ahora. Con la acción de dibujar obtenemos el valor old_owner y lo reemplazamos con la nueva cuenta de propietario cambiando el valor de color de todos los píxeles dentro del vector PixelRequest, luego enviamos recompensas al propietario anterior mientras cargamos el nuevo. Las marcas de tiempo de las recompensas se restablecen y el recuento comienza de nuevo desde cero con un píxel menos para el antiguo propietario y uno más para el nuevo. La acción setPixelRequest está definida en el archivo board.rs de nuestro contrato, pero volvamos a nuestro libs.rs.

¿Qué aspecto tiene la función maybe_send_rewards()? Aquí está en todo su esplendor:

impl Place {
    fn maybe_send_reward(&mut self) {
        let current_time = env::block_timestamp();
        let next_reward_timestamp: u64 = self.get_next_reward_timestamp().into();
        if next_reward_timestamp > current_time {
            return;
        }
        self.last_reward_timestamp = current_time;
        let reward: Balance = self.get_expected_reward().into();
        env::log(format!("Distributed reward of {}", reward).as_bytes());
        Promise::new(format!(
            "{}.{}",
            FARM_CONTRACT_ID_PREFIX,
            env::current_account_id()
        ))
        .function_call(
            b"take_my_near".to_vec(),
            b"{}".to_vec(),
            reward,
            GAS_BASE_COMPUTE,
        );
    }
}

Por favor, no seas perezoso, si no puedes esperar para saber más, puedes ver este video del autor del juego. ¡Las explicaciones que voy a usar también están tomadas de ese video!

La función verifica el tiempo en la blockchain (no estamos usando el temporizador en la interfaz aquí, ¡porque queremos estar seguros!) Y usa las capacidades de farming del contrato en una marca de tiempo global con la función get_next_reward_timestamp() y last_reward_timestamp() luego finalmente llama a get_expected_reward() para calcular las recompensas adeudadas a la cuenta.

pub fn get_expected_reward(&self) -> U128 {
        let account_balance = env::account_balance();
        let storage_usage = env::storage_usage();
        let locked_for_storage = Balance::from(storage_usage) * STORAGE_PRICE_PER_BYTE + SAFETY_BAR;
        if account_balance <= locked_for_storage {
            return 0.into();
        }
        let liquid_balance = account_balance - locked_for_storage;
        let reward = liquid_balance / PORTION_OF_REWARDS;
        reward.into()
    }

Así que tomamos el saldo actual de la cuenta de Berryclub (¿Recuerdas que tenemos un campo de saldo en la cuenta?), el uso y los costos actuales de almacenamiento y un umbral de seguridad de 50 aguacates. Si el saldo es seguro para su uso fuera del costo de almacenamiento, lo dividimos en una porción de recompensa de 24 (horas) * 60 (minutos), lo que significa que básicamente obtiene exactamente el mismo saldo que tiene una vez si lo llama cada minuto, puede encontrarlo definido al comienzo del archivo lib.rs:

const PORTION_OF_REWARDS: Balance = 24 * 60;
const SAFETY_BAR: Balance = 50_000000_000000_000000_000000;

Apuesto a que piensan que el proceso de recompensa ha terminado. Incorrecto.

De hecho, necesitamos volver a nuestra función maybe_send_reward() para ver que llama al nuevo contrato de farming de berryclub para distribuir las recompensas de participación, que son … pepinos, el token de participación en berryclub

const FARM_CONTRACT_ID_PREFIX: &str = "farm";

En realidad, esa no es la única fuente de distribución de ingresos con esta función, sino que también nivela los costos de gas que paga la gente para comprar aguacates e intercambiar plátanos ¡Para recompensar a toda la comunidad!.

¿Cómo es esto posible? Lo primero es lo primero, GAS_BASE_COMPUTE se define en el archivo token.rs, es en donde se establece la cantidad de gas para el contrato inteligente. Sí, ¡tienes razón! El precio del gas es bajo en NEAR y ¡se puede utilizar para recompensar a los usuarios que interactúen con tu videojuego!

Para comprender mejor cómo funcionan las tarifas de GAS en Near, por favor consulta esta documentación detallada.

Este tutorial es traído a ti por jilt.near y su proyecto NFT Gaming, ¡Apoyala comprando NFTs!

11
Ir arriba