ブロックチェーンゲームの作り方12 勝敗と賞金送付、ランキング

[Previous << 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
Flowブロックチェーンでは商取引の自分が関わらない取引もプログラムする事が出来ます。自分がお金をもらう、お金を払う以外の取引もプログラムすることができるのがブロックチェーンの面白いところです。

Aさんが直接Bさんにお金を払う取引、またはBさんがAさんから返金してもらう取引もこのページを読むとどうするのかが大体分かります。スマートコントラクト内に保存したAさんBさんの受け取り先(入金先)を使います。支払う時はその為のトランザクションを行いますが、その時に手数料をもらうなどの処理を自由にプログラムすることができます。

より詳しい取引の仕組みをプログラムするにはFlow公式が作ったコアコントラクトの[NFT StoreFront](https://qiita.com/flowcadence/items/93a05e653883a93b617a)をご覧ください。NFT StoreFrontはかなり高度ですので、このページ(勝敗と賞金送付、ランキング)で賞金を配布するにはどうするのか、を理解してから上記ページを読むことをお勧めします。
:::

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

#### 勝敗を判定する

Day11 ターンチェンジの`turn_change`メソッドの最後のコメントを外します。
```ruby
  /* judge the winner */
  self.judgeTheWinner(player_id: player_id)
```
10ターンが終わるときに勝敗を決するので、`turn_change`の中に勝敗を判定するロジックを埋め込みます。

また、ランキングや、期間内のランキング報酬配布も、このロジックの中に埋め込みます。こうすることで、世界中で1万ゲーム行われるごとに`500FLOW`を１位に自動で配布する、と言ったことができます。

:::note
💡賞金の配布はそのきっかけとなるトランザクションが行われる時に行う
:::

勝敗と賞金送付、ランキングを私は以下のように実装しました。
```ruby:AwesomeCardGame.cdc
           :
  access(self) let rankingPeriod: UInt
  access(self) var rankingBattleCount: UInt
  access(self) var ranking1stWinningPlayerId: UInt
  access(self) var ranking2ndWinningPlayerId: UInt

           :

  access(all) struct CyberScoreStruct {
    access(contract) fun set_win_count(new_value: UInt) {
      self.win_count = new_value
    }
    access(contract) fun set_loss_count(new_value: UInt) {
      self.loss_count = new_value
    }
    access(contract) fun set_period_win_count(new_value: UInt) {
      self.period_win_count = new_value
    }
    access(contract) fun set_period_loss_count(new_value: UInt) {
      self.period_loss_count = new_value
    }
    access(contract) fun set_ranking_win_count(new_value: UInt) {
      self.ranking_win_count = new_value
    }
    access(contract) fun set_ranking_2nd_win_count(new_value: UInt) {
      self.ranking_2nd_win_count = new_value
    }
           :
  }
           :
  /*
  ** [Resource] Admin (Game Server Processing)
  */
  access(all) resource Admin {
           :

    access(all) fun judgeTheWinner(player_id: UInt) :Bool {
      pre {
        AwesomeCardGame.battleInfo[player_id] != nil : "This guy doesn't do match."
      }

      if let info = AwesomeCardGame.battleInfo[player_id] {
        if (info.turn > 10) {
          if (info.your_life > info.opponent_life || (info.your_life == info.opponent_life && info.is_first == false)) { /* Second Attack wins if lives are same. */
            let opponent = info.opponent
            AwesomeCardGame.battleInfo.remove(key: player_id)
            AwesomeCardGame.battleInfo.remove(key: opponent)
            if let cyberScore = AwesomeCardGame.playerList[player_id] {
              cyberScore.score.append({getCurrentBlock().timestamp: 1})
              cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
              cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
              AwesomeCardGame.playerList[player_id] = cyberScore
            }
            if let cyberScore = AwesomeCardGame.playerList[opponent] {
              cyberScore.score.append({getCurrentBlock().timestamp: 0})
              cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
              cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
              AwesomeCardGame.playerList[opponent] = cyberScore
            }
            AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
            AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
            /* Game Reward */
            let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
            AwesomeCardGame.PlayerFlowTokenVault[player_id]!.borrow()!.deposit(from: <- reward)
            self.rankingTotalling(playerid: player_id);
            return true
          } else {
            let opponent = info.opponent
            AwesomeCardGame.battleInfo.remove(key: player_id)
            AwesomeCardGame.battleInfo.remove(key: opponent)
            if let cyberScore = AwesomeCardGame.playerList[player_id] {
              cyberScore.score.append({getCurrentBlock().timestamp: 0})
              cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
              cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
              AwesomeCardGame.playerList[player_id] = cyberScore
            }
            if let cyberScore = AwesomeCardGame.playerList[opponent] {
              cyberScore.score.append({getCurrentBlock().timestamp: 1})
              cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
              cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
              AwesomeCardGame.playerList[opponent] = cyberScore
            }
            AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
            AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
            /* Game Reward */
            let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
            AwesomeCardGame.PlayerFlowTokenVault[opponent]!.borrow()!.deposit(from: <- reward)
            self.rankingTotalling(playerid: opponent);
            return true
          }
        } else if (info.turn == 10 && info.is_first_turn == false) { /* 10 turn and second attack */
          if (info.your_life <= info.opponent_life && info.is_first == true) { /* Lose if palyer is First Attack & life is less than opponent */
            let opponent = info.opponent
            AwesomeCardGame.battleInfo.remove(key: player_id)
            AwesomeCardGame.battleInfo.remove(key: opponent)
            if let cyberScore = AwesomeCardGame.playerList[player_id] {
              cyberScore.score.append({getCurrentBlock().timestamp: 0})
              cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
              cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
              AwesomeCardGame.playerList[player_id] = cyberScore
            }
            if let cyberScore = AwesomeCardGame.playerList[opponent] {
              cyberScore.score.append({getCurrentBlock().timestamp: 1})
              cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
              cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
              AwesomeCardGame.playerList[opponent] = cyberScore
            }
            AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
            AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
            /* Game Reward */
            let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
            AwesomeCardGame.PlayerFlowTokenVault[opponent]!.borrow()!.deposit(from: <- reward)
            self.rankingTotalling(playerid: opponent);
            return true
          } else if (info.your_life >= info.opponent_life && info.is_first == false) {// Win if palyer is Second Attack & life is more than opponent
            let opponent = info.opponent
            AwesomeCardGame.battleInfo.remove(key: player_id)
            AwesomeCardGame.battleInfo.remove(key: opponent)
            if let cyberScore = AwesomeCardGame.playerList[player_id] {
              cyberScore.score.append({getCurrentBlock().timestamp: 1})
              cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
              cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
              AwesomeCardGame.playerList[player_id] = cyberScore
            }
            if let cyberScore = AwesomeCardGame.playerList[opponent] {
              cyberScore.score.append({getCurrentBlock().timestamp: 0})
              cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
              cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
              AwesomeCardGame.playerList[opponent] = cyberScore
            }
            AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
            AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
            /* Game Reward */
            let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
            AwesomeCardGame.PlayerFlowTokenVault[player_id]!.borrow()!.deposit(from: <- reward)
            self.rankingTotalling(playerid: player_id);
            return true
          }
        }
        if (info.opponent_life == 0) {
          let opponent = info.opponent
          AwesomeCardGame.battleInfo.remove(key: player_id)
          AwesomeCardGame.battleInfo.remove(key: opponent)
          if let cyberScore = AwesomeCardGame.playerList[player_id] {
            cyberScore.score.append({getCurrentBlock().timestamp: 1})
            cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
            cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
            AwesomeCardGame.playerList[player_id] = cyberScore
          }
          if let cyberScore = AwesomeCardGame.playerList[opponent] {
            cyberScore.score.append({getCurrentBlock().timestamp: 0})
            cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
            cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
            AwesomeCardGame.playerList[opponent] = cyberScore
          }
          AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
          AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
          /* Game Reward */
          let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
          AwesomeCardGame.PlayerFlowTokenVault[player_id]!.borrow()!.deposit(from: <- reward)
          self.rankingTotalling(playerid: player_id);
          return true
        } else if (info.your_life == 0) {
          let opponent = info.opponent
          AwesomeCardGame.battleInfo.remove(key: player_id)
          AwesomeCardGame.battleInfo.remove(key: opponent)
          if let cyberScore = AwesomeCardGame.playerList[player_id] {
            cyberScore.score.append({getCurrentBlock().timestamp: 0})
            cyberScore.set_loss_count(new_value: cyberScore.loss_count + 1)
            cyberScore.set_period_loss_count(new_value: cyberScore.period_loss_count + 1)
            AwesomeCardGame.playerList[player_id] = cyberScore
          }
          if let cyberScore = AwesomeCardGame.playerList[opponent] {
            cyberScore.score.append({getCurrentBlock().timestamp: 1})
            cyberScore.set_win_count(new_value: cyberScore.win_count + 1)
            cyberScore.set_period_win_count(new_value: cyberScore.period_win_count + 1)
            AwesomeCardGame.playerList[opponent] = cyberScore
          }
          AwesomeCardGame.playerMatchingInfo[player_id] = PlayerMatchingStruct()
          AwesomeCardGame.playerMatchingInfo[opponent] = PlayerMatchingStruct()
          /* Game Reward */
          let reward <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 0.5) as! @FlowToken.Vault
          AwesomeCardGame.PlayerFlowTokenVault[opponent]!.borrow()!.deposit(from: <- reward)
          self.rankingTotalling(playerid: opponent);
          return true
        }
      }
      return false
    }

    /* Totalling Ranking values. */
    access(all) fun rankingTotalling(playerid: UInt) {
      AwesomeCardGame.rankingBattleCount = AwesomeCardGame.rankingBattleCount + 1;
      if let cyberScore = AwesomeCardGame.playerList[playerid] {
        /* When this game just started */
        if (AwesomeCardGame.ranking2ndWinningPlayerId == 0 || AwesomeCardGame.ranking1stWinningPlayerId == 0) {
          if (AwesomeCardGame.ranking1stWinningPlayerId == 0) {
            AwesomeCardGame.ranking1stWinningPlayerId = playerid;
          } else if(AwesomeCardGame.ranking2ndWinningPlayerId == 0) {
            AwesomeCardGame.ranking2ndWinningPlayerId = playerid;
          }
        } else {
          for player_id in AwesomeCardGame.playerList.keys {
            if let score = AwesomeCardGame.playerList[player_id] {
              if (score.win_count + score.loss_count > 0) {
                if (player_id != AwesomeCardGame.ranking2ndWinningPlayerId && player_id != AwesomeCardGame.ranking1stWinningPlayerId) {
                  if let rank2ndScore = AwesomeCardGame.playerList[AwesomeCardGame.ranking2ndWinningPlayerId] { /* If it's equal, first come first served. */
                    if (AwesomeCardGame.calcPoint(win_count: rank2ndScore.period_win_count, loss_count: rank2ndScore.period_loss_count) < AwesomeCardGame.calcPoint(win_count: cyberScore.period_win_count, loss_count: cyberScore.period_loss_count)) {
                      if let rank1stScore = AwesomeCardGame.playerList[AwesomeCardGame.ranking1stWinningPlayerId] {
                        if (AwesomeCardGame.calcPoint(win_count: rank1stScore.period_win_count, loss_count: rank1stScore.period_loss_count) < AwesomeCardGame.calcPoint(win_count: cyberScore.period_win_count, loss_count: cyberScore.period_loss_count)) {
                          AwesomeCardGame.ranking2ndWinningPlayerId = AwesomeCardGame.ranking1stWinningPlayerId;
                          AwesomeCardGame.ranking1stWinningPlayerId = player_id;
                        } else {
                          AwesomeCardGame.ranking2ndWinningPlayerId = player_id;
                        }
                      }
                    }
                  }
                } else if (player_id != AwesomeCardGame.ranking1stWinningPlayerId) {
                  if let rank1stScore = AwesomeCardGame.playerList[AwesomeCardGame.ranking1stWinningPlayerId] {
                    if (AwesomeCardGame.calcPoint(win_count: rank1stScore.period_win_count, loss_count: rank1stScore.period_loss_count) < AwesomeCardGame.calcPoint(win_count: cyberScore.period_win_count, loss_count: cyberScore.period_loss_count)) { /* If it's equal, first come first served. */
                      AwesomeCardGame.ranking2ndWinningPlayerId = AwesomeCardGame.ranking1stWinningPlayerId;
                      AwesomeCardGame.ranking1stWinningPlayerId = player_id;
                    }
                  }
                }
              }
            }
          }
        }
      }
      if (AwesomeCardGame.rankingBattleCount >= AwesomeCardGame.rankingPeriod) {
        /* Initialize the ranking win count. */
        for playerId in AwesomeCardGame.playerList.keys {
          if let score = AwesomeCardGame.playerList[playerId] {
            score.set_period_win_count(new_value: 0)
            score.set_period_loss_count(new_value: 0);
            AwesomeCardGame.playerList[playerId] = score; /* Save */
          }
        }
        /* Initialize the count. */
        AwesomeCardGame.rankingBattleCount = 0;
        /* Pay ranking reward(20 $FLOW) */
        if let rank1stScore = AwesomeCardGame.playerList[AwesomeCardGame.ranking1stWinningPlayerId] {
          rank1stScore.set_ranking_win_count(new_value: rank1stScore.ranking_win_count + 1)
          AwesomeCardGame.playerList[AwesomeCardGame.ranking1stWinningPlayerId] = rank1stScore; /* Save */
          let reward1st <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 20.0) as! @FlowToken.Vault
          AwesomeCardGame.PlayerFlowTokenVault[AwesomeCardGame.ranking1stWinningPlayerId]!.borrow()!.deposit(from: <- reward1st)
        }
        /* Pay ranking reward(10 $FLOW) */
        if let rank2ndScore = AwesomeCardGame.playerList[AwesomeCardGame.ranking2ndWinningPlayerId] {
          rank2ndScore.set_ranking_2nd_win_count(new_value: rank2ndScore.ranking_2nd_win_count + 1)
          AwesomeCardGame.playerList[AwesomeCardGame.ranking2ndWinningPlayerId] = rank2ndScore; // Save
          let reward1st <- AwesomeCardGame.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: 10.0) as! @FlowToken.Vault
          AwesomeCardGame.PlayerFlowTokenVault[AwesomeCardGame.ranking2ndWinningPlayerId]!.borrow()!.deposit(from: <- reward1st)
        }
      }
    }
  }
           :

  access(all) fun calcPoint(win_count: UInt, loss_count: UInt): UInt {
    if ((win_count + loss_count) > 25) {
      return UInt(UFix64(win_count) / UFix64(win_count + loss_count) * 50.0) + win_count;
    } else if ((win_count + loss_count) > 5) {
      return UInt(UFix64(win_count) / UFix64(win_count + loss_count) * 20.0) + win_count;
    } else {
      return UInt(UFix64(win_count) / UFix64(win_count + loss_count) * 10.0) + win_count;
    }
  }
           :

    self.rankingPeriod = 1000
    self.rankingBattleCount = 0
    self.ranking1stWinningPlayerId = 0
    self.ranking2ndWinningPlayerId = 0
  }
}
```

`rankingPeriod`や`rankingBattleCount`は新しいプロパティの為、この場合、コントラクトのアップデートができません。エミュレータを再起動して、デプロイからやり直します。

#### ターンが制限ターンを超える

10ターンが終了した時点でライフの多い方か、同ライフの場合は後攻の勝ちになります。(カードゲームは先に攻撃できる方が有利な為)

10ターンまでターン交代をするのは大変なので、3ターンでやってみます。`judgeTheWinner`の判定箇所を10から3に変更してコントラクトを更新します。

ゲームが終了するまでに必要なコマンドは以下:
```ruby
/* 対戦相手のマッチング */
node ../backend/send_to_decide_who_to_play_against.js 1
node ../backend/send_to_decide_who_to_play_against.js 2

/* ゲーム開始 */
node ../backend/send_game_start.js 1 "[23,10,9,9]"
node ../backend/send_game_start.js 2 "[14,2,1,11]"

/* ターン交代 */
node ../backend/send_change_turn.js 1 "{}"
node ../backend/send_change_turn.js 2 "{}"
node ../backend/send_change_turn.js 1 "{}"
node ../backend/send_change_turn.js 2 "{}"
node ../backend/send_change_turn.js 1 "{}"

ここで決着がつくはずです。同点であれば後攻の勝ちです。
```

3ターン先攻まで進めました。
![スクリーンショット 2025-01-04 13.50.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/e67cebe4-eebd-fdac-a184-821cb3cb737a.png)

写真のプレイヤーは後攻でFLOW残高が`998.0`あります。次にターン交代があると勝敗が決します。

結果:
![スクリーンショット 2025-01-04 13.54.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/9025da93-b6ad-647c-d453-83eefca03c1c.png)

ゲームが終了し、FLOWが`998.5`に増えています。（賞金は`0.5FLOW`です。）


ちなみにAdminは以下のように`0.5FLOW`減っています。

![スクリーンショット 2025-01-04 13.57.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/dbb27f10-2d0e-4411-d476-34a796ca4e06.png)


#### ランキングで入賞する

ランキングは期間内のランキングと、全期間を通じてのランキングが存在します。期間内のランキングはその期間で1位か2位になると、賞金が`20FLOW`と`10FLOW`が得られます。その期間は`rankingPeriod`で決めてあり、1000ゲームがセットされています。

全世界で1000ゲームごとにランキング上位者に対して賞金が即座に振り込まれる仕様です。

テストのために5試合毎のランキングにして確認します。

結果:
5試合目３ターン目のPlayer2:
![スクリーンショット 2025-01-04 14.31.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/8f7cef0c-d776-c6ae-049e-75aba45e2ed2.png)

5試合目終了後
![スクリーンショット 2025-01-04 14.33.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/9f9e9ed1-d864-32b3-0831-f1ed3833c0f3.png)

ちょっとバグがあったみたいで、１位の賞金と２位の賞金どちらも受け取ってしまっていますが、`30FLOW`を受け取っています。

もちろん、5試合毎にこんなに賞金を払っていては、Adminは赤字になってしまいますが...
![スクリーンショット 2025-01-04 14.40.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/48b29067-404f-37fe-2461-5ee5c02cbebd.png)

`30FLOW`の赤字です。

以上です。

:::note
Conclusion:
Cadenceを使えば、簡単に賞金制ゲーム(ESports)を作れることが分かってもらえたと思います。

その他にもピアツーピア決済アプリを誰でも、それこそ子供でも一人で作ってお金を稼ぐことができる事を知ってもらいたかったです。
:::
***

:::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 << 11 - ターンチェンジ](https://qiita.com/flowcadence/items/a1566910f4bafd162f1e)
</b>

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