# buildspace的create-turn-based-nft-game课程

By [Coinman.eth](https://paragraph.com/@coinman-eth) · 2022-12-02

---

大家好，我是帝哥（推特：@[CoinmanLabs](https://twitter.com/coinmanlabs)），大家或多或少都知道帝哥是程序员出身，因为确实官方的项目的文档很久没更新了，帝哥就跟官方沟通了，官方要邀请帝哥参与修改他们的代码，所以今天继续buildspace的课程-create-turn-based-nft-game，这里也按照官方给的阶段来。

### 1.入门

大致介绍了下这次的项目主要是做什么的，目的是什么，大家了解下就行了，最后问你，特别希望将任何加密货币公司添加到列表中，我选择了**币安，大家可以自行答题**，然后给我们介绍了下Axie Infinity游戏，最后问你喜欢什么游戏，我选择了**Mario NFT**填写。

### 2.构建NFT角色

**step1.设置好replit环境**

使用的编码网站还是[replit](https://replit.com/)， 我们来到replit中首先新建一个项目，这里我就叫做003了（这里可能很多人就奇怪了为啥不直接跟着官网走，创建一个项目后直接安装依赖呢，如果使用replit较多的话，你就知道了，replit不可创建.env文件,但是采用这样的方式是可以创建的）

![创建项目](https://storage.googleapis.com/papyrus_images/5a0e09798a03568e76b8b9a05f76a8ea8a96886d3764ab61920ce432b6340cfe.png)

创建项目

还是原来的，这里在说下，我将整个页面分为了三个区，便于后续针对每个区操作说明简单。

![分区](https://storage.googleapis.com/papyrus_images/906f95eb7817fb9b4c5ac06747060461b05e513bba0bfea1ec469209d9c69f1a.png)

分区

当我们的工作台准备工作完成后，在shell区执行下面的命令

    mkdir epic-game
    cd epic-game
    npm init -y
    npm install --save-dev hardhat@latest
    # 等待上面都执行完成后在输入下面的命令，全部按回车即可
    npx hardhat
    npm install --save-dev chai @nomiclabs/hardhat-ethers ethers @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers
    npm install @openzeppelin/contracts
    

![按照依赖](https://storage.googleapis.com/papyrus_images/cbcb1191e50a29148e16dd8848f60ea183096c957f798f32cb87fc593fdef203.png)

按照依赖

当我们上面的依赖全部安装完成后，因为hardhat项目框架本身就携带了合约和测试部署的脚本（默认使用的是hardhat的本地节点），当你需要测试是否安装成功则需要执行下面的命令。

    npx hardhat run scripts/deploy.js
    

如果你正常执行则说明你配置正确，在这章节就可以把**你正常运行deploy.js的截图上传。**

![上传deploy结果](https://storage.googleapis.com/papyrus_images/a8d6bced35e4f6938c22a215bef43205cbc3e57fadeca6b0eb0c9b0d8d590bd3.png)

上传deploy结果

**step2.运行一个简单的合约**

在contracts文件夹下新建一个MyEpicGame.sol文件，写入下面的代码

    // SPDX-License-Identifier: UNLICENSED
    
    pragma solidity ^0.8.17;
    
    import "hardhat/console.sol";
    
    contract MyEpicGame {
      constructor() {
        console.log("THIS IS MY GAME CONTRACT. NICE.");
      }
    }
    

同时在scripts文件夹下面新建一个run.js，写入下面的代码

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      const gameContract = await gameContractFactory.deploy();
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

当前面全部完成后在shell区输入下面的命令，将**输出的结果作为这一步的截图提交**。

![第二步提交的截图](https://storage.googleapis.com/papyrus_images/ad4dc5dd54b8ec39bdfbbf6c0219e8f7b08b02fa3f429d793e6e447e8946ced6.png)

第二步提交的截图

**step3.设置人物属性**

当我们完成了上面的测试之后，首先将我们的MyEpicGame.sol文件替换成下面的。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    import "hardhat/console.sol";
    
    contract MyEpicGame {
      // We'll hold our character's attributes in a struct. Feel free to add
      // whatever you'd like as an attribute! (ex. defense, crit chance, etc).
      struct CharacterAttributes {
        uint characterIndex;
        string name;
        string imageURI;        
        uint hp;
        uint maxHp;
        uint attackDamage;
      }
      // A lil array to help us hold the default data for our characters.
      // This will be helpful when we mint new characters and need to know
      // things like their HP, AD, etc.
      CharacterAttributes[] defaultCharacters;
    
      // Data passed in to the contract when it's first created initializing the characters.
      // We're going to actually pass these values in from run.js.
      constructor(
        string[] memory characterNames,
        string[] memory characterImageURIs,
        uint[] memory characterHp,
        uint[] memory characterAttackDmg
      )
      {
        // Loop through all the characters, and save their values in our contract so
        // we can use them later when we mint our NFTs.
        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];
          console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI);
        }
      }
    }
    

同时需要替换run.js的内容如下。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      const gameContract = await gameContractFactory.deploy(
        ["Leo", "Aang", "Pikachu"],       // Names
        ["https://i.imgur.com/pKd5Sdk.png", // Images
        "https://i.imgur.com/xVu4vFL.png", 
        "https://i.imgur.com/WMB6g9u.png"],
        [100, 200, 300],                    // HP values
        [100, 50, 25]                       // Attack damage values
      );
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

当替换完成后再次执行测试脚本，将**测试脚本的结果截图作为本步骤**。

    npx hardhat run scripts/run.js
    

![第三步提交的截图](https://storage.googleapis.com/papyrus_images/fb1be20b4b9e1d73661d980ef295d3856c0a9bfddf4af829b72c4e7fa877c1bb.png)

第三步提交的截图

**step4.在本地铸造 NFT**

我们再次更新合约sol文件，将下面的代码进行替换。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    // NFT contract to inherit from.
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    
    // Helper functions OpenZeppelin provides.
    import "@openzeppelin/contracts/utils/Counters.sol";
    import "@openzeppelin/contracts/utils/Strings.sol";
    
    
    import "hardhat/console.sol";
    import "./Base64.sol";
    
    // Our contract inherits from ERC721, which is the standard NFT contract!
    contract MyEpicGame is ERC721 {
    
      struct CharacterAttributes {
        uint characterIndex;
        string name;
        string imageURI;        
        uint hp;
        uint maxHp;
        uint attackDamage;
      }
    
      // The tokenId is the NFTs unique identifier, it's just a number that goes
      // 0, 1, 2, 3, etc.
      using Counters for Counters.Counter;
      Counters.Counter private _tokenIds;
    
      CharacterAttributes[] defaultCharacters;
    
      // We create a mapping from the nft's tokenId => that NFTs attributes.
      mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
    
      // A mapping from an address => the NFTs tokenId. Gives me an ez way
      // to store the owner of the NFT and reference it later.
      mapping(address => uint256) public nftHolders;
    
      constructor(
        string[] memory characterNames,
        string[] memory characterImageURIs,
        uint[] memory characterHp,
        uint[] memory characterAttackDmg
        // Below, you can also see I added some special identifier symbols for our NFT.
        // This is the name and symbol for our token, ex Ethereum and ETH. I just call mine
        // Heroes and HERO. Remember, an NFT is just a token!
      )
        ERC721("Heroes", "HERO")
      {
        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);
        }
    
        // I increment _tokenIds here so that my first NFT has an ID of 1.
        // More on this in the lesson!
        _tokenIds.increment();
      }
    
      // Users would be able to hit this function and get their NFT based on the
      // characterId they send in!
      function mintCharacterNFT(uint _characterIndex) external {
        // Get current tokenId (starts at 1 since we incremented in the constructor).
        uint256 newItemId = _tokenIds.current();
    
        // The magical function! Assigns the tokenId to the caller's wallet address.
        _safeMint(msg.sender, newItemId);
    
        // We map the tokenId => their character attributes. More on this in
        // the lesson below.
        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);
        
        // Keep an easy way to see who owns what NFT.
        nftHolders[msg.sender] = newItemId;
    
        // Increment the tokenId for the next person that uses it.
        _tokenIds.increment();
      }
    
      function tokenURI(uint256 _tokenId) public view override returns (string memory) {
      CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId];
    
      string memory strHp = Strings.toString(charAttributes.hp);
      string memory strMaxHp = Strings.toString(charAttributes.maxHp);
      string memory strAttackDamage = Strings.toString(charAttributes.attackDamage);
    
      string memory json = Base64.encode(
        abi.encodePacked(
          '{"name": "',
          charAttributes.name,
          ' -- NFT #: ',
          Strings.toString(_tokenId),
          '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
          charAttributes.imageURI,
          '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ',
          strAttackDamage,'} ]}'
        )
      );
    
      string memory output = string(
        abi.encodePacked("data:application/json;base64,", json)
      );
      
      return output;
    }
    }
    

同时我们在contract下面新建一个Base64.sol文件，写入下面的内容。

    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    /// [MIT License]
    /// @title Base64
    /// @notice Provides a function for encoding some bytes in base64
    /// @author Brecht Devos <brecht@loopring.org>
    library Base64 {
        bytes internal constant TABLE =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    
        /// @notice Encodes some bytes to the base64 representation
        function encode(bytes memory data) internal pure returns (string memory) {
            uint256 len = data.length;
            if (len == 0) return "";
    
            // multiply by 4/3 rounded up
            uint256 encodedLen = 4 * ((len + 2) / 3);
    
            // Add some extra buffer at the end
            bytes memory result = new bytes(encodedLen + 32);
    
            bytes memory table = TABLE;
    
            assembly {
                let tablePtr := add(table, 1)
                let resultPtr := add(result, 32)
    
                for {
                    let i := 0
                } lt(i, len) {
    
                } {
                    i := add(i, 3)
                    let input := and(mload(add(data, i)), 0xffffff)
    
                    let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
                    out := shl(8, out)
                    out := add(
                        out,
                        and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
                    )
                    out := shl(8, out)
                    out := add(
                        out,
                        and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
                    )
                    out := shl(8, out)
                    out := add(
                        out,
                        and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
                    )
                    out := shl(224, out)
    
                    mstore(resultPtr, out)
    
                    resultPtr := add(resultPtr, 4)
                }
    
                switch mod(len, 3)
                case 1 {
                    mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
                }
                case 2 {
                    mstore(sub(resultPtr, 1), shl(248, 0x3d))
                }
    
                mstore(result, encodedLen)
            }
    
            return string(result);
        }
    }
    

当将sol文件替换完之后，将我们的run.js进行替换，替换的如下。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      const gameContract = await gameContractFactory.deploy(
        ["Leo", "Aang", "Pikachu"],       // Names
        ["https://i.imgur.com/pKd5Sdk.png", // Images
          "https://i.imgur.com/xVu4vFL.png",
          "https://i.imgur.com/WMB6g9u.png"],
        [100, 200, 300],                    // HP values
        [100, 50, 25]                       // Attack damage values
      );
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
      
    let txn;
    // We only have three characters.
    // an NFT w/ the character at index 2 of our array.
    txn = await gameContract.mintCharacterNFT(2);
    await txn.wait();
    
    // Get the value of the NFT's URI.
    let returnedTokenUri = await gameContract.tokenURI(1);
    console.log("Token URI:", returnedTokenUri);
    
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

再次运行run.js，**将输出的token url新开一个tab页显示作为本步骤截图。**

    npx hardhat run scripts/run.js
    

![运行结果](https://storage.googleapis.com/papyrus_images/b30eea05b12778b871ca8eebe8cda832470aa4f13f6d5fbb0f214af4743c50d4.png)

运行结果

![本步骤提交截图](https://storage.googleapis.com/papyrus_images/dd5f089cb3375a4d18b751e355102451cf0c5a3588eadea2f0ebdf60a2acae4d.png)

本步骤提交截图

**step5.将合约部署到测试网络**

将deploy.js将下面的内容替换过去即可。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      const gameContract = await gameContractFactory.deploy(                     
        ["Leo", "Aang", "Pikachu"],       
        ["https://i.imgur.com/pKd5Sdk.png", 
        "https://i.imgur.com/xVu4vFL.png", 
        "https://i.imgur.com/u7T87A6.png"],
        [100, 200, 300],                    
        [100, 50, 25]                       
      );
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    
      
      let txn;
      txn = await gameContract.mintCharacterNFT(0);
      await txn.wait();
      console.log("Minted NFT #1");
    
      txn = await gameContract.mintCharacterNFT(1);
      await txn.wait();
      console.log("Minted NFT #2");
    
      txn = await gameContract.mintCharacterNFT(2);
      await txn.wait();
      console.log("Minted NFT #3");
    
      txn = await gameContract.mintCharacterNFT(1);
      await txn.wait();
      console.log("Minted NFT #4");
    
      console.log("Done deploying and minting!");
    
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

现在 让我们来修改下hardhat.config.js，替换如下。

    require("@nomicfoundation/hardhat-toolbox");
    
    module.exports = {
      solidity: '0.8.17',
      networks: {
        goerli: {
          url: '你的rpc的地址 可以使用alchemy',
          accounts: ['你的私钥'],
        },
      },
    };
    

都完成后在shell区输入下面的命令部署。

    npx hardhat run scripts/deploy.js --network goerli
    

![部署结果](https://storage.googleapis.com/papyrus_images/a37c7825649ff13637280e021a9b6206c421a1a75d86b85dd831779638194c75.png)

部署结果

当部署完成后，去到

    https://goerli.pixxiti.com/nfts/你的合约地址/你的tokenid（选择一个即可1-4）
    # 比如帝哥的地址查看的https://goerli.pixxiti.com/nfts/0x5410BF0144d2f6844815f629CE4Fb29456FE9C8a/1
    

![本步骤提交的截图](https://storage.googleapis.com/papyrus_images/2a4037b16d35a83680c5093d925372ebc64edaf3a378f553a166c5d030d92018.png)

本步骤提交的截图

### 2.构建游戏逻辑

**step1.构建boss和购机逻辑**

将我们的sol文件替换为下面的内容。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    // NFT contract to inherit from.
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    
    // Helper functions OpenZeppelin provides.
    import "@openzeppelin/contracts/utils/Counters.sol";
    import "@openzeppelin/contracts/utils/Strings.sol";
    
    
    import "hardhat/console.sol";
    import "./Base64.sol";
    
    // Our contract inherits from ERC721, which is the standard NFT contract!
    contract MyEpicGame is ERC721 {
    
      struct CharacterAttributes {
        uint characterIndex;
        string name;
        string imageURI;        
        uint hp;
        uint maxHp;
        uint attackDamage;
      }
    
      struct BigBoss {
      string name;
      string imageURI;
      uint hp;
      uint maxHp;
      uint attackDamage;
    }
    
    
      // The tokenId is the NFTs unique identifier, it's just a number that goes
      // 0, 1, 2, 3, etc.
      using Counters for Counters.Counter;
      Counters.Counter private _tokenIds;
    
      CharacterAttributes[] defaultCharacters;
      BigBoss public bigBoss;
    
    
      // We create a mapping from the nft's tokenId => that NFTs attributes.
      mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
    
      // A mapping from an address => the NFTs tokenId. Gives me an ez way
      // to store the owner of the NFT and reference it later.
      mapping(address => uint256) public nftHolders;
    
      uint randNonce = 0; 
    
      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")
      {
        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]
          }));
    
          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);
    
          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);
        }
    
        // I increment _tokenIds here so that my first NFT has an ID of 1.
        // More on this in the lesson!
        _tokenIds.increment();
      }
    function attackBoss() public {
      // Get the state of the player's NFT.
      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);
      
      // Make sure the player has more than 0 HP.
      require (
        player.hp > 0,
        "Error: character must have HP to attack boss."
      );
    
      // Make sure the boss has more than 0 HP.
      require (
        bigBoss.hp > 0,
        "Error: boss must have HP to attack character."
      );
      
    console.log("%s swings at %s...", player.name, bigBoss.name);        
            if (bigBoss.hp < player.attackDamage) {
                bigBoss.hp = 0;
                console.log("The boss is dead!");
            } else {
                if (randomInt(10) > 5) {                                 // by passing 10 as the mod, we elect to only grab the last digit (0-9) of the hash!
                    bigBoss.hp = bigBoss.hp - player.attackDamage;
                    console.log("%s attacked boss. New boss hp: %s", player.name, bigBoss.hp);
                } else {
                    console.log("%s missed!\n", player.name);
                }
            }
      
      // Console for ease.
      console.log("Player attacked boss. New boss hp: %s", bigBoss.hp);
      console.log("Boss attacked player. New player hp: %s\n", player.hp);
    }
    
      function randomInt(uint _modulus) internal returns(uint) {
       randNonce++;                                                     // increase nonce
       return uint(keccak256(abi.encodePacked(block.timestamp,                      // an alias for 'block.timestamp'
                                              msg.sender,               // your address
                                              randNonce))) % _modulus;  // modulo using the _modulus argument
     }
      // Users would be able to hit this function and get their NFT based on the
      // characterId they send in!
      function mintCharacterNFT(uint _characterIndex) external {
        // Get current tokenId (starts at 1 since we incremented in the constructor).
        uint256 newItemId = _tokenIds.current();
    
        // The magical function! Assigns the tokenId to the caller's wallet address.
        _safeMint(msg.sender, newItemId);
    
        // We map the tokenId => their character attributes. More on this in
        // the lesson below.
        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);
        
        // Keep an easy way to see who owns what NFT.
        nftHolders[msg.sender] = newItemId;
    
        // Increment the tokenId for the next person that uses it.
        _tokenIds.increment();
      }
    
      function tokenURI(uint256 _tokenId) public view override returns (string memory) {
      CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId];
    
      string memory strHp = Strings.toString(charAttributes.hp);
      string memory strMaxHp = Strings.toString(charAttributes.maxHp);
      string memory strAttackDamage = Strings.toString(charAttributes.attackDamage);
    
      string memory json = Base64.encode(
        abi.encodePacked(
          '{"name": "',
          charAttributes.name,
          ' -- NFT #: ',
          Strings.toString(_tokenId),
          '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
          charAttributes.imageURI,
          '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ',
          strAttackDamage,'} ]}'
        )
      );
    
      string memory output = string(
        abi.encodePacked("data:application/json;base64,", json)
      );
      
      return output;
    }
    }
    

