初心者文系主婦がブロックチェーンを学ぶブログ

IT革命のビッグウェーブに乗り遅れた主婦が、ブロックチェーン革命の波にのるべく、ブロックチェーン技術を学ぶブログです。

【CryptZombies】レッスン4チャプター11:ゾンビの敗北(elseステートメント)

ゾンビは負けてもレベルダウンしません。単純にlossCountに負けを追加し、さらにクールダウンが始まるため、また攻撃をするのに一日待たなくてはなりません。

elseステートメント

elseステートメントはifステートメントの延長です。
ifステートメントは、ifを使用した時に、条件が一致すればサブステートメントを実行します。
elseステートメントは、ifの条件式が一致しなければ、違うことを実行させたい時に使用します。

<書式>

if (条件式){
サブステートメント1;
} else{
サブステートメント2;
}

<例>

if(手持ちのお金 <= 50000) {
 テレビを買う;
}else{
 テレビは買わない。;
}

else,else if条件ステートメント、条件式


テストの実行

①elseステートメントを追加せよ。もし我々のゾンビが負けたら、

a. myZombieのlossCountを増やせ。

function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
   中略
    if (rand <= attackVictoryProbability) {
      myZombie.winCount++;
      myZombie.level++;
      enemyZombie.lossCount++;
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    } else {
      myZombie.lossCount++;
    }
  }

b. enemyZombieのwinCountを増やせ。

function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
   中略
    if (rand <= attackVictoryProbability) {
      myZombie.winCount++;
      myZombie.level++;
      enemyZombie.lossCount++;
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    } else {
      myZombie.lossCount++;
      enemyZombie.winCount++;
    }
  }

②このelseステートメント外で、myZombieにある_triggerCooldown関数を動かすのだ。こうするとゾンビは一日一回だけ攻撃できるようになる。

_triggerCooldown関数は、zombiefeeding.solにあります。

 function _triggerCooldown(Zombie storage _zombie) internal {
    _zombie.readyTime = uint32(now + cooldownTime);
  }

これは、レッスン3チャプター6で学習したZombie structを引数にとる関数です。
storageポインタをつけているので、関数にゾンビの参照そのもの(この問ではmyZombie)を渡すことが可能です。

function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
   中略
    if (rand <= attackVictoryProbability) {
      myZombie.winCount++;
      myZombie.level++;
      enemyZombie.lossCount++;
      feedAndMultiply(_zombieId, enemyZombie.dna, "zombie");
    } else {
      myZombie.lossCount++;
      enemyZombie.winCount++;
    }
    _triggerCooldown(myZombie);
  }


お疲れさまでした!


<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター10:ゾンビの勝利

チャプター4では、0から100の乱数を計算しました。今度はその乱数を使って、誰が闘いに勝つかを決めて、それに従って成績を更新してみます。

テストの実行

①ifステートメントを作成し、rand変数がattackVictoryProbability変数 より少ないか同等 であるかチェックするようにせよ。

ifの後にスペースをつけるのを忘れずに…!

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)
  }

②もしこの条件が真であれば、我らがゾンビの勝利だ!従って、

a. myZombieのwinCountを増やせ。

attack()関数ですでに定義しましたが、myZombieはstorage内のzombies[_zombieId]を示すポインタなので、次のようになります。

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++;        
    }
  }

b. myZombieのlevelを増やせ。 (レベルアップだ!!!!!!!)

aと同様に、myZombieはstorage内のzombies[_zombieId]を示すポインタなので、次のようになります。

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++;                
    }
  }

c. enemyZombieのlossCountを増やせ。 (敗者だ!!!!!! )

enemyZombieはstorage内のzombies[_targetId]を示すポインタなので、次のようになります。

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++;        
    }
  }

d. feedAndMultiply関数を動かすのだ。関数を呼び出す構文を見るには、zombiefeeding.solをチェックせよ。三番目の引数(_species)に、"zombie"の文字列を渡せ。(今はまだ何も起こらないのだが、後でゾンビをベースとして新たなゾンビを生みだす機能を追加できるからな。)

feedAndMultiply関数の構文は次のとおり。

function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal ownerOf(_zombieId) {
    中略
  }

関数を呼び出すために、(uint _zombieId, uint _targetDna, string _species)が必要です。
_zombieIdは、attack関数内で使用しているので、そのまま使います。
_targetDnaについては、enemyZombieはstorage内のzombies[_targetId]を示すポインタなので、enemyZombie.dnaになります。
回答は次のようになります。

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");        
    }
  }


お疲れさまでした!


<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター9:ゾンビの勝敗

ゾンビたちがバトルを何回勝ち負けしたかをトラックしていくために、Zombie構造体でバトルの成績を保存していきます。
勝敗については、winCountとlossCountとします。

テストの実行

①Zombie構造体を修正し、さらに2つのプロパティを持つようにせよ。

a. uint16であるwinCount

struct Zombie {
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
      uint16 winCount;
   }

b. 同じくuint16であるlossCount

struct Zombie {
      string name;
      uint dna;
      uint32 level;
      uint32 readyTime;
      uint16 winCount;
      uint16 lossCount;
    }


注: ここで思い出して欲しい。構造体内部にuintは格納可能であるが、使用するuintは最小に済ませたいのだ。uint8だと2の8乗は256なので、もし一日一回ゾンビが攻撃をした場合は一年以内にオーバーフローとなり得る。だが2の16乗は65536なので、ユーザーが179年にわたって毎日勝つか負けるかしない限り、これで大丈夫だ。

②Zombie構造体に新たなプロパティができたから、今度は_createZombie()の関数定義を変えることが必要だゾンビの作成定義を、新たなゾンビを0勝0敗で作成するように変更せよ。

_createZombie()の関数定義にwinCount(0)とlossCount(0)を追加します。

function _createZombie(string _name, uint _dna) internal {
        // 2. 新たなゾンビ生成をここで修正せよ
        uint id = zombies.push(Zombie(_name, _dna, 1, uint32(now + cooldownTime), 0, 0)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        NewZombie(id, _name, _dna);
    }


お疲れさまでした!


<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター8:Attackへ戻ろう!

テストの実行

①ownerOf修飾詞をattack関数に加え、関数を呼び出した者が_zombieIdを所有しているか確認せよ。

function attack(uint _zombieId, uint _targetId) external ownerOf(_zombieId) {
 中略
}

②まず最初に、我々の関数は両方のゾンビにstorageのポインタをゲットしなくてはならない。そうするとより簡単にそれらとやり取りできる。

a. myZombieという名のZombie storageを宣言せよ。そしてそれがzombies[_zombieId]と等しくなるよう設定するのだ。

myZombieはZombie storage内のzombies[_zombieId]を示すポインタです。

Zombie storage myZombie = Zombies[_zombieId];

b. enemyZombieという名のZombie storageを宣言し、そしてそれがzombies[_targetId]と等しくなるよう設定せよ。

enemyZombieはZombie storage内のzombies[_targetId]を示すポインタです。

Zombie storage enemyZombie = zombies[_targetId];

③バトルの結果を決めるために、0から99のランダムな数字を使っていく。そのためrandという名前のuintを宣言し、これが引数を100としたrandMod関数と同等であるよう設定せよ。

uint rand = randMod(100);

お疲れさまでした!



<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター6&7:共通ロジックのリファクタリング

今まで、

require(msg.sender == zombieToOwner[_zombieId]);

を何度も使用してきましたが、attack関数にも同じロジックを使用します。
コードを綺麗にして繰り返しを避けるために、このロジックを独自のmodifierに格納します。

チャプター6のテストの実行

①ownerOfというmodifierを作成せよ。これは1つの引数_zombieId(uint)を受け取る。本文では、msg.senderがzombieToOwner[_zombieId]と同等であるようにrequire(要求)し、そうであれば関数を続けるようにせよ。もし修飾詞の構文を覚えていなければ、zombiehelper.solを参照してよい。

modifier ownerOf(uint _zombieId) {
  require(msg.sender == zombieToOwner[_zombieId]);
  _;
}

②feedAndMultiplyの関数定義を変更し、ownerOf修飾詞を使うようにせよ。

function feedAndMultiply(uint _zombieId, uint _targetDna, string _species) internal ownerOf(_zombieId) {
 中略
}

③今はmodifierを使っているので、以下の一行は削除するのだ。require(msg.sender == zombieToOwner[_zombieId]);

削除します。


チャプター7のテストの実行

①changeName()関数をアップデートし、ownerOfを使用せよ。

changeName()関数にownerOfを追加します。これに伴い、require(msg.sender == zombieToOwner[_zombieId]);を削除します。

function changeName(uint _zombieId, string _newName) external aboveLevel(2, _zombieId) ownerOf(_zombieId) {
    zombies[_zombieId].name = _newName;
  }

②changeDna()関数をアップデートし、ownerOfを使用せよ。

changeDna()関数にownerOfを追加します。これに伴い、require(msg.sender == zombieToOwner[_zombieId]);を削除します。

function changeDna(uint _zombieId, uint _newDna) external aboveLevel(20, _zombieId) ownerOf(_zombieId) {
    zombies[_zombieId].dna = _newDna;
  }



お疲れさまでした!


<参考>
CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター5:ゾンビが闘う

ゾンビバトルは次のような仕組みでできています。

・自分のゾンビから一体を選び、さらに攻撃する相手のゾンビを選ぶ。

・攻撃するゾンビは勝率70%、守備するゾンビは30%の勝率となる。

・全ゾンビ(攻撃するものも守備するものも)が、winCountとlossCountを持ち。これれらはバトル結果次第で増えていく。

・もし攻撃するゾンビが勝ったら、レベルアップして新たなゾンビを産む。

・もし攻撃するゾンビが負けた場合は、何も起こらない(lossCountの増加を除く)。

・勝っても負けても、攻撃するゾンビのクールダウン時間は開始される。

 

テストの実行

①我々のコントラクトにattackVictoryProbabilityというuintを与え、それが70と等しくなるようにせよ。

f:id:nomadomama:20181214231341p:plain

 

②attackという関数を作成せよ。これは_zombieId(uint)と_targetId(uint)の2つのパラメータを受け取り、external関数であるとせよ。

f:id:nomadomama:20181214231445p:plain

 

お疲れさまでした!

 

 

<参考>

CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network

【CryptZombies】レッスン4チャプター4:乱数

Solidityにおいて、安全な乱数の生成は不可能です。

理由は次の通り。

 

keccak256経由での乱数生成

Solidityではkeccak256ハッシュ関数を使って乱数を生成します。

// 1から100までの乱数を生成せよ:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

このコードが行うのは、nowのタイムスタンプとmsg.sender、増加する値nonce(一度飲み使用される数字なので同じ入力パラメータ値を持つハッシュ関数が二度実行されることはない)の受取りです。

keccakでこれらの入力値をランダムなハッシュ値に変換、そしてそのハッシュ値をuint型に変換したら、その末尾2桁のみ残すように%100をします。こうして0から99の間の、完全にランダムな数値を生成します。

 

この方法は、不誠実なノードの攻撃に対して脆弱

Proof of Work

イーサリアムでは、コントラクトの関数を呼び出す際、ネットワーク上の一つまたは複数のノードにトランザクションとして送信し、ネットワーク上のノードはトランザクションの束を集め、Proof of Work(PoW、仕事の証明)として計算集約的数学の問題を一番速く解こうとします。そして彼らのProof of Workも併せたトランザクションのグループをブロックとしてネットワークの残りのノードに発行します。

一度あるノードがPoWを解いてしまうと、他のノードはそのPoWを解くのをやめ、トランザクションリストが有効であることを確認してブロックを承認し、さらに次のブロックを解くことに取り掛かります。

乱数関数のセキュリティホール

コイン・トスのコントラクトがあると仮定します。表ならお金が2倍、裏ならお金を失うというルールです。表裏を決定するのに、乱数関数を使うとします。(random >= 50なら表、random < 50なら裏)

Aさんがノードを立てているとして、Aさんが自分のノードだけに向けてトランザクションを発行します。すると、コイン・トスの関数の勝ち負けを見て、次のブロックにそのトランザクションが含まれないように選択することができてしまいます。コイン・トスに勝って次のブロックを解くまでこれを無期限で行い続け、利益を出すことが可能です。

ただし、ブロックを解いて利益があるようにするためには、膨大な時間とリソースが必要です。なので、その報酬が十分に高くなければ、不正を働くメリットはありません。

したがって、使用する乱数関数が大金を危険にさらさない限りは、乱数関数を使用することのトレード・オフを受け入れていくことも必要です。

 

テストの実行

コントラクトに、randNonceというuintを与え、0に同等となるよう設定せよ。

f:id:nomadomama:20181213235706p:plain

 

②randMod(random-modulud)という関数を作成せよ。これは_modulusという名のuintを受け取るinternal関数であり、uintを返す(returns)。

f:id:nomadomama:20181214000120p:plain

 

③この関数はまずrandNonceを増やさなくてはならない。(randNoce++という構文を使うのだ)

f:id:nomadomama:20181214000157p:plain

 

④最後に、now、msg.senderそしてrandNonceのkeccak256ハッシュ値の型変換uintに計算し、その値を% _modulusしてreturnせよ。これはコード1行で行うこと。

まず赤線から見ていきます。

f:id:nomadomama:20181214000748p:plain

 

次に、青線を見ていきます。

上で変換したkeccak256ハッシュ値をuint型に変換します。

f:id:nomadomama:20181214000942p:plain

 

それから、オレンジ線を見ていきます。

f:id:nomadomama:20181214001048p:plain

 

最後に緑線を見ていきます。 

f:id:nomadomama:20181214001148p:plain

 

お疲れさまでした!

 

 

<参考>

CryptoZombies - イーサリアム上でゲームを開発する方法を学習。Powered by Loom Network