# My First Project: A Buildspace NFT Game

By [Brandon Feist](https://paragraph.com/@brandon-feist) · 2021-12-17

---

Very quickly my interest in the crypto space has started to grow over the past month. My philosophy about learning something quickly is to find a simple idea, a good source of information about the topic, and to just jump right in. As a primarily backend engineer in my day job, smart contracts and building up infrastructure in the crypto space has been my biggest interest. While researching my first steps into the world of Web3 development I stumbled upon Buildspace on [twitter](https://twitter.com/_buildspace).

[Buildspace](https://buildspace.so/) is pretty cool, especially for those just starting out and looking to jump right in. They have 7 “projects,” which are different tutorials covering different aspects of Web3 development. They also have a really helpful and active community on discord! If you are looking to talk with other like minded individuals! I chose their _NFT browser game project_, due to it having a pretty decently sized Solidity component.

What I’ll Be Going Over
=======================

I’ll be focusing on several parts of this project that I felt were pretty pivotal to starting in Web3 development. I’ll be sure to have headers for each part, so feel free to skip to whatever interests you the most!

*   A brief dive into NFTs (EIP-721)?
    
*   Important technologies and services
    
*   Building a blockchain driven web app
    

A Brief Dive Into NFTs (EIP-721)
================================

In its most basic form, an NFT is a unique asset that cannot be replaced by another. The name “non-fungible” by definition means that these tokens cannot be replaced by another identical token, making them wholly unique. This is extremely useful when it comes to digital goods and assets, as they can be tied to ownership …. (finish this later when more focused)

**TODO FINISH THIS SECTION**

Technologies and Services
=========================

Several technologies went into the development of this project. I’m going to mainly be focusing on Web3 specific technologies. If you are interested in learning more about the details on what React is, you can find some great starter info on their [documentation site](https://reactjs.org/docs/getting-started.html).

### Solidity (Smart Contracts)

If you are interested in smart contract development, I would have to say Solidity should be one of your top priorities to learn. Solidity is an object-oriented programming language, taking influences from the likes of C++ and JavaScript. Solidity is meant to target the Ethereum Virtual Machine (EVM), this topic is beyond the scope of this post, but the Ethereum Org wrote [amazing documentation](https://ethereum.org/en/developers/docs/evm/) going over the basics on how the EVM works.

### Hardhat (Smart Contract Development Assist)

Hardhat is an amazing tool that helps with Ethereum smart contract development and testing. I wish I had found it sooner! The reason hardhat is so great, is its ability to allow you to deploy and run smart contracts locally on your own machine, in your own “private” block chain. This, plus the ability to print out console.log() statements from a contract in your terminal make it extremely easy to debug and test contracts on the fly.

Along with testing, hardhat also allows you to compile contracts and deploy them to the Ethereum networks (Rinkeby, Mainnet, etc). I will be going deeper into these parts later on.

### Alchemy (DAPP Deployment and Blockchain Middleman)

Alchemy works as a type of middleman between your dapp and the blockchain. They’re useful as you don’t have to worry about hosting your own node and all the responsibilities that come with that. It’s as easy as compiling your smart contract and deploying it to alchemy using hardhat. Although a small project like this does not use the service to its fullest potential, it is great for when you start deploying dapps many people will use! Alchemy also has a suite of detailed metrics showing data ranging from how many requests are being made to your app to the average response time, which could be useful when building and fine tuning a great product.

Building a Blockchain Driven Web App
====================================

### The Login Flow

In all my time of working as a web developer, I’ve never seen a login flow as elegant and easy to set up as this. It’s amazing how having a universal source of truth such as the blockchain simplifies so many things. It also opens up a huge treasure trove of possibilities of creating customizable experiences tailored for each user that can be maintained across any platform that uses the blockchain for authentication. Imagine, being able to store configurations for each user on chain and then picking up that data elsewhere, bringing these specific experiences into a different platform.

For this project I simply added a login flow using the Metamask wallet, of course you can have your app support multiple wallets. With Metamask, it simply injects an `ethereum` object in window. This object can be used to validate if Metamask is installed (checking if the `ethereum` object exists at all), request user info, and sign messages/transactions (this happens whenever Metamask asks you to approve a transaction).

On the main site load, I use an `useEffect` to initialize the loading spinner and check if the user is logged in.

    useEffect(() => {
       setIsLoading(true);
       checkIfWalletIsConnected();
     }, []);
    

The check wallet function is quite simple. It checks if the `ethereum` object exists at all (checking if Metamask is installed), requests authorized accounts from Metamask, and sets the user if they’re currently logged in. Depending on if Metamask exists or if the user is logged in or not, these could be used to trigger more states on your app.

    const checkIfWalletIsConnected = async () => {
       try {
         const { ethereum } = window;
     
         if (!ethereum) {
           console.log('Make sure you have MetaMask!');
           setIsLoading(false);
           return;
         } else {
           console.log('We have the ethereum object', ethereum);
     
           const accounts = await ethereum.request({ method: 'eth_accounts' });
     
           if (accounts.length !== 0) {
             const account = accounts[0];
             console.log('Found an authorized account:', account);
             setCurrentAccount(account);
           } else {
             console.log('No authorized account found');
           }
         }
       } catch (error) {
         console.log(error);
       }
     
       setIsLoading(false);
     };
    

When the user is not logged in, I decided to display a button on the site that can trigger a `connectWalletAction` triggering Metamask to ask for authentication from the user and fetch the user data.

    <button
      className="cta-button connect-wallet-button"
      onClick={connectWalletAction}
     >
       Connect Wallet To Get Started
     </button>
    

The `connectWalletAction` method simply checks if `ethereum` object exists, and then makes a request to Metamask using `eth_requestAccounts`.

    const connectWalletAction = async () => {
       try {
         const { ethereum } = window;
     
         if (!ethereum) {
           alert('Get MetaMask!');
           return;
         }
     
         const accounts = await ethereum.request({
           method: 'eth_requestAccounts',
         });
     
         console.log('Connected', accounts[0]);
         setCurrentAccount(accounts[0]);
       } catch (error) {
         console.log(error);
       }
     };
    

### Fetching Data from the Blockchain

**Solidity and Hardhat**

In the smart contract, each NFT character is built from a struct that lays out important information such as the monster’s ID, name, image URL, HP, etc; along with the type of data each variable is. Monsters held by a user that is interfacing with the smart contract and default monster’s a new user can choose to mint from are all composed from this struct. You can adapt this struct to meet whatever goal your NFT is trying to achieve.

    struct CharacterAttributes {
       uint characterIndex;
       string name;
       string imageURI;       
       uint hp;
       uint maxHp;
       uint attackDamage;
     }
    

Monsters that have been minted and are owned by users are stored in two maps that are _global state variables_ saved on the blockchain. The first mapping allows us to easily take a calling user’s public address and fetch the ID for the minted monster they own. The second mapping takes the monster’s ID and provides the monster object that contains the information referenced in the struct above.

    mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
    mapping(address => uint256) public nftHolders;
    

For the default characters that a new user can select from to mint, like the `nftHolderAttributes` and `nftHolders`, the list of select-able NFTs are saved on a global state variable on the smart contract also, but as a list of `CharacterAttributes`.

    CharacterAttributes[] defaultCharacters;
    

This list is populated when the contract is first deployed to the blockchain in the constructor. In Solidity, the constructor only runs the first time the contract is deployed, this is useful to set states or in this case populate a list of monsters users will be able to choose from. Also a side note, as you can see below the constructor has a syntax `ERC721("Heroes", "HERO")`, this is from the `​​ERC721` interface the whole smart contract extends. This allows us to set the name of the token as well as its symbol, in this case Heroes token $HERO. One last note, you should pay attention to the last action in the constructor `_tokenIds.increment()`. This is the ID that is set to a monster when a user mints it, there is an important reason that we increment the id from 0 to 1 before any monster is minted, and that will become more clear later on in the next section.

    constructor(
       string[] memory characterNames,
       string[] memory characterImageURIs,
       uint[] memory characterHp,
       uint[] memory characterAttackDmg,
       string memory bossName,
       string memory bossImageURI,
       uint bossHp,
       uint bossAttackDamage
     )
       ERC721("Heroes", "HERO")
     {
       // Initialize the boss. Save it to our global "bigBoss" state variable.
       bigBoss = BigBoss({
         name: bossName,
         imageURI: bossImageURI,
         hp: bossHp,
         maxHp: bossHp,
         attackDamage: bossAttackDamage
       });
     
       console.log("Done initializing boss %s w/ HP %s, img %s", bigBoss.name, bigBoss.hp, bigBoss.imageURI);
     
       for(uint i = 0; i < characterNames.length; i += 1) {
         defaultCharacters.push(CharacterAttributes({
           characterIndex: i,
           name: characterNames[i],
           imageURI: characterImageURIs[i],
           hp: characterHp[i],
           maxHp: characterHp[i],
           attackDamage: characterAttackDmg[i]
         }));
     
         CharacterAttributes memory c = defaultCharacters[i];
        
         // Hardhat's use of console.log() allows up to 4 parameters in any order of following types: uint, string, bool, address
         console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI);
       }
     
       // Token ID is incremented on first build since 0 can be treated is a default value in Solidity
       // This can cause issues deciphering the difference between a NONE value (0) or ID 0 of a character.
       _tokenIds.increment();
     }
    

For the final touches, I needed to write some utility functions within the contract for applications to be able to easily fetch this data. The first is a utility function to fetch all default monsters for when a user needs to select their first monster to mint. This method is a simple `view` function that simply fetches and returns the `defaultCharacters` list from the contract. A view function is a function that is appended with the `view` symbol. This declares that the function is simply fetching data from the blockchain and not performing any actions or changing the data. It’s important to remember that view functions cost **NO** gas to run.

    function getAllDefaultCharacters() public view returns (CharacterAttributes[] memory) {
       return defaultCharacters;
    }
    

The second utility function is to check if a user already has an NFT. Similar to `getAllDefaultCharacters`, this too is a view function. It uses the global variable `userNftTokenId` to see if the requesting user has a monster ID tied to their address and then returns the monster from `nftHolderAttributes` if it exists. Remember during the constructor section with the `_tokenId`. Solidity sets non-existing values to 0, if we had set a monster’s ID to 0 we would not have been able to decipher from a user not having an NFT with us or having the first monster ever minted.

    function checkIfUserHasNFT() public view returns (CharacterAttributes memory) {
       uint256 userNftTokenId = nftHolders[msg.sender];
     
       if (userNftTokenId > 0) {
         return nftHolderAttributes[userNftTokenId];
       } else {
         CharacterAttributes memory emptyStruct;
         return emptyStruct;
       }
     }
    

**React**

After a user logs in using Metamask, the `currentAccount` state is updated with the user data triggering another `useEffect` that is observing that state. The effect calls the contract using its utility function we wrote above to check if a user has a monster NFT.

    useEffect(() => {
       const fetchNFTMetadata = async () => {
         console.log('Checking for Character NFT on address:', currentAccount);
          const provider = new ethers.providers.Web3Provider(window.ethereum);
         const signer = provider.getSigner();
         const gameContract = new ethers.Contract(
           CONTRACT_ADDRESS,
           myEpicGame.abi,
           signer
         );
          const characterNFT = await gameContract.checkIfUserHasNFT();
         if (characterNFT.name) {
           console.log('User has character NFT');
           setCharacterNFT(transformCharacterData(characterNFT));
         }
        
         setIsLoading(false);
       };
        if (currentAccount) {
         console.log('CurrentAccount:', currentAccount);
         fetchNFTMetadata();
       }
     }, [currentAccount]);
    

You may be asking, how does our `gameContract` know what functions exits on the contract? Using `ethers` a helpful JavaScript dependency, we can build a contract object by passing in the `CONTRACT_ADDRESS`, `myEpicGame.json` (a JSON file of our smart contract’s variables and functions compiled by Hardhat), and a signer (the provider that requests a user to sign a blockchain request, in this instance Metamask).

After this check, React uses the `characterNFT` state to decide if the minting component or battle component should be rendered.

    } else if (currentAccount && !characterNFT) {
      return <SelectCharacter setCharacterNFT={setCharacterNFT} />;
    } else if (currentAccount && characterNFT) {
      return <Arena characterNFT={characterNFT} setCharacterNFT={setCharacterNFT} />;
    }
    

After the `gameContract` is initialized in the `SelectCharacter` component, an effect is triggered to fetch all default characters from the contract. Along with this we set up a listener that will receive events from the blockchain when a character is minted. When a character does get minted from the user it will trigger `onCharacterMint` triggering a state change to and React to render the `Arena` component. It’s important that when components are disposed of, the event listeners are closed (`gameContract.off('CharacterNFTMinted', onCharacterMint)`).

    useEffect(() => {
       const getCharacters = async () => {
         try {
           console.log('Getting contract characters to mint');
            const charactersTxn = await gameContract.getAllDefaultCharacters();
           console.log('charactersTxn:', charactersTxn);
            const characters = charactersTxn.map((characterData) =>
             transformCharacterData(characterData)
           );
            setCharacters(characters);
         } catch (error) {
           console.error('Something went wrong fetching characters:', error);
         }
       };
     
       const onCharacterMint = async (sender, tokenId, characterIndex) => {
         console.log(
           `CharacterNFTMinted - sender: ${sender} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
         );
     
         if (gameContract) {
           const characterNFT = await gameContract.checkIfUserHasNFT();
           console.log('CharacterNFT: ', characterNFT);
           setCharacterNFT(transformCharacterData(characterNFT));
         }
       };
        if (gameContract) {
         getCharacters();
         gameContract.on('CharacterNFTMinted', onCharacterMint);
       }
        return () => {
         if (gameContract) {
           gameContract.off('CharacterNFTMinted', onCharacterMint);
         }
       };
     }, [gameContract, setCharacterNFT]);
    

### Minting to the Blockchain

**Solidity**

The minting function is pretty simple. First it fetches the current ID that the new monster will be identified by. Then we use `_safeMint(address, to)`, a function from the `ERC721` interface that will mint the monster’s ID and send it to the given address; it's important that the ID given to mint is wholly unique and has never been used. After minting, the unique monster is generated with the default values from a monster from the `defaultCharacters` list and assigned to the user in the smart contract. Finally an event `CharacterNFTMinted` is broadcasted to let anyone listening know what has occurred. This is what the React event listener used above!

    function mintCharacterNFT(uint _characterIndex) external {
       uint256 newItemId = _tokenIds.current();
     
       // The magical function! Assigns the tokenId to the caller's wallet address.
       _safeMint(msg.sender, newItemId);
     
       nftHolderAttributes[newItemId] = CharacterAttributes({
         characterIndex: _characterIndex,
         name: defaultCharacters[_characterIndex].name,
         imageURI: defaultCharacters[_characterIndex].imageURI,
         hp: defaultCharacters[_characterIndex].hp,
         maxHp: defaultCharacters[_characterIndex].maxHp,
         attackDamage: defaultCharacters[_characterIndex].attackDamage
       });
     
       console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex);
      
       nftHolders[msg.sender] = newItemId;
     
       _tokenIds.increment();
     
       emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);
     }
    

The `CharacterNFTMinted` was defined with the contract’s other global variables.

    event CharacterNFTMinted(address sender, uint256 tokenId, uint256 characterIndex);
    

**React**

I tied a function to each button on the `SelectCharacter` component, each one with the ID that represents each default character. It simply uses the function above to initiate a minting transaction that the user signs and initiates.

    const mintCharacterNFTAction = (characterId) => async () => {
       try {
         if (gameContract) {
           setMintingCharacter(true);
           console.log('Minting character in progress...');
           const mintTxn = await gameContract.mintCharacterNFT(characterId);
           await mintTxn.wait();
           console.log(mintTxn);
           setMintingCharacter(false);
         }
       } catch (error) {
         console.warn('MintCharacterAction Error:', error);
         setMintingCharacter(false);
       }
     };
    

### Building Basic Gameplay Mechanics

Once the user has minted a character, it is time to move them to the battle arena to fight the boss!

**Solidity**

Only two functions are needed for this arena, as it is an extremely simple game. The first function is another view function that retrieves the boss from the contracts memory on the blockchain and returns it the requester.

    function getBigBoss() public view returns (BigBoss memory) {
       return bigBoss;
    }
    

The second function is a bit more complicated, but not too bad. After fetching the user’s monster, there is a new syntax we haven’t seen yet called `require` that checks if the transaction is valid. You may be asking, why not just use a conditional `if`? `Requires` are useful because if the requirements are not meant, it will roll the whole transaction back, making it as if nothing ever happened and reverting all changes that may have occurred beforehand. _The only thing a user doesn’t get back is the gas fees spent on miners in the process_. If all requirements are met, the user will attack the boss reducing their HP and the boss will do the same. After all is said and done, an event is broadcasted to clients that an attack has occurred, similar to the `CharacterNFTMinted` event.

    function attackBoss() public {
       uint256 nftTokenIdOfPlayer = nftHolders[msg.sender];
       CharacterAttributes storage player = nftHolderAttributes[nftTokenIdOfPlayer];
      
       console.log("\nPlayer w/ character %s about to attack. Has %s HP and %s AD", player.name, player.hp, player.attackDamage);
       console.log("Boss %s has %s HP and %s AD", bigBoss.name, bigBoss.hp, bigBoss.attackDamage);
     
       require (
         player.hp > 0,
         "Error: character must have HP to attack boss."
       );
     
       require (
         bigBoss.hp > 0,
         "Error: boss must have HP to attack boss."
       );
     
       if (bigBoss.hp < player.attackDamage) {
         bigBoss.hp = 0;
       } else {
         bigBoss.hp = bigBoss.hp - player.attackDamage;
       }
     
       if (player.hp < bigBoss.attackDamage) {
         player.hp = 0;
       } else {
         player.hp = player.hp - bigBoss.attackDamage;
       }
      
       console.log("Player attacked boss. New boss hp: %s", bigBoss.hp);
       console.log("Boss attacked player. New player hp: %s\n", player.hp);
     
       emit AttackComplete(bigBoss.hp, player.hp);
     }
    

**React**

In the `Arena` component, we populate the render with the `boss` fetched from the contract state and `characterNFT` to display the health of both characters. We also setup the event listener and its respective function for later on when an attack is initiated and completed.

    useEffect(() => {
       const fetchBoss = async () => {
           const bossTxn = await gameContract.getBigBoss();
           console.log('Boss:', bossTxn);
           setBoss(transformCharacterData(bossTxn));
       };
     
       /*
       * Setup logic when this event is fired off
       */
       const onAttackComplete = (newBossHp, newPlayerHp) => {
           const bossHp = newBossHp.toNumber();
           const playerHp = newPlayerHp.toNumber();
     
           console.log(`AttackComplete: Boss Hp: ${bossHp} Player Hp: ${playerHp}`);
     
           /*
           * Update both player and boss Hp
           */
           setBoss((prevState) => {
               return { ...prevState, hp: bossHp };
           });
     
           setCharacterNFT((prevState) => {
               return { ...prevState, hp: playerHp };
           });
       };
     
       if (gameContract) {
           fetchBoss();
           gameContract.on('AttackComplete', onAttackComplete);
       }
     
       /*
       * Make sure to clean up this event when this component is removed
       */
       return () => {
           if (gameContract) {
               gameContract.off('AttackComplete', onAttackComplete);
           }
       }
     }, [gameContract, setCharacterNFT]);
    

Finally the grand finale! The boss function that will be tied to the attack button. Like all the other functions seen before, this will simply use the smart contracts `attackBoss` function and then pop up a toast when the attack transaction is all said and done.

    const runAttackAction = async () => {
       try {
         if (gameContract) {
           setAttackState('attacking');
           console.log('Attacking boss...');
           const txn = await gameContract.attackBoss();
           await txn.wait();
           console.log(txn);
           setAttackState('hit');
                
           setShowToast(true);
           setTimeout(() => {
             setShowToast(false);
           }, 5000);
         }
       } catch (error) {
         console.error('Error attacking boss:', error);
         setAttackState('');
       }
     };
    

Conclusion
----------

In conclusion, would I say this Buildspace tutorial was useful? Most definitely! I do feel that at times it was a bit easy, being given code snippets without having to put a single thought into it. But the great thing about this blog post is that it forced me to digest what happened a little bit more and hopefully it will help you a bit also.

### Thoughts on Web3, Will I Continue, What’s Next?

I am extremely bullish and excited for what Web3 has to offer, although I am quite disappointed that I did not realize the potential sooner. I might work on another Buildspace project, but my excitement is building quickly, so I will probably start to forge my own path very soon.

I am looking for other programmers to work along with or budding communities or DOAs that are looking for engineers. I plan to keep working on small projects that will enhance the Web3 experience and update that progress here. I also have a large project in the works, but I will reveal more about that as time goes on!

Useful Content
--------------

*   [Buildspace.so](https://buildspace.so/)
    
*   This projects code can be found in this: [GitHub Repo](https://github.com/brandonfeist/buildspace-nft-game)
    
*   My NFT Game hosted on Heroku, it should be noted that it is hosted on a free Heroku service and the images on a free CDN so loading can be pretty slow at first. If you want to use the app, please set your Metamask wallet to the **Rinkeby Network**: [NFT Game](https://buildspace-nft-game-skylark.herokuapp.com/)
    
*   If you this content was useful and you want to stay up to date, be sure to follow me on twitter at: [@heyskylark](https://twitter.com/heyskylark)

---

*Originally published on [Brandon Feist](https://paragraph.com/@brandon-feist/my-first-project-a-buildspace-nft-game)*