将run,js代码替换如下。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      const gameContract = await gameContractFactory.deploy(
        ["Leo", "Aang", "Pikachu"],
        ["https://i.imgur.com/pKd5Sdk.png",
          "https://i.imgur.com/xVu4vFL.png",
          "https://i.imgur.com/u7T87A6.png"],
        [100, 200, 300],
        [100, 50, 25],
        "Elon Musk", // Boss name
        "https://i.imgur.com/AksR0tt.png", // Boss image
        10000, // Boss hp
        50 // Boss attack damage
      );
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    
      let txn;
      // We only have three characters.
      // an NFT w/ the character at index 2 of our array.
      txn = await gameContract.mintCharacterNFT(2);
      await txn.wait();
      txn = await gameContract.attackBoss();
      await txn.wait();
    
      txn = await gameContract.attackBoss();
      await txn.wait();
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

当上面两者都替换完成后，执行run.js将**输出的结果作为本步骤的截图**。

![本步骤的截图](https://storage.googleapis.com/papyrus_images/238950af2f1bdbe31c881d106fdca887b342c3bf07fbe242efebb64e4ffa8abe.png)

本步骤的截图

step2.

将下面的代码替换到deploy.js。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      
      const gameContract = await gameContractFactory.deploy(                        
        ["Leo", "Aang", "Pikachu"],       
        ["https://i.imgur.com/pKd5Sdk.png", 
        "https://i.imgur.com/xVu4vFL.png", 
        "https://i.imgur.com/u7T87A6.png"],
        [100, 200, 300],                    
        [100, 50, 25],
        "Elon Musk",
        "https://i.imgur.com/AksR0tt.png",
        10000,
        50
      );
    
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    
      let txn;
      // We only have three characters.
      // an NFT w/ the character at index 2 of our array.
      txn = await gameContract.mintCharacterNFT(2);
      await txn.wait();
    
      txn = await gameContract.attackBoss();
      await txn.wait();
    
      txn = await gameContract.attackBoss();
      await txn.wait();
    
      console.log("Done!");
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

运行部署脚本，**去到网站查看属性作为了本步骤的提交结果**。

    npx hardhat run scripts/deploy.js --network goerli
    

当部署完成后进行查看。

    https://goerli.pixxiti.com/nfts/你的合约地址/你的tokenid（选择一个即可1-4）
    # 比如帝哥的地址查看的https://goerli.pixxiti.com/nfts/0x5410BF0144d2f6844815f629CE4Fb29456FE9C8a/1
    

**step3.部署并查看 NFT 在产品中的变化。（合约也需要修改才能看到变化，请继续下一步完善合约）**

将下面的代码替换到deploy.js中。

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      
      const gameContract = await gameContractFactory.deploy(                        
        ["Leo", "Aang", "Pikachu"],       
        ["https://i.imgur.com/pKd5Sdk.png", 
        "https://i.imgur.com/xVu4vFL.png", 
        "https://i.imgur.com/u7T87A6.png"],
        [100, 200, 300],                    
        [100, 50, 25],
        "Elon Musk",
        "https://i.imgur.com/AksR0tt.png",
        10000,
        50
      );
    
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    
      let txn;
      // We only have three characters.
      // an NFT w/ the character at index 2 of our array.
      txn = await gameContract.mintCharacterNFT(2);
      await txn.wait();
    
      txn = await gameContract.attackBoss();
      await txn.wait();
    
      txn = await gameContract.attackBoss();
      await txn.wait();
    
      console.log("Done!");
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

这里我们需要注意一个地方，**因为在产生攻击的时候作者采用了一个10以内的随机数可能会存在你的nft的属性还是原来的，不会有更改**，我给大家解释下上面deploy.js里面的意思。

    # 这里部署了一个名字叫做Pikachu hp为300 攻击力为25的人物
    txn = await gameContract.mintCharacterNFT(2);
    

**上面说了可能不会触发攻击来导致属性变化，那我们怎么改下，可以让他一定攻击的到呢？记得这一步提交截图后再次改回来。**

![需要修改的地方](https://storage.googleapis.com/papyrus_images/a2676274b50380578cc46ca3c45a6067f352e40b9d970535ad3b791f5ece5f86.png)

需要修改的地方

**我们可以看到任务的生命值确实变化了。记得将刚才修改的值改回去。**

![本步骤提交的截图](https://storage.googleapis.com/papyrus_images/26fc7b850efd730a2ac776e7bc01497f9c170ebdd818770540b530e6750da363.png)

本步骤提交的截图

**step3.完善合约**

我们将合约文件sol再次替换为下面的。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.17;
    
    // NFT contract to inherit from.
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    
    // Helper functions OpenZeppelin provides.
    import "@openzeppelin/contracts/utils/Counters.sol";
    import "@openzeppelin/contracts/utils/Strings.sol";
    
    
    import "hardhat/console.sol";
    import "./Base64.sol";
    
    // Our contract inherits from ERC721, which is the standard NFT contract!
    contract MyEpicGame is ERC721 {
    
    
      event CharacterNFTMinted(address sender, uint256 tokenId, uint256 characterIndex);
    event AttackComplete(address sender, uint newBossHp, uint newPlayerHp);
    
      struct CharacterAttributes {
        uint characterIndex;
        string name;
        string imageURI;        
        uint hp;
        uint maxHp;
        uint attackDamage;
      }
    
      struct BigBoss {
      string name;
      string imageURI;
      uint hp;
      uint maxHp;
      uint attackDamage;
    }
    
    
      // The tokenId is the NFTs unique identifier, it's just a number that goes
      // 0, 1, 2, 3, etc.
      using Counters for Counters.Counter;
      Counters.Counter private _tokenIds;
    
      CharacterAttributes[] defaultCharacters;
      BigBoss public bigBoss;
    
    
      // We create a mapping from the nft's tokenId => that NFTs attributes.
      mapping(uint256 => CharacterAttributes) public nftHolderAttributes;
    
      // A mapping from an address => the NFTs tokenId. Gives me an ez way
      // to store the owner of the NFT and reference it later.
      mapping(address => uint256) public nftHolders;
    
      uint randNonce = 0; 
    
      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")
      {
        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]
          }));
    
          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);
    
          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);
        }
    
        // I increment _tokenIds here so that my first NFT has an ID of 1.
        // More on this in the lesson!
        _tokenIds.increment();
        
      }
       function attackBoss() public {
      // Get the state of the player's NFT.
      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);
      
      // Make sure the player has more than 0 HP.
      require (
        player.hp > 0,
        "Error: character must have HP to attack boss."
      );
    
      // Make sure the boss has more than 0 HP.
      require (
        bigBoss.hp > 0,
        "Error: boss must have HP to attack character."
      );
      
    
      // Allow boss to attack player.
      if (player.hp < bigBoss.attackDamage) {
        player.hp = 0;
      } else {
        player.hp = player.hp - bigBoss.attackDamage;
      }
    
         console.log("%s swings at %s...", player.name, bigBoss.name);        
            if (bigBoss.hp < player.attackDamage) {
                bigBoss.hp = 0;
                console.log("The boss is dead!");
            } else {
                if (randomInt(10) > 9) {                                 // by passing 10 as the mod, we elect to only grab the last digit (0-9) of the hash!
                    bigBoss.hp = bigBoss.hp - player.attackDamage;
                    console.log("%s attacked boss. New boss hp: %s", player.name, bigBoss.hp);
                } else {
                    console.log("%s missed!\n", player.name);
                }
            }
      
      // Console for ease.
      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(msg.sender, bigBoss.hp, player.hp);
    
    }
    
      function randomInt(uint _modulus) internal returns(uint) {
       randNonce++;                                                     // increase nonce
       return uint(keccak256(abi.encodePacked(block.timestamp,                      // an alias for 'block.timestamp'
                                              msg.sender,               // your address
                                              randNonce))) % _modulus;  // modulo using the _modulus argument
     }
      // Users would be able to hit this function and get their NFT based on the
      // characterId they send in!
      function mintCharacterNFT(uint _characterIndex) external {
        // Get current tokenId (starts at 1 since we incremented in the constructor).
        uint256 newItemId = _tokenIds.current();
    
        // The magical function! Assigns the tokenId to the caller's wallet address.
        _safeMint(msg.sender, newItemId);
    
        // We map the tokenId => their character attributes. More on this in
        // the lesson below.
        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);
        
        // Keep an easy way to see who owns what NFT.
        nftHolders[msg.sender] = newItemId;
    
        // Increment the tokenId for the next person that uses it.
        _tokenIds.increment();
        emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);
      }
    
      function tokenURI(uint256 _tokenId) public view override returns (string memory) {
      CharacterAttributes memory charAttributes = nftHolderAttributes[_tokenId];
    
      string memory strHp = Strings.toString(charAttributes.hp);
      string memory strMaxHp = Strings.toString(charAttributes.maxHp);
      string memory strAttackDamage = Strings.toString(charAttributes.attackDamage);
    
      string memory json = Base64.encode(
        abi.encodePacked(
          '{"name": "',
          charAttributes.name,
          ' -- NFT #: ',
          Strings.toString(_tokenId),
          '", "description": "This is an NFT that lets people play in the game Metaverse Slayer!", "image": "',
          charAttributes.imageURI,
          '", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ',
          strAttackDamage,'} ]}'
        )
      );
    
      string memory output = string(
        abi.encodePacked("data:application/json;base64,", json)
      );
      
      return output;
    }
    
      function checkIfUserHasNFT() public view returns (CharacterAttributes memory) {
      // Get the tokenId of the user's character NFT
      uint256 userNftTokenId = nftHolders[msg.sender];
      // If the user has a tokenId in the map, return their character.
      if (userNftTokenId > 0) {
        return nftHolderAttributes[userNftTokenId];
      }
      // Else, return an empty character.
      else {
        CharacterAttributes memory emptyStruct;
        return emptyStruct;
       }
    }
    
      function getBigBoss() public view returns (BigBoss memory) {
      return bigBoss;
    }
      function getAllDefaultCharacters() public view returns (CharacterAttributes[] memory) {
      return defaultCharacters;
    }
    }
    

同步替换我们的deploy.js

    const main = async () => {
      const gameContractFactory = await hre.ethers.getContractFactory('MyEpicGame');
      
      const gameContract = await gameContractFactory.deploy(                        
        ["Leo", "Aang", "Pikachu"],       
        ["https://i.imgur.com/pKd5Sdk.png", 
        "https://i.imgur.com/xVu4vFL.png", 
        "https://i.imgur.com/u7T87A6.png"],
        [100, 200, 300],                    
        [100, 50, 25],
        "Elon Musk",
        "https://i.imgur.com/AksR0tt.png",
        10000,
        50
      );
    
      await gameContract.deployed();
      console.log("Contract deployed to:", gameContract.address);
    
    };
    
    const runMain = async () => {
      try {
        await main();
        process.exit(0);
      } catch (error) {
        console.log(error);
        process.exit(1);
      }
    };
    
    runMain();
    

当上面都替换好之后，运行部署脚本。

    npx hardhat run scripts/deploy.js --network goerli
    

当部署完成后，我们可以去到测试网络的区块浏览器，查看mint事件和攻击事件的event。

![](https://storage.googleapis.com/papyrus_images/532213a907bef24a7d2f40647279c72a6bcb049bfefdc9f7bbdf43bc724ea5a3.png)

这一步不是提交截图了，**但是你需要把你的合约的地址记录下来**，而是问我们event的作用，大家可以采用我的：

    It is convenient to present the data visually to the user, and it is also convenient for the project side to better view the log
    

### 4.创建前端

上面我们把合约编写完成了，下面就来做一个前端也便于用户操作。

**step1.获取前端代码**

还是使用replit来操作，首先去到该项目的[github地址](https://github.com/buildspace/buildspace-nft-game-starter.git) ，在新建项目的时候选择从github导入。

![从github导入](https://storage.googleapis.com/papyrus_images/f1e7f1269b8d93f8e9902d8a2be52a80f2e488922e4ecd7765770041302c3aea.png)

从github导入

![选择模版](https://storage.googleapis.com/papyrus_images/ad39ef65c23fafbdbc7c778a9a903610a1917cae222458f6d9e4a12d9e1cc608.png)

选择模版

等待工作台安装完毕，当准备好之后，在shell去输入下面的命令。

    npm install
    npm run start
    

![安装依赖](https://storage.googleapis.com/papyrus_images/11bc07293271e5dd9a8d7f5552fc9b1ded7c2c56509e0a93a22f74b8bb6af0dd.png)

安装依赖

当我们使用start命令启动项目后，将**项目的截图最为这一步的提交截图。**

![本步骤的截图](https://storage.googleapis.com/papyrus_images/c254757335f7bee96f1f03e4b48aaa110c54d084fb948dbf77fa27b9986db36c.png)

本步骤的截图

**step2.添加连接钱包按钮**

将下面的代码替换到app,jsx中。

    import React, { useEffect, useState } from 'react';
    import './App.css';
    import twitterLogo from './assets/twitter-logo.svg';
    
    // Constants
    const TWITTER_HANDLE = '_buildspace';
    const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
    
    const App = () => {
      // State
      const [currentAccount, setCurrentAccount] = useState(null);
    
      // Actions
      const checkIfWalletIsConnected = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            console.log('Make sure you have MetaMask!');
            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);
        }
      };
    
      /*
       * Implement your connectWallet method here
       */
      const connectWalletAction = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            alert('Get MetaMask!');
            return;
          }
    
          /*
           * Fancy method to request access to account.
           */
          const accounts = await ethereum.request({
            method: 'eth_requestAccounts',
          });
    
          /*
           * Boom! This should print out public address once we authorize Metamask.
           */
          console.log('Connected', accounts[0]);
          setCurrentAccount(accounts[0]);
        } catch (error) {
          console.log(error);
        }
      };
    
      useEffect(() => {
        checkIfWalletIsConnected();
      }, []);
    
      return (
        <div className="App">
          <div className="container">
            <div className="header-container">
              <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
              <p className="sub-text">Team up to protect the Metaverse!</p>
              <div className="connect-wallet-container">
                <img
                  src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv"
                  alt="Monty Python Gif"
                />
                {/*
                 * Button that we will use to trigger wallet connect
                 * Don't forget to add the onClick event to call your method!
                 */}
                <button
                  className="cta-button connect-wallet-button"
                  onClick={connectWalletAction}
                >
                  Connect Wallet To Get Started
                </button>
              </div>
            </div>
            <div className="footer-container">
              <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
              <a
                className="footer-text"
                href={TWITTER_LINK}
                target="_blank"
                rel="noreferrer"
              >{`built with @${TWITTER_HANDLE}`}</a>
            </div>
          </div>
        </div>
      );
    };
    
    export default App;
    

