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位に自動で配布する、と言ったことができます。
💡賞金の配布はそのきっかけとなるトランザクションが行われる時に行う
勝敗と賞金送付、ランキングを私は以下のように実装しました。
:
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に変更してコントラクトを更新します。
ゲームが終了するまでに必要なコマンドは以下:
/* 対戦相手のマッチング */
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 "{}"
ここで決着がつくはずです。同点であれば後攻の勝ちです。
写真のプレイヤーは後攻でFLOW残高が998.0
あります。次にターン交代があると勝敗が決します。
ゲームが終了し、FLOWが998.5
に増えています。(賞金は0.5FLOW
です。)
ちなみにAdminは以下のように0.5FLOW
減っています。
ランキングで入賞する
ランキングは期間内のランキングと、全期間を通じてのランキングが存在します。期間内のランキングはその期間で1位か2位になると、賞金が20FLOW
と10FLOW
が得られます。その期間はrankingPeriod
で決めてあり、1000ゲームがセットされています。
全世界で1000ゲームごとにランキング上位者に対して賞金が即座に振り込まれる仕様です。
テストのために5試合毎のランキングにして確認します。
ちょっとバグがあったみたいで、1位の賞金と2位の賞金どちらも受け取ってしまっていますが、30FLOW
を受け取っています。
もちろん、5試合毎にこんなに賞金を払っていては、Adminは赤字になってしまいますが...
30FLOW
の赤字です。
以上です。
Conclusion:
Cadenceを使えば、簡単に賞金制ゲーム(ESports)を作れることが分かってもらえたと思います。
その他にもピアツーピア決済アプリを誰でも、それこそ子供でも一人で作ってお金を稼ぐことができる事を知ってもらいたかったです。
もっと高度なGraphQLによる対戦相手と情報を共有しながらトランザクションを実施したいケースの場合は、こちらが参考になります。(本は薄めですが、無駄を省いてピンポイントでコードを参照出来るようにする為です、レビュー滅茶甘めで付けていただけると喜びます😭)
この記事のソースコードはこちらにあります。