这两天使用Solidity编写了一个猜数字大小的游戏,合约的代码仅仅提供学习交流。
这两天使用Solidity编写了一个猜数字大小的游戏,合约的代码仅仅提供学习交流。游戏规则是这样的:
Contract 1: 定义两个用户地址
按照需求,我编写了第一份合约。我把两个用户地址写死在构造函数里面,设定了游戏的开始时间和结束时间。
constructor() payable {
require(msg.value == 1 ether,"deploy need 1 ether!");
player.playerA = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
player.playerB = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
startTime = block.timestamp;
endTime = 10 minutes + startTime;
}
通过一个player_input()方法让两个用户可以进行输入任意数字,但是我在这里的限定的条件是只能是写死的两个用户地址才能参与,而且如果已经输入过了就不能再次输入。
// 玩家输入一个任意值
function player_input(uint _number) payable external {
require(msg.value == 0.5 ether,"must give 0.5 ether to contract");
require(msg.sender == player.playerA || msg.sender == player.playerB,"you do not have right!");
require(block.timestamp <= endTime , "Too late!");
require(isInput[msg.sender] == false,"had input before");
accountToNum[msg.sender] = _number;
isInput[msg.sender] = true;
}
之后,我用伪随机数的方式生成随机数,这当然在生产环境中,使用伪随机数是不安全的,这点我们要清楚。在后面,我们尝试用chainlink来实现随机数的生成。在这段代码中,我将两个数字进行取模操作,如果结果是0,那么数字是num1,如果是1,那么数字是num2。
function _getRandom(uint num1,uint num2) internal view returns(uint) {
uint256 random = uint256(keccak256(abi.encodePacked(block.coinbase,block.prevrandao, block.timestamp))) % 2;
if(random == 0) return num1;
if(random == 1) return num2;
return 0;
}
在开奖的方法中,我把具体的逻辑写在了匿名函数里,这样可以避免详细的逻辑暴露在外面,也是规范写代码的方式之一。在这段代码中,我们获取到两个用户输入到数字,然后我们计算出两个数字的平均值跟随机数进行比较。根据规则,如果随机数的数字大于平均值则A获胜,否则,B获胜。这个方法有个问题是,开奖之后旧的数据还在,所以无法满足新一轮的猜数字游戏。后面,我对代码进行了改进。
function _pickWinner() internal {
// require(block.timestamp >= endTime,"time not over yet!");
require(msg.sender == owner(),"only owner can pickWinner");
require(address(this).balance >= 1 ether,"not enough balance in contract!");
uint playerA_NUM = accountToNum[player.playerA];
uint playerB_NUM = accountToNum[player.playerB];
uint avg_num = (playerA_NUM + playerB_NUM) / 2;
// 如果随机数大于平均值
if(_getRandom(playerA_NUM,playerB_NUM) > avg_num){
_playerA_winer();
}
// 如果随机数小于平均值
if(_getRandom(playerA_NUM,playerB_NUM) < avg_num){
_playerB_winer();
}
}
Contract 2: 使用结构体放映射
在 Contract 1 合约中,我们将两个用户地址都写死了,那如果我们不写死两个用户的钱包地址,使得任意的两个钱包用户地址都能参与猜数字游戏。下面,我在player_input()方法中,使用counts来限定参与游戏的用户顶多只能是2个。另外,我们将用户放在playersArray数组里面。
// 玩家输入一个任意值
function player_input(uint _number) payable external {
require(block.timestamp <= endTime , "Too late!");
require(players.isInput[msg.sender] == false,"account had input before");
require(players.hasInput[_number] == false,"number had input before");
require(msg.value == 0.5 ether,"must give 0.5 ether to contract");
require(counts < 2,"must two accounts are allowed!");
playersArray.push(msg.sender);
counts++;
players.accountToNum[msg.sender] = _number;
players.isInput[msg.sender] = true;
players.hasInput[_number] = true;
}
对于玩家我用一个结构体来表示它们每个属性的状态。我本来是想通过players.accountToNum这种方法来获取玩家的值,但是我发现这样根本行不通。所以,我们之后要定义结构体里的字段,如果想要查看的话还是不建议这样的写法。
struct Players {
mapping(address => uint) accountToNum;
mapping(address => bool) isInput;
mapping(uint => bool) hasInput;
}
Players players;
在开奖的方法中,我用了大量的状态来修改旧的数据,使得可以满足我们新一轮的游戏。但是我发现,如果某个字段被恶意攻击修改了的话,整个合约也将面临灭顶之灾。所以,我们真要写生产上的合约的时候,一定也要考虑到关于大量的状态的修改问题,应该考虑不采取这种大量修改变量的方式下,还有没有更加安全周到的方式来实现合约的功能。
// 开始游戏,只有创建游戏的玩家A可以操作
function pickWinner() external nonReentrant {
// require(block.timestamp >= endTime,"time not over yet!");
require(msg.sender == owner(),"only owner can pickWinner");
require(address(this).balance >= 1 ether,"not enough balance in contract!");
require(playersArray[0] != address(0x0) || playersArray[1] != address(0x0),"0x0 is not allowed!");
uint playerA_NUM = players.accountToNum[playersArray[0]];
uint playerB_NUM = players.accountToNum[playersArray[1]];
uint avg_num = (playerA_NUM + playerB_NUM) / 2;
// 如果随机数大于平均值
if(_getRandom(playerA_NUM,playerB_NUM) > avg_num){
payable(playersArray[0]).transfer(1 ether);
// 监听转账事件
emit TransferEvent(address(this),playersArray[0],1 ether);
players.accountToNum[playersArray[0]] = 0;
players.accountToNum[playersArray[1]] = 0;
players.hasInput[playerA_NUM] = false;
players.hasInput[playerB_NUM] = false;
players.isInput[playersArray[0]] = false;
players.isInput[playersArray[1]] = false;
playersArray = new address[](0);
counts = 0;
}
// 如果随机数小于平均值
if(_getRandom(playerA_NUM,playerB_NUM) < avg_num){
payable(playersArray[1]).transfer(1 ether);
// 监听转账事件
emit TransferEvent(address(this),playersArray[1],1 ether);
players.accountToNum[playersArray[0]] = 0;
players.accountToNum[playersArray[1]] = 0;
players.hasInput[playerA_NUM] = false;
players.hasInput[playerB_NUM] = false;
players.isInput[playersArray[1]] = false;
players.isInput[playersArray[1]] = false;
playersArray = new address[](0);
counts = 0;
}
}
contract 3: 不使用结构体放映射
之前我们把用户的mapping映射都放在结构体里面,我们就无法直接查看到具体的状态信息。我们现在把结构体去掉,将用户放在数组里,将映射从结构体中拿出来。
address[] public playersArray;
mapping(address => uint) public accountToNum;
mapping(address => bool) public isInput;
mapping(uint => bool) public hasInput;
输入数字player_input()的方法我们还是跟contract 2是一样的。
// 玩家输入一个任意值
function player_input(uint _number) payable external {
require(block.timestamp <= endTime , "Too late!");
require(isInput[msg.sender] == false,"account had input before");
require(hasInput[_number] == false,"number had input before");
require(msg.value == 0.5 ether,"must give 0.5 ether to contract");
require(counts < 2,"must two accounts are allowed!");
playersArray.push(msg.sender);
counts++;
accountToNum[msg.sender] = _number;
isInput[msg.sender] = true;
hasInput[_number] = true;
}
开奖的逻辑我们也是跟contract 2是一样的,只是将代码进行的优化重构。
function _pickWinner() internal {
// require(block.timestamp >= endTime,"time not over yet!");
require(msg.sender == owner(),"only owner can pickWinner");
require(address(this).balance >= 1 ether,"not enough balance in contract!");
require(playersArray[0] != address(0x0) || playersArray[1] != address(0x0),"0x0 is not allowed!");
uint playerA_NUM = accountToNum[playersArray[0]];
uint playerB_NUM = accountToNum[playersArray[1]];
uint avg_num = (playerA_NUM + playerB_NUM) / 2;
// 如果随机数大于平均值
if(_getRandom(playerA_NUM,playerB_NUM) > avg_num){
_playerA_winer();
}
// 如果随机数小于平均值
if(_getRandom(playerA_NUM,playerB_NUM) < avg_num){
_playerB_winer();
}
}
像这种方式,我们还是使用了大量的状态修改来进行数据的处理,而且代码看起来非常庞大,不够简单清晰。后来,我还是决定用结构体来记录玩家的属性,使得这些属性集成在一个结构体中,这样通过查看结构体就能看到用户的具体属性信息了。
contract 4: 使用结构体定义用户属性
在这份合约中,我将玩家用结构体包起来,将玩家的属性放在结构体里面,这样我们也避免了用大量的映射,代码稍微就比之前的的好看一些了。我们来看看结构体代码和player_input()方法的代码。
...
struct Wanjia {
address account;
uint number;
bool isInput;
bool hasInput;
}
mapping(address => Wanjia) public addrToWanjia;
address[] public playersArray;
...
// 玩家输入一个任意值
function player_input(uint _number) payable external {
Wanjia storage wanjia = addrToWanjia[msg.sender];
require(block.timestamp <= endTime , "Too late!");
require(wanjia.isInput == false,"account had input before");
require(wanjia.hasInput == false,"number had input before");
require(msg.value == 0.5 ether,"must give 0.5 ether to contract");
require(counts < 2,"must two accounts are allowed!");
addrToWanjia[msg.sender] = Wanjia({
account: msg.sender,
number: _number,
isInput: true,
hasInput: true
});
playersArray.push(msg.sender);
counts++;
}
在开奖方法中,我们用结构体来修改属性的状态,而且对代码进行了优化重构,整体看起来代码有很大的改善。我们最终就选用了以这种方式来确定这份合约的编写。但是有人想问了,怎么使用chainlink来实现伪随机数啊,下面contract 5我们再来实现一下如何用chainlink来实现。
function _pickWinner() internal {
// require(block.timestamp >= endTime,"time not over yet!");
require(msg.sender == owner(),"only owner can pickWinner");
require(address(this).balance >= 1 ether,"not enough balance in contract!");
require(playersArray[0] != address(0x0) || playersArray[1] != address(0x0),"0x0 is not allowed!");
Wanjia storage wanjia0 = addrToWanjia[playersArray[0]];
Wanjia storage wanjia1 = addrToWanjia[playersArray[1]];
uint playerA_NUM = wanjia0.number;
uint playerB_NUM = wanjia1.number;
uint avg_num = (playerA_NUM + playerB_NUM) / 2;
// 如果随机数大于平均值
if(_getRandom(playerA_NUM,playerB_NUM) > avg_num){
_playerA_winer();
}
// 如果随机数小于平均值
if(_getRandom(playerA_NUM,playerB_NUM) < avg_num){
_playerB_winer();
}
}
...
...
function tool() internal {
Wanjia storage wanjia0 = addrToWanjia[playersArray[0]];
Wanjia storage wanjia1 = addrToWanjia[playersArray[1]];
wanjia0.number = wanjia1.number = 0;
wanjia0.hasInput = wanjia1.hasInput = false;
wanjia0.isInput = wanjia1.isInput = false;
playersArray = new address[](0);
counts = 0;
}
contract 5: 使用chainlink实现随机数
之前的代码我们一直用的是伪随机数,接下来,我们使用chainlink来与我们的合约进行交互。chainlink需要用到VRFConsumerbase合约,并且需要指定它的网络地址,以及link代币作为部署手续费和随机数的实现。由于我没有link代币,官方的网站领取也存在问题,所以,在我的的代码中,我随意写死了一个 _requestId。如果是正常的话,需要先部署chainlink合约获取到_requestId的结果,再将结果给到游戏合约。下面代码中,我们将byte32字节转换成uint之后再进行取模操作。
// bytes32 转 uint
function bytes32ToUint(bytes32 _number) internal pure returns (uint256){
uint number;
for(uint i= 0; i<_number.length; i++){
number = number + uint8(_number[i])*(2**(8*(_number.length-(i+1))));
}
return number;
}
function _getRandom(uint num1,uint num2) internal view returns(uint) {
require(_requestId != 0x0,"requestId 0x0 error!");
uint256 random = bytes32ToUint(_requestId) % 2;
if(random == 0) return num1;
if(random == 1) return num2;
return 0;
}
好的,以上就是我在编写游戏合约时如何一步步摸索的过程,当然,还能联想到的需求就是,如果是多个用户参与猜数字大小那又是什么逻辑呢,那应该是是将所有的用户地址装进数组,将所有用户的输入值进行遍历从大到小的排序。这也不难,只是这遍历的过程稍微复杂一些,涉及到一些算法。另一个能联想到的需求就是,如何将现有的合约编写成可升级的合约,这也不难,引用一些升级依赖库,将构造函数编写成初始化方法。
猜数字大小的学习,我们就讲到这,有相关问题的可以留言交流。代码地址仓库在这里,供大家查看。
源码:https://github.com/zhihaozhong123/NumberGuessingGame/tree/master
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!