查看我们的网站，**将这一步的结果作为本步骤的截图。**

![本步骤的截图](https://storage.googleapis.com/papyrus_images/e65830b6cdd06196684411f0292425f2517d97e2b14936d7593614d43dd2e239.png)

本步骤的截图

**step3.初始化应用**

首先去到src/Components/SelectCharacter新建一个index.js文件，内容如下。

    import React, { useEffect, useState } from 'react';
    import './SelectCharacter.css';
    
    /*
     * Don't worry about setCharacterNFT just yet, we will talk about it soon!
     */
    const SelectCharacter = ({ setCharacterNFT }) => {
      return (
        <div className="select-character-container">
          <h2>Mint Your Hero. Choose wisely.</h2>
        </div>
      );
    };
    
    export default SelectCharacter;
    

同时将app.jsx替换。

    import React, { useEffect, useState } from 'react';
    import './App.css';
    import SelectCharacter from './Components/SelectCharacter';
    import twitterLogo from './assets/twitter-logo.svg';
    
    // Constants
    const TWITTER_HANDLE = '_buildspace';
    const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
    
    const App = () => {
      // State
      const [currentAccount, setCurrentAccount] = useState(null);
      const [characterNFT, setCharacterNFT] = useState(null);
    
    
      // Actions
      const checkIfWalletIsConnected = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            console.log('Make sure you have MetaMask!');
            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);
        }
      };
    
      // Render Methods
    const renderContent = () => {
      /*
       * Scenario #1
       */
      if (!currentAccount) {
        return (
          <div className="connect-wallet-container">
            <img
              src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv"
              alt="Monty Python Gif"
            />
            <button
              className="cta-button connect-wallet-button"
              onClick={connectWalletAction}
            >
              Connect Wallet To Get Started
            </button>
          </div>
        );
        /*
         * Scenario #2
         */
      } else if (currentAccount && !characterNFT) {
        return <SelectCharacter setCharacterNFT={setCharacterNFT} />;
      }
    };
    
    
      /*
       * Implement your connectWallet method here
       */
      const connectWalletAction = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            alert('Get MetaMask!');
            return;
          }
    
          /*
           * Fancy method to request access to account.
           */
          const accounts = await ethereum.request({
            method: 'eth_requestAccounts',
          });
    
          /*
           * Boom! This should print out public address once we authorize Metamask.
           */
          console.log('Connected', accounts[0]);
          setCurrentAccount(accounts[0]);
        } catch (error) {
          console.log(error);
        }
      };
    
      useEffect(() => {
        checkIfWalletIsConnected();
      }, []);
    
      return (
      <div className="App">
        <div className="container">
          <div className="header-container">
            <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
            <p className="sub-text">Team up to protect the Metaverse!</p>
            {/* This is where our button and image code used to be!
             *	Remember we moved it into the render method.
             */}
            {renderContent()}
          </div>
          <div className="footer-container">
            <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
            <a
              className="footer-text"
              href={TWITTER_LINK}
              target="_blank"
              rel="noreferrer"
            >{`built with @${TWITTER_HANDLE}`}</a>
          </div>
        </div>
      </div>
    );
    };
    
    export default App;
    

