0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブロックチェーンゲームの作り方11 ターンチェンジ

Last updated at Posted at 2025-01-04

Previous << 10 - マリガンおよびゲーム開始
Next >> 12 - 勝敗と賞金送付、ランキング

11 - ターンチェンジは、ゲームの進捗、つまり自分と対戦相手のターンが終わった時の情報をブロックチェーンに記録します。

この情報は誰でも見る事ができます。賞金がかかったゲームでは透明性が大事ですから、ゲームの進捗をブロックチェーンに保存することで、その透明性を世界中の人に理解してもらう事ができます。

ブロックチェーンは新しいデータベースとしても利用できます。そして暗号通貨のやり取りを記録できることから、その透明性をアピールする為にそれに関連するデータを保存する、という使い道が存在します。

Why Flow

FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。

ということでやっていきます、P2P(ピアツーピア)決済アプリ開発!

💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。

カードを場に出す、攻撃、防御アクション

カードゲームにおいて、カードを場に出す攻撃防御アクションはゲームの勝敗を決める大事なものですが、そこにはゲームのロジックの内容を書く必要があり、ソースコードが非常に長くなります。フルオンチェーンのカードゲームは作るのは面白いですが、その理由のため、今回は割愛致します。

ハッカソンで作ったゲームロジック詳細のコードに興味のある方はこちらからスマートコントラクトを見ることができます。

攻撃ターンを交代する

自分のターンを終わらせて、相手にターンを譲ります。

ターンが変わるときに新しいカードを2枚引きます。そこで乱数生成関数revertibleRandomを使います。revertibleRandomは、整数を与えると、0~(整数-1)までの乱数を生成してくれる非常に使い勝手のいいBuilt-In関数です。

randomは0~29の値になります。

let modulo: UInt8 = 30
let random = revertibleRandom(modulo: modulo) 

攻撃ターンを交代するロジックは以下のように実装しました。

AwesomeCardGame.cdc
           :
  access(all) struct BattleStruct {
           :
    access(contract) fun set_is_first_turn(new_value: Bool) {
      self.is_first_turn = new_value
    }
           :
  }
           :
  /*
  ** [Resource] Admin (Game Server Processing)
  */
  access(all) resource Admin {
           :

    access(all) fun turn_change(player_id: UInt, from_opponent: Bool, trigger_cards: {UInt8: UInt16}) {
      if let info = AwesomeCardGame.battleInfo[player_id] {

        /* Check is turn already changed. */
        if info.is_first != info.is_first_turn {
          return;
        }

        /** 今回はバトルはしないので、以下部分は割愛
        if (info.your_attacking_card != nil && info.your_attacking_card!.attacked_time + 20.0 > info.last_time_turnend!) {
                :
        **/

        /* トリガーゾーンのカードを合わせる */
        for position in trigger_cards.keys {
          /* セット済みは除外 */
          if info.your_trigger_cards[position] != trigger_cards[position] {
            /* ハンドの整合性を合わせる(トリガーゾーンに移動した分、ハンドから取る) */
            var isRemoved = false
            if info.your_trigger_cards[position] != trigger_cards[position] && trigger_cards[position] != 0 {
              let card_id = trigger_cards[position]
              info.your_trigger_cards[position] = card_id
              for hand_position in info.your_hand.keys {
                  if card_id == info.your_hand[hand_position] && isRemoved == false {
                    info.your_hand[hand_position] = nil
                    isRemoved = true
                  }
              }
              if (isRemoved == false) {
                panic("You set the card on trigger zone which is not exist in your hand")
              }
            }
          }
        }

        var handCnt = 0
        let handPositions: [UInt8] = [1, 2, 3, 4, 5 ,6, 7]
        for hand_position in handPositions {
          if info.your_hand[hand_position] != nil {
            handCnt = handCnt + 1
          }
        }

        /** 今回はバトルはしないので、以下部分は割愛
        for position in info.your_field_unit.keys {
                :
        **/

        /* Process Turn Change */
        info.set_last_time_turnend(new_value: getCurrentBlock().timestamp)
        info.set_is_first_turn(new_value: !info.is_first_turn)
        if (info.is_first_turn) {
          info.set_turn(new_value: info.turn + 1)
        }

        let opponent = info.opponent
        if let infoOpponent = AwesomeCardGame.battleInfo[opponent] {

          /* Turn Change */
          infoOpponent.set_last_time_turnend(new_value: info.last_time_turnend!)
          infoOpponent.set_is_first_turn(new_value: !infoOpponent.is_first_turn)
          infoOpponent.set_turn(new_value: info.turn)
          /** 今回はバトルはしないので、以下部分は割愛
          infoOpponent.opponent_hand = handCnt
                  :
          **/

          /* draw card */
          let cardRemainCounts = infoOpponent.your_remain_deck.length

          /* 乱数生成関数`revertibleRandom`を使います。 */
          let modulo: UInt8 = cardRemainCounts as! UInt8
          let withdrawPosition1 = revertibleRandom(modulo: modulo - 1)
          let withdrawPosition2 = revertibleRandom(modulo: modulo - 2)

          var isSetCard1 = false
          var isSetCard2 = false
          var handCnt2 = 0
          let handPositions: [UInt8] = [1, 2, 3, 4, 5 ,6, 7]
          let nextPositions: [UInt8] = [1, 2, 3, 4, 5 ,6]
          /* カード位置を若い順に整列 */
          for hand_position in handPositions {
            var replaced: Bool = false
            if infoOpponent.your_hand[hand_position] == nil {
              for next in nextPositions {
                if replaced == false && hand_position + next <= 7 && infoOpponent.your_hand[hand_position + next] != nil {
                  infoOpponent.your_hand[hand_position] = infoOpponent.your_hand[hand_position + next]
                  infoOpponent.your_hand[hand_position + next] = nil
                  replaced = true
                }
              }
            }
          }
          for hand_position in handPositions {
            if infoOpponent.your_hand[hand_position] == nil && isSetCard1 == false {
              infoOpponent.your_hand[hand_position] = infoOpponent.your_remain_deck.remove(at: withdrawPosition1)
              isSetCard1 = true
            }
            if infoOpponent.your_hand[hand_position] == nil && isSetCard2 == false {
              infoOpponent.your_hand[hand_position] = infoOpponent.your_remain_deck.remove(at: withdrawPosition2)
              isSetCard2 = true
            }

            if infoOpponent.your_hand[hand_position] != nil {
              handCnt2 = handCnt2 + 1
            }
          }
          /** 今回はバトルはしないので、以下部分は割愛
          infoOpponent.your_field_unit_bp_amount_of_change = {} /* Damage are reset */
                  :
          **/

          AwesomeCardGame.battleInfo[opponent] = infoOpponent
        }
        /* save */
        AwesomeCardGame.battleInfo[player_id] = info
      }

      /* judge the winner */
      // self.judgeTheWinner(player_id: player_id)
    }
  }
           :

