cryptozombies4

第 5 章:僵尸战斗 (Zombie Fightin’)

现在我们的合约中有了一个随机数来源,我们可以在僵尸对战中用它来计算战斗结果了。
我们的僵尸对战机制如下:

  • 你选择你的一只僵尸,然后选择一个对手的僵尸进行攻击。
  • 如果你是攻击方僵尸,你将有 70% 的胜率。防守方僵尸将有 30% 的胜率。
  • 所有僵尸(无论攻击方还是防守方)都将拥有 winCount(胜场数)和 lossCount(败场数),这些数值会根据战斗结果而递增。
  • 如果攻击方僵尸胜利,它会升级并生成一只新的僵尸。
  • 如果它失败了,什么也不会发生(除了它的 lossCount 增加)。
  • 无论胜利还是失败,攻击方僵尸的冷却时间都会被触发。

这需要实现很多逻辑,所以我们将在接下来的章节中分步骤进行。

实战练习 (Put it to the test)

  1. 给我们的合约添加一个 uint 类型的变量,名为 attackVictoryProbability,并将其值设置为 70。
  2. 创建一个名为 attack 的函数。它接收两个参数:_zombieId(一个 uint)和 _targetId(也是一个 uint)。它应该是一个 external 函数。
    目前,让函数体保持为空。

第 6 章:重构通用逻辑

无论是谁调用我们的 attack 函数,我们都需要确保调用者确实是他们所使用的僵尸的所有者。如果你可以用别人的僵尸去攻击,这将是一个严重的安全问题!
你能想到如何添加一个检查,来验证调用者是否是他们传入的 _zombieId 所对应僵尸的所有者吗?
仔细思考一下,看看你能否自己想出答案。

花一点时间…… 可以回顾我们之前课程的代码来寻找灵感……
答案在下面,请在思考之后再继续阅读。

答案

我们在之前的课程中已经多次做过这种检查了。在 changeName()、changeDna() 和 feedAndMultiply() 函数中,我们使用了如下的检查:

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

这和我们在 attack 函数中需要的逻辑完全相同。由于我们多次使用了相同的逻辑,让我们将其移到一个独立的修饰符(modifier)中,以清理代码并避免重复(Don’t Repeat Yourself)。

实战练习 (Put it to the test)

我们回到 zombiefeeding.sol 文件,因为这是我们第一次使用该逻辑的地方。让我们将其重构为一个独立的修饰符。

  1. 创建一个名为 ownerOf 的修饰符。它接收一个参数 _zombieId(uint 类型)。
    修饰符的函数体应该包含一个 require 语句,检查 msg.sender 是否等于 zombieToOwner[_zombieId],然后使用 _ 继续执行原函数。如果你不记得修饰符的语法,可以参考 zombiehelper.sol。
  2. 修改 feedAndMultiply 函数的定义,使其使用 ownerOf 修饰符。
  3. 既然我们已经使用了修饰符,你就可以删除原来函数体中的 require(msg.sender == zombieToOwner[_zombieId]); 这一行了。

第 7 章:更多重构

在 zombiehelper.sol 文件中,我们还有几个地方需要实现我们新的 ownerOf 修饰符。
动手实践
更新 changeName() 函数,使其使用 ownerOf 修饰符。
更新 changeDna() 函数,使其使用 ownerOf 修饰符。

第 8 章:回到攻击!

重构得差不多了 —— 回到 zombieattack.sol 文件。
既然我们已经有了 ownerOf 修饰符可以使用,我们就要继续定义我们的 attack 函数。

动手实践

为 attack 函数添加 ownerOf 修饰符,以确保调用者拥有 _zombieId 对应的僵尸。
我们的函数首先应该获取两个僵尸的 storage 指针,这样我们就能更容易地与它们交互:

a. 声明一个名为 myZombie 的 Zombie 类型 storage 变量,并将其设置为 zombies[_zombieId]。

b. 声明一个名为 enemyZombie 的 Zombie 类型 storage 变量,并将其设置为 zombies[_targetId]。

我们将使用一个介于 0 和 99 之间的随机数来决定战斗的结果。因此,声明一个名为 rand 的 uint 变量,并将其设置为 randMod 函数传入 100 作为参数后的结果。

第9章:僵尸的胜利与失败

在我们的僵尸游戏中,我们需要跟踪僵尸的胜利和失败次数。这样,我们就可以在游戏状态中维护一个“僵尸排行榜”。

我们可以通过多种方式存储这些数据——作为单独的映射、作为排行榜结构体,或者直接存储在 Zombie 结构体中。

根据我们打算如何与这些数据交互,每种方式都有其优缺点。在本教程中,为了简化起见,我们将把这些统计信息存储在 Zombie 结构体中,并将其命名为 winCountlossCount

现在让我们回到 zombiefactory.sol 文件,并向我们的 Zombie 结构体中添加这些属性。

动手实践
修改我们的 Zombie 结构体,增加两个新属性:

a. winCount,类型为 uint16

b. lossCount,类型为 uint16

注意:记住,由于我们可以将多个 uint 紧凑地存储在结构体中,因此我们希望使用尽可能小的 uint 类型。uint8 太小了,因为 2^8 = 256——如果我们的僵尸每天都参与一次战斗,这个值在一年内就会溢出。但是 2^16 = 65536——所以除非一个用户在 179 年内每天都赢或输了,否则我们应该不会遇到溢出问题。

现在我们在 Zombie 结构体中添加了新属性,我们需要修改 _createZombie() 函数的定义。

修改僵尸创建函数,使其为每个新创建的僵尸设置 0 胜利和 0 失败。

第10章:僵尸胜利 😄

现在我们有了 winCountlossCount,我们可以根据哪只僵尸赢得了战斗来更新这些统计数据。

在第6章中,我们计算了一个从 0 到 100 的随机数。现在,让我们使用这个数字来决定谁赢得了战斗,并相应地更新我们的统计信息。

动手实践
创建一个 if 语句,检查 rand 是否小于或等于 attackVictoryProbability

如果这个条件为真,我们的僵尸就赢了!那么:

a. 增加 myZombiewinCount

b. 增加 myZombie 的等级(升级!!!)。

c. 增加 enemyZombielossCount(失败者!!!!!! 😫 😫 😫)。

d. 运行 feedAndMultiply 函数。查看 zombiefeeding.sol 以了解如何调用它。在第三个参数(_species)中传递字符串 “zombie”。(目前它实际上不做任何事情,但如果我们以后想要为生成僵尸类型的僵尸添加额外的功能时,可能会用到它)。

第11章:僵尸失败 😞

现在我们已经编写了僵尸胜利时的逻辑,让我们来处理僵尸失败时的情况。

在我们的游戏中,当僵尸失败时,它们不会降级——它们只会将失败次数加到 lossCount 中,并且触发冷却时间,这样它们就必须等待一天才能再次攻击。

为了实现这个逻辑,我们需要使用一个 else 语句。

else 语句的写法与 JavaScript 和许多其他语言一样:

1
2
3
4
5
if (zombieCoins[msg.sender] > 100000000) {
// You rich!!!
} else {
// We require more ZombieCoins...
}

动手实践

添加一个 else 语句。如果我们的僵尸失败:

a. 增加 myZombielossCount

b. 增加 enemyZombiewinCount

c. 在 myZombie 上运行 _triggerCooldown 函数。这样,僵尸每天只能攻击一次。(记住,_triggerCooldown 已经在 feedAndMultiply 中调用过了。因此,无论僵尸是胜利还是失败,它的冷却时间都会被触发。)

第12章:总结

恭喜你!这就是第4课的内容。

测试战斗功能
现在可以在右侧测试你的战斗功能了!

领取奖励
在赢得战斗后:

  • 你的僵尸将升级
  • 你的僵尸将增加 winCount
  • 你将会生成一个新的僵尸,加入到你的军队中!

现在,尝试一下战斗功能,然后继续下一章,完成本节课的学习。

https://share.cryptozombies.io/en/lesson/4/share/test1124?id=WyJjenw2NjU1MDgiLDIsMTRd

第1章:以太坊上的代币

让我们来聊聊代币。

如果你在以太坊领域待过一段时间,你可能听过人们谈论代币——特别是 ERC20 代币。

以太坊上的代币基本上就是一个智能合约,它遵循一些通用的规则——也就是说,它实现了一组标准化的函数,这些函数是所有代币合约共有的,比如 transferFrom(address _from, address _to, uint256 _amount)balanceOf(address _owner)

在合约内部,通常会有一个映射:mapping(address => uint256) balances,它跟踪每个地址的余额。

所以,基本上,代币就是一个跟踪谁拥有多少代币的合约,并且提供一些功能,让用户可以将代币转移到其他地址。

为什么这很重要?
由于所有 ERC20 代币共享一组相同的函数和名称,它们可以以相同的方式进行交互。

这意味着,如果你构建了一个能够与某个 ERC20 代币交互的应用,那么它也能够与任何 ERC20 代币交互。这样,未来可以更轻松地将更多代币添加到你的应用中,而无需进行自定义编码。你只需将新代币的合约地址插入,哗啦,应用就能使用新的代币了。

一个例子就是交易所。当交易所添加一个新的 ERC20 代币时,它实际上只需要添加另一个智能合约进行交互。用户可以告诉这个合约将代币发送到交易所的钱包地址,交易所则可以指示合约将代币返回给用户,当他们请求提款时。

交易所只需要实现一次这种转账逻辑,然后,当它想要添加一个新的 ERC20 代币时,只需要将新的合约地址添加到它的数据库中。

其他代币标准
ERC20 代币非常适合充当货币的代币。但是,它们并不特别适合代表我们游戏中的僵尸。

首先,僵尸不像货币那样可以被分割——我可以向你转账 0.237 ETH,但转账 0.237 个僵尸显然没有意义。

其次,所有僵尸并不相等。你的 2 级僵尸 “Steve” 和我的 732 级僵尸 “H4XF13LD MORRIS 💯💯😎💯💯” 完全不同(Steve,根本无法比)。

有另一种代币标准,它更适合像 CryptoZombies 这样的加密收藏品——那就是 ERC721 代币。

ERC721 代币是不可互换的,因为每个代币都是唯一的,且不可分割。你只能以整单位进行交易,每个代币都有一个独特的 ID。所以,它们非常适合使我们的僵尸可以进行交易。

注意,使用像 ERC721 这样的标准有一个好处:我们不需要在合约中实现拍卖或托管逻辑来决定玩家如何交易或出售我们的僵尸。如果我们遵循该标准,其他人可以为加密可交易的 ERC721 资产构建一个交换平台,我们的 ERC721 僵尸就可以在该平台上使用。所以,使用代币标准而不是自己编写交易逻辑是有明确好处的。

动手实践
我们将在下一章深入探讨 ERC721 的实现。但是在此之前,我们先设置本节课的文件结构。

我们将把所有 ERC721 逻辑存储在一个名为 ZombieOwnership 的合约中。

在文件顶部声明我们的 pragma 版本(请参考之前课时的文件语法)。

该文件应从 zombieattack.sol 导入。

声明一个新的合约 ZombieOwnership,它继承自 ZombieAttack。现在先将合约的主体留空。

第2章:ERC721 标准与多重继承

让我们来看看 ERC721 标准:

1
2
3
4
5
6
7
8
9
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
}

这是我们需要实现的方法列表,我们将在接下来的章节中逐步实现。

看起来方法很多,但不要感到压倒!我们会一步步带你完成。

实现代币合约
在实现代币合约时,第一步是将接口复制到一个独立的 Solidity 文件中,并导入它:import "./erc721.sol";。然后,我们让我们的合约继承它,并为每个方法覆盖一个函数定义。

但等一下——ZombieOwnership 已经继承了 ZombieAttack——它怎么还能继承 ERC721 呢?

幸运的是,在 Solidity 中,合约可以继承多个合约,如下所示:

1
2
3
contract SatoshiNakamoto is NickSzabo, HalFinney {
// Omg, the secrets of the universe revealed!
}

正如你所看到的,当使用多重继承时,只需要用逗号 , 分隔你继承的多个合约。在这个例子中,我们的合约继承了 NickSzaboHalFinney

让我们来试试。

动手实践
我们已经为你创建了 erc721.sol,其中包含上述接口。

erc721.sol 导入到 zombieownership.sol 中。

声明 ZombieOwnership 继承自 ZombieAttackERC721