美文网首页区块链研习社互联网科技区块链大学
2-从零开发EOS区块链小游戏系列 - 智能合约设计与编写

2-从零开发EOS区块链小游戏系列 - 智能合约设计与编写

作者: Jan_gogogo | 来源:发表于2019-08-28 20:37 被阅读0次

    目录

    智能合约表结构设计

      回顾上一章的游戏规则,首先必须有一张玩家表,用来保存注册玩家的数据,还要有英雄表,用来存放出战英雄的数据。最后一张宝箱表,存放玩家所获得的宝箱,结构如下:

    玩家表:

    • 玩家EOS账号
    • 拥有的SJ token(水晶)
    • 游戏局数计数器

    英雄表:

    • 唯一id
    • 英雄名称
    • 攻击力最小值
    • 攻击力最大值
    • 血量

    宝箱表:

    • 唯一id
    • 所属玩家
    • 宝箱的级别(金、银、铜)
        //player
        //scope is self
          TABLE players {
            name player_account;
            asset coin;
            uint64_t created_at;
            
            uint64_t primary_key() const { return player_account.value;}
          };
        
        //hero
        //scope is player 
         TABLE heros {
          uint64_t id = 10000;
          string hero_name;
          uint32_t atk;
          uint32_t hp;
          uint64_t created_at;
          
          uint64_t primary_key() const { return id;}
        };
        
         //box
         //scope is self
         //index: player_account
         TABLE boxs {
          uint64_t id = 10000;
          name player_account;
          uint8_t level;
          uint64_t created_at;
          
          uint64_t get_player() const { return player_account.value;}
          uint64_t primary_key() const { return id;}
        };
        
        
       using player_index = multi_index<"players"_n, players>;
       using hero_index = multi_index<"heros"_n, heros>;
       using box_index = multi_index<"boxs"_n, boxs,
        indexed_by<"byplayer"_n, const_mem_fun<boxs, uint64_t, &boxs::primary_key>>
        >;
    

      uint64_t primary_key() const { return player_account.value;}表示将player_account 设置为表的主键,主键具有唯一约束,且每个表都有一个主键。
      第三个boxs表的uint64_t get_player() const { return player_account.value;}表示将player_account设为索引,是为了后面方便查询表的数据。
      最后using开头的三行是对表进行配置:multi_index表明示多索引表,即可以存放多条数据,另外一种singleton声明的,表示只能存放一条数据,但相对地CRUD操作就非常方便。一搬用在存放合约全局配置数据的表。
      大家看上面herosboxs表的结构有没有发现有些问题,boxs有一个所属玩家的字段,而heros却没有。那怎么查询某个玩家下的hero呢?答案是根据表的scope来查询的,什么是scope?我们先看下使用一个表之前,对表进行实例化的代码:

    box_index box_table(get_self(), gete_self().value ); //实例化boxs表
    hero_index hero_table(get_self(),"bob"_n.value ); //实例化heros表
    

    实例化需要两个参数:

    • 第一个参数指定表的拥有者(owner),owner的账号需要为存入该表的数据支付RAM,表的数据也只能owner能够修改。我们使用get_self()表示使用合约的账号。
    • 第二个参数用来将数据分区,如上代码,如果传入玩家bob的账号,那么我们拿到的hero_table就是一个只针对bob的数据,也只能对部分数据进行操作。不知道大家发现没有,如果你要查询这张表的所有数据,是没有办法查的,因为scope必须指定。eosio.token 的accounts也是用户账户作为scope。

      使用scope区分和使用索引其实都可以达到目的,至于使用的时机就要看情况。还是以上面两个表为例,根据我之前的经验,个人认为如果在同一个交易中,如果需要同时操作多个玩家的英雄数据,就最好使用索引了,例如:在某个版本,需要给所有攻击力低于30的英雄+10攻击力,因为觉得英雄太弱鸡了。这时查询的维度不是玩家,而是攻击力,如果将攻击力设置为索引,就很方便了。这里heros用scope的方式,是因为确保每次交易,我们只操作当前玩家的数据。

    智能合约代码编写

      还记得上一章节新建的项目,有两个文件.hpp和.cpp,hpp用来编写对外描述action、表结构和一些私有方法和变量。cpp主要编写action的具体实现。
      按照上一章游戏规则的顺序,先编写注册的action,打开hpp文件,把系统自动生成的 ACTION hi(name user)删掉,替换为:ACTION signup(const name player);。在实现前回顾下游戏规则:

    玩家需要注册账号,同时获得1000个SJ币(水晶),并得到一个人物用于战斗,人物有攻击力和血量2个属性,攻击力初始35-70,血量初始500。

      切换到.cpp文件,实现如下:

    ACTION kingofighter::signup(name player) {
      //要求必须玩家本人注册
      require_auth(player);
      
      //实例化player表
      player_index player_tb(get_self(),get_self().value);
      //主键获取玩家的数据
      auto itr = player_tb.find(player.value);
      //如果玩家数据已存在,抛出异常
      check(itr==player_tb.end(), "player account exist!" );
      //声明水晶数量1000个 乘10000是为了抵消0.0001
      const uint64_t amt = 1000 * 10000;
      //插入一条玩家数据
      player_tb.emplace(get_self(), [&]( auto& r ) {
          r.player_account = player;                            //玩家账号
          r.coin = asset(amt, symbol(symbol_code("SJ"), 4););   //初始水晶数量:1000
          r.counter = 0;                                        //玩家游戏局数
          r.created_at = time_point_sec(current_time_point());  //当前区块链时间
      });
      
      //实例化hero表
      //第二个入参(scope)为玩家账号
      hero_index hero_tb(get_self(),player.value);
      hero_tb.emplace(get_self(), [&]( auto& r ) {
          r.id = hero_tb.available_primary_key();
          r.hero_name = "jakiro";                               //英雄名称:杰奇诺
          r.min_atk = 35;                                       //攻击力最小值
          r.max_atk = 70;                                       //攻击力最大值
          r.hp = 500;                                           //血量
          r.created_at = time_point_sec(current_time_point());  //当前区块链时间
      });
    }
    

      入参为注册玩家的EOS账号,注册action可分三部分:校验权限、插入玩家数据、插入英雄数据。上面代码根据注释很好理解,但又两句需要说下r.coin = asset(1000 * 10000, symbol(symbol_code("SJ"), 4));,这里构造1000个符号为“SJ”,小数点后保留4位的asset资产。r.id = hero_tb.available_primary_key();表示使用hero表维护的自增id,默认从0开始自增。
      注册有了,现在可以开始构思对战的过程。对于玩家来说,应该是只需要调用一个[对战]的action,然后等待战斗结果就可以了。再仔细想想会发觉,其实目前所有战局的因素都是固定的:玩家英雄的属性、BOSS的属性。所以需要外部给一个生产随机的因素。为了提高玩家的参与感,让他觉得可以影响到战局,我们允许玩家提供一个随机数,然后再结合我们在合约的时间戳,产生一个新的随机数。对战逻辑中的所有动作都将会与这个随机数相关,下面贴出部分核心代码:

    ACTION kingofighter::battle(const name player, const capi_checksum256 &seed_hash) {
        ...
        const uint32_t NOW_TS = current_time_point().sec_since_epoch();
        const uint32_t BOSS_MIN_ATK = 50;
        const uint32_t BOSS_MAX_ATK = 70;
        const uint32_t BOSS_HP = 700;
        uint32_t hero_hp = hero->hp;
        uint32_t boss_hp = BOSS_HP;
        ...
        for (size_t i = 0; i < 32; i++) {
            const uint32_t hash_val =(uint32_t) seed_hash.extract_as_byte_array()[i] + NOW_TS;
            uint32_t damage;
            if (i & 1) {
                //i为奇数,BOSS攻击
                damage = hash_val % (BOSS_MAX_ATK - BOSS_MIN_ATK + 1) + BOSS_MIN_ATK;
                hero_hp = hero_hp > damage ? hero_hp - damage : 0;
            } else {
                //i为偶数,玩家攻击
                damage = hash_val % (hero->max_atk - hero->min_atk + 1) + hero->min_atk;
                //是否暴击,暴击概率25%
                if (hash_val % 4 == 0)
                    damage += 100;
                boss_hp = boss_hp > damage ? boss_hp - damage : 0;
            }
    
            //这一轮的战斗结果
            scoreboard sb_item = {
                    .round_no = i + 1,
                    .attacker = i & 1 ? get_self() : player,
                    .defender = i & 1 ? player : get_self(),
                    .damage = damage,
                    .defender_hp = i & 1 ? hero_hp : boss_hp
            };
            scoreboards.emplace_back(sb_item);
            //如果任何一方血量归0,战斗结束
            if (hero_hp == 0 || boss_hp == 0)
                break;
        }
    ...
    }
    

      首先看看第二个入参seed_hash 是一个checksum256类型,其实就是一个长度64的哈希值。也就是玩家的随机数算哈希:hash(random)。我们在使用的时候体现为一个uint8_t[32],分布为32个0-255的数字。这个32个数字加上当前时间戳就是随机数,且BOSS最小攻击50,玩家英雄血量500,假设每次攻击都脸黑,BOSS击败玩家英雄需要500 / 50 = 10 轮,10 * 2 < 32,所以我们目前的需求,32个够用了。我们来运行一下看结果:

    • 切回到EOS Studio,构建,然后部署。
    • 创建一个玩家账号:点击右上角-》Create Account,这里输入 sweetsummer1
    • 点击右上角-》Contract,选择你的合约账号,可以看到一下界面:



      区域1是合约的action列表,及每个action的入参,区域2是合约的表数据。

    • 我们选择signup,在入参填写sweetsummer1,即需要注册的玩家账号:
      注册后表数据
    • 注册成功,开始调用对战的action,需要两个入参:player和seed_hash,其中player填写刚注册的账号,seed_hash需要一个64长度的字符,可以在http://tool.oschina.net/encrypt?type=2网站生成一个,随便输入一些东西, 点击生成 sha256:
      对账结果
      看到对战的一局结果已经出来,且是玩家获胜,右边的数据表示每一轮攻击的详细信息,有发动攻击者、攻击血量、是否暴击等...
    • 再将表切换到boxs,可以看到玩家获得了一个铜宝箱:

      就这样,按理说对战的结果受玩家的影响,也受时间戳的影响。好像一切都很公平很顺利,但其实是这种使用时间戳生成随机数的方法是可以被攻击的,攻击过程大概是攻击者事先计算好未来的哪个时间戳对自己有利,然后当到达这个时间点,才去调用对战action。例如:攻击者通过计算,知道在1569643270这个时间戳发动攻击,是一定会获胜,所以他自己写一个智能合约,当到达这个时间点,发动攻击就可以了,十分简单,为什么一定要使用智能合约来调用呢?因为这样可以保证时间一致。
      怎么样防止攻击?现在开发者应该都已经达成共识,纯链上生成随机数是不可靠的,官方也不建议,我们需要合约开发者也生成一个随机哈希,然后加上玩家的随机哈希,合并在一起,产生随机数。但这种做法有个缺点,就是需要有个server端,下一章节我们会server端的设计,以及智能合约的相关改动。

    本章节源代码地址:https://github.com/jan-gogogo/kof-chapter2

    参考资料

    相关文章

      网友评论

        本文标题:2-从零开发EOS区块链小游戏系列 - 智能合约设计与编写

        本文链接:https://www.haomeiwen.com/subject/zjyydctx.html