Solidity - 猜数字大小游戏

这两天使用Solidity编写了一个猜数字大小的游戏,合约的代码仅仅提供学习交流。

猜数字.png

这两天使用Solidity编写了一个猜数字大小的游戏,合约的代码仅仅提供学习交流。游戏规则是这样的:

  1. 部署合约的用户需要付手续费,并且是owner,拥有开奖的权利
  2. 游戏参与者每次只能是2个用户,输入数字需要花费ETH
  3. 如果两个数字的伪随机数大于这两个数字的平均数,则A获胜,否则,B获胜
  4. 开奖过后,所有的数据从头开始

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

点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
心辰说区块链
心辰说区块链
0xc15d...f612
区块链技术从业者!