How I built a Christmas themed NFT game

Sukhpal Saini
4 min readJan 13, 2022

I wanted to work on a Web3 project in December 2021 so clearly, I built an NFT themed Christmas game.

Characters

I tried my hand at drawing some pixel artwork but it was TERRIBLE. I ended up buying some online.

Gameplay

  • The player logs in with their Rinkeby testnet account.
  • They choose 1 character to mint:
  • Based on the character, they are placed either in either of the teams: Santa Squad or the Grinch Pack.
  • Every player has Attack Damage, HP, and Healing Power.
  • They can attack someone on the enemy team or heal someone on their team.
  • The idea of the game is for the players to fight each other until Christmas day hits. The team with the most surviving members wins the brawl.
  • No more minting or gameplay is allowed after Christmas Day 2021.

Smart Contract

I followed an ERC-721 contract tutorial on buildspace and modified it with my own rules.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./libraries/Base64.sol";

import "hardhat/console.sol";

contract SantaWars is ERC721 {

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

using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

uint public deadlineTime;
CharacterAttributes[] defaultCharacters;

mapping(uint256 => CharacterAttributes) public nftHolderAttributes;

mapping(address => uint256) public nftHolders;
uint public totalNFTHolders;

address[] allPlayers;
mapping(address => bool) userExists;

constructor(
string[] memory characterNames,
string[] memory characterImageURIs,
uint[] memory characterHp,
uint[] memory characterAttackDmg,
uint[] memory characterHealingPwr
)
ERC721("Christmasers", "CRMS")
{
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],
healingPower: characterHealingPwr[i]
}));

CharacterAttributes memory c = defaultCharacters[i];

console.log("Done initializing %s w/ HP %s, img %s", c.name, c.hp, c.imageURI);
}

deadlineTime = 1640455200;

_tokenIds.increment();
}

// _characterIndex -> which character you want to mint
function mintCharacterNFT(uint _characterIndex) external {

require (
!userExists[msg.sender],
"Error: You already own an NFT"
);

uint256 newItemId = _tokenIds.current();
_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,
healingPower: defaultCharacters[_characterIndex].healingPower
});

console.log("Minted NFT w/ tokenId %s and characterIndex %s", newItemId, _characterIndex);

nftHolders[msg.sender] = newItemId;
allPlayers.push(msg.sender);
userExists[msg.sender] = true;

_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 strHealingPower = Strings.toString(charAttributes.healingPower);

string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "',
charAttributes.name,
' -- NFT #: ',
Strings.toString(_tokenId),
'", "description": "This is an NFT that lets people play in the game SantaWars game", "image": "',
charAttributes.imageURI,
'", "attributes": [ { "trait_type": "Health Points", "value": ',strHp,', "max_value":',strMaxHp,'}, { "trait_type": "Attack Damage", "value": ', strAttackDamage,'}, { "trait_type": "Healing Power", "value": ', strHealingPower,'} ]}'
)
)
)
);

string memory output = string(
abi.encodePacked("data:application/json;base64,", json)
);

return output;
}

function attack(address targetAddress) public {
uint time = block.timestamp;
require (
time < deadlineTime,
"Error: contract was only valid until Dec 25, 2021."
);

uint256 nftTokenIdOfTarget = nftHolders[targetAddress];
CharacterAttributes storage target = nftHolderAttributes[nftTokenIdOfTarget];

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("Target %s has %s HP and %s AD", target.name, target.hp, target.attackDamage);

require (
player.hp > 0,
"Error: character must have HP to attack target."
);

require (
target.hp > 0,
"Error: target must have HP to be attacked."
);

if (target.hp < player.attackDamage) {
target.hp = 0;
} else {
target.hp = target.hp - player.attackDamage;
}

console.log("Player attacked target. New target hp: %s", target.hp);
emit AttackComplete(target.hp, player.hp);
}

function heal(address targetAddress) public {
uint time = block.timestamp;
require (
time < deadlineTime,
"Error: contract was only valid until Dec 25, 2021."
);

uint256 nftTokenIdOfTarget = nftHolders[targetAddress];
CharacterAttributes storage target = nftHolderAttributes[nftTokenIdOfTarget];

uint256 nftTokenIdOfPlayer = nftHolders[msg.sender];
CharacterAttributes storage player = nftHolderAttributes[nftTokenIdOfPlayer];

console.log("\nPlayer w/ character %s about to attack. Has %s HP and %s Heal Power", player.name, player.hp, player.healingPower);
console.log("Target %s has %s HP", target.name, target.hp, target.healingPower);

require (
player.hp > 0,
"Error: character must have more than 0 HP to heal target."
);

require (
target.hp > 0 ,
"Error: target must be alive to be healed"
);

if ((target.maxHp - target.hp) < player.healingPower) {
target.hp = target.maxHp;
} else {
target.hp = target.hp + player.healingPower;
}

console.log("Player attacked target. New target hp: %s", target.hp);
emit HealComplete(target.hp, player.hp);
}

function getNFTOnUser(address targetAddress) public view returns (CharacterAttributes memory) {
uint256 userNftTokenId = nftHolders[targetAddress];
if (userNftTokenId > 0) {
return nftHolderAttributes[userNftTokenId];
}
else {
CharacterAttributes memory emptyStruct;
return emptyStruct;
}
}

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

function getAllPlayers() public view returns (address[] memory) {
return allPlayers;
}

event CharacterNFTMinted(address sender, uint256 tokenId, uint256 characterIndex);
event AttackComplete(uint newTargetHp, uint newPlayerHp);
event HealComplete(uint newTargetHp, uint newPlayerHp);
}

Front End

I built the frontend in React. It was pretty straightforward by using the ethers package.

Key Learnings

  • Building applications in Web3 is not difficult, it’s just a different paradigm. A developer should spend more time learning about different protocols, capabilities, user cases, than the best libraries etc. to use.
  • Knowing what data to put on-chain and what to leave for off-chain. Transactions can take a few seconds to a few minutes to settle. Building a true real-time game with every action recorded on-chain is very difficult.
  • Minimizing gas fees. Gas fees are killer. Optimizations can be made to reduce the cost when committing a transaction. These add up over time.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Sukhpal Saini
Sukhpal Saini

Written by Sukhpal Saini

Full Stack Engineer. Prev at IBM, Apple, Saks. Now building https://engyne.ai to help B2B SaaS get customers with content marketing using AI.

No responses yet

Write a response