再次运行npm run start 启动项目，**点击连接钱包后将页面作为本步骤作为截图提交。**

![本步骤截图](https://storage.googleapis.com/papyrus_images/186b89a2dc7ec66e2432856b410dab98f4282419cea35f7ee7f209e38ee3b5a9.png)

本步骤截图

**step4.校验角色是否有NFT**

首先在src下面新建一个constants.js，写入下面的内容，**需要替换你的合约地址。**

    const CONTRACT_ADDRESS = '你的合约地址';
    
    /*
     * Add this method and make sure to export it on the bottom!
     */
    const transformCharacterData = (characterData) => {
      return {
        name: characterData.name,
        imageURI: characterData.imageURI,
        hp: characterData.hp.toNumber(),
        maxHp: characterData.maxHp.toNumber(),
        attackDamage: characterData.attackDamage.toNumber(),
      };
    };
    
    export { CONTRACT_ADDRESS, transformCharacterData };
    

在src下新建一个MyEpicGame.json文件，同时去到你编写合约的replit的仓库找到artifacts/contracts/MyEpicGame.sol/MyEpicGame.json，将这个文件全部复制到src下面的MyEpicGame.json。使用你们自己的哦。

修改你的app.jsx替换为下面的。

    import React, { useEffect, useState } from 'react';
    import './App.css';
    import SelectCharacter from './Components/SelectCharacter';
    import twitterLogo from './assets/twitter-logo.svg';
    import { CONTRACT_ADDRESS, transformCharacterData } from './constants';
    import myEpicGame from './MyEpicGame.json';
    import { ethers } from 'ethers';
    
    // Constants
    const TWITTER_HANDLE = '_buildspace';
    const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
    
    const App = () => {
      // State
      const [currentAccount, setCurrentAccount] = useState(null);
      const [characterNFT, setCharacterNFT] = useState(null);
    
    
      // Actions
      const checkIfWalletIsConnected = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            console.log('Make sure you have MetaMask!');
            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);
        }
      };
    
      // Render Methods
    const renderContent = () => {
      /*
       * Scenario #1
       */
      if (!currentAccount) {
        return (
          <div className="connect-wallet-container">
            <img
              src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv"
              alt="Monty Python Gif"
            />
            <button
              className="cta-button connect-wallet-button"
              onClick={connectWalletAction}
            >
              Connect Wallet To Get Started
            </button>
          </div>
        );
        /*
         * Scenario #2
         */
      } else if (currentAccount && !characterNFT) {
        return <SelectCharacter setCharacterNFT={setCharacterNFT} />;
      }
    };
    
    
      /*
       * Implement your connectWallet method here
       */
      const connectWalletAction = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            alert('Get MetaMask!');
            return;
          }
    
          /*
           * Fancy method to request access to account.
           */
          const accounts = await ethereum.request({
            method: 'eth_requestAccounts',
          });
    
          /*
           * Boom! This should print out public address once we authorize Metamask.
           */
          console.log('Connected', accounts[0]);
          setCurrentAccount(accounts[0]);
        } catch (error) {
          console.log(error);
        }
      };
      const checkNetwork = async () => {
      try { 
        if (window.ethereum.networkVersion !== '5') {
          alert("Please connect to Goerli!")
        }
      } catch(error) {
        console.log(error)
      }
    }
    
      useEffect(() => {
        checkIfWalletIsConnected();
        checkNetwork();
        
      }, []);
    
    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 txn = await gameContract.checkIfUserHasNFT();
        if (txn.name) {
          console.log('User has character NFT');
          setCharacterNFT(transformCharacterData(txn));
        } else {
          console.log('No character NFT found');
        }
      };
    
      /*
       * We only want to run this, if we have a connected wallet
       */
      if (currentAccount) {
        console.log('CurrentAccount:', currentAccount);
        fetchNFTMetadata();
      }
    }, [currentAccount]);
    
      return (
      <div className="App">
        <div className="container">
          <div className="header-container">
            <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
            <p className="sub-text">Team up to protect the Metaverse!</p>
            {/* This is where our button and image code used to be!
             *	Remember we moved it into the render method.
             */}
            {renderContent()}
          </div>
          <div className="footer-container">
            <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
            <a
              className="footer-text"
              href={TWITTER_LINK}
              target="_blank"
              rel="noreferrer"
            >{`built with @${TWITTER_HANDLE}`}</a>
          </div>
        </div>
      </div>
    );
    };
    
    export default App;
    

当我们再次npm run start可以看到输出结果（No character NFT found），将**这一步结果作本步骤的截图提交**。切记如果你连接的钱包和你部署的合约的钱包是同一个，则会输（User has character NFT）。

![本步骤提交截图](https://storage.googleapis.com/papyrus_images/4516069445e1509884e28ce9df59e1cf98c12b39d3f12ad069a1a6dd06516ace.png)

本步骤提交截图

**step5.构建角色选择页面**

将我们的Components/SelectCharacter/index.js替换为下面的即可。

    import React, { useEffect, useState } from 'react';
    import './SelectCharacter.css';
    import { ethers } from 'ethers';
    import { CONTRACT_ADDRESS, transformCharacterData } from '../../constants';
    import myEpicGame from '../../MyEpicGame.json';
    
    const SelectCharacter = ({ setCharacterNFT }) => {
      const [characters, setCharacters] = useState([]);
      const [gameContract, setGameContract] = useState(null);
    
    
      // UseEffect
    useEffect(() => {
      const { ethereum } = window;
    
      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const gameContract = new ethers.Contract(
          CONTRACT_ADDRESS,
          myEpicGame.abi,
          signer
        );
    
        /*
         * This is the big difference. Set our gameContract in state.
         */
        setGameContract(gameContract);
      } else {
        console.log('Ethereum object not found');
      }
    }, []);
    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);
        }
      };
    
      /*
       * Add a callback method that will fire when this event is received
       */
      const onCharacterMint = async (sender, tokenId, characterIndex) => {
        console.log(
          `CharacterNFTMinted - sender: ${sender} tokenId: ${tokenId.toNumber()} characterIndex: ${characterIndex.toNumber()}`
        );
    
        /*
         * Once our character NFT is minted we can fetch the metadata from our contract
         * and set it in state to move onto the Arena
         */
        if (gameContract) {
          const characterNFT = await gameContract.checkIfUserHasNFT();
          console.log('CharacterNFT: ', characterNFT);
          setCharacterNFT(transformCharacterData(characterNFT));
        }
      };
    
      if (gameContract) {
        getCharacters();
    
        /*
         * Setup NFT Minted Listener
         */
        gameContract.on('CharacterNFTMinted', onCharacterMint);
      }
    
      return () => {
        /*
         * When your component unmounts, let;s make sure to clean up this listener
         */
        if (gameContract) {
          gameContract.off('CharacterNFTMinted', onCharacterMint);
        }
      };
    }, [gameContract]);
    
      // Render Methods
    const renderCharacters = () =>
      characters.map((character, index) => (
        <div className="character-item" key={character.name}>
          <div className="name-container">
            <p>{character.name}</p>
          </div>
          <img src={character.imageURI} alt={character.name} />
          <button
            type="button"
            className="character-mint-button"
            onClick={()=> mintCharacterNFTAction(index)}
          >{`Mint ${character.name}`}</button>
        </div>
      ));
    
      // Actions
    const mintCharacterNFTAction = async (characterId) => {
      try {
        if (gameContract) {
          console.log('Minting character in progress...');
          const mintTxn = await gameContract.mintCharacterNFT(characterId);
          await mintTxn.wait();
          console.log('mintTxn:', mintTxn);
        }
      } catch (error) {
        console.warn('MintCharacterAction Error:', error);
      }
    };
    
     return (
      <div className="select-character-container">
        <h2>Mint Your Hero. Choose wisely.</h2>
        {/* Only show this when there are characters in state */}
        {characters.length > 0 && (
          <div className="character-grid">{renderCharacters()}</div>
        )}
      </div>
    );
    };
    
    
    
    export default SelectCharacter;
    

点击mint按钮即可去mint属于你的nft，**去到opesnsea的测试网络找到这次你mint放入nft将链接作为本次的提交。**

![mint结果](https://storage.googleapis.com/papyrus_images/f2114d2f2d27e84fc5eab218fa74306667f8a05fb92c4e34230d17f42bac8e66.png)

mint结果

![os显示](https://storage.googleapis.com/papyrus_images/280992343c194a9dcc5f75d02daf7f492745404c6bb057fdc41f87b662ec8707.png)

os显示

**step6.构建竞技场**

在Arena下面新建一个index.js,内容如下。

    import React, { useEffect, useState } from 'react';
    import { ethers } from 'ethers';
    import { CONTRACT_ADDRESS, transformCharacterData } from '../../constants';
    import myEpicGame from '../../MyEpicGame.json';
    import './Arena.css';
    
    /*
     * We pass in our characterNFT metadata so we can show a cool card in our UI
     */
    const Arena = ({ characterNFT }) => {
      // State
      const [gameContract, setGameContract] = useState(null);
      const [boss, setBoss] = useState(null);
      const [attackState, setAttackState] = useState('');
      const runAttackAction = async () => {
      try {
        if (gameContract) {
          setAttackState('attacking');
          console.log('Attacking boss...');
          const attackTxn = await gameContract.attackBoss();
          await attackTxn.wait();
          console.log('attackTxn:', attackTxn);
          setAttackState('hit');
        }
      } catch (error) {
        console.error('Error attacking boss:', error);
        setAttackState('');
      }
    };
    
      const Arena = ({ characterNFT, setCharacterNFT }) => {
        
        ...
    
        // UseEffects
        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 = (from, newBossHp, newPlayerHp) => {
                const bossHp = newBossHp.toNumber();
                const playerHp = newPlayerHp.toNumber();
                const sender = from.toString();
    
                console.log(`AttackComplete: Boss Hp: ${bossHp} Player Hp: ${playerHp}`);
    
                /*
                * If player is our own, update both player and boss Hp
                */
                if (currentAccount === sender.toLowerCase()) {
    
                  setBoss((prevState) => {
                      return { ...prevState, hp: bossHp };
                  });
                  setCharacterNFT((prevState) => {
                      return { ...prevState, hp: playerHp };
                  });
                }
                /*
                * If player isn't ours, update boss Hp only
                */
                else {
                  setBoss((prevState) => {
                      return { ...prevState, hp: bossHp };
                  });
                }
            }
    
            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]);
    }
    
      useEffect(() => {
      /*
       * Setup async function that will get the boss from our contract and sets in state
       */
      const fetchBoss = async () => {
        const bossTxn = await gameContract.getBigBoss();
        console.log('Boss:', bossTxn);
        setBoss(transformCharacterData(bossTxn));
      };
    
      if (gameContract) {
        /*
         * gameContract is ready to go! Let's fetch our boss
         */
        fetchBoss();
      }
    }, [gameContract]);
    
      // UseEffects
      useEffect(() => {
        const { ethereum } = window;
    
        if (ethereum) {
          const provider = new ethers.providers.Web3Provider(ethereum);
          const signer = provider.getSigner();
          const gameContract = new ethers.Contract(
            CONTRACT_ADDRESS,
            myEpicGame.abi,
            signer
          );
    
          setGameContract(gameContract);
        } else {
          console.log('Ethereum object not found');
        }
      }, []);
    
    return (
      <div className="arena-container">
        {/* Boss */}
        {boss && (
          <div className="boss-container">
            {/* Add attackState to the className! After all, it's just class names */}
            <div className={`boss-content ${attackState}`}>
              <h2>🔥 {boss.name} 🔥</h2>
              <div className="image-content">
                <img src={boss.imageURI} alt={`Boss ${boss.name}`} />
                <div className="health-bar">
                  <progress value={boss.hp} max={boss.maxHp} />
                  <p>{`${boss.hp} / ${boss.maxHp} HP`}</p>
                </div>
              </div>
            </div>
            <div className="attack-container">
              <button className="cta-button" onClick={runAttackAction}>
                {`💥 Attack ${boss.name}`}
              </button>
            </div>
          </div>
        )}
    
    
        
        {/* Replace your Character UI with this */}
        {characterNFT && (
          <div className="players-container">
            <div className="player-container">
              <h2>Your Character</h2>
              <div className="player">
                <div className="image-content">
                  <h2>{characterNFT.name}</h2>
                  <img
                    src={characterNFT.imageURI}
                    alt={`Character ${characterNFT.name}`}
                  />
                  <div className="health-bar">
                    <progress value={characterNFT.hp} max={characterNFT.maxHp} />
                    <p>{`${characterNFT.hp} / ${characterNFT.maxHp} HP`}</p>
                  </div>
                </div>
                <div className="stats">
                  <h4>{`⚔️ Attack Damage: ${characterNFT.attackDamage}`}</h4>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>
    );
    
    };
    
    export default Arena;
    

同时去替换app.js,代码如下。

    import React, { useEffect, useState } from 'react';
    import './App.css';
    import SelectCharacter from './Components/SelectCharacter';
    import twitterLogo from './assets/twitter-logo.svg';
    import { CONTRACT_ADDRESS, transformCharacterData } from './constants';
    import myEpicGame from './MyEpicGame.json';
    import { ethers } from 'ethers';
    import Arena from './Components/Arena';
    
    // Constants
    const TWITTER_HANDLE = '_buildspace';
    const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
    
    const App = () => {
      // State
      const [currentAccount, setCurrentAccount] = useState(null);
      const [characterNFT, setCharacterNFT] = useState(null);
      
    
      // Actions
      const checkIfWalletIsConnected = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            console.log('Make sure you have MetaMask!');
            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);
        }
      };
    
      // Render Methods
    const renderContent = () => {
      if (!currentAccount) {
        return (
          <div className="connect-wallet-container">
            <img
              src="https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv"
              alt="Monty Python Gif"
            />
            <button
              className="cta-button connect-wallet-button"
              onClick={connectWalletAction}
            >
              Connect Wallet To Get Started
            </button>
          </div>
        );
      } else if (currentAccount && !characterNFT) {
        return <SelectCharacter setCharacterNFT={setCharacterNFT} />;	
        /*
        * If there is a connected wallet and characterNFT, it's time to battle!
        */
      } else if (currentAccount && characterNFT) {
        return <Arena characterNFT={characterNFT} setCharacterNFT={setCharacterNFT} />;
      }
    };
    
    
      /*
       * Implement your connectWallet method here
       */
      const connectWalletAction = async () => {
        try {
          const { ethereum } = window;
    
          if (!ethereum) {
            alert('Get MetaMask!');
            return;
          }
    
          /*
           * Fancy method to request access to account.
           */
          const accounts = await ethereum.request({
            method: 'eth_requestAccounts',
          });
    
          /*
           * Boom! This should print out public address once we authorize Metamask.
           */
          console.log('Connected', accounts[0]);
          setCurrentAccount(accounts[0]);
        } catch (error) {
          console.log(error);
        }
      };
      const checkNetwork = async () => {
      try { 
        if (window.ethereum.networkVersion !== '5') {
          alert("Please connect to Goerli!")
        }
      } catch(error) {
        console.log(error)
      }
    }
    
      useEffect(() => {
        checkIfWalletIsConnected();
        checkNetwork();
        
      }, []);
    
    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 txn = await gameContract.checkIfUserHasNFT();
        if (txn.name) {
          console.log('User has character NFT');
          setCharacterNFT(transformCharacterData(txn));
        } else {
          console.log('No character NFT found');
        }
      };
    
      /*
       * We only want to run this, if we have a connected wallet
       */
      if (currentAccount) {
        console.log('CurrentAccount:', currentAccount);
        fetchNFTMetadata();
      }
    }, [currentAccount]);
    
      return (
      <div className="App">
        <div className="container">
          <div className="header-container">
            <p className="header gradient-text">⚔️ Metaverse Slayer ⚔️</p>
            <p className="sub-text">Team up to protect the Metaverse!</p>
            {/* This is where our button and image code used to be!
             *	Remember we moved it into the render method.
             */}
            {renderContent()}
          </div>
          <div className="footer-container">
            <img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
            <a
              className="footer-text"
              href={TWITTER_LINK}
              target="_blank"
              rel="noreferrer"
            >{`built with @${TWITTER_HANDLE}`}</a>
          </div>
        </div>
      </div>
    );
    };
    
    export default App;
    

大家可以试玩下，帝哥玩了两次后小李子，已经挂了。。。，**因为官方需要你的人物死的截图，真变态。**

![本步骤截图](https://storage.googleapis.com/papyrus_images/c1466a2036cb121f6e13b25f39127fbe71afb059cee3f0345bf700c6323b9f50.png)

本步骤截图

### 5.优化网站

**step1.ui改变，提示等**

因为这一步的代码太多了，我将自己的仓库公开了，大家去replit复制即可，地址在下面：

[https://replit.com/@paulCoinman/buildspace-nft-game-starter-coinmanlabs?v=1](https://replit.com/@paulCoinman/buildspace-nft-game-starter-coinmanlabs?v=1)

需要复制的文件：

*   app.js
    
*   Components/SelectCharacter/index.js和css
    
*   Arena的index.js和css
    

最终的结果，大家后面的可以选择的做。

![最终结果](https://storage.googleapis.com/papyrus_images/a1e75501648cc16f4f6f9ac15606fe73b8c98d1feba7fb264ef7564bafe35417.png)

最终结果

关注coinmanlabs获取区块链最新讯息。

推特：[coinmanlabs](https://twitter.com/coinmanlabs)

公众号：coinmanlabs

加客服微信进群了解更多信息。

![客服微信](https://storage.googleapis.com/papyrus_images/e0cc1b235b816c0ac37ead8f440ed29476609c4cbb01b81be07f1e447fba7aa1.jpg)

客服微信

---

*Originally published on [Coinman.eth](https://paragraph.com/@coinman-eth/buildspace-create-turn-based-nft-game)*
