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?

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

Last updated at Posted at 2025-01-04

Previous << 11 - ターンチェンジ

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

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

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

Why Flow

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

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

💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。

勝敗を判定する

Day11 ターンチェンジのturn_changeメソッドの最後のコメントを外します。

  /* judge the winner */
  self.judgeTheWinner(player_id: player_id)

10ターンが終わるときに勝敗を決するので、turn_changeの中に勝敗を判定するロジックを埋め込みます。

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

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

勝敗と賞金送付、ランキングを私は以下のように実装しました。

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
  }
}

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

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

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

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

ゲームが終了するまでに必要なコマンドは以下:

/* 対戦相手のマッチング */
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

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

結果:
スクリーンショット 2025-01-04 13.54.07.png

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

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

スクリーンショット 2025-01-04 13.57.43.png

ランキングで入賞する

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

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

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

結果:
5試合目3ターン目のPlayer2:
スクリーンショット 2025-01-04 14.31.53.png

5試合目終了後
スクリーンショット 2025-01-04 14.33.01.png

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

もちろん、5試合毎にこんなに賞金を払っていては、Adminは赤字になってしまいますが...
スクリーンショット 2025-01-04 14.40.45.png

30FLOWの赤字です。

以上です。

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

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


ハンヅオンで学べる書籍もあります。 こちら↗︎ (Version1.0ではないですが、先端技術を使用します!)
特色: ブロックチェーンのトランザクションは送信から情報取得までに7-10秒かかります。それを時間を感じさせなくする為には、バックエンドにGraphQLサーバーを採用することが考えられます。GraphQLは大人数による同時接続通信が可能なので、ゲームで何をしたのかを対戦相手に前もって通知することができるからです。(YouTube動画の右下部分、またはここ。)
GraphQL はAWS Lambdaと相性が良く(特に金銭面で)、ブロックチェーンにもスムーズにトランザクションを送れるという利点があります。GraphQLはNode.jsで動作し、そのためAWS Lambdaで動きます。そしてAWS Lambdaがreturnする値を現在ブラウザに接続しているすべての端末に対して、Push送信することができます。Svelteに関しては他にも良い書籍がありますが、AWS LambdaをGraphQLサーバーとして立ち上げる方法をきちんと図説している情報媒体は少ないはずです(僕は英語でAWSの開発陣から知りました)。インフラを自分で立ち上げたことがある人なら、この本を読みながら簡単に低コストのバックエンドをセットアップすることができます。
(個人出版ですので帯とかはありませんが、GraphQLの概念や実装はシンプルですので、ブロックチェーンゲームを誰でも作れるようになります。レビューは極甘で...お願いします🙇🙇‍♂️)

(English ver.▶️入門書もあります。入門書以外は実務書ですのでご注意ください。)

この動画のロジックは全てスマートコントラクトに書かれてます。画面上に出ている情報は全てブロックチェーンから取得したものです。(注:画像ファイルはSvelteフレームワークで管理しています)

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


Previous << 11 - ターンチェンジ

Flow blockchain / Cadence version1.0ドキュメント

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?