一天一个区块链游戏——猜拳

在本文中将实现一个简单的区块链小游戏(blockchain game)——猜拳。这个游戏基于以太坊智能合约,实现的功能是在页面上选择石头剪刀布中的一个手势,然后和电脑随机选择的一个手势通过智能合约判定并返回游戏结果。效果如下:
guess

一、初始化项目

参照上一篇中的步骤新建一个项目,目录结构如下:

.
├── app
│   ├── app.js
│   ├── index.html
│   ├── js
│   │   ├── jquery.min.js
│   │   ├── truffle-contract.js
│   │   └── web3.min.js
│   └── package.json
├── contracts
│   ├── GuessGame.sol
│   └── Migrations.sol
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_deploy_contracts.js
├── test
├── truffle-config.js
└── truffle.js

更新配置文件truffle.js为:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};

二、实现智能合约

合约的功能是实现输入选项,返回游戏结果,代码如下:

pragma solidity ^0.4.16;

contract GuessGame {

    // 定义一个事件,用来发送结果
    event GuessResult(uint playerChoice, uint computerChoice, uint result);

    // 处理游戏结果并返回
    function play(uint playerChoice, uint computerChoice) public returns (bool) {
        if (playerChoice > 0 && playerChoice <= 3 && computerChoice > 0 && computerChoice <= 3) {
            // 如果两者相同,则代表平手
            if (playerChoice == computerChoice) {
                GuessResult(playerChoice, computerChoice, 1);   // 回调1 代表平手
            } else if (playerChoice == (computerChoice + 1) % 3) {
                GuessResult(playerChoice, computerChoice, 2);  // 回调2 代表电脑赢了
            } else {
                GuessResult(playerChoice, computerChoice, 3);  // 回调3 代表玩家赢了
            }
            return true; //执行成功返回true
        } else {
            return false;  // 执行错误返回false
        }
    }
}

这里需要注意的是, web3.js中调用play返回非常量的函数不能直接获取返回值,因为这个函数中会产生交易,所以这个函数直接返回的是产生交易的hash值,这个函数的返回值只能在智能合约中获取到。要在web3.js中获取返回需要通过事件这种方式:

event GuessResult(uint playerChoice, uint computerChoice, uint result);

事件event

在智能合约中,事件(event)是太坊虚拟机提供的一种操作日志的工具,也可以用来实现一些交互功能,比如通知UI,返回函数调用结果等。DApp开发中可以通过事件的方式,回调JavaScript中的监听函数。比如上面的play函数可以通过以下方式进行监听:

var event = guess_contract.GuessResult();
event.watch(function(err, result) {
    if (!err) {
        console.log(result);
    } else {
        console.log(err);
    }
})

或者直接回调的方法

var event = guess_contract.GuessResult(function(err, result) {
    if (!err) {
        console.log(result);
    } else {
        console.log(err);
    }
}

这样当智能合约中执行该事件时,上面的监听就会被调用。

实现完成就进行编译和部署.

$ truffle compile
Compiling ./contracts/GuessGame.sol...
Compiling ./contracts/Migrations.sol...
Writing artifacts to ./build/contracts

$ truffle migrate 
Using network 'development'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x78c3d80f68b986265437c1cace4eecd7753dcb6195d0eaaebf6327d88eb6536f
  Migrations: 0xa6b82506089e630976e6f45713d5b4214daa3217
Saving successful migration to network...
  ... 0x03d41abd82a95435b6f7b62f2ec9a3b64b73818a7975023b8eb62b4a9d8341bb
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying GuessGame...
  ... 0xee9295ec1205f149e9c5e89711c74b6e50ea0b4df4062bd5949e038d19153ee2
  GuessGame: 0x9363055c516c2d85e890afad79516c18f21cff4f
Saving successful migration to network...
  ... 0x5d671936768913275ddc4006f03846f4c03a5e612664c324228dbfd1a723b4e0
Saving artifacts...

三、实现DApp

智能合约完成后就是实现一个DApp将之整合进来。先编写一个界面让用户输入,代码如下:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>DApp猜拳游戏</title>
  <style>
  *{margin:0; padding: 0; font-weight: 200;}
  .player,.computer{
      width: 50%;
      float: left;
      padding-top: 30px;
      text-align: center
  }
  .player,.computer dt{
      font-size: 28px;
  }
  .player img,.computer img{
      margin-top: 30px;
      width: 30%;
  }
  .player img{
      transform:rotateY(180deg);
  }
  .select{
      text-align: center;
      font-size: 18px;
      max-width: 800px;
      margin: 0 auto;
      padding-top: 2%;
  }
  .select dt{
      width: 100%;
      overflow: hidden;
      line-height: 50px;
  }
  .select button{
      width: 20%;
      border:none;
      color: #fff;
      border-radius: 8px;
      line-height: 45px;
      margin: 0 5%;
      outline: none;
      font-size: 18px;
      cursor: pointer;
  }
  #info{
      width: 100%;
      text-align: center;
      overflow: hidden;
      font-size: 25px;
      line-height: 50px;
      color: red;
      padding-top: 2%;
      opacity: 0;
  }
  </style>
</head>

<body>
    <div class="computer">
        <dl>
            <dt>对手</dt>
            <dd><img src="images/2.png" id="computer" alt=""></dd>
        </dl>
    </div>
    <div class="player">
        <dl>
            <dt>你</dt>
            <dd><img src="images/2.png" id="player" alt=""></dd>
        </dl>
    </div>
    <div id="info">平手</div>
    <div class="select">
        <dl>
            <dt>点击下列图标选择要出的选项:</dt>
            <dd>
                <button value="1"><img src='images/1.png' style="width:80px"></button>
                <button value="2"><img src='images/2.png' style="width:80px"></button>
                <button value="3"><img src='images/3.png' style="width:80px"></button>
            </dd>
        </dl>
    </div>
</body>
<script src="js/jquery.min.js"></script>
<script src="js/web3.min.js"></script>
<script src="js/truffle-contract.js"></script>
<script src="app.js"></script>
</html>

接着在app.js中随机生成一个手势,然后调用智能合约,并监督智能合约的返回结果,之所以要用事件的方式获取返回值,是因为在web3.js不能直接获取智能合约中函数的返回值。代码如下:

window.onload = function() {
      var web3, provider, guess_contract, guess_result, refresh_timer;

      if (typeof window.web3 !== 'undefined') {
          // 安装Metamask插件后web3已经定义在window对象下
          web3 = new Web3(window.web3.currentProvider);
      } else {
          web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
      }

      // 获取智能合约的ABI(Application Binary Interface)文件
      $.getJSON('GuessGame.json', function(data){
          var GuessGameArtifact = data;

          // 初始化智能合约
          GuessGameContract = TruffleContract(GuessGameArtifact);
          GuessGameContract.setProvider(web3.currentProvider);

          // 通过默认的合约地址获取实例
          GuessGameContract.deployed()
          .then(function(instance){

              guess_contract = instance;
              guess_contract.GuessResult(function(err, result) {
                if (!err) {
                    var player_choice = result.args.playerChoice.toNumber();
                    var computer_choice = result.args.computerChoice.toNumber();
                    var r = result.args.result.toNumber();
                    var info = "未知";
                    if(r == 1){
                        info = '平手';
                    }else if(r == 2){
                        info = '你输了';
                    }else if(r == 3){
                        info = '你赢了';
                    }
                    update_page(player_choice,computer_choice, info);
                } else {
                    console.log(err);
                }
            });
         }).catch(function(err){
                console.log(err.message);
         });
      })

      /*
        更新页面显示
      */
      function update_page(player,computer, result){
          var info = document.getElementById('info');
          var playerImg = document.getElementById('player');
          var comImg = document.getElementById('computer');
          info.style.opacity = '0';
          clearInterval(refresh_timer);
          playerImg.src = 'images/'+player+'.png';
          comImg.src = 'images/'+computer+'.png';
          info.style.opacity = 1;
          info.innerText = result;

      }

      /*
        猜拳
      */
      function guess(player_choice){
          // 1:石头、2:剪刀、3:布
          var result;
          player_choice = parseInt(player_choice);
          computer_choice = parseInt(Math.random()*3)+1;  // 电脑
          document.getElementById('info').innerText = '';
          guess_contract.play(player_choice, computer_choice).then(function(result){
              if(result) {
                  var playerImg = document.getElementById('player');
                  var comImg = document.getElementById('computer');
                  refresh_timer = setInterval(function(){
                      this.n?this.n:this.n=1;this.n++
                      this.n>3?this.n=1:this.n;
                      playerImg.src = 'images/'+this.n+'.png';
                      comImg.src = 'images/'+this.n+'.png';
                  },100);
              }
          }).catch(function(err){
                 console.log(err.message);
          })
      }


      // 绑定页面按钮事件
      choices = $('button');
      for(var i=0; i<choices.length; i++){
          choices[i].onclick = function(){
              guess(this.value);
          }
      }
};

完成后启动DApp.

$ npm run dev

访问localhost:3000开始玩游戏。在调用智能合约中的函数时时需要消耗Gas的,如下图:

result

输入一定量的以太坊, 输入后点击“SUBMIT”按钮就可以看到执行结果。
更多区块链游戏(blockchain game)还在创作中,敬请关注~

0 0 vote
Article Rating
Subscribe
提醒
guest
0 评论
Inline Feedbacks
View all comments