iT邦幫忙

2022 iThome 鐵人賽

DAY 12
1
Web 3

從以太坊白皮書理解 web 3 概念系列 第 13

從以太坊白皮書理解 web 3 概念 - Day12

  • 分享至 

  • xImage
  •  

從以太坊白皮書理解 web 3 概念 - Day12

Learn Solidity - day 4 Battle System

今天將會建立 Zombie 物件的 Battle System

讓 Zombie 互動更多元

讀者可以一起透過以下連結來操作

Lession 4: Battle System

Payable 修飾子

在 Ethereum 中, payload 修飾子是用來讓一個 function 可以接收呼叫者的 Ether

透過這種方式,可以讓使用者把 Ether 存入到某個 Contract 之中

因此可以用這種邏輯來實作一些收款的功能如下

contract OnlineStore {
  function buySomething() external payable {
    // Check to make sure 0.001 ether was sent to the function call:
    require(msg.value == 0.001 ether);
    // If so, some logic to transfer the digital item to the caller of the function:
    transferThing(msg.sender);
  }
}

在範例中, msg.value 是用來察看呼叫者傳輸量的方法,用 ether 作為單位。

上面的 contract 可以透過 Web3.js 使用以下的 code 來呼叫

// Assuming `OnlineStore` points to your contract on Ethereum:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})

特別要注意:如果傳送 value 給非 payable 的 function,交易會被拒絕。

新增 levelUp 邏輯

  1. 新增 uint 變數 levelUpFee , 設定值為 0.001 ether
  2. 建立 function 叫作 levelUp
    需要一個參數: _zombieId(uint)
    存取權限是 external
    設定為 payable
  3. function 需要做以下檢查
    require(msg.value == levelUpFee);
  4. 新增以下邏輯到 function
    zombies[_zombieId].level++;
pragma solidity >=0.5.0 <0.6.0;

import "./zombiefeeding.sol";

contract ZombieHelper is ZombieFeeding {

  // 1. Define levelUpFee here
  uint levelUpFee = 0.001 ether;
  modifier aboveLevel(uint _level, uint _zombieId) {
    require(zombies[_zombieId].level >= _level);
    _;
  }

  // 2. Insert levelUp function here
  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;
  }

}

Withdraws

前面提到透過可以透過 payable function 把 ether 送到 Contract

在執行後,那筆 ether 就會存在 Contract 帳號裡,並且被鎖住。

除非新增一個從 Contract 提取 ether 的功能,這筆 ether 才能被提取出來。

提取 ether 的實作語法如下

contract GetPaid is Ownable {
    function withdraw() external onlyOwner {
        address payable _owner = address(uint160(owner()));
        _owner.transfer(address(this).balance);
    }
}

owner() 與 onlyOwner 都是在 Ownable Contract 內的功能

很重要的一點是只有俱備 address payable 的 address 可以做 transfer

透過把 uint160 轉換成 address payable 之後,就可以使用 transfer function

其中 address(this).balance 會回傳當下 Contract 的所有 balance

transfer function 可以用來傳送 ether 到任何 Ethereum 地址。

舉例來說:

假設有一個 function 是用來把 etehr 傳回去給 msg.sender 當付太多 ether 時

則會有以下邏輯:

uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee); // msg.value 代表原本送的 ether

新增 withdraw 邏輯

  1. 建立 withdraw function 如同範例的 GetPaid 內的 withdraw 一樣
  2. 建立 setLevelUpFee function 。
    需要一個參數: _fee(uint)
    存取權限是 external
    設定成 onlyOwner
  3. setLevelUpFee function 內部設定 levelUpFee = _fee;
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);
    _;
  }

  // 1. Create withdraw function here
  function withdraw() external onlyOwner {
      address payable _owner = address(uint160(owner()));
      _owner.transfer(address(this).balance);
  }
  // 2. Create setLevelUpFee function here
  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;
  }

}

新增 Zombie Battles 邏輯

新建一個 zombieattack.sol 來把 Zombie Battles 邏輯封裝在此

內容如下

pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  
}

Random Numbers

為了讓 Battle 有趣

需要新增一個隨機數機制

然而要在 solidity 有辦法製作 random number 嗎?

答案是無法,至少無法製造出絕對隨機的數字

如下:

// 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;

即使使用 timestamp 還有 msg.sender 還有一個 randNonce 當作種子

來使用 keccak256 做 hash 仍然無法做到絕對隨即數

可以發現還是有機會透過時間與 randNonce 來預測下一個隨機數。

那該如何產生隨機數呢?

有一個可行的辦法是透過 Oracle 從鏈外引入隨機數功能來達成隨機數這個方法。

但由於這邊還沒講到 Oracle 的部份

所以先採取上面的方式來實作

實作 Random 功能

  1. 新增一個 uint 變數 randNonce 設定值為 0
  2. 新增一個 randMod function
    需要一個參數: _modulus(uint)
    回傳值是: uint
    讀取權限是 internal
  3. randMod 需要先更新 randNonce 如下
    randNonce++;
  4. 回傳 uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;

更新入下

pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  // Start here
  uint randNonce = 0;
  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
  }
}

Zombie Fighting

Zombie battle 的場景如下

  1. 使用者可以選出自己的一個 zombie 對敵人的一個 zombie 做攻擊
  2. 假設你是攻擊方,會有 70% 機率會贏,防守方則有 30% 會贏
  3. 所有 zombie 都會有 winCount 與 lossCount 用來紀錄勝利過與輸過的數目
  4. 如果攻擊方贏了,攻擊方 zombie 會升級並且產生一個新的 zombie
  5. 如果輸了,只會更新 lossCount
  6. 無論輸或贏,攻擊方的 cooldown 機制都會驅動。

以上的功能將會逐步實作

目前先處理一些狀態設定

更新步驟如下:

  1. 新增 uint 變數 attackVictoryProbability,設定其值為 70
  2. 新增 attack function
    需要兩個參數:_zombieId(uint), _targetId(uint)
    讀取權限是 external
pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  uint randNonce = 0;
  // Create attackVictoryProbability here
  uint attackVictoryProbability = 70;
  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
  }

  // Create new function here
    function attack(uint _zombieId, uint _targetId) external {
        
    }
}

重構 helper function

為了避免 attack function 被濫用

因此需要加入一個檢查確認 zombieId 是屬於呼叫者的

具體作法如下

  1. 建立一個 modifier 叫作 ownerOf
    需要一個參數: _zombieId(uint)
    內容需要檢驗: require(msg.sender == zombieToOwner[_zombieOd]);
  2. 更改 feedAndMultiply 的 modifier 為 ownerOf
  3. 因為 feedAndMultiply 有加入 modifier 所以可以移除 require 邏輯
pragma solidity >=0.5.0 <0.6.0;

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;

  // 1. Create modifier here
  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);
  }

  // 2. Add modifier to function definition:
  function feedAndMultiply(uint _zombieId, uint _targetDna, string memory _species) internal ownerOf(_zombieId){
    // 3. Remove this line
    
    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");
  }
}

修改 zombiehelper 內部的 function

  1. 修改 changeName 使用 modifier ownerOf
  2. 修改 changeOne 使用 modifier ownerOf
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 _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++;
  }

  // 1. Modify this function to use `ownerOf`:
  function changeName(uint _zombieId, string calldata _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
    zombies[_zombieId].name = _newName;
  }

  // 2. Do the same with this function:
    function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) ownerOf(_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;
  }

}

實作 attack function

  1. 修改 attack 的 modifier 為 ownerOf(_zombieId)
  2. 新增 Zombie storage 變數 myZombie,並且設定值為 zombies[_zombieId];
  3. 新增 Zombie storage 變數 enemyZombie,並且設定值為 zombies[_targetId];
  4. 新增 uint 變數 rand ,設定值為 randMod(100)

更新如下

pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;

  }

  // 1. Add modifier here
  function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
    // 2. Start function definition here
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
    uint rand = randMod(100);  
  }
}

新增 Zombie 勝負紀錄結構

首先是在 Zombie struct 新增兩個 uint16 變數 winCount 與 lossCount

個別紀錄勝利次數與失敗次數

具體實作如下

  1. 新增兩個屬性到 Zombie struct: winCount(uint16), lossCount(uint16)
  2. 修改 _createZombie 內部建立 Zombie 的邏輯多傳入 0,0 作為初始值。

修改如下:

pragma solidity >=0.5.0 <0.6.0;

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;
      // 1. Add new properties here
      uint16 winCount;
      uint16 lossCount;
    }

    Zombie[] public zombies;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string memory _name, uint _dna) internal {
        // 2. Modify new zombie creation here:
        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 memory _str) private view returns (uint) {
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        return rand % dnaModulus;
    }

    function createRandomZombie(string memory _name) public {
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        randDna = randDna - randDna % 100;
        _createZombie(_name, randDna);
    }

}

新增 Zombie 勝利判斷邏輯

  1. 新增 if (rand >= attackVictoryProbability) 判斷式
  2. 如果成立,則做以下更新
    2.1 把 myZombie winCount +1
    2.2 把 myZombie level + 1
    2.3 把 enemyZombie lossCount + 1
    2.4 執行 feedAndMultiply 並且把 species 參數帶入 "zombie"
pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
  }

  function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
    Zombie storage myZombie = zombies[_zombieId];
    Zombie storage enemyZombie = zombies[_targetId];
    uint rand = randMod(100);
    // Start here
    if (rand <= attackVictoryProbability) {
      myZombie.winCount++;
      myZombie.level++;
      enemyZombie.lossCount++;
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    }  
  }
}

新增 attack 失敗的邏輯

  1. 加入 else 在剛剛的 if statement block 之後
  2. 在 else block 加入以下更新
    2.1 把 myZombie lossCount + 1
    2.2 把 enemyZombie winCount + 1
    2.3 對 myZombie 執行 _triggerCooldown

更新如下

pragma solidity >=0.5.0 <0.6.0;

import "./zombiehelper.sol";

contract ZombieAttack is ZombieHelper {
  uint randNonce = 0;
  uint attackVictoryProbability = 70;

  function randMod(uint _modulus) internal returns(uint) {
    randNonce++;
    return uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % _modulus;
  }

  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 {// start here
      myZombie.lossCount++;
      enemyZombie.winCount++;
      _triggerCooldown(myZombie);
    }
  }
}

到這邊就完成了戰鬥的邏輯了!


上一篇
從以太坊白皮書理解 web 3 概念 - Day11
下一篇
從以太坊白皮書理解 web 3 概念 - Day13
系列文
從以太坊白皮書理解 web 3 概念32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言