ブロックチェーンゲームの作り方8 デッキ編集

[Previous << 7 - ゲーム内通貨購入](https://qiita.com/flowcadence/items/2ef6bb711f8e1446814d)
[Next >> 9 - マッチング処理](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8)

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

:::note
**`Admin`リソースとは何のこと?**

前回`Admin`リソースという言葉が出ました。[Day3](https://qiita.com/flowcadence/items/2911220eb06a20beea14)では「システムを作った人だけができる処理、というのが探してもありません」と書きました。ではどうやってシステム管理者だけが動かせるロジックを書くのでしょうか。

これはCadence言語の特長を捉えることによって、それを可能とする実装を生み出すことができます。Cadence言語の特徴は以下です。

* リソースはスマートコントラクトの中でだけ`create`できる
* リソースのメソッドはCapabilityを公開しない限りリソースを持つアカウント本人しか実行できない
* コントラクト、リソースはそのプロパティ(Flowではフィールドと呼ぶ)を定義している場合、初期化のinit関数を必ず実行しなければならない

管理者のみが実行できるロジックは上記を工夫することで生み出せます。Adminというリソースは定義してもスマートコントラクト内に`create`している場所がなければ、誰もそのリソースを持つことができないのと同じことです。Adminリソースを`create`するコードがコントラクトの `init` 関数の中にだけある場合、そのロジックはスマートコントラクトをデプロイする時の一度だけしか呼ばれません。ですから、その場合Adminリソースは一つだけしか存在しなくなります。

こう工夫することで、Adminというリソースが一つだけ存在し、しかもスマートコントラクトをデプロイしたシステム管理者だけが持つことができます。

これはFlowブロックチェーンの作者が安全を考え、強力なメソッドを実装するのは危険と判断した結果、このような実装スタイルになりました。（彼は高校時代日本に留学していたので、無駄をしない日本人らしさが出ていますね）


**TL;DR 今回、Adminリソースを使って大量のトランザクションを自動化できるようにします。**
:::

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

#### 現在のデッキを表示する

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

`AwesomeCardGame.cdc`の`Player`リソースの定義に以下のように`get_player_deck`メソッドを追加します。
```ruby: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 deck = ` のところで使われています。CadenceはSwift言語に非常に似ているので `if let swift` でGoogle検索すると情報が出てきます。何か目新しい文法が出てきたらSwiftで検索してみるといいかもしれません。

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

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

---
`starterDeck`は[Day6の時点で既に`AwesomeCardGame.cdc`に実装](https://qiita.com/flowcadence/items/8443d8965a8f98486fa8#%E3%83%97%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BCresource%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)していましたが、`playerDeck`はまだ実装していませんでしたので追記します。

```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(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でリソース定義を変更したとき](https://qiita.com/flowcadence/items/2ef6bb711f8e1446814d#%E7%8F%BE%E5%9C%A8%E3%81%AE%E3%82%B2%E3%83%BC%E3%83%A0%E5%86%85%E9%80%9A%E8%B2%A8%E6%AE%8B%E9%AB%98%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)と同じように、一旦アカウントストレージに保存したリソースを削除するためエミュレータを再起動します。

#### プレイヤーのデッキをjsで取得する
プレイヤーのデッキを取得するスクリプトと、それをjsで取得するコードは以下のようになります。
```ruby: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とロジックを書く

メイン画面のロジックを以下のようにします。
```ruby:+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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/f23f3d76-f2ea-40e4-67fa-9e35e415c8cb.png)

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

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

#### デッキを編集する

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

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

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

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

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

`Admin`リソースの作成を`AwesomeCardGame.cdc`に追加していきます。
```ruby: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方式のトランザクション](https://qiita.com/flowcadence/items/27ef976c2f2493146dd6#non-custodial-account)を書きます。
```ruby:/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);
}
```
:::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`は全く同じ関数になっています。
:::

#### デッキ編集処理をする

プレイヤーが以下のデッキに変更したいとしてデッキ編集ボタンを押したとします。
```ruby
[
   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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/2dd86adf-bfd0-0210-682e-8b5f5e2b6c74.png)
UInt16がDeprecatedだという警告が上がっていますが、トランザクションはエラーなくブロックに封印されました。

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

結果:
![スクリーンショット 2025-01-02 18.15.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/8ba6c2f7-53b0-3dc9-b9cb-3c1e3e715e72.png)


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

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

***


:::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 << 7 - ゲーム内通貨購入](https://qiita.com/flowcadence/items/2ef6bb711f8e1446814d)
</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 >> 9 - マッチング処理](https://qiita.com/flowcadence/items/e1e67cd8d904ece357e8)</b>