Part 1: How to Create an NFT Game Smart Contract
Learn how to create a contract that powers an on-chain NFT game similar to CryptoKitties and Axie Infinity
Gaming has become one of the most popular applications of NFTs. Since 2021, there have been multiple games that have witnessed millions of dollars worth of transactions.
NFTs have been used in pet collectible games like Cryptokitties, Pokemon style monster battling games like Axie Infinity and fantasy trading card games like Sorare. These games have been the closest thing to mainstream adoption of NFTs.
In this two part series, we will build a Cryptokitties-style breeding game from scratch. This tutorial will focus on writing a smart contract that will power our game on-chain. The next part will focus on building a frontend using Next.js that interfaces with the aforementioned contract.
About the Game
As stated above, the game will implement breeding mechanics seen in games like Cryptokitties. Users will be able to do the following:
- Mint a genesis monster by paying some ETH.
- Breed a new monster using any two existing monsters that they own (genesis or otherwise)
- View all their monsters on a dapp
We will call our monsters Alchemons. In the subsequent parts of this tutorial, we will write an ERC-721 smart contract that allows users to mint genesis Alchemons and breed new Alchemons into existence.
Creating the NFT Game Smart Contract
Step 1: Create an Alchemy app
We're going to deploy and test our contract on the Goerli testnet. In order to do this, we will require a free Alchemy account.
Create an Alchemy app by following these steps:
- From Alchemy's dashboard, hover over the Apps drop-down menu and choose Create App.
- Provide a Name and Description for your app. For Chain, select Ethereum and for Network select Goerli.
- Click the Create App button.
Once you have created your app, click on your app's View Key button in the dashboard and save the HTTP URL. We will use this later.
Step 2: Install Node and npm
In case you haven't already, install node and npm on your local machine.
Make sure that node is at least v14 or higher by typing the following in your terminal:
node -v
Step 3: Create a Hardhat project
We're going to set up our project using Hardhat, the industry-standard development environment for Ethereum smart contracts. Additionally, we'll also install OpenZeppelin contracts.
To set up Hardhat, run the following commands in your terminal:
mkdir alchemon && cd alchemon
npm init -y
npm install --save-dev hardhat
npx hardhat
Choose Create a Javascript project
from the menu and accept all defaults. To ensure everything is installed correctly, run the following command in your terminal:
npx hardhat test
To install OpenZeppelin:
npm install @openzeppelin/contracts
Step 4: Write the smart contract
Let's now write a smart contract that implements our on-chain game. To do this, we need to broadly implement three things:
- A
mintGenesis
function that allows users to mint genesis (or generation 0) NFTs by paying a certain amount of ETH. - A
breed
function that allows users to use any two of their NFTs to breed a new NFT. - A
generateMetadata
function that generates metadata that will be stored on-chain.
Open the project in your favorite code editor (e.g., VS Code), and create a new file called AlchemonNft.sol
in the contracts
folder. Add the following code to this file:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract AlchemonNft is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// Set price for the genesis NFTs
uint public constant PRICE = 0.0005 ether;
constructor() ERC721("Alchemon", "Alch") {}
// Mint Genesis NFTs
function mintGenesis(uint _count) public payable {
require(msg.value >= PRICE, "Not enough ether to purchase genesis NFT.");
// Genesis NFTs are generation 0
for (uint i = 0; i < _count; i++) {
string memory metadata = generateMetadata(_tokenIds.current(), 0);
_mintSingleNft(metadata);
}
}
// Generate NFT metadata
function generateMetadata(uint tokenId, uint generation) public pure returns (string memory) {
string memory svg = string(abi.encodePacked(
"<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinyMin meet' viewBox='0 0 350 350'>",
"<style>.base { fill: white; font-family: serif; font-size: 25px; }</style>",
"<rect width='100%' height='100%' fill='blue' />",
"<text x='50%' y='40%' class='base' dominant-baseline='middle' text-anchor='middle'>",
"<tspan y='40%' x='50%'>Alchemon #",
Strings.toString(tokenId),
"</tspan>",
"<tspan y='50%' x='50%'>Generation ",
Strings.toString(generation),
"</tspan></text></svg>"
));
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "Alchemon #',
Strings.toString(tokenId),
'", "description": "An in-game monster", "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(svg)),
'", "attributes": [{"trait_type": "Generation", "value": "',
Strings.toString(generation),
'"}]}'
)
)
)
);
string memory metadata = string(
abi.encodePacked("data:application/json;base64,", json)
);
return metadata;
}
// Breed a new NFT
function breed(uint parent1Id, uint parent2Id) public {
require(parent1Id != parent2Id, "Parents must be different");
require(ownerOf(parent1Id) == msg.sender && ownerOf(parent2Id) == msg.sender, "Sender doesn't own NFTs");
// Get generations of each parent and compute new generation
// New Generation = Max(Parent 1, Parent 2) + 1
uint newGen;
if (parent1Id >= parent2Id) {
newGen = parent1Id + 1;
} else {
newGen = parent2Id + 1;
}
// Generate metadata
string memory metadata = generateMetadata(_tokenIds.current(), newGen);
// Mint offspring
_mintSingleNft(metadata);
}
// Mint a single NFT with on-chain metadata
function _mintSingleNft(string memory _tokenURI) private {
uint newTokenID = _tokenIds.current();
_safeMint(msg.sender, newTokenID);
_setTokenURI(newTokenID, _tokenURI);
_tokenIds.increment();
}
// Get tokens of an owner
function tokensOfOwner(address _owner) external view returns (uint[] memory) {
uint tokenCount = balanceOf(_owner);
uint[] memory tokensId = new uint256[](tokenCount);
for (uint i = 0; i < tokenCount; i++) {
tokensId[i] = tokenOfOwnerByIndex(_owner, i);
}
return tokensId;
}
// Withdraw ether
function withdraw() public payable onlyOwner {
uint balance = address(this).balance;
require(balance > 0, "No ether left to withdraw");
(bool success, ) = (msg.sender).call{value: balance}("");
require(success, "Transfer failed.");
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(address from, address to, uint256 tokenId, uint256 batchSize)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
Compile the contract and make sure everything works by running:
npx hardhat compile
Step 5: Install MetaMask and get ETH from the Alchemy faucet
Next, let's set up a MetaMask wallet. You can download the wallet extension for your browser here. MetaMask comes configured with the Goerli network by default, and you can switch to this once your wallet is set up.
In order to deploy our contract, mint NFTs, and pay for the genesis pieces, we will require some test goerliETH. You can obtain this for free from Alchemy's Goerli faucet.
Depending on demand, you may be required to sign with your Alchemy account.
Step 6: Configure hardhat.config.js
hardhat.config.js
Once we have a wallet set up with some test ETH, we can proceed to configure hardhat so that it deploys our contract to Goerli. Replace the contents of hardhat.config.js
with the following:
require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
networks: {
goerli: {
url: '<-- ALCHEMY APP HTTP URL -->',
accounts: ['<-- METAMASK WALLET PRIVATE KEY -->']
}
}
};
Make sure you do not make your HTTP URL or wallet private key public.
Step 7: Write the contract deployment script
We are now in a good position to write a script that allows us to do the following:
- Deploy the Alchemon contract to the Goerli testnet.
- Mint 2 Genesis NFTs by paying 0.001 ETH.
- Breed a new NFT using the Genesis NFTs minted in step 2.
In the scripts
folder, replace the contents of deploy.js
with the following:
const hre = require("hardhat");
const ethers = require("ethers");
async function main() {
// Deploy the Alchemon contract
const contractFactory = await hre.ethers.getContractFactory('AlchemonNft');
const alchContract = await contractFactory.deploy();
await alchContract.deployed();
console.log("Contract deployed to:", alchContract.address);
// Mint 2 genesis NFTs
let txn;
txn = await alchContract.mintGenesis(2, { value: ethers.utils.parseEther('0.001') });
await txn.wait();
console.log("2 NFTs minted");
// Breed genesis NFTs
txn = await alchContract.breed(0, 1);
await txn.wait();
console.log("1 NFT bred");
}
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (e) {
console.log(e);
process.exit(1);
}
}
runMain();
Run this script by running the following command in your terminal:
npx hardhat run scripts/deploy.js --network goerli
If all goes well, you should see output that looks something like this:
Contract deployed to: 0xDDd5bBAFd1F4318277446e17C9ebDcD079977d0C
2 NFTs minted
1 NFT bred
You can check out your new NFTs by searching your contract address in testnets.opensea.io.
Conclusion
Congratulations! You now know how to create a contract that implements an on-chain breeding game. In the next tutorial Part 2: How to create an NFT game frontend, we will build a frontend that will allow users to breed their NFTs from an easy-to-use interface.
If you enjoyed this tutorial about creating NFT games, tweet us at @AlchemyPlatform and give us a shoutout!
Don't forget to join our Discord server to meet other blockchain devs, builders, and entrepreneurs!
Ready to start building your NFT game?
Create a free Alchemy account and do share your project with us!
Updated almost 2 years ago