Skip to content

Instantly share code, notes, and snippets.

@adrianhajdin
Last active March 16, 2024 19:15
Show Gist options
  • Save adrianhajdin/cc3befc6cc1ed69f59334edacbcdb91e to your computer and use it in GitHub Desktop.
Save adrianhajdin/cc3befc6cc1ed69f59334edacbcdb91e to your computer and use it in GitHub Desktop.
Avax Gods - Online Multiplayer Web3 NFT Card Game
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import '@openzeppelin/contracts/token/ERC1155/ERC1155.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol';
/// @title AVAXGods
/// @notice This contract handles the token management and battle logic for the AVAXGods game
/// @notice Version 1.0.0
/// @author Ava-Labs
/// @author Julian Martinez
/// @author Gabriel Cardona
/// @author Raj Ranjan
contract AVAXGods is ERC1155, Ownable, ERC1155Supply {
string public baseURI; // baseURI where token metadata is stored
uint256 public totalSupply; // Total number of tokens minted
uint256 public constant DEVIL = 0;
uint256 public constant GRIFFIN = 1;
uint256 public constant FIREBIRD = 2;
uint256 public constant KAMO = 3;
uint256 public constant KUKULKAN = 4;
uint256 public constant CELESTION = 5;
uint256 public constant MAX_ATTACK_DEFEND_STRENGTH = 10;
enum BattleStatus{ PENDING, STARTED, ENDED }
/// @dev GameToken struct to store player token info
struct GameToken {
string name; /// @param name battle card name; set by player
uint256 id; /// @param id battle card token id; will be randomly generated
uint256 attackStrength; /// @param attackStrength battle card attack; generated randomly
uint256 defenseStrength; /// @param defenseStrength battle card defense; generated randomly
}
/// @dev Player struct to store player info
struct Player {
address playerAddress; /// @param playerAddress player wallet address
string playerName; /// @param playerName player name; set by player during registration
uint256 playerMana; /// @param playerMana player mana; affected by battle results
uint256 playerHealth; /// @param playerHealth player health; affected by battle results
bool inBattle; /// @param inBattle boolean to indicate if a player is in battle
}
/// @dev Battle struct to store battle info
struct Battle {
BattleStatus battleStatus; /// @param battleStatus enum to indicate battle status
bytes32 battleHash; /// @param battleHash a hash of the battle name
string name; /// @param name battle name; set by player who creates battle
address[2] players; /// @param players address array representing players in this battle
uint8[2] moves; /// @param moves uint array representing players' move
address winner; /// @param winner winner address
}
mapping(address => uint256) public playerInfo; // Mapping of player addresses to player index in the players array
mapping(address => uint256) public playerTokenInfo; // Mapping of player addresses to player token index in the gameTokens array
mapping(string => uint256) public battleInfo; // Mapping of battle name to battle index in the battles array
Player[] public players; // Array of players
GameToken[] public gameTokens; // Array of game tokens
Battle[] public battles; // Array of battles
function isPlayer(address addr) public view returns (bool) {
if(playerInfo[addr] == 0) {
return false;
} else {
return true;
}
}
function getPlayer(address addr) public view returns (Player memory) {
require(isPlayer(addr), "Player doesn't exist!");
return players[playerInfo[addr]];
}
function getAllPlayers() public view returns (Player[] memory) {
return players;
}
function isPlayerToken(address addr) public view returns (bool) {
if(playerTokenInfo[addr] == 0) {
return false;
} else {
return true;
}
}
function getPlayerToken(address addr) public view returns (GameToken memory) {
require(isPlayerToken(addr), "Game token doesn't exist!");
return gameTokens[playerTokenInfo[addr]];
}
function getAllPlayerTokens() public view returns (GameToken[] memory) {
return gameTokens;
}
// Battle getter function
function isBattle(string memory _name) public view returns (bool) {
if(battleInfo[_name] == 0) {
return false;
} else {
return true;
}
}
function getBattle(string memory _name) public view returns (Battle memory) {
require(isBattle(_name), "Battle doesn't exist!");
return battles[battleInfo[_name]];
}
function getAllBattles() public view returns (Battle[] memory) {
return battles;
}
function updateBattle(string memory _name, Battle memory _newBattle) private {
require(isBattle(_name), "Battle doesn't exist");
battles[battleInfo[_name]] = _newBattle;
}
// Events
event NewPlayer(address indexed owner, string name);
event NewBattle(string battleName, address indexed player1, address indexed player2);
event BattleEnded(string battleName, address indexed winner, address indexed loser);
event BattleMove(string indexed battleName, bool indexed isFirstMove);
event NewGameToken(address indexed owner, uint256 id, uint256 attackStrength, uint256 defenseStrength);
event RoundEnded(address[2] damagedPlayers);
/// @dev Initializes the contract by setting a `metadataURI` to the token collection
/// @param _metadataURI baseURI where token metadata is stored
constructor(string memory _metadataURI) ERC1155(_metadataURI) {
baseURI = _metadataURI; // Set baseURI
initialize();
}
function setURI(string memory newuri) public onlyOwner {
_setURI(newuri);
}
function initialize() private {
gameTokens.push(GameToken("", 0, 0, 0));
players.push(Player(address(0), "", 0, 0, false));
battles.push(Battle(BattleStatus.PENDING, bytes32(0), "", [address(0), address(0)], [0, 0], address(0)));
}
/// @dev Registers a player
/// @param _name player name; set by player
function registerPlayer(string memory _name, string memory _gameTokenName) external {
require(!isPlayer(msg.sender), "Player already registered"); // Require that player is not already registered
uint256 _id = players.length;
players.push(Player(msg.sender, _name, 10, 25, false)); // Adds player to players array
playerInfo[msg.sender] = _id; // Creates player info mapping
createRandomGameToken(_gameTokenName);
emit NewPlayer(msg.sender, _name); // Emits NewPlayer event
}
/// @dev internal function to generate random number; used for Battle Card Attack and Defense Strength
function _createRandomNum(uint256 _max, address _sender) internal view returns (uint256 randomValue) {
uint256 randomNum = uint256(keccak256(abi.encodePacked(block.difficulty, block.timestamp, _sender)));
randomValue = randomNum % _max;
if(randomValue == 0) {
randomValue = _max / 2;
}
return randomValue;
}
/// @dev internal function to create a new Battle Card
function _createGameToken(string memory _name) internal returns (GameToken memory) {
uint256 randAttackStrength = _createRandomNum(MAX_ATTACK_DEFEND_STRENGTH, msg.sender);
uint256 randDefenseStrength = MAX_ATTACK_DEFEND_STRENGTH - randAttackStrength;
uint8 randId = uint8(uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100);
randId = randId % 6;
if (randId == 0) {
randId++;
}
GameToken memory newGameToken = GameToken(
_name,
randId,
randAttackStrength,
randDefenseStrength
);
uint256 _id = gameTokens.length;
gameTokens.push(newGameToken);
playerTokenInfo[msg.sender] = _id;
_mint(msg.sender, randId, 1, '0x0');
totalSupply++;
emit NewGameToken(msg.sender, randId, randAttackStrength, randDefenseStrength);
return newGameToken;
}
/// @dev Creates a new game token
/// @param _name game token name; set by player
function createRandomGameToken(string memory _name) public {
require(!getPlayer(msg.sender).inBattle, "Player is in a battle"); // Require that player is not already in a battle
require(isPlayer(msg.sender), "Please Register Player First"); // Require that the player is registered
_createGameToken(_name); // Creates game token
}
function getTotalSupply() external view returns (uint256) {
return totalSupply;
}
/// @dev Creates a new battle
/// @param _name battle name; set by player
function createBattle(string memory _name) external returns (Battle memory) {
require(isPlayer(msg.sender), "Please Register Player First"); // Require that the player is registered
require(!isBattle(_name), "Battle already exists!"); // Require battle with same name should not exist
bytes32 battleHash = keccak256(abi.encode(_name));
Battle memory _battle = Battle(
BattleStatus.PENDING, // Battle pending
battleHash, // Battle hash
_name, // Battle name
[msg.sender, address(0)], // player addresses; player 2 empty until they joins battle
[0, 0], // moves for each player
address(0) // winner address; empty until battle ends
);
uint256 _id = battles.length;
battleInfo[_name] = _id;
battles.push(_battle);
return _battle;
}
/// @dev Player joins battle
/// @param _name battle name; name of battle player wants to join
function joinBattle(string memory _name) external returns (Battle memory) {
Battle memory _battle = getBattle(_name);
require(_battle.battleStatus == BattleStatus.PENDING, "Battle already started!"); // Require that battle has not started
require(_battle.players[0] != msg.sender, "Only player two can join a battle"); // Require that player 2 is joining the battle
require(!getPlayer(msg.sender).inBattle, "Already in battle"); // Require that player is not already in a battle
_battle.battleStatus = BattleStatus.STARTED;
_battle.players[1] = msg.sender;
updateBattle(_name, _battle);
players[playerInfo[_battle.players[0]]].inBattle = true;
players[playerInfo[_battle.players[1]]].inBattle = true;
emit NewBattle(_battle.name, _battle.players[0], msg.sender); // Emits NewBattle event
return _battle;
}
// Read battle move info for player 1 and player 2
function getBattleMoves(string memory _battleName) public view returns (uint256 P1Move, uint256 P2Move) {
Battle memory _battle = getBattle(_battleName);
P1Move = _battle.moves[0];
P2Move = _battle.moves[1];
return (P1Move, P2Move);
}
function _registerPlayerMove(uint256 _player, uint8 _choice, string memory _battleName) internal {
require(_choice == 1 || _choice == 2, "Choice should be either 1 or 2!");
require(_choice == 1 ? getPlayer(msg.sender).playerMana >= 3 : true, "Mana not sufficient for attacking!");
battles[battleInfo[_battleName]].moves[_player] = _choice;
}
// User chooses attack or defense move for battle card
function attackOrDefendChoice(uint8 _choice, string memory _battleName) external {
Battle memory _battle = getBattle(_battleName);
require(
_battle.battleStatus == BattleStatus.STARTED,
"Battle not started. Please tell another player to join the battle"
); // Require that battle has started
require(
_battle.battleStatus != BattleStatus.ENDED,
"Battle has already ended"
); // Require that battle has not ended
require(
msg.sender == _battle.players[0] || msg.sender == _battle.players[1],
"You are not in this battle"
); // Require that player is in the battle
require(_battle.moves[_battle.players[0] == msg.sender ? 0 : 1] == 0, "You have already made a move!");
_registerPlayerMove(_battle.players[0] == msg.sender ? 0 : 1, _choice, _battleName);
_battle = getBattle(_battleName);
uint _movesLeft = 2 - (_battle.moves[0] == 0 ? 0 : 1) - (_battle.moves[1] == 0 ? 0 : 1);
emit BattleMove(_battleName, _movesLeft == 1 ? true : false);
if(_movesLeft == 0) {
_awaitBattleResults(_battleName);
}
}
// Awaits battle results
function _awaitBattleResults(string memory _battleName) internal {
Battle memory _battle = getBattle(_battleName);
require(
msg.sender == _battle.players[0] || msg.sender == _battle.players[1],
"Only players in this battle can make a move"
);
require(
_battle.moves[0] != 0 && _battle.moves[1] != 0,
"Players still need to make a move"
);
_resolveBattle(_battle);
}
struct P {
uint index;
uint move;
uint health;
uint attack;
uint defense;
}
/// @dev Resolve battle function to determine winner and loser of battle
/// @param _battle battle; battle to resolve
function _resolveBattle(Battle memory _battle) internal {
P memory p1 = P(
playerInfo[_battle.players[0]],
_battle.moves[0],
getPlayer(_battle.players[0]).playerHealth,
getPlayerToken(_battle.players[0]).attackStrength,
getPlayerToken(_battle.players[0]).defenseStrength
);
P memory p2 = P(
playerInfo[_battle.players[1]],
_battle.moves[1],
getPlayer(_battle.players[1]).playerHealth,
getPlayerToken(_battle.players[1]).attackStrength,
getPlayerToken(_battle.players[1]).defenseStrength
);
address[2] memory _damagedPlayers = [address(0), address(0)];
if (p1.move == 1 && p2.move == 1) {
if (p1.attack >= p2.health) {
_endBattle(_battle.players[0], _battle);
} else if (p2.attack >= p1.health) {
_endBattle(_battle.players[1], _battle);
} else {
players[p1.index].playerHealth -= p2.attack;
players[p2.index].playerHealth -= p1.attack;
players[p1.index].playerMana -= 3;
players[p2.index].playerMana -= 3;
// Both player's health damaged
_damagedPlayers = _battle.players;
}
} else if (p1.move == 1 && p2.move == 2) {
uint256 PHAD = p2.health + p2.defense;
if (p1.attack >= PHAD) {
_endBattle(_battle.players[0], _battle);
} else {
uint256 healthAfterAttack;
if(p2.defense > p1.attack) {
healthAfterAttack = p2.health;
} else {
healthAfterAttack = PHAD - p1.attack;
// Player 2 health damaged
_damagedPlayers[0] = _battle.players[1];
}
players[p2.index].playerHealth = healthAfterAttack;
players[p1.index].playerMana -= 3;
players[p2.index].playerMana += 3;
}
} else if (p1.move == 2 && p2.move == 1) {
uint256 PHAD = p1.health + p1.defense;
if (p2.attack >= PHAD) {
_endBattle(_battle.players[1], _battle);
} else {
uint256 healthAfterAttack;
if(p1.defense > p2.attack) {
healthAfterAttack = p1.health;
} else {
healthAfterAttack = PHAD - p2.attack;
// Player 1 health damaged
_damagedPlayers[0] = _battle.players[0];
}
players[p1.index].playerHealth = healthAfterAttack;
players[p1.index].playerMana += 3;
players[p2.index].playerMana -= 3;
}
} else if (p1.move == 2 && p2.move == 2) {
players[p1.index].playerMana += 3;
players[p2.index].playerMana += 3;
}
emit RoundEnded(
_damagedPlayers
);
// Reset moves to 0
_battle.moves[0] = 0;
_battle.moves[1] = 0;
updateBattle(_battle.name, _battle);
// Reset random attack and defense strength
uint256 _randomAttackStrengthPlayer1 = _createRandomNum(MAX_ATTACK_DEFEND_STRENGTH, _battle.players[0]);
gameTokens[playerTokenInfo[_battle.players[0]]].attackStrength = _randomAttackStrengthPlayer1;
gameTokens[playerTokenInfo[_battle.players[0]]].defenseStrength = MAX_ATTACK_DEFEND_STRENGTH - _randomAttackStrengthPlayer1;
uint256 _randomAttackStrengthPlayer2 = _createRandomNum(MAX_ATTACK_DEFEND_STRENGTH, _battle.players[1]);
gameTokens[playerTokenInfo[_battle.players[1]]].attackStrength = _randomAttackStrengthPlayer2;
gameTokens[playerTokenInfo[_battle.players[1]]].defenseStrength = MAX_ATTACK_DEFEND_STRENGTH - _randomAttackStrengthPlayer2;
}
function quitBattle(string memory _battleName) public {
Battle memory _battle = getBattle(_battleName);
require(_battle.players[0] == msg.sender || _battle.players[1] == msg.sender, "You are not in this battle!");
_battle.players[0] == msg.sender ? _endBattle(_battle.players[1], _battle) : _endBattle(_battle.players[0], _battle);
}
/// @dev internal function to end the battle
/// @param battleEnder winner address
/// @param _battle battle; taken from attackOrDefend function
function _endBattle(address battleEnder, Battle memory _battle) internal returns (Battle memory) {
require(_battle.battleStatus != BattleStatus.ENDED, "Battle already ended"); // Require that battle has not ended
_battle.battleStatus = BattleStatus.ENDED;
_battle.winner = battleEnder;
updateBattle(_battle.name, _battle);
uint p1 = playerInfo[_battle.players[0]];
uint p2 = playerInfo[_battle.players[1]];
players[p1].inBattle = false;
players[p1].playerHealth = 25;
players[p1].playerMana = 10;
players[p2].inBattle = false;
players[p2].playerHealth = 25;
players[p2].playerMana = 10;
address _battleLoser = battleEnder == _battle.players[0] ? _battle.players[1] : _battle.players[0];
emit BattleEnded(_battle.name, battleEnder, _battleLoser); // Emits BattleEnded event
return _battle;
}
// Turns uint256 into string
function uintToStr(uint256 _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return '0';
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
// Token URI getter function
function tokenURI(uint256 tokenId) public view returns (string memory) {
return string(abi.encodePacked(baseURI, '/', uintToStr(tokenId), '.json'));
}
// The following functions are overrides required by Solidity.
function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal override(ERC1155, ERC1155Supply) {
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
}
}
import { ethers } from 'hardhat';
import console from 'console';
const _metadataUri = 'https://gateway.pinata.cloud/ipfs/https://gateway.pinata.cloud/ipfs/QmX2ubhtBPtYw75Wrpv6HLb1fhbJqxrnbhDo1RViW3oVoi';
async function deploy(name: string, ...params: [string]) {
const contractFactory = await ethers.getContractFactory(name);
return await contractFactory.deploy(...params).then((f) => f.deployed());
}
async function main() {
const [admin] = await ethers.getSigners();
console.log(`Deploying a smart contract...`);
const AVAXGods = (await deploy('AVAXGods', _metadataUri)).connect(admin);
console.log({ AVAXGods: AVAXGods.address });
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
});
const emptyAccount = '0x0000000000000000000000000000000000000000';
import dotenv from 'dotenv';
import '@nomiclabs/hardhat-ethers';
dotenv.config();
//* Notes for deploying the smart contract on your own subnet
//* More info on subnets: https://docs.avax.network/subnets
//* Why deploy on a subnet: https://docs.avax.network/subnets/when-to-use-subnet-vs-c-chain
//* How to deploy on a subnet: https://docs.avax.network/subnets/create-a-local-subnet
//* Transactions on the C-Chain might take 2-10 seconds -> the ones on the subnet will be much faster
//* On C-Chain we're relaying on the Avax token to confirm transactions -> on the subnet we can create our own token
//* You are in complete control over the network and it's inner workings
export default {
solidity: {
version: '0.8.16',
settings: {
viaIR: true,
optimizer: {
enabled: true,
runs: 100,
},
},
},
networks: {
fuji: {
url: 'https://api.avax-test.network/ext/bc/C/rpc',
gasPrice: 225000000000,
chainId: 43113,
accounts: [process.env.PRIVATE_KEY],
},
// subnet: {
// url: process.env.NODE_URL,
// chainId: Number(process.env.CHAIN_ID),
// gasPrice: 'auto',
// accounts: [process.env.PRIVATE_KEY],
// },
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment