ブロックチェーンゲームの作り方9 対戦相手とのマッチング

[Previous << 8 - デッキ編集](https://qiita.com/flowcadence/items/a701a42d833c63ef3b9a)
[Next >> 10 - マリガンおよびゲーム開始](https://qiita.com/flowcadence/items/824808bf581f41c08676)

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

:::note
ここでは以下のグローバル関数を使用します。
* `revertibleRandom(modulo: modulo)`: [乱数を生成](https://qiita.com/flowcadence/items/e1528810ca4a72c8f521#revertiblerandom)します。
* `getCurrentBlock().timestamp`: [現在時刻を取得](https://qiita.com/flowcadence/items/1aefd14bce23a00518ec#using-the-timestamp)します 
:::

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

#### 対戦相手とのマッチングをする

まず、最初に言っておきたいのは、私はWebエンジニアであって、マッチング処理を実装したことは一度もありません。ここで紹介しているコードは５日しかない第一回ハッカソンで、限りある時間の中でなんとかマッチングができる機能として実装したものです。ゲームエンジニアから見ると笑うレベルのものであるということです。

要件は以下
* マッチング時間は1分とする（1分を超えると再度マッチング処理を開始できる）
* 対戦相手が決まるとすぐにマリガン処理（カード引き直し）を開始するのでマッチングが成立したらマリガンカードを決める（４回引き直し可能）
* 自分自身とはマッチングしない
* 毎秒ブラウザで情報を更新するのでこれら情報を取得できるメソッド
* マッチングが成立した時点でゲーム内通貨`30cyber_energy`を徴収する

ゲーム開始ボタンを押すと、マッチング処理が開始します。その後ブラウザから毎秒最新情報を取得しますので、その情報を返す関数が必要になります。
```ruby:AwesomeCardGame.cdc
           :

  access(all) fun get_current_status(): AnyStruct {
    if let info = AwesomeCardGame.battleInfo[self.player_id] {
      return info
    }
    if let obj = AwesomeCardGame.playerMatchingInfo[self.player_id] {
      return obj.lastTimeMatching
    }
    return nil
  }

           :
```
`Player`リソースの下に上記のような関数を追記します。`battleInfo`と`playerMatchingInfo`が初出なのでこれを定義します。

```ruby:AwesomeCardGame.cdc
import "FlowToken"
import "FungibleToken"

access(all) contract AwesomeCardGame {

  access(self) let playerList: {UInt: CyberScoreStruct}
  access(self) var totalPlayers: UInt
  access(self) let playerDeck: {UInt: [UInt16]}
  access(self) let battleInfo: {UInt: BattleStruct} <- 追加
  access(self) let playerMatchingInfo: {UInt: PlayerMatchingStruct} <- 追加
  access(all) let starterDeck: [UInt16]
  access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
  access(self) let PlayerFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}

  /* [Struct] PlayerMatchingStruct */
  access(all) struct PlayerMatchingStruct { <-- 追加
    access(all) var lastTimeMatching: UFix64?
    access(all) var marigan_cards: [[UInt8]]

    init() {
      self.lastTimeMatching = nil
      self.marigan_cards = []
    }

    /* Setter関数 */
    access(contract) fun set_lastTimeMatching(new_value: UFix64) {
      self.lastTimeMatching = new_value
    }
    access(contract) fun set_marigan_cards(new_value: [[UInt8]]) {
      self.marigan_cards = new_value
    }
  }

  /* [Struct] BattleStruct (Setter関数は省略) */
  access(all) struct BattleStruct { <-- 追加
    access(all) var turn: UInt8 /* 現在のターン */
    access(all) var is_first_turn: Bool /* 先行or後攻 */
    access(all) let is_first: Bool /* 自分は先攻か後攻か */
    access(all) let opponent: UInt
    access(all) let matched_time: UFix64
    access(all) var game_started: Bool
    access(all) var last_time_turnend: UFix64?
    access(all) var opponent_life: UInt8
    access(all) var opponent_cp: UInt8
    access(all) var opponent_field_unit: {UInt8: UInt16}
    access(all) var opponent_field_unit_action: {UInt8: UInt8}
    access(all) var opponent_field_unit_bp_amount_of_change: {UInt8: Int}
    access(all) var opponent_trigger_cards: Int
    access(all) var opponent_remain_deck: Int
    access(all) var opponent_hand: Int
    access(all) var opponent_dead_count: Int
    access(all) var your_life: UInt8
    access(all) var your_cp: UInt8
    access(all) var your_field_unit: {UInt8: UInt16}
    access(all) var your_field_unit_action: {UInt8: UInt8}
    access(all) var your_field_unit_bp_amount_of_change: {UInt8: Int}
    access(all) var your_trigger_cards: {UInt8: UInt16}
    access(all) var your_remain_deck: [UInt16]
    access(all) var your_hand: {UInt8: UInt16}
    access(all) var your_dead_count: Int

    init(is_first: Bool, opponent: UInt, matched_time: UFix64) {
      self.turn = 1
      self.is_first_turn = true
      self.is_first = is_first
      self.opponent = opponent
      self.matched_time = matched_time
      self.game_started = false
      self.last_time_turnend = nil
      self.opponent_life = 7
      self.opponent_cp = 2
      self.opponent_field_unit = {}
      self.opponent_field_unit_action = {}
      self.opponent_field_unit_bp_amount_of_change = {}
      self.opponent_trigger_cards = 0
      self.opponent_remain_deck = 30
      self.opponent_hand = 0
      self.opponent_dead_count = 0
      self.your_life = 7
      self.your_cp = 2
      self.your_field_unit = {}
      self.your_field_unit_action = {}
      self.your_field_unit_bp_amount_of_change = {}
      self.your_trigger_cards = {}
      self.your_remain_deck = []
      self.your_hand = {}
      self.your_dead_count = 0
    }
  }

           :
           :

  init() {
    self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
    self.PlayerFlowTokenVault = {}
    self.starterDeck = [1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
    self.totalPlayers = 0
    self.playerList = {}
    self.playerDeck = {}
    self.battleInfo = {} <- 追加
    self.playerMatchingInfo = {} <- 追加
  }
}
```
#### 現在のゲーム進捗状況をjsで取得する
現在のゲーム進捗状況を取得するスクリプトと、それをjsで取得するコードは以下のようになります。
```ruby:src/scripts.js
import { query } from "@onflow/fcl";

export const getCurrentStatus = 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_current_status()
    }
    `,
    args: (arg, t) => [arg(address, t.Address)],
  });
  return result;
};
```
:::note warn
アカウントのストレージが持つリソースは古いため、`get_current_status`を見つけられないので一度エミュレータを止めて再起動します。そしてコントラクトをデプロイして新規登録から再度行います。
:::

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

メイン画面のロジックを以下のようにします。
```ruby:+page.svelte
<script>
import { config, authenticate, unauthenticate, currentUser } from '@onflow/fcl';
import { getBalance, isRegistered, getPlayerDeck, getCurrentStatus } 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; <- 追加
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);
      setInterval(async () => {
        currentStatus = await getCurrentStatus(user.addr); <- 追加
      }, 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>
  {/if}
{/if}
```
これで画面を表示します。

結果:
![スクリーンショット 2025-01-03 8.05.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/99f541b4-2531-6d41-b148-1097a2020b39.png)

ゲーム内通貨を買います。

結果:
![スクリーンショット 2025-01-03 8.06.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/211636d6-9c6a-661d-256c-f505bea38b86.png)
(プレイヤーがAdminなのでFLOWは減っていません。)

ゲームが開始していないし、`Play`ボタンも押していない為マッチングもしていないので、ゲーム進捗状況はなし、と表示されています。

#### マッチング処理の実装をする

要件に合わせてマッチング処理を実装します。

要件:
* マッチング時間は1分
* 対戦相手が決まるとすぐにマリガン処理（カード引き直し）を開始するのでマッチングが成立したらマリガンカードを決める（４回引き直し可能）
* 自分自身とはマッチングしない
* マッチングが成立した時点でゲーム内通貨`30cyber_energy`を徴収する

マッチングが成立した時点でマリガンカードを決めるので擬似乱数を使います。
```ruby:AwesomeCardGame.cdc
           :
  access(self) var matchingLimits: [UFix64]
  access(self) var matchingPlayers: [UInt]
           :
  /*
  ** [Resource] Admin (Game Server Processing)
  */
  access(all) resource Admin {
    /*
    ** Save the Player's Card Deck
    */
    access(all) fun save_deck(player_id: UInt, player_deck: [UInt16]) {
      if player_deck.length == 30 {
        AwesomeCardGame.playerDeck[player_id] = player_deck
      }
    }

    /*
    ** Player Matching Transaction
    */
    access(all) fun matching_start(player_id: UInt) {
      pre {
        /* preの中の条件に合わない場合はエラーメッセージが返ります。 ここでは"Still matching."。 */
        AwesomeCardGame.playerMatchingInfo[player_id] == nil ||
        AwesomeCardGame.playerMatchingInfo[player_id]!.lastTimeMatching == nil ||
        AwesomeCardGame.playerMatchingInfo[player_id]!.lastTimeMatching! + 60.0 <= getCurrentBlock().timestamp : "Still matching."
      }
      var counter = 0
      var outdated = -1
      let current_time = getCurrentBlock().timestamp
      if let obj = AwesomeCardGame.playerMatchingInfo[player_id] {
        obj.set_lastTimeMatching(new_value: current_time)
        AwesomeCardGame.playerMatchingInfo[player_id] = obj /* save */
      } else {
        let newObj = PlayerMatchingStruct()
        newObj.set_lastTimeMatching(new_value: current_time)
        AwesomeCardGame.playerMatchingInfo[player_id] = newObj
      }

      /* Search where matching times are already past 60 seconds */
      for time in AwesomeCardGame.matchingLimits {
        if outdated == -1 && current_time > time + 60.0 {
          outdated = counter
        }
        counter = counter + 1
      }

      /* If there are some expired matching times */
      if outdated > -1 {
        /* Save only valid matchin times */
        if (outdated == 0) {
          AwesomeCardGame.matchingLimits = []
          AwesomeCardGame.matchingPlayers = []
        } else {
          AwesomeCardGame.matchingLimits = AwesomeCardGame.matchingLimits.slice(from: 0, upTo: outdated)
          AwesomeCardGame.matchingPlayers = AwesomeCardGame.matchingPlayers.slice(from: 0, upTo: outdated)
        }
      }

      /* 既にマッチングリストに入っている場合。このまま進むと自分とマッチングしかねない */
      if (AwesomeCardGame.matchingPlayers.firstIndex(of: player_id) != nil) {
        return
      }

      if AwesomeCardGame.matchingLimits.length >= 1 {
        /* Pick the opponent from still matching players. */
        let time = AwesomeCardGame.matchingLimits.removeLast()
        let opponent = AwesomeCardGame.matchingPlayers.removeLast()

        var is_first = false
        /* Decides which is first */
        if (AwesomeCardGame.matchingLimits.length % 2 == 1) {
          is_first = true
        }

        /* マッチング成立したのでnilで初期化 */
        AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
        AwesomeCardGame.battleInfo[player_id] = BattleStruct(is_first: is_first, opponent: opponent, matched_time: current_time)
        AwesomeCardGame.battleInfo[opponent] = BattleStruct(is_first: !is_first, opponent: player_id, matched_time: current_time)

        /* charge the play fee (料金徴収) */
        if let cyberScore = AwesomeCardGame.playerList[player_id] {
          cyberScore.set_cyber_energy(new_value: cyberScore.cyber_energy - 30)
          AwesomeCardGame.playerList[player_id] = cyberScore
        }

        /* charge the play fee (料金徴収) */
        if let cyberScore = AwesomeCardGame.playerList[opponent] {
          cyberScore.set_cyber_energy(new_value: cyberScore.cyber_energy - 30)
          AwesomeCardGame.playerList[opponent] = cyberScore
        }
      } else {
        /* Put player_id in the matching list. */
        AwesomeCardGame.matchingLimits.append(current_time)
        AwesomeCardGame.matchingPlayers.append(player_id)
      }

      /* Creates Pseudorandom Numbe for the marigan cards(擬似乱数生成関数、revertibleRandomを使います。) */
      let modulo: UInt8 = 30
      var marigan_cards1: [UInt8] = []
      var marigan_cards2: [UInt8] = []
      var marigan_cards3: [UInt8] = []
      var marigan_cards4: [UInt8] = []
      var marigan_cards5: [UInt8] = []
      for i in [0, 1, 2, 3, 4] {
        var used1: UInt8 = 99
        var used2: UInt8 = 99
        var used3: UInt8 = 99
        var used4: UInt8 = 99
        let tmp: [UInt8] = []
        while (used4 == 99) {
          let withdrawPosition = revertibleRandom(modulo: modulo)
          if (used1 == 99) {
            used1 = withdrawPosition
            tmp.append(withdrawPosition)
          } else if (used1 != withdrawPosition && used2 == 99) {
            used2 = withdrawPosition
            tmp.append(withdrawPosition)
          } else if (used1 != withdrawPosition && used2 != withdrawPosition && used3 == 99) {
            used3 = withdrawPosition
            tmp.append(withdrawPosition)
          } else if (used1 != withdrawPosition && used2 != withdrawPosition && used3 != withdrawPosition) {
            used4 = withdrawPosition
            tmp.append(withdrawPosition)
          }
        }
        if (i == 0) {
          marigan_cards1 = tmp.slice(from: 0, upTo: tmp.length)
        } else if (i == 1) {
          marigan_cards2 = tmp.slice(from: 0, upTo: tmp.length)
        } else if (i == 2) {
          marigan_cards3 = tmp.slice(from: 0, upTo: tmp.length)
        } else if (i == 3) {
          marigan_cards4 = tmp.slice(from: 0, upTo: tmp.length)
        } else if (i == 4) {
          marigan_cards5 = tmp.slice(from: 0, upTo: tmp.length)
        }
      }
      if let playerMatchingInfo = AwesomeCardGame.playerMatchingInfo[player_id] {
        playerMatchingInfo.set_marigan_cards(new_value: [marigan_cards1, marigan_cards2, marigan_cards3, marigan_cards4, marigan_cards5])
        AwesomeCardGame.playerMatchingInfo[player_id] = playerMatchingInfo /* save */
      }
    }
  }

  init() {

           :
    self.matchingLimits = []
    self.matchingPlayers = []
  }
}
```
`matching_start`関数は`Admin`リソースの中に配置します。

エミュレータを再起動して、`AwesomeCardGame`コントラクトをデプロイします。

#### 対戦相手とマッチングする

対戦相手のマッチングのトランザクションはバックエンドで実施します。

:::note warn
**新規登録**や**ゲーム内通貨購入**はアカウントのストレージにアクセスする必要がありました。その為、ユーザー自身にウォレット上でApproveを押してもらう必要がどうしてもありました。

しかし、対戦相手のマッチングはスマートコントラクト内のデータを修正するだけなので、バックエンドでNode.jsを使ってトランザクションを行うことが出来ます。これによってユーザーの負担を和らげることが出来ます。

これを実現するためには`Admin`リソースを作成する必要があります。`Admin`リソースはスマートコントラクトのデプロイ時に`init`関数内で作成したもので、(`Admin`を`create`するコードが他の場所にない場合)スマートコントラクトのデプロイ者にだけ許される処理を実装することが出来ます。

実際ゲームコントラクトの実装のほとんどは、この`Admin`リソースの関数が占めることなります。
:::



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

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

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

      transaction(player_id: UInt) {
        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.matching_start(player_id: player_id)
        }
        execute {
          log("success")
        }
      }
    `,
    args: (arg, t) => [
      arg(player_id, t.UInt),
    ],
    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);
}
```
:::note warn
[Day4](https://qiita.com/flowcadence/items/27ef976c2f2493146dd6#%E6%89%BF%E8%AA%8D%E8%80%85%E6%94%AF%E6%89%95%E8%80%85-%E3%81%9D%E3%81%97%E3%81%A6-%E6%8F%90%E6%A1%88%E8%80%85)で述べた通り、提案者だけはDOS攻撃対策としてKeyIDを一定期間内に同じものを使うことが出来ません。そのため、`Proposer`だけはauthorizationFunctionを`Payer`や`Authorizations`と分けて実装する必要が出てきて、本番環境では数百のKey(公開鍵)を秘密鍵から作成しておき、それにKeyIDを紐づけておきます。Adminリソースのように何度も呼ばれるファイルにはこのKeyIDをローテーションさせます。ここではエミュレータですのでパソコンを連打しない限り大丈夫なのでKeyIDを0固定にしました。結果、`authorizationFunctionProposer`と`authorizationFunction`は全く同じ関数になっています。
:::

#### マッチング開始処理をする

プレイヤーが画面上で`Play`ボタンを押したとします。
***
それをバックエンドで受け取って実行すると仮定します。コマンドは以下とします。
```
node ../backend/send_to_decide_who_to_play_against.js 1
```
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 9.43.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/71f329c1-ef7e-cc8e-aa5d-cd524f2edece.png)

#### 現在のゲーム進捗状況からわかること

上記結果から分かることは以下です。
* ゲーム進捗状況に、最後にマッチング開始した時間が表示されました。
* マリガンカードを擬似乱数生成関数を使って生成するコードも実行されました。

:::note
なぜ擬似乱数生成関数をこのタイミングで実行しているかというと、マッチングは２パターンあるからです。マッチングしようと思ったけど他に人がいなかった場合と、マッチングしようとしてすぐに人が見つかった場合です。前者も誰かがマッチング処理を開始した時点でマッチングが成立するので、擬似乱数生成関数はどちらの場合でもこのタイミングで実施してその結果を使ってマリガンカードリストを保存しておく必要がありました。
:::

少しわかりにくいので、最後にマッチング開始した時間を人間が見える形にします。

![スクリーンショット 2025-01-03 10.01.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/bd07491a-1a6b-320e-91b2-4b38f6fbc77c.png)

**現在時刻**を取得する`getCurrentBlock().timestamp`は、秒以下がスラッシュされていますので、Date関数の引数に渡すときは `* 1000`して渡します。

***
このままではずっと対戦相手が決まらないのでもう１プレイヤーにもマッチング処理をしてもらいます。既に1分以上経っているので、時間を空けて２人分のコマンドを実行します。

```ruby
node ../backend/send_to_decide_who_to_play_against.js 1
# (Proposerは同じKeyIDで連続トランザクションを行えないので少し時間を空ける)
node ../backend/send_to_decide_who_to_play_against.js 2
```

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

結果:
![スクリーンショット 2025-01-03 10.22.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/38195182-74b3-9fe5-4d4b-cc4b985da128.png)
`[object Object]`ではわかりにくいので少し加工しています。

対戦相手のも見てみます。
![スクリーンショット 2025-01-03 10.23.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/9dd1e89a-256a-63c1-e916-08aecda4b8f6.png)

:::note
上記結果から分かることは以下です。(2人とも同じニックネームなのは忘れて下さい)
* 対戦相手が確定したのでゲーム内通貨(30 cyber_energy)が徴収されました。
* player_id:1が先攻、player_id:2が後攻。
:::

すごいですよね、スマートコントラクトだけでマッチング処理が *モダン言語によるロジック* を記述するだけでできるのです。**FlowとCadenceはもっと知られるべきだと思います。** (CadenceはSwift言語に似ています。`if let`文などは、`if let swift`のように"swift"を付けてGoogle検索すると情報が出てきます。)
***

:::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 << 8 - デッキ編集](https://qiita.com/flowcadence/items/a701a42d833c63ef3b9a)
</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 >> 10 - マリガンおよびゲーム開始](https://qiita.com/flowcadence/items/824808bf581f41c08676)</b>