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

目录

智能合约表结构设计

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

玩家表:

  • 玩家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

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容