James Bachini

Solidity Virtual Pet

solidity Virtual Pet

Let’s create a virtual pet in Solidity and deploy it to the blockchain.

Full frontend and contract code for this tutorial can be found here: https://github.com/jamesbachini/Solidity-Virtual-Pet

Demo here: https://jamesbachini.com/misc/SolidityPet/index.html

Smart Contract

Our pet is going to have two attributes for hunger and happiness. Hunger should increase over time and happiness should have a mechanism where if all the other pets are getting played with they get less happy.

The contract should handle multiple pets, one for each address that wants to create one.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract SolidityPet {
    struct Pet {
        string name;
        uint256 hunger;
        uint256 happiness;
        uint256 lastInteraction;
        uint256 lastHungerUpdate;
    }

    mapping(address => Pet) public pets;
    address[] public petOwners;

    uint256 public constant MAX_PETS = 10000;
    uint256 public constant MAX_HUNGER = 100;
    uint256 public constant MAX_HAPPINESS = 100;
    uint256 public constant HUNGER_RATE = 1;
    uint256 public constant JEALOUS_RATE = 1;

    event PetCreated(address owner, string name);
    event PetFed(address owner, uint256 newHunger);
    event PetPlayed(address owner, uint256 newHappiness);

    function min(uint256 a, uint256 b) private pure returns (uint256) {
        return a < b ? a : b;
    }

    function createPet(string memory _name) public {
        require(petOwners.length < MAX_PETS, "All pets minted");
        require(pets[msg.sender].lastInteraction == 0, "You already have a pet");
        pets[msg.sender] = Pet(_name, 50, 50, block.timestamp, block.timestamp);
        petOwners.push(msg.sender);
        emit PetCreated(msg.sender, _name);
    }

    function updateHunger(address _owner) private {
        Pet storage pet = pets[_owner];
        uint256 hoursPassed = (block.timestamp - pet.lastHungerUpdate) / 1 hours;
        if (hoursPassed > 0) {
            pet.hunger = min(pet.hunger + hoursPassed * HUNGER_RATE, MAX_HUNGER);
            pet.lastHungerUpdate = block.timestamp;
        }
    }

    function feedPet() public {
        Pet storage pet = pets[msg.sender];
        require(pet.lastInteraction > 0, "You don't have a pet");
        require(block.timestamp - pet.lastInteraction >= 1 minutes, "You can only interact once per hour");
        updateHunger(msg.sender);
        if (pet.hunger >= 10) {
            pet.hunger -= 10;
        } else {
            pet.hunger = 0;
        }
        pet.lastInteraction = block.timestamp;
        emit PetFed(msg.sender, pet.hunger);
    }

    function playWithPet() public {
        Pet storage pet = pets[msg.sender];
        require(pet.lastInteraction > 0, "You don't have a pet");
        require(block.timestamp - pet.lastInteraction >= 1 minutes, "You can only interact once per hour");
        updateHunger(msg.sender);
        if (pet.happiness <= 90) {
            pet.happiness += 10;
        } else {
            pet.happiness = 100;
        }
        for (uint i = 0; i < petOwners.length; i++) {
            if (petOwners[i] != msg.sender) {
                Pet storage otherPet = pets[petOwners[i]];
                if (otherPet.happiness >= JEALOUS_RATE) {
                    otherPet.happiness -= JEALOUS_RATE;
                } else {
                    otherPet.happiness = 0;
                }
            }
        }
        pet.lastInteraction = block.timestamp;
        emit PetPlayed(msg.sender, pet.happiness);
    }

    function getPetStatus(address _owner) public view returns (string memory name, uint256 hunger, uint256 happiness) {
        Pet storage pet = pets[_owner];
        require(pet.lastInteraction > 0, "This address doesn't have a pet");
        uint256 currentHunger = pet.hunger;
        uint256 hoursPassed = (block.timestamp - pet.lastHungerUpdate) / 1 hours;
        if (hoursPassed > 0) {
            currentHunger = min(pet.hunger + hoursPassed * HUNGER_RATE, MAX_HUNGER);
        }
        return (pet.name, currentHunger, pet.happiness);
    }
}

The SolidityPet contract allows users to create, feed, and play with digital pets, demonstrating various concepts.

  • Pet Struct Defines attributes for each pet, including name, hunger, happiness, and timestamps for interactions.
  • State Management Uses mappings and arrays to track pet ownership and status.
  • Constants Defines maximum values and rates for pet attributes and interactions.
  • Events Logs important actions like pet creation, feeding, and playing.
  • Create Pet Allows users to mint a new pet, with checks for maximum pet limit and one pet per user rule.
  • Feed Pet Decreases a pet’s hunger level, with a cooldown period between interactions.
  • Play with Pet Increases a pet’s happiness while slightly decreasing other pets’ happiness, simulating jealousy. Note there is a limited total number of pets (10,000 max) to avoid unbounded loop in this function.
  • Get Pet Status Retrieves current pet attributes, including real time hunger calculation.
  • Update Hunger Automatically increases hunger based on time elapsed since last interaction.

The contract creates an interactive experience where pets require regular care. Hunger increases over time, and playing with one pet affects others. This design encourages regular user engagement and creates a dynamic virtual ecosystem.

Frontend

Here is a demo of the frontend: https://jamesbachini.com/misc/SolidityPet/index.html

The contract is deployed to Sepolia testnet so connect metamask to testnet before interacting.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SolidityPet Frontend</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            text-align: center;
        }
        #petStatus {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        button {
            margin: 10px 5px;
            padding: 5px 10px;
        }
        #petAnimation {
            width: 100px;
            height: 100px;
            margin: 20px auto;
            background: url('pet.png') 0 0 no-repeat;
            background-size: 300px 100px;
            animation: play 10s steps(30) infinite;
        }
        @keyframes play {
            100% { background-position: -300px 0; }
        }
        .pet-happy { filter: hue-rotate(0deg); }
        .pet-neutral { filter: hue-rotate(30deg); }
        .pet-sad { filter: hue-rotate(180deg); }
    </style>
</head>
<body>
    <h1>SolidityPet Game</h1>
    <div id="connectWallet">
        <button onclick="connectWallet()">Connect Wallet</button>
    </div>
    <div id="petActions" style="display:none;">
        <input type="text" id="petName" placeholder="Enter pet name">
        <button onclick="createPet()">Create Pet</button>
        <button onclick="feedPet()">Feed Pet</button>
        <button onclick="playWithPet()">Play with Pet</button>
        <button onclick="getPetStatus()">Get Pet Status</button>
    </div>
    <div id="petAnimation" class="pet-neutral"></div>
    <div id="petStatus"></div>

    <script src="https://cdn.ethers.io/lib/ethers-5.0.umd.min.js"></script>
    <script>
        let provider;
        let signer;
        let contract;
        const contractAddress = '0x22063bC54243d5cc0B66269C0d15449C95e8eA6c';
        const contractABI = [{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"createPet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"feedPet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"string","name":"name","type":"string"}],"name":"PetCreated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"newHunger","type":"uint256"}],"name":"PetFed","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"newHappiness","type":"uint256"}],"name":"PetPlayed","type":"event"},{"inputs":[],"name":"playWithPet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"getPetStatus","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"hunger","type":"uint256"},{"internalType":"uint256","name":"happiness","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"HUNGER_RATE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"JEALOUS_RATE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_HAPPINESS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_HUNGER","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_PETS","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"petOwners","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"pets","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"hunger","type":"uint256"},{"internalType":"uint256","name":"happiness","type":"uint256"},{"internalType":"uint256","name":"lastInteraction","type":"uint256"},{"internalType":"uint256","name":"lastHungerUpdate","type":"uint256"}],"stateMutability":"view","type":"function"}];        

        async function connectWallet() {
            if (typeof window.ethereum !== 'undefined') {
                try {
                    await window.ethereum.request({ method: 'eth_requestAccounts' });
                    provider = new ethers.providers.Web3Provider(window.ethereum);
                    signer = provider.getSigner();
                    contract = new ethers.Contract(contractAddress, contractABI, signer);
                    document.getElementById('connectWallet').style.display = 'none';
                    document.getElementById('petActions').style.display = 'block';
                    getPetStatus();
                } catch (error) {
                    console.error("User denied account access");
                }
            } else {
                console.log('Please install MetaMask!');
            }
        }

        async function createPet() {
            const name = document.getElementById('petName').value;
            try {
                const tx = await contract.createPet(name);
                await tx.wait();
                alert('Pet created successfully!');
                getPetStatus();
            } catch (error) {
                console.error("Error creating pet:", error);
                alert('Failed to create pet. See console for details.');
            }
        }

        async function feedPet() {
            try {
                const tx = await contract.feedPet();
                await tx.wait();
                alert('Pet fed successfully!');
                getPetStatus();
            } catch (error) {
                console.error("Error feeding pet:", error);
                alert('Failed to feed pet. See console for details.');
            }
        }

        async function playWithPet() {
            try {
                const tx = await contract.playWithPet();
                await tx.wait();
                alert('Played with pet successfully!');
                getPetStatus();
            } catch (error) {
                console.error("Error playing with pet:", error);
                alert('Failed to play with pet. See console for details.');
            }
        }

        async function getPetStatus() {
            try {
                const address = await signer.getAddress();
                const status = await contract.getPetStatus(address);
                document.getElementById('petStatus').innerHTML = `
                    <h3>Pet Status</h3>
                    <p>Name: ${status.name}</p>
                    <p>Hunger: ${status.hunger}</p>
                    <p>Happiness: ${status.happiness}</p>
                `;
                updatePetAnimation(status.hunger, status.happiness);
            } catch (error) {
                console.error("Error getting pet status:", error);
                document.getElementById('petStatus').innerHTML = '<p>No pet found. Create one to get started!</p>';
                document.getElementById('petAnimation').className = 'pet-neutral';
            }
        }

        function updatePetAnimation(hunger, happiness) {
            const petAnimation = document.getElementById('petAnimation');
            const averageStatus = (100 - parseInt(hunger) + parseInt(happiness)) / 2;

            if (averageStatus > 66) {
                petAnimation.className = 'pet-happy';
            } else if (averageStatus > 33) {
                petAnimation.className = 'pet-neutral';
            } else {
                petAnimation.className = 'pet-sad';
            }
        }

        // Update pet status every 60 seconds
        setInterval(getPetStatus, 60000);
    </script>
</body>
</html>

This frontend provides a user interface for interacting with the SolidityPet smart contract on the Ethereum blockchain. Here’s a breakdown of its key components and functionality:

  • User Interface
    • Connect Wallet button: Allows users to connect their Ethereum wallet (e.g., MetaMask).
    • Pet creation: Users can enter a name and create a new virtual pet.
    • Interaction buttons: “Feed Pet” and “Play with Pet” buttons for pet care.
    • Status button: “Get Pet Status” to check the pet’s current condition.
    • Pet animation: A visual representation of the pet that changes based on its mood.
  • Contract Interaction
    • Uses ethers.js library to communicate with the Ethereum blockchain.
    • Connects to the user’s wallet and interacts with the smart contract.
    • createPet(): Creates a new pet with a given name.
    • feedPet(): Reduces the pet’s hunger level.
    • playWithPet(): Increases the pet’s happiness level.
    • getPetStatus(): Retrieves the current status of the pet.
  • Pet Visualization
    • Not a lot of effort went into this obviously it’s a picture of a cat that uses CSS animations with a sprite sheet to display an animated pet.
    • Changes the pet’s color using CSS filters to represent different moods (happy, neutral, sad).
    • Pet’s state (hunger and happiness) is stored on the blockchain and the frontend updates the visual representation based on the blockchain state.

This frontend provides a simple and interactive way for users to engage with their blockchain based virtual pet, visualizing the pet’s state and allowing for easy interaction with the underlying smart contract.

I hope you enjoyed this demonstration using Solidity to create a virtual pet. This is a simple, fun example of what web3 can do now, in the future I believe it will compete with cloud computing infrastructure like AWS and become a crucial tool in all web developers tool kits.


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.