限定ジャンケンをブロックチェーンゲームとして実装する
こんにちは。次世代システム研究室のT.M です。
はじめに
ブロックチェーンゲームのプラットフォームとして、Oasysチェーンが着目されています。前回、Oasysチェーンにおいて、トークン発行や、Verse構築を行いました。今回は応用として、ゲームを作ります。作成するゲームは、簡単かつWeb3と相性の良い、限定ジャンケンです。限定ジャンケンとは
賭博目次録カイジにおいて登場するジャンケンをモチーフにしたゲームです。ルールは以下となっています。- 初期状態
- グー、チョキ、パーのカードが4枚ずつ
- 星が3つ
- 勝利条件
- 星が3つ
- ジャンケンの手のカードが0枚
- 対戦
- 一対一で行う
- ジャンケンの手のカードをセットし、ジャンケンを行う
- ジャンケンの勝敗は通常のジャンケンと同じ
- 勝利すると相手から星が一つ移動する
- ジャンケンの勝敗に関わらず、セットした手のカードは破棄される
限定ジャンケンとブロックチェーンゲーム
ジャンケンの手のカードと星をERC20で表現する。これにより、限定ジャンケンのゲームの醍醐味である、ジャンケンの手のカードや星の取引をDeFiで行うことができる。前提
本記事は以下を前提とします。- ブロックチェーンとしてOasysチェーンを利用する。
- 前回の記事を理解しており、VerseでTXを実行することができるものとする。
- DeFi部分は実装しない。ジャンケンゲームのみを実装する。
- フロントは作成しない。
- 仕様上、実装上の脆弱性が存在する。
ジャンケンの手トークン
グー、チョキ、パーそれぞれをERC20トークンのコントラクトを用意する。コントラクト名をそれぞれRockToken, ScissorsToken、PaperTokenとする。OpenZeppelinのERC20, ERC20Burnable, Ownableを利用する。// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "github.com/OpenZeppelin/openzeppelin-contracts/blob/audit/2023-07-24/contracts/token/ERC20/ERC20.sol"; import "github.com/OpenZeppelin/openzeppelin-contracts/blob/audit/2023-07-24/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "github.com/OpenZeppelin/openzeppelin-contracts/blob/audit/2023-07-24/contracts/access/Ownable.sol"; contract RockToken is ERC20, ERC20Burnable, Ownable { constructor() ERC20("Rock", "JGR") Ownable(msg.sender){} function mint(uint256 amount) public onlyOwner { _mint(msg.sender, amount); } }
星トークン
星をERC20トークンのコントラクトを用意する。コントラクト名をStarTokenとする。コードはジャンケンの手トークンと同じである。ジャンケンコントラクト
ジャンケンのロジックコントラクトを用意する。コントラクト名をJankenGameとする。出す手を決めるchooseHand()と勝負をするplay()の二つの関数を実装する。chooseHand()では、指定した手のトークンを所有しているかをチェックし、手を記録する。play()では、対戦相手を決め、ジャンケンの勝敗を計算し、transferFrom()を利用して敗者から勝者に星トークンを移動させ、使用した手のトークンをburn()する。// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import "github.com/OpenZeppelin/openzeppelin-contracts/blob/audit/2023-07-24/contracts/token/ERC20/extensions/ERC20Burnable.sol"; contract JankenGame { enum Hand { None, Rock, Scissors, Paper } enum Result { Draw, Win, Lose } address public rockToken; address public scissorsToken; address public paperToken; address public starToken; mapping(address => Hand) public playerHands; event ChoooseHand(address indexed player, Hand indexed hand); event PlayResult(address indexed player, address indexed opponent, Result indexed result); constructor(address _rockToken, address _scissorsToken, address _paperToken, address _starToken) { rockToken = _rockToken; scissorsToken = _scissorsToken; paperToken = _paperToken; starToken = _starToken; } function chooseHand(Hand _hand) public { require(_hand == Hand.Rock || _hand == Hand.Scissors || _hand == Hand.Paper, "Invalid hand"); require(ERC20Burnable(starToken).balanceOf(msg.sender) >= 1, "Insufficient Star tokens"); // ユーザーが選択した手に対応するトークンを所持しているか確認 if (_hand == Hand.Rock) { require(ERC20Burnable(rockToken).balanceOf(msg.sender) >= 1, "Insufficient Rock tokens"); } else if (_hand == Hand.Scissors) { require(ERC20Burnable(scissorsToken).balanceOf(msg.sender) >= 1, "Insufficient Scissors tokens"); } else if (_hand == Hand.Paper) { require(ERC20Burnable(paperToken).balanceOf(msg.sender) >= 1, "Insufficient Paper tokens"); } playerHands[msg.sender] = _hand; emit ChoooseHand(msg.sender, _hand); } function play(address opponent) public { require(playerHands[msg.sender] != Hand.None, "You must choose a hand"); require(playerHands[opponent] != Hand.None, "Opponent must choose a hand"); Result result = getResult(msg.sender, opponent); emit PlayResult(msg.sender, opponent, result); // 星トークンの転送 if (result == Result.Win) { require(ERC20Burnable(starToken).transferFrom(opponent, msg.sender, 1), "Star token transfer failed"); } else if (result == Result.Lose) { require(ERC20Burnable(starToken).transferFrom(msg.sender, opponent, 1), "Star token transfer failed"); } // 使われた手をburn burnToken(playerHands[msg.sender], msg.sender); burnToken(playerHands[opponent], opponent); // 手のリセット playerHands[msg.sender] = Hand.None; playerHands[opponent] = Hand.None; } function getResult(address player, address opponent) private view returns (Result) { require(playerHands[player] != Hand.None, "You must choose a hand"); require(playerHands[opponent] != Hand.None, "Opponent must choose a hand"); Hand senderHand = playerHands[player]; Hand opponentHand = playerHands[opponent]; if (senderHand == opponentHand) { return Result.Draw; } if ((senderHand == Hand.Rock && opponentHand == Hand.Scissors) || (senderHand == Hand.Scissors && opponentHand == Hand.Paper) || (senderHand == Hand.Paper && opponentHand == Hand.Rock)) { return Result.Win; } return Result.Lose; } function burnToken(Hand _hand, address player) private { if (_hand == Hand.Rock) { ERC20Burnable(rockToken).burnFrom(player, 1); } else if (_hand == Hand.Scissors) { ERC20Burnable(scissorsToken).burnFrom(player, 1); } else if (_hand == Hand.Paper) { ERC20Burnable(paperToken).burnFrom(player, 1); } } }
準備
- 各コントラクトをデプロイする。
- RockToken, ScissorsToken、PaperToken、StarTokenを必要な分だけmint()する。
- プレイヤーに各トークンを配布する。
- プレイヤーはJankenGameコントラクトをspenderとして、各トークンにおいてapprove()を行う。
遊び方
- プレイヤーはchooseHand()で出す手を設定する。
- 対戦相手を探し、play()で対戦する。
問題点
一つ目の問題点として、他プレイヤーの出す手が事前にわかる、という問題があります。chooseHand()で事前に出す手を決めているので、トランザクションを見ることで、相手の出す手がわかり、必ず勝ててしまいます。この問題は、ゼロ知識証明などの技術を用いることで解決することができるのではないか、と考えています。もう一つの問題点として、他プレイヤーの所有している手の数がわかる、という問題があります。ジャンケンの手トークンは、ERC20なので、誰がいくつ持っているかが公開されています。必勝のために、ジャンケンの手トークンを独占した場合、他プレイヤーにバレてしまい勝負されない、という問題があります。この問題の解決策は見つけられていません。
まとめ
限定ジャンケンをブロックチェーンゲームとして実装をしました。ただ、ゲーム性のためには解決しなければいけない問題が残っています。これらの問題の解決にはより深くブロックチェーン技術についての理解が必要と考えており、ブロックチェーン技術の調査を続けていきたいと思います。次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
参考
https://dic.pixiv.net/a/限定ジャンケン