ブロックチェーンゲームの作り方10 マリガンおよびゲーム開始

[Previous << 9 - 対戦相手のマッチング](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8)
[Next >> 11 - ターンチェンジ](https://qiita.com/flowcadence/items/a1566910f4bafd162f1e)

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

:::note
スマートコントラクトにはEventsというのがあります。イーサリアムが発祥ですが、Flow, Sui, Aptos などモダン言語は全てEventsを備えてあります。Eventsはブロックチェーンで何が起きているか監視にも使えますが、蓄積して統計データとして提供するサービスにも使えます。

例えば、ブロックチェーンゲームで最初の手札をEventsでゲームのIDと一緒に飛ばしたとします。(スマートコントラクト上部で定義し、あとはそれを[呼び出す(emit)](https://qiita.com/flowcadence/items/07a1cd2e6ca15272900f#emitting-events)だけです)

そのゲームのIDで勝敗が決まった時にもEventsを飛ばせば、それを監視・収集すれば、どのカードを初手に持ってきた時に勝率がよいのか統計を得られます。ブロックチェーンですから誰でもこのようなサービスを展開できます。

一旦面白いブロックチェーンゲームがモダン言語で出れば、コミュニティの人達が役立ちそうなサービスを、彼ら自身で展開する事が可能です。

コミュニティが費用面等の兼ね合いでDBに蓄えないのであれば、公式が蓄積し、そのデータをサービスにすることができます。

このページではゲーム開始トランザクションでEventsの処理を省いていますが、有効ですので[Events](https://qiita.com/flowcadence/items/07a1cd2e6ca15272900f)の併用がおすすめです。([Events取得サンプル。<sub>v1.0時のではありません](https://github.com/temt-ceo/game-built-on-flow/blob/hackathon/amplify/backend/function/hackathon/src/index.js#L110-L118)</sub>)

---
ブロックチェーンでどこまでのゲームが出来るの？とお考えの方、[ここ☞](https://www.youtube.com/watch?app=desktop&v=TzxgaaAhM3c)にハッカソンで作成したゲームの戦闘シーンも含めた全ムービーがあります。

こちらのゲームは目まぐるしく動くゲームをオマージュしています(音声がないのでメッセージがよく出ます)。

このゲームはユーザーの操作に応じてGraphQLをAWS Lambdaに送信、AWS LambdaはFlowのトランザクション送信とGraphQL通知を両者に送信。それによってテンポがよいブロックチェーンゲームにしています。
:::

#### [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)を参照してください。
:::

#### マリガンをする

対戦相手が確定したので、スマートコントラクトからマリガンカードリストを取得します。

```ruby:AwesomeCardGame.cdc
           :

    access(all) fun get_marigan_cards(): [[UInt16]] {
      if let playerMatchingInfo = AwesomeCardGame.playerMatchingInfo[self.player_id] {
        var ret_arr: [[UInt16]] = []
        for i in [0, 1, 2, 3, 4] {
          if let deck = AwesomeCardGame.playerDeck[self.player_id] {
            var tmp = deck.slice(from: 0, upTo: deck.length)
            ret_arr.append([tmp[playerMatchingInfo.marigan_cards[i][0]], tmp[playerMatchingInfo.marigan_cards[i][1]], tmp[playerMatchingInfo.marigan_cards[i][2]], tmp[playerMatchingInfo.marigan_cards[i][3]]])
          } else {
            var tmp = AwesomeCardGame.starterDeck.slice(from: 0, upTo: AwesomeCardGame.starterDeck.length)
            ret_arr.append([tmp[playerMatchingInfo.marigan_cards[i][0]], tmp[playerMatchingInfo.marigan_cards[i][1]], tmp[playerMatchingInfo.marigan_cards[i][2]], tmp[playerMatchingInfo.marigan_cards[i][3]]])
          }
        }
        return ret_arr
      }
      return []
    }

           :
```
`Player`リソースの下に上記のような関数を追記します。


#### マリガンカードリストをjsで取得する
マリガンカードリストを取得するスクリプトと、それをjsで取得するコードは以下のようになります。
```ruby:src/scripts.js
import { query } from "@onflow/fcl";

export const getMariganCards = async function (address) {
  const result = await query({
    cadence: `
    import "AwesomeCardGame"
    access(all) fun main(address: Address): AnyStruct {
      let cap = getAccount(address).capabilities.borrow<&AwesomeCardGame.Player>(/public/AwesomeCardGamePlayer)
        ?? panic("Doesn't have capability!")
      return cap.get_marigan_cards()
    }
    `,
    args: (arg, t) => [arg(address, t.Address)],
  });
  return result;
};
```
:::note warn
アカウントのストレージが持つリソースは古いため、`get_marigan_cards`を見つけられないので一度エミュレータを止めて再起動します。そしてコントラクトをデプロイして新規登録から再度行います。そのため、[マッチング処理も再度行う](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8#%E3%83%9E%E3%83%83%E3%83%81%E3%83%B3%E3%82%B0%E9%96%8B%E5%A7%8B%E5%87%A6%E7%90%86%E3%82%92%E3%81%99%E3%82%8B)必要があります。
:::

#### HTMLとロジックを書く

メイン画面のロジックを以下のようにします。
```ruby:+page.svelte
<script>
import { config, authenticate, unauthenticate, currentUser } from '@onflow/fcl';
import { getBalance, isRegistered, getPlayerDeck, getCurrentStatus, getMariganCards } from '../scripts'; <- 修正
import { createPlayer, buyCyberEn } from '../transactions'
import flowJSON from '../flow.json';
const network = 'emulator';
let walletUser;
let playerName;
let flowBalance;
let cyberEnergyBalance;
let hasResource;
let playerDeck;
let currentStatus;
let mariganCards; <- 追加
config({
  'flow.network': network,
  'accessNode.api': 'http://localhost:8888',
  'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
currentUser.subscribe(async (user) => {
  walletUser = user;
  if (user.addr) {
    hasResource = await isRegistered(user.addr);
    if (hasResource) {
      const [flowTokenBalance, cyberEnergy, pName] = await getBalance(user.addr);
      flowBalance = flowTokenBalance;
      cyberEnergyBalance = cyberEnergy;
      playerName = pName;
      playerDeck = await getPlayerDeck(user.addr);
      mariganCards = await getMariganCards(user.addr); <- 追加
      mariganCards = `[` + mariganCards.join('], [') + ']' <- 追加
      setInterval(async () => {
        currentStatus = await getCurrentStatus(user.addr);
        if (!Number.isNaN(parseInt(currentStatus))) {
          currentStatus = `最後にマッチングを行った時刻=${new Date(currentStatus * 1000).toLocaleString()}`
        } else if (currentStatus !== null) {
          console.log(currentStatus, Object.keys(currentStatus), currentStatus["turn"])
          const keys = Object.keys(currentStatus);
          const tmp = currentStatus;
          if (keys.length) {
          currentStatus = '';
            for (let key of keys) {
              currentStatus += `${key} : ${tmp[key]}; `;
            }
          }
        }
      }, 1000)
    }
  }
});
</script>

{#if !walletUser?.addr}
  <button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
  <b>{playerName}さん</b> FLOW残高: {flowBalance} / ゲーム内通貨: {cyberEnergyBalance}<br>
  <button onclick={unauthenticate}>ログアウト</button>
  {#if !hasResource}
    Playerリソースが作成されていません。
   <button onclick={() => createPlayer('新規ユーザーA')}>新規登録</button>
  {/if}
  {#if hasResource}
    Playerリソース作成済みです。
    <br>
    <button onclick={buyCyberEn}>CyberEnergy購入</button>
    <hr>
    <div>
      現在のデッキ: { playerDeck }
    </div>
    <div>
      現在のゲーム進捗状況: { currentStatus ?? 'なし' }
    </div>
    <hr>
    <div>
      マリガンカード: { mariganCards } <- 追加
    </div>
  {/if}
{/if}
```
これで画面を表示します。

結果:
![スクリーンショット 2025-01-03 13.13.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/12ab57f1-5d4e-40b8-5e25-adc68faa2280.png)

乱数はうまくいっていそうです。


#### スマートコントラクトのゲームを開始するロジック

マリガンでカードを4回まで引き直しできて、それが決定したら、ゲームスタートになります。

ゲームスタートロジックを実装します。
```ruby:AwesomeCardGame.cdc
           :
  access(all) struct BattleStruct {
           :
    access(contract) fun set_game_started(new_value: Bool) {
      self.game_started = new_value
    }
    access(contract) fun set_your_remain_deck(new_value: [UInt16]) {
      self.your_remain_deck = new_value
    }
    access(contract) fun set_last_time_turnend(new_value: UFix64) {
      self.last_time_turnend = new_value
    }
    access(contract) fun set_turn(new_value: UInt8) {
      self.turn = new_value
    }
           :
  }
           :
  /*
  ** [Resource] Admin (Game Server Processing)
  */
  access(all) resource Admin {
           :

    /* 
    ** Game Start Transaction
    */
    access(all) fun game_start(player_id: UInt, drawed_cards: [UInt16]) {
      pre {
        drawed_cards.length == 4 : "Invalid argument."
        AwesomeCardGame.battleInfo[player_id] != nil && AwesomeCardGame.battleInfo[player_id]!.game_started == false : "Game already started."
      }
      var drawed_pos: [UInt8] = []
      if let playerMatchingInfo = AwesomeCardGame.playerMatchingInfo[player_id] {
        for arr in playerMatchingInfo.marigan_cards {
          if let deck = AwesomeCardGame.playerDeck[player_id] {
            var arrCopy = deck.slice(from: 0, upTo: deck.length)
            let card_id1 = arrCopy[arr[0]]
            let card_id2 = arrCopy[arr[1]]
            let card_id3 = arrCopy[arr[2]]
            let card_id4 = arrCopy[arr[3]]
            if (card_id1 == drawed_cards[0] && card_id2 == drawed_cards[1] && card_id3 == drawed_cards[2] && card_id4 == drawed_cards[3]) {
              drawed_pos = arr
            }
          } else {
            var arrCopy = AwesomeCardGame.starterDeck.slice(from: 0, upTo: AwesomeCardGame.starterDeck.length)
            let card_id1 = arrCopy[arr[0]]
            let card_id2 = arrCopy[arr[1]]
            let card_id3 = arrCopy[arr[2]]
            let card_id4 = arrCopy[arr[3]]
            if (card_id1 == drawed_cards[0] && card_id2 == drawed_cards[1] && card_id3 == drawed_cards[2] && card_id4 == drawed_cards[3]) {
              drawed_pos = arr
            }
          }
        }
        if (drawed_pos.length == 0) {
          /* Maybe the player did marigan more than 5 times. Set first cards to avoid errors. */
          drawed_pos = playerMatchingInfo.marigan_cards[0]
        }
      }

      if let info = AwesomeCardGame.battleInfo[player_id] {
        info.set_game_started(new_value: true)
        if let deck = AwesomeCardGame.playerDeck[player_id] {
          info.set_your_remain_deck(new_value: deck)
        } else {
          info.set_your_remain_deck(new_value: AwesomeCardGame.starterDeck)
        }
        info.set_last_time_turnend(new_value: getCurrentBlock().timestamp)
        /* Set hand */
        var key: UInt8 = 1
        for pos in drawed_pos {
          let card_id = info.your_remain_deck.remove(at: pos)
          info.your_hand[key] = card_id
          key = key + 1
        }
        /** 今回はバトルはしない予定なので、以下部分は省略 
        if (info.is_first == true) {
          info.your_cp = 2
        } else {
          info.your_cp = 3
        }
        **/
        /* Save */
        AwesomeCardGame.battleInfo[player_id] = info

        let opponent = info.opponent
        if let opponentInfo = AwesomeCardGame.battleInfo[opponent] {
          /** 今回はバトルはしない予定なので、以下部分は省略 
          opponentInfo.last_time_turnend = info.last_time_turnend
          opponentInfo.opponent_remain_deck = info.your_remain_deck.length
          opponentInfo.opponent_hand = info.your_hand.keys.length
          opponentInfo.opponent_cp = info.your_cp
          **/
          /* Save */
          AwesomeCardGame.battleInfo[opponent] = opponentInfo
        }
      }
    }
  }
           :
```
`game_start`関数は`Admin`リソースの中に配置します。

:::note
マリガンしたカードを渡し、それをデッキから引いてハンドカードにします。
:::

`AwesomeCardGame`コントラクトを以下コマンドで更新します。
```ruby
flow accounts update-contract ./AwesomeCardGame.cdc
```

#### ゲームを開始する

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

`send_game_start.js`ファイルを作成して、[Non-Custodial方式のトランザクション](https://qiita.com/flowcadence/items/27ef976c2f2493146dd6#non-custodial-account)を書きます。
```ruby:/backend/send_game_start.js
import fs from "fs";
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 drawed_cards;

argv.forEach((val, index) => {
  if (index == 2) {
    player_id = val;
  } else if (index == 3) {
    drawed_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 hash = (message) => {
    const sha = new SHA3(256);
    sha.update(Buffer.from(message, "hex"));
    return sha.digest();
  };
  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");
  };

  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, drawed_cards, KEY_ID, KEY_ID_IT);
  /* player matching. */
  let transactionId = await fcl.mutate({
    cadence: `
      import AwesomeCardGame from 0xf8d6e0586b0a20c7

      transaction(player_id: UInt, drawed_cards: [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.game_start(player_id: player_id, drawed_cards: drawed_cards)
        }
        execute {
          log("success")
        }
      }
    `,
    args: (arg, t) => [
      arg(player_id, t.UInt),
      arg(drawed_cards, t.Array(t.UInt16)),
    ],
    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);
}
```

#### ゲーム開始処理をする

プレイヤーが画面上でマリガンによるカードが確定したとします。(`[16,20,4,3]`)

***
それをバックエンドで受け取って実行すると仮定します。コマンドは以下とします。
```
node ../backend/send_game_start.js 1 "[16,20,4,3]"
```
Output:
![スクリーンショット 2025-01-03 9.42.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/eb7e5d04-6aad-cbfc-8462-6d7c7750c3c2.png)
トランザクションはエラーなくブロックに封印されました。

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

結果:
![スクリーンショット 2025-01-03 14.07.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/5414fb43-ed0e-ce72-e0ea-91c14e492ea4.png)

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

結果:
![スクリーンショット 2025-01-03 14.09.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/0c260c17-e137-6dc4-97a1-1354fca698a0.png)

:::note
上記結果から分かることは以下です。
* ハンドのカードが`[16,20,4,3]`になっています(keyはハンドのポジション)。と思ったけど20のはずが21になってますね。デッキから取り除く時に位置がズレるバグがあるようです。。
* ハンドのカードがデッキから取り除かれています。
* game_startedがtrueになっています。
:::

CadenceスマートコントラクトにはArrayやDictionaryの組み込み関数が多く、言語の文法がモダンで実装が非常にしやすかったです。Swiftに近いと言われますが、Javascriptの知識だけでも十分にサクサクと開発ができます。
***

:::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 << 9 - 対戦相手のマッチング](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 >> 11 - ターンチェンジ](https://qiita.com/flowcadence/items/a1566910f4bafd162f1e)</b>