2-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 智能合約設(shè)計(jì)與編寫

目錄

智能合約表結(jié)構(gòu)設(shè)計(jì)

?回顧上一章的游戲規(guī)則,首先必須有一張玩家表,用來保存注冊玩家的數(shù)據(jù),還要有英雄表,用來存放出戰(zhàn)英雄的數(shù)據(jù)。最后一張寶箱表,存放玩家所獲得的寶箱,結(jié)構(gòu)如下:

玩家表:

  • 玩家EOS賬號
  • 擁有的SJ token(水晶)
  • 游戲局?jǐn)?shù)計(jì)數(shù)器

英雄表:

  • 唯一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 設(shè)置為表的主鍵,主鍵具有唯一約束,且每個(gè)表都有一個(gè)主鍵。
?第三個(gè)boxs表的uint64_t get_player() const { return player_account.value;}表示將player_account設(shè)為索引,是為了后面方便查詢表的數(shù)據(jù)。
??最后using開頭的三行是對表進(jìn)行配置:multi_index表明示多索引表,即可以存放多條數(shù)據(jù),另外一種singleton聲明的,表示只能存放一條數(shù)據(jù),但相對地CRUD操作就非常方便。一搬用在存放合約全局配置數(shù)據(jù)的表。
??大家看上面herosboxs表的結(jié)構(gòu)有沒有發(fā)現(xiàn)有些問題,boxs有一個(gè)所屬玩家的字段,而heros卻沒有。那怎么查詢某個(gè)玩家下的hero呢?答案是根據(jù)表的scope來查詢的,什么是scope?我們先看下使用一個(gè)表之前,對表進(jìn)行實(shí)例化的代碼:

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

實(shí)例化需要兩個(gè)參數(shù):

  • 第一個(gè)參數(shù)指定表的擁有者(owner),owner的賬號需要為存入該表的數(shù)據(jù)支付RAM,表的數(shù)據(jù)也只能owner能夠修改。我們使用get_self()表示使用合約的賬號。
  • 第二個(gè)參數(shù)用來將數(shù)據(jù)分區(qū),如上代碼,如果傳入玩家bob的賬號,那么我們拿到的hero_table就是一個(gè)只針對bob的數(shù)據(jù),也只能對部分?jǐn)?shù)據(jù)進(jìn)行操作。不知道大家發(fā)現(xiàn)沒有,如果你要查詢這張表的所有數(shù)據(jù),是沒有辦法查的,因?yàn)閟cope必須指定。eosio.token 的accounts也是用戶賬戶作為scope。

?使用scope區(qū)分和使用索引其實(shí)都可以達(dá)到目的,至于使用的時(shí)機(jī)就要看情況。還是以上面兩個(gè)表為例,根據(jù)我之前的經(jīng)驗(yàn),個(gè)人認(rèn)為如果在同一個(gè)交易中,如果需要同時(shí)操作多個(gè)玩家的英雄數(shù)據(jù),就最好使用索引了,例如:在某個(gè)版本,需要給所有攻擊力低于30的英雄+10攻擊力,因?yàn)橛X得英雄太弱雞了。這時(shí)查詢的維度不是玩家,而是攻擊力,如果將攻擊力設(shè)置為索引,就很方便了。這里heros用scope的方式,是因?yàn)榇_保每次交易,我們只操作當(dāng)前玩家的數(shù)據(jù)。

智能合約代碼編寫

?還記得上一章節(jié)新建的項(xiàng)目,有兩個(gè)文件.hpp和.cpp,hpp用來編寫對外描述action、表結(jié)構(gòu)和一些私有方法和變量。cpp主要編寫action的具體實(shí)現(xiàn)。
?按照上一章游戲規(guī)則的順序,先編寫注冊的action,打開hpp文件,把系統(tǒng)自動生成的 ACTION hi(name user)刪掉,替換為:ACTION signup(const name player);。在實(shí)現(xiàn)前回顧下游戲規(guī)則:

玩家需要注冊賬號,同時(shí)獲得1000個(gè)SJ幣(水晶),并得到一個(gè)人物用于戰(zhàn)斗,人物有攻擊力和血量2個(gè)屬性,攻擊力初始35-70,血量初始500。

?切換到.cpp文件,實(shí)現(xiàn)如下:

ACTION kingofighter::signup(name player) {
  //要求必須玩家本人注冊
  require_auth(player);
  
  //實(shí)例化player表
  player_index player_tb(get_self(),get_self().value);
  //主鍵獲取玩家的數(shù)據(jù)
  auto itr = player_tb.find(player.value);
  //如果玩家數(shù)據(jù)已存在,拋出異常
  check(itr==player_tb.end(), "player account exist!" );
  //聲明水晶數(shù)量1000個(gè) 乘10000是為了抵消0.0001
  const uint64_t amt = 1000 * 10000;
  //插入一條玩家數(shù)據(jù)
  player_tb.emplace(get_self(), [&]( auto& r ) {
      r.player_account = player;                            //玩家賬號
      r.coin = asset(amt, symbol(symbol_code("SJ"), 4););   //初始水晶數(shù)量:1000
      r.counter = 0;                                        //玩家游戲局?jǐn)?shù)
      r.created_at = time_point_sec(current_time_point());  //當(dāng)前區(qū)塊鏈時(shí)間
  });
  
  //實(shí)例化hero表
  //第二個(gè)入?yún)?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());  //當(dāng)前區(qū)塊鏈時(shí)間
  });
}

?入?yún)樽酝婕业腅OS賬號,注冊action可分三部分:校驗(yàn)權(quán)限、插入玩家數(shù)據(jù)、插入英雄數(shù)據(jù)。上面代碼根據(jù)注釋很好理解,但又兩句需要說下r.coin = asset(1000 * 10000, symbol(symbol_code("SJ"), 4));,這里構(gòu)造1000個(gè)符號為“SJ”,小數(shù)點(diǎn)后保留4位的asset資產(chǎn)。r.id = hero_tb.available_primary_key();表示使用hero表維護(hù)的自增id,默認(rèn)從0開始自增。
?注冊有了,現(xiàn)在可以開始構(gòu)思對戰(zhàn)的過程。對于玩家來說,應(yīng)該是只需要調(diào)用一個(gè)[對戰(zhàn)]的action,然后等待戰(zhàn)斗結(jié)果就可以了。再仔細(xì)想想會發(fā)覺,其實(shí)目前所有戰(zhàn)局的因素都是固定的:玩家英雄的屬性、BOSS的屬性。所以需要外部給一個(gè)生產(chǎn)隨機(jī)的因素。為了提高玩家的參與感,讓他覺得可以影響到戰(zhàn)局,我們允許玩家提供一個(gè)隨機(jī)數(shù),然后再結(jié)合我們在合約的時(shí)間戳,產(chǎn)生一個(gè)新的隨機(jī)數(shù)。對戰(zhàn)邏輯中的所有動作都將會與這個(gè)隨機(jī)數(shù)相關(guān),下面貼出部分核心代碼:

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為奇數(shù),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為偶數(shù),玩家攻擊
            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;
        }

        //這一輪的戰(zhàn)斗結(jié)果
        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,戰(zhàn)斗結(jié)束
        if (hero_hp == 0 || boss_hp == 0)
            break;
    }
...
}

?首先看看第二個(gè)入?yún)?code>seed_hash 是一個(gè)checksum256類型,其實(shí)就是一個(gè)長度64的哈希值。也就是玩家的隨機(jī)數(shù)算哈希:hash(random)。我們在使用的時(shí)候體現(xiàn)為一個(gè)uint8_t[32],分布為32個(gè)0-255的數(shù)字。這個(gè)32個(gè)數(shù)字加上當(dāng)前時(shí)間戳就是隨機(jī)數(shù),且BOSS最小攻擊50,玩家英雄血量500,假設(shè)每次攻擊都臉黑,BOSS擊敗玩家英雄需要500 / 50 = 10 輪,10 * 2 < 32,所以我們目前的需求,32個(gè)夠用了。我們來運(yùn)行一下看結(jié)果:

  • 切回到EOS Studio,構(gòu)建,然后部署。
  • 創(chuàng)建一個(gè)玩家賬號:點(diǎn)擊右上角-》Create Account,這里輸入 sweetsummer1
  • 點(diǎn)擊右上角-》Contract,選擇你的合約賬號,可以看到一下界面:



    區(qū)域1是合約的action列表,及每個(gè)action的入?yún)ⅲ瑓^(qū)域2是合約的表數(shù)據(jù)。

  • 我們選擇signup,在入?yún)⑻顚憇weetsummer1,即需要注冊的玩家賬號:
    注冊后表數(shù)據(jù)
  • 注冊成功,開始調(diào)用對戰(zhàn)的action,需要兩個(gè)入?yún)ⅲ簆layer和seed_hash,其中player填寫剛注冊的賬號,seed_hash需要一個(gè)64長度的字符,可以在http://tool.oschina.net/encrypt?type=2網(wǎng)站生成一個(gè),隨便輸入一些東西, 點(diǎn)擊生成 sha256:
    對賬結(jié)果

    看到對戰(zhàn)的一局結(jié)果已經(jīng)出來,且是玩家獲勝,右邊的數(shù)據(jù)表示每一輪攻擊的詳細(xì)信息,有發(fā)動攻擊者、攻擊血量、是否暴擊等...
  • 再將表切換到boxs,可以看到玩家獲得了一個(gè)銅寶箱:

?就這樣,按理說對戰(zhàn)的結(jié)果受玩家的影響,也受時(shí)間戳的影響。好像一切都很公平很順利,但其實(shí)是這種使用時(shí)間戳生成隨機(jī)數(shù)的方法是可以被攻擊的,攻擊過程大概是攻擊者事先計(jì)算好未來的哪個(gè)時(shí)間戳對自己有利,然后當(dāng)?shù)竭_(dá)這個(gè)時(shí)間點(diǎn),才去調(diào)用對戰(zhàn)action。例如:攻擊者通過計(jì)算,知道在1569643270這個(gè)時(shí)間戳發(fā)動攻擊,是一定會獲勝,所以他自己寫一個(gè)智能合約,當(dāng)?shù)竭_(dá)這個(gè)時(shí)間點(diǎn),發(fā)動攻擊就可以了,十分簡單,為什么一定要使用智能合約來調(diào)用呢?因?yàn)檫@樣可以保證時(shí)間一致。
?怎么樣防止攻擊?現(xiàn)在開發(fā)者應(yīng)該都已經(jīng)達(dá)成共識,純鏈上生成隨機(jī)數(shù)是不可靠的,官方也不建議,我們需要合約開發(fā)者也生成一個(gè)隨機(jī)哈希,然后加上玩家的隨機(jī)哈希,合并在一起,產(chǎn)生隨機(jī)數(shù)。但這種做法有個(gè)缺點(diǎn),就是需要有個(gè)server端,下一章節(jié)我們會server端的設(shè)計(jì),以及智能合約的相關(guān)改動。

本章節(jié)源代碼地址:https://github.com/jan-gogogo/kof-chapter2

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容