turn_change関数はAdminリソースの中に配置します。

AwesomeCardGameコントラクトを以下コマンドで更新します。

flow accounts update-contract ./AwesomeCardGame.cdc

攻撃ターンを交代する

次にバックエンドでNode.jsで実行するjsファイルを作っていきます。ここでは/backendフォルダの中のファイルはサーバーサイドで実行するものと仮定します。

トランザクションを実行するときDictionaryの値も送れるのですが送る方法が少し独特です。
例えば、{1: 10, 2: 20, 3: 30, 4: 40}という値をトランザクションで送信したいとします。
その値を送る方法はこうです。

  message = {1: 10, 2: 20, 3: 30, 4: 40};
  /* 値が無かった時のfallback値は0とする。 */
  const trigger_cards = [
    { key: 1, value: message["1"] || 0 },
    { key: 2, value: message["2"] || 0 },
    { key: 3, value: message["3"] || 0 },
    { key: 4, value: message["4"] || 0 },
  ];
            :
            :

    args: (arg, t) => [
      arg(player_id, t.UInt),
      arg(false, t.Bool),
      arg(trigger_cards, t.Dictionary({ key: t.UInt8, value: t.UInt16 })),
    ],

Dictionaryのkeyはkeyというキーに対するvalueになります。なので私はトランザクションコードではkeyを固定にしていました。Object.keysとかを使ってやると、固定にしない方法もあると思いますが。。。

send_change_turn.jsファイルを作成して、Non-Custodial方式のトランザクションを書きます。

/backend/send_change_turn.js
import fcl from "@onflow/fcl";
import { SHA3 } from "sha3";
import pkg from "elliptic";
const { ec } = pkg;
import { argv } from "node:process";

var player_id;
var set_cards;

argv.forEach((val, index) => {
  if (index == 2) {
    player_id = val;
  } else if (index == 3) {
    set_cards = JSON.parse(val);
  }
});

fcl
  .config()
  .put("flow.network", "emulator")
  .put("accessNode.api", "http://localhost:8888");

try {
  var KEY_ID_IT = 0;
  // 以下はProposerのKeyが300ある場合を想定したものなのでエミュレータでは0とする
  // if (fs.existsSync("/tmp/sequence.txt")) {
  //   KEY_ID_IT = parseInt(
  //     fs.readFileSync("/tmp/sequence.txt", { encoding: "utf8" })
  //   );
  // } else {
  //   KEY_ID_IT = new Date().getMilliseconds() % 300;
  // }
  // KEY_ID_IT = !KEY_ID_IT || KEY_ID_IT >= 300 ? 1 : KEY_ID_IT + 1;
  // fs.writeFileSync("/tmp/sequence.txt", KEY_ID_IT.toString());

  const ec_ = new ec("p256");

  /* CHANGE THESE THINGS FOR YOU */
  /* 注意! これはエミュレータのプライベートキーなので、テストネットやメインエットに影響がないのでコードに埋めているが、本来は決して行ってはならない */
  const PRIVATE_KEY = `9ba63c9cd20a8214bcd8178b6d65d6cb54725670bba95a56f30d3bb1de9baaf4`;
  const ADDRESS = "0xf8d6e0586b0a20c7";
  const KEY_ID = 0;

  const sign = (message) => {
    const key = ec_.keyFromPrivate(Buffer.from(PRIVATE_KEY, "hex"));
    const sig = key.sign(hash(message)); // hashMsgHex -> hash
    const n = 32;
    const r = sig.r.toArrayLike(Buffer, "be", n);
    const s = sig.s.toArrayLike(Buffer, "be", n);
    return Buffer.concat([r, s]).toString("hex");
  };
  const hash = (message) => {
    const sha = new SHA3(256);
    sha.update(Buffer.from(message, "hex"));
    return sha.digest();
  };

  async function authorizationFunction(account) {
    return {
      ...account,
      tempId: `${ADDRESS}-${KEY_ID}`,
      addr: fcl.sansPrefix(ADDRESS),
      keyId: Number(KEY_ID),
      signingFunction: async (signable) => {
        return {
          addr: fcl.withPrefix(ADDRESS),
          keyId: Number(KEY_ID),
          signature: sign(signable.message),
        };
      },
    };
  }
  async function authorizationFunctionProposer(account) {
    return {
      ...account,
      tempId: `${ADDRESS}-${KEY_ID_IT}`,
      addr: fcl.sansPrefix(ADDRESS),
      keyId: Number(KEY_ID_IT),
      signingFunction: async (signable) => {
        return {
          addr: fcl.withPrefix(ADDRESS),
          keyId: Number(KEY_ID_IT),
          signature: sign(signable.message),
        };
      },
    };
  }

  console.log(player_id, set_cards, KEY_ID, KEY_ID_IT);
  const trigger_cards = [
    { key: 1, value: set_cards["1"] || 0 },
    { key: 2, value: set_cards["2"] || 0 },
    { key: 3, value: set_cards["3"] || 0 },
    { key: 4, value: set_cards["4"] || 0 },
  ];
  let transactionId = await fcl.mutate({
    cadence: `
      import AwesomeCardGame from 0xf8d6e0586b0a20c7

      transaction(player_id: UInt, from_opponent: Bool, trigger_cards: {UInt8: UInt16}) {
        prepare(signer: auth(BorrowValue) &Account) {
          let admin = signer.storage.borrow<&AwesomeCardGame.Admin>(from: /storage/AwesomeCardGameAdmin)
            ?? panic("Could not borrow reference to the Administrator Resource.")
          admin.turn_change(player_id: player_id, from_opponent: from_opponent, trigger_cards: trigger_cards)
        }
        execute {
          log("success")
        }
      }
    `,
    args: (arg, t) => [
      arg(player_id, t.UInt),
      arg(false, t.Bool),
      arg(trigger_cards, t.Dictionary({ key: t.UInt8, value: t.UInt16 })), // trigger_cards
    ],
    proposer: authorizationFunctionProposer,
    payer: authorizationFunction,
    authorizations: [authorizationFunction],
    limit: 999,
  });
  console.log(`TransactionId: ${transactionId}`);
  fcl.tx(transactionId).subscribe((res) => {
    console.log(res);
  });
} catch (error) {
  console.error(error);
}

ターン交代処理をする

プレイヤーが画面上でTurn Endボタンを押したとします。


それをバックエンドで受け取って実行すると仮定します。コマンドは以下とします。

node ../backend/send_change_turn.js 1 "{}"

ではplayer_id=1(emulator_account)の画面を表示します。

結果:
スクリーンショット 2025-01-04 8.50.35.png

is_first_turnfalseであればターンが後攻ですのでターンが変わっています。

次に、後攻のプレイヤーもturn_changeトランザクションを実行してみます。コマンドは以下とします。

node ../backend/send_change_turn.js 2 "{}"

結果:
スクリーンショット 2025-01-04 8.55.18.png

is_first_turntrueですので先攻に変わっています。また、ターンが2ターン目になっています。

Conclusion:
Cadenceスマートコントラクトの組み込み関数、revertibleRandom乱数生成関数は非常に使い勝手が良く、開発の生産性が格段に上がります。


本番環境のブロックチェーンのトランザクションは発行から情報取得までに7-10秒かかります。それを時間を感じさせなくする為には、トランザクション発行にGraphQLを採用することが考えられます。GraphQLは大人数による同時接続通信が可能ですので、ゲームの相手が何をしたかを前もって通知しておくことができます。GraphQL とAWS Lambdaは相性が良く、ブロックチェーンにもスムーズにトランザクションを送る事ができます。Svelteについては他にも良い情報がありますが、AWS LambdaをGraphQLサーバーにして立ち上げる方法をきちんと説明しているところは少ないはず。ブロックチェーンゲームにはバックエンドが必要ですので、その費用を限りなく小さく出来るAWS Lambdaを使った方法がこちらの本で紹介されています。(参照用でありコードが中心ですのでGraphQLの動作など前提知識は必要となります。レビューでは限りなく甘めに星をつけて頂きますよう、お願いします🙇🙇‍♂️)


この記事のソースコードはこちらにあります。


Previous << 10 - マリガンおよびゲーム開始

Flow blockchain / Cadence version1.0ドキュメント

Next >> 12 - 勝敗と賞金送付、ランキング

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?