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

[Previous << 10 - マリガンおよびゲーム開始](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8)
[Next >> 12 - 勝敗と賞金送付、ランキング](https://qiita.com/flowcadence/items/0d3f809c2a4b331b1ca4)

https://qiita.com/flowcadence/items/3073d69c6e9895e68b5a#%E9%96%8B%E7%99%BA%E6%96%B9%E6%B3%95

:::note

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

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

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

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

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

:::note warn
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合は[こちら](https://qiita.com/flowcadence/items/8cc25249cda37364cac1)を参照してください。
:::

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

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

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

https://devfolio.co/projects/code-of-flow-a0cd

#### 攻撃ターンを交代する

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

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

:::note warn
randomは0~29の値になります。
```
let modulo: UInt8 = 30
let random = revertibleRandom(modulo: modulo) 
```
:::

攻撃ターンを交代するロジックは以下のように実装しました。
```ruby: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`コントラクトを以下コマンドで更新します。
```ruby
flow accounts update-contract ./AwesomeCardGame.cdc
```

#### 攻撃ターンを交代する

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

:::note warn
トランザクションを実行するとき`Dictionary`の値も送れるのですが送る方法が少し独特です。
**例えば、`{1: 10, 2: 20, 3: 30, 4: 40}`という値をトランザクションで送信したいとします。**
その値を送る方法はこうです。
```ruby
  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方式のトランザクション](https://qiita.com/flowcadence/items/27ef976c2f2493146dd6#non-custodial-account)を書きます。
```ruby:/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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/8cc0a5d6-0436-34e6-e253-10223f9afec3.png)

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

次に、後攻のプレイヤーも`turn_change`トランザクションを実行してみます。コマンドは以下とします。
```
node ../backend/send_change_turn.js 2 "{}"
```
結果:
![スクリーンショット 2025-01-04 8.55.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/ef1b3043-500b-be54-e7f9-13592e47f1c2.png)

`is_first_turn`が`true`ですので先攻に変わっています。また、ターンが２ターン目になっています。

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

:::note warn
ハンヅオンで学べる書籍もあります。 **[こちら](https://www.amazon.co.jp/dp/B0D3JBRJF5)↗︎**  (Version1.0ではないですが、先端技術を使用します)
特色: ブロックチェーンのトランザクションは送信から情報取得までに7-10秒かかります。それを時間を感じさせなくする為には、バックエンドにGraphQLサーバーを採用することが考えられます。GraphQLは大人数による同時接続通信が可能なので、ゲームで何をしたのかを対戦相手に前もって通知することができるからです。(YouTube動画(下部)の右下部分、または[ここのYouTube動画](https://www.youtube.com/watch?v=TzxgaaAhM3c)の下部に表示される部分。)
<sup>*GraphQLはX(旧Twitter)など大手ソフトウェア企業が積極採用している通信技術です。</sup>
GraphQL はAWS Lambdaと相性が良く(特に金銭面で)、ブロックチェーンにもスムーズにトランザクションを送れるという利点があります。**GraphQL**はNode.jsで動作し、そのためAWS Lambdaで動きます。そしてAWS Lambdaがreturnする値を現在ブラウザに接続しているすべての端末に対して、Push送信することができます。Svelteに関しては他にも良い書籍がありますが、**AWS LambdaをGraphQLサーバーとして立ち上げる方法をきちんと図説**している情報媒体は少ないはずです(僕は英語でAWSの開発陣から知りました)。インフラを自分で立ち上げたことがある人なら、この本を読みながら簡単に低コストのバックエンドをセットアップすることができます。
:::
<iframe width="560" height="315" src="https://www.youtube.com/embed/Qo5rhA147o4?si=vWoJvRfcSxcIQYwv" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<sup>この動画のロジックは全てスマートコントラクトに書かれてます。<b>画面上に出ている情報は全てブロックチェーンから取得したものです。</b>(注:画像ファイルはSvelteフレームワークで管理しています)</sup>

***
この記事のソースコードは[こちら](https://github.com/temt-ceo/awesome-cardgame)にあります。

***
<b>[Previous << 10 - マリガンおよびゲーム開始](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8)
</b>

[Flow blockchain / Cadence version1.0ドキュメント](https://qiita.com/flowcadence/items/3073d69c6e9895e68b5a#%E9%96%8B%E7%99%BA%E6%96%B9%E6%B3%95)

<b>[Next >> 12 - 勝敗と賞金送付、ランキング](https://qiita.com/flowcadence/items/0d3f809c2a4b331b1ca4)</b>