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?

How to develop a blockchain game. Day11 ターンチェンジ

Last updated at Posted at 2025-01-04

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

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乱数生成関数は非常に使い勝手が良く、開発の生産性が格段に上がります。


もっと高度なGraphQLによる対戦相手と情報を共有しながらトランザクションを実施したいケースの場合は、こちらが参考になります。(本は薄めですが、無駄を省いてピンポイントでコードを参照出来るようにする為です、レビュー滅茶甘めで付けていただけると喜びます😭)


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


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

Flow BlockchainのCadence version1.0ドキュメント

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

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?