CryptoZombie: Zombie Battle System
try it yourself
pragma solidity ^0.4.25;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper {
uint randNonce = 0;
uint attackVictoryProbability = 70; //70%의 확률로 이길 수 있다
// 특정 범위의 랜덤한 숫자를 생성한다
function randMod(uint _modulus) internal returns(uint) {
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
}
// 랜덤 숫자를 생성하여 70%의 확률로 타깃에게 승리를 거둔다
// 이긴 경우 레벨업하고 각 좀비의 이긴 횟수와 진 횟수를 업데이트
// 또한 새로운 좀비를 생성한다
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}
}
}
pragma solidity ^0.4.25;
import "./ownable.sol";
contract ZombieFactory is Ownable {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
uint cooldownTime = 1 days;
// 레벨, 준비 시간 추가(쿨타임 지난 시간)
// 이긴 횟수, 진 횟수 추가
struct Zombie {
string name;
uint dna;
uint32 level;
uint32 readyTime;
uint16 winCount;
uint16 lossCount;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) internal {
uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
randDna = randDna - randDna % 100;
_createZombie(_name, randDna);
}
}
pragma solidity ^0.4.25;
import "./zombiefactory.sol";
contract KittyInterface {
function getKitty(uint256 _id) external view returns (
bool isGestating,
bool isReady,
uint256 cooldownIndex,
uint256 nextActionAt,
uint256 siringWithId,
uint256 birthTime,
uint256 matronId,
uint256 sireId,
uint256 generation,
uint256 genes
);
}
contract ZombieFeeding is ZombieFactory {
KittyInterface kittyContract;
// 특정 아이디의 좀비의 주인이 그 함수를 부른 주소와 같도록 제한
modifier ownerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
function setKittyContractAddress(address _address) external onlyOwner {
kittyContract = KittyInterface(_address);
}
// 블록에 저장된 좀비의 쿨타임 생성
function _triggerCooldown(Zombie storage _zombie) internal {
_zombie.readyTime = uint32(now + cooldownTime);
}
// 좀비의 쿨타임이 지났는지 확인
function _isReady(Zombie storage _zombie) internal view returns (bool) {
return (_zombie.readyTime <= now);
}
// 먹어서 생기는 새로운 좀비 생성에 위에 두 함수를 이용하여 쿨타임을 적용한다
function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal ownerOf(_zombieId) {
// 좀비를 함수에 넘겨야 하므로 storage
Zombie storage myZombie = zombies[_zombieId];
require(_isReady(myZombie));
_targetDna = _targetDna % dnaModulus;
uint newDna = (myZombie.dna + _targetDna) / 2;
if (keccak256(abi.encodePacked(_species)) == keccak256(abi.encodePacked("kitty"))) {
newDna = newDna - newDna % 100 + 99;
}
_createZombie("NoName", newDna);
_triggerCooldown(myZombie);
}
function feedOnKitty(uint _zombieId, uint _kittyId) public {
uint kittyDna;
(,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId);
feedAndMultiply(_zombieId, kittyDna, "kitty");
}
}
pragma solidity ^0.4.25;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
// 레벨업에 필요한 이더를 정한다
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
// 레벨업 비용으로 받은 이더를 컨트렉트 주인의 계좌로 옮긴다
function withdraw() external onlyOwner {
address _owner = owner();
_owner.transfer(address(this).balance);
}
// 레벨업 비용을 바꿀 수 있도록 한다
function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}
// 지불한 비용이 제시된 비용과 같으면 해당 좀비를 레벨업해준다
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) ownerOf(_zombieId) {
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[]) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
payable functions are part of what makes Solidity and Ethereum so cool — they are a special type of function that can receive Ether.
...
This allows for some really interesting logic, like requiring a certain payment to the contract in order to execute a function.
Only payable functions may receive ether. ether
is an built-in unit. msg.value
is a way to see how much ether was sent.
We will let users pay ether to level up their zombies:
uint levelUpFee = 0.001 ether;
...
function levelUp (uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
What happens to the ether that the contract received?
After you send Ether to a contract, it gets stored in the contract's Ethereum account, and it will be trapped there — unless you add a function to withdraw the Ether from the contract.
The address receiving the ether needs to be typed address payable
. Cast explicitly if necessary. After this, you can send ether to this address using transfer
function. address(this).balance
indicates the amount of ether stored in the current contract.
Ex1) If the user overpaid:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
Ex2) Buyer-seller intermedium:
seller.transfer(msg.value);
//automatical purchase can be made without central system.
withdraw()
and setLevelUpFee()
:
// Transfer ether in this contract
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
// Ether price may change in the futrue.
function setLevelUpFee (uint _fee) external onlyOwner {
levelUpFee =_fee;
}
Finished zombiehelper.sol
:
pragma solidity >=0.5.0 <0.6.0;
import "./zombiefeeding.sol";
contract ZombieHelper is ZombieFeeding {
uint levelUpFee = 0.001 ether;
modifier aboveLevel(uint _level, uint _zombieId) {
require(zombies[_zombieId].level >= _level);
_;
}
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
function setLevelUpFee(uint _fee) external onlyOwner {
levelUpFee = _fee;
}
function levelUp(uint _zombieId) external payable {
require(msg.value == levelUpFee);
zombies[_zombieId].level++;
}
function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].name = _newName;
}
function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
zombies[_zombieId].dna = _newDna;
}
function getZombiesByOwner(address _owner) external view returns(uint[] memory) {
uint[] memory result = new uint[](ownerZombieCount[_owner]);
uint counter = 0;
for (uint i = 0; i < zombies.length; i++) {
if (zombieToOwner[i] == _owner) {
result[counter] = i;
counter++;
}
}
return result;
}
}
Let's make a new file for new contract, ZombieAttack.
pragma solidity >=0.5.0 <0.6.0;
import "./zombiehelper.sol";
contract ZombieAttack is ZombieHelper{}
Random number generation is quite tricky and unsafe with solidity. The best that can be done is with keccak256:
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
randNonce++;
uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
What this would do is take the timestamp of now, the msg.sender, and an incrementing nonce (a number that is only ever used once, so we don't run the same hash function with the same input parameters twice).
But this method is vulnerable to attack by a dishonest node. For example, someone may try out different inputs to get certain values and selectively only share more profitable outcomes. This cheating requires a lot of computing power, though. So unless the benefit is very high, let's not worry about it for now.
Inside ZombieAttack contract:
uint randNonce = 0;
function randMod (uint _modulus) internal returns(uint){
randNonce++;
return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce)))% _modulus;
}
Things to do over the next couple of chapters:
Our zombie battles will work as follows:
1. You choose one of your zombies, and choose an opponent's zombie to attack.
2. If you're the attacking zombie, you will have a 70% chance of winning. The defending zombie will have a 30% chance of winning.
3. All zombies (attacking and defending) will have a winCount and a lossCount that will increment depending on the outcome of the battle.
4. If the attacking zombie wins, it levels up and spawns a new zombie.
5. If it loses, nothing happens (except its lossCount incrementing).
6. Whether it wins or loses, the attacking zombie's cooldown time will be triggered.
For now create a probability variable and set it to 70(uint).
We need to confirm that the zombie attacks are commanded by the rightful owner.
require(msg.sender == zombieToOwner[_zombieId]);
This logic is used multiple times, so let's make it into a modifier.
modifier ownerOf(uint _zombieId) {
require(msg.sender == zombieToOwner[_zombieId]);
_;
}
Replace the above two in all functions using this logic.
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
}
Q. Does putting "storage" make the variable pointer variable?
Let's add score feature for each zombie. In our zombie struct add winCount
and lossCount
. Changes must be made in _createZombie()
as well.
function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
Zombie storage myZombie = zombies[_zombieId];
Zombie storage enemyZombie = zombies[_targetId];
uint rand = randMod(100);
if (rand <= attackVictoryProbability) {
myZombie.winCount++;
myZombie.level++;
enemyZombie.lossCount++;
feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
} else {
myZombie.lossCount++;
enemyZombie.winCount++;
_triggerCooldown(myZombie);
}
}