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?

How to develop a blockchain game. Day8 デッキ編集

Last updated at Posted at 2025-01-02

Previous << Day7 - ゲーム内通貨購入
Next >> Day9 - マッチング処理

Why Flow

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

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

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

現在のデッキを表示する

カードゲームで使用する自分のカードリストのことをデッキといいます。

AwesomeCardGame.cdcPlayerリソースの定義に以下のようにget_player_deckメソッドを追加します。

AwesomeCardGame.cdc
           :

  access(all) resource Player {
    access(all) let player_id: UInt
    access(all) let nickname: String

    init(nickname: String) {
      AwesomeCardGame.totalPlayers = AwesomeCardGame.totalPlayers + 1
      self.player_id = AwesomeCardGame.totalPlayers
      self.nickname = nickname

      AwesomeCardGame.playerList[self.player_id] = CyberScoreStruct(player_name: nickname)
    }

    access(all) fun get_player_score(): CyberScoreStruct {
      return AwesomeCardGame.playerList[self.player_id]!
    }

    access(all) fun buy_en(payment: @FlowToken.Vault) {
      pre {
        payment.balance == 2.0: "payment is not 2FLOW coin."
        AwesomeCardGame.playerList[self.player_id] != nil: "CyberScoreStruct not found."
      }
      AwesomeCardGame.FlowTokenVault.borrow()!.deposit(from: <- payment)
      if let cyberScore = AwesomeCardGame.playerList[self.player_id] {
        cyberScore.set_cyber_energy(new_value: cyberScore.cyber_energy + 100)
        AwesomeCardGame.playerList[self.player_id] = cyberScore
      }
    }

    access(all) fun get_player_deck(): [UInt16] {
      if let deck = AwesomeCardGame.playerDeck[self.player_id] {
        return deck
      } else {
        return AwesomeCardGame.starterDeck;
      }
    }

           :

if let 文が if let cyberScore = のところで使われています。CadenceはSwift言語に非常に似ているので if let swift でGoogle検索すると情報が出てきます。何か目新しい文法が出てきたらSwiftで検索してみるといいかもしれません。

if let 文は、もし値があれば、左辺に値が入り、括弧内の処理が行われます。その時、左辺の変数を利用することができます。スマートコントラクトでは価値を持つ資産を扱う関係上、値があるかどうか、は非常に大事になってきます。それを確実に条件分岐で処理する方法として if let 文はスマートコントラクトの中でたくさん出てきます。

if let 文以外で値を抽出しようとすると、それが安全でない可能性が高いため、エラーが出る可能性が高いです。万が一にも資産を失うことがないように、if let文で安全にロジックを書くことが推奨されています。


starterDeckDay6の時点で既にAwesomeCardGame.cdcに実装していましたが、playerDeckはまだ実装していませんでしたので追記します。

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(all) let starterDeck: [UInt16]
  access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
  access(self) let PlayerFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}

           :
           :

  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 = {} <- 追加
  }
}

Day7でリソース定義を変更したときと同じように、一旦アカウントストレージに保存したリソースを削除するためエミュレータを再起動します。

プレイヤーのデッキをjsで取得する

プレイヤーのデッキを取得するスクリプトと、それをjsで取得するコードは以下のようになります。

src/scripts.js
import { query } from "@onflow/fcl";

export const getPlayerDeck = async function (address) {
  const result = await query({
    cadence: `
    import "AwesomeCardGame"
    access(all) fun main(address: Address): [UInt16] {
      let cap = getAccount(address).capabilities
        .borrow<&AwesomeCardGame.Player>(/public/AwesomeCardGamePlayer)
        ?? panic("Doesn't have capability!")
      return cap.get_player_deck()
    }
    `,
    args: (arg, t) => [arg(address, t.Address)],
  });
  return result;
};

HTMLとロジックを書く

メイン画面のロジックを以下のようにします。

+page.svelte
<script>
import { config, authenticate, unauthenticate, currentUser } from '@onflow/fcl';
import { getBalance, isRegistered, getPlayerDeck } 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; <- 追加
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); <- 追加
    }
  }
});

</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>
  {/if}
{/if}

これで画面を表示します。

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

カード画像も何もなくてすみませんが、デッキのカードID一覧を取得できました。

まだデッキ編集をしていないのでスマートコントラクトAwesomeCardGameinit関数で定義したstarterDeckのデータが表示されています。

デッキを編集する

デッキ編集のトランザクションはバックエンドで実施します。

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

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

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

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

Adminリソースの作成をAwesomeCardGame.cdcに追加していきます。

AwesomeCardGame.cdc
           :

  /*
  ** [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
      }
    }
  }

  init() {
    self.account.storage.save( <- create Admin(), to: /storage/AwesomeCardGameAdmin) // grant admin resource
    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 = {}
  }
}

これでAdminリソースとその関数のsave_deckを準備できました。

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

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

send_save_deck.jsファイルを作成して、Non-Custodial方式のトランザクションを書きます。

/backend/send_save_deck.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 player_deck;

argv.forEach((val, index) => {
  if (index == 2) {
    player_id = val;
  } else if (index == 3) {
    player_deck = 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, player_deck, KEY_ID, KEY_ID_IT);
  /* Save the player's card deck. */
  let transactionId = await fcl.mutate({
    cadence: `
      import AwesomeCardGame from 0xf8d6e0586b0a20c7

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

Day4で述べた通り、提案者だけはDOS攻撃対策としてKeyIDを一定期間内に同じものを使うことが出来ません。そのため、ProposerだけはauthorizationFunctionをPayerAuthorizationsと分けて実装する必要が出てきて、本番環境では数百のKey(公開鍵)を秘密鍵から作成しておき、それにKeyIDを紐づけておきます。Adminリソースのように何度も呼ばれるファイルにはこのKeyIDをローテーションさせます。ここではエミュレータですのでパソコンを連打しない限り大丈夫なのでKeyIDを0固定にしました。結果、authorizationFunctionProposerauthorizationFunctionは全く同じ関数になっています。

デッキ編集処理をする

プレイヤーが以下のデッキに変更したいとしてデッキ編集ボタンを押したとします。

[
   2,  2,  3,  3,  4,  4,  6,  6,  7,
   7,  8,  8,  9,  9, 10, 10, 11, 11,
  12, 12, 13, 13, 14, 14, 15, 15, 16,
  16, 17, 17
] 

それをバックエンドで受け取って実行すると仮定します。コマンドは以下とします。

node ../backend/send_save_deck.js 1 "[2,2,3,3,4,4,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,14,14,15,15,16,16,17,17]"

Output:
スクリーンショット 2025-01-02 18.12.49.png
UInt16がDeprecatedだという警告が上がっていますが、トランザクションはエラーなくブロックに封印されました。

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

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

スターターデッキではなく、ユーザーが指定したデッキが画面上に表示されました。

プレイヤーは画面上でデッキ編集ボタンを押しただけですが、スマートコントラクトに保存されましたので、このNon-Custodial方式の威力を分かっていただけたかと思います。これだけですので全く難しくありません。


もっと高度な、GraphQLによる対戦相手と情報を共有しながらリアルタイム対戦を行える、トランザクションを実施したいという場合には、こちらが参考になるでしょう。(本は薄めですが、無駄を省いてピンポイントでコードを参照出来るようにした為です。ですので、レビューを滅茶甘めで付けていただけると喜びます😭)


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


Previous << Day7 - ゲーム内通貨購入

Flow BlockchainのCadence version1.0ドキュメント

Next >> Day9 - マッチング処理

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?