Ethereumブロックチェーンでどのようにゲーム「Rock-Scissors-Paper」を作成したか。 パート2テクニカル

前の記事へのコメントを考慮して、私はゲームの技術的要素をより詳細に検討する第2部を書くことにしました。



それでは、始めましょう。 nodejs Meteorフレームワークを使用して、JavaScriptでクライアント部分を作成しました。 ゲーム内の唯一のサーバーソリューションは、mongoDBでのチャットです。 仕事を始める前に、私たちが考えているゲームのアルゴリズムを確認してください。







スマートコントラクトの説明。 ブロックチェーン上にゲーム「石、はさみ、紙」を作成するための空白またはテンプレートオプションはありません。 これを行うために、独自の研究開発を実施しました。 当社のスマートコントラクトにより、ゲームテーブルを作成して閉じることができます。 すべてのテーブルに関する情報は、スマートコントラクトのメモリに含まれています。



struct room { address player1; address player2; bytes32 bit1; bytes32 bit2; uint8 res1; uint8 res2; uint256 bet; uint8 counter; uint8 c1; uint8 c2; uint roomTime; uint startTime; bool open; bool close; } mapping (uint => room) rooms;
      
      





ご覧のとおり、キーがテーブル番号で、値がテーブルオブジェクトであるマッピング(連想配列)ルームを作成しています。 テーブルオブジェクトでは、プレーヤーのアドレスとその動きのためのスペースを宣言します。 テーブルには、試合の合計スコア、各サイドの勝利数、テーブルを作成してゲームを開始するためのタイムスタンプも保存されます。 テーブル番号は、クライアント部分によってランダムに生成されます。 この番号は0から99999999です。作成されたテーブルが正常に閉じられていれば、同じ番号を何度も使用できます。 テーブル番号を使用したオーバーレイが発生することはほとんどありませんが、この場合でも、スマートコントラクトの動作を妨げることはなく、複製を作成するトランザクションは実行できません。 テーブルが適切に閉じられていないと、しばらくの間使用された数は、次のテーブルを作成するための可能なオプションから外れます。 スマートコントラクトのロジックによると、オープンテーブルには1ノックの関連期間があります。 その後、テーブルに対するアクションはすべて、そのテーブルに誰かを接続しようとする試みを含む、そのクローズにつながります。 テーブルを閉じるのは、revertRoom関数、次にcloseRoomが原因です。これにより、テーブルの情報が部屋の連想配列から削除されます。



 function revertRoom(uint id) internal { if( rooms[id].bet > 0 ) { rooms[id].player1.transfer(rooms[id].bet); if( rooms[id].player2 != 0x0 ) rooms[id].player2.transfer(rooms[id].bet); } RevertRoom(id); closeRoom(id); } function closeRoom(uint id) internal { rooms[id].close = true; RoomClosed( id ); delete rooms[id]; }
      
      





大文字で始まるスマートコントラクトの機能はイベントです。 クライアント側のゲームプレイは、現在のテーブルに関連する特定のイベントを聞くことに大きく依存しています。 14のイベントを作成しました:



 event RoomOpened( uint indexed room, address indexed player1, uint256 bet, uint8 counter, uint openedTime, bool indexed privat ); event RoomClosed( uint indexed room ); event JoinPlayer1( uint indexed room, address indexed player1 ); event JoinPlayer2( uint indexed room, address indexed player2, uint countdownTime ); event BetsFinished(uint indexed room ); event BetsAdd(address indexed from, uint indexed room ); event OneMoreGame(uint indexed room ); event SeedOpened( uint indexed room ); event RoundFinished( uint indexed room, uint8 res1, uint8 res2 ); event Revard(address win, uint256 amount, uint indexed room ); event Winner(address win, uint indexed room ); event Result(address indexed player, uint8 r, uint indexed room ); event RevertRoom(uint indexed room); event ScoreChanged(uint indexed room, uint8 score1, uint8 score2);
      
      





変数の型を宣言した後にインデックスを付けると、指定したフィールドでイベントをフィルタリングできます。 クライアント側からのブロックチェーン上のイベントのリッスンは次のとおりです(コーヒースクリプトを使用します。javascriptサービスhttps://js2.coffeeで変換できます )。



 #  this.autorun => filter5 = contractInstance.Winner {room: Number(FlowRouter.getParam('id')), }, {fromBlock:0, toBlock: 'latest', address: contrAdress} filter5.watch (error, result) -> console.log result if result instance.winner.set result.args.win console.log result.args.win UIkit.modal("#modal-winner").show()
      
      





ご覧のとおり、スマートコントラクトconstactInstanceのイメージを事前に準備した、Winnerと呼ばれるブロックチェーン上のイベントを参照しています。 Web3のセットアップとイメージの準備は、次のスクリプトによって実行されます。



ロングコード
 if (typeof web3 !== 'undefined') { web3 = new Web3(web3.currentProvider); var contrAdress = '0x80dd7334a28579a9e96601573555db15b7fe523a'; var contrInterface = [ { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": false, "name": "score1", "type": "uint8" }, { "indexed": false, "name": "score2", "type": "uint8" } ], "name": "ScoreChanged", "type": "event" }, { "constant": false, "inputs": [], "name": "deleteContract", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "exitRoom", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "fixResults", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "fixTimerResults", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "joinRoom", "outputs": [ { "name": "", "type": "uint256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "count", "type": "uint8" }, { "name": "privat", "type": "bool" } ], "name": "newRoom", "outputs": [ { "name": "", "type": "uint256" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "bet", "type": "bytes32" } ], "name": "setBet", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "OneMoreGame", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "player", "type": "address" }, { "indexed": false, "name": "r", "type": "uint8" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Result", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "SeedOpened", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "win", "type": "address" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Winner", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": false, "name": "res1", "type": "uint8" }, { "indexed": false, "name": "res2", "type": "uint8" } ], "name": "RoundFinished", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "win", "type": "address" }, { "indexed": false, "name": "amount", "type": "uint256" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "Revard", "type": "event" }, { "constant": false, "inputs": [ { "name": "mreic", "type": "uint256" } ], "name": "setMaxReic", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "BetsFinished", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player2", "type": "address" }, { "indexed": false, "name": "countdownTime", "type": "uint256" } ], "name": "JoinPlayer2", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "RevertRoom", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player1", "type": "address" } ], "name": "JoinPlayer1", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" } ], "name": "RoomClosed", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "room", "type": "uint256" }, { "indexed": true, "name": "player1", "type": "address" }, { "indexed": false, "name": "bet", "type": "uint256" }, { "indexed": false, "name": "counter", "type": "uint8" }, { "indexed": false, "name": "openedTime", "type": "uint256" }, { "indexed": true, "name": "privat", "type": "bool" } ], "name": "RoomOpened", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "from", "type": "address" }, { "indexed": true, "name": "room", "type": "uint256" } ], "name": "BetsAdd", "type": "event" }, { "constant": false, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "seed", "type": "uint256" } ], "name": "setSeed", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [], "name": "transferOutAll", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet1", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomBet2", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomCounter", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" }, { "name": "player", "type": "address" } ], "name": "checkRoomIsBet", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomNotClosed", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomOpened", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomPlayer1", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomPlayer2", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomRes1", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomRes2", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomScore1", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomScore2", "outputs": [ { "name": "", "type": "uint8" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkRoomStartTime", "outputs": [ { "name": "", "type": "uint256" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "id", "type": "uint256" } ], "name": "checkSenderBet", "outputs": [ { "name": "", "type": "bytes32" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" } ]; var contr = web3.eth.contract(contrInterface); var contractInstance = contr.at(contrAdress); var address = web3.eth.defaultAccount; var block = 0; web3.eth.getBlockNumber( function(er, res){ if(res) block = res }); }
      
      







ContractInterfaceはスマートコントラクトのabiです。スマートコントラクトにアクセスして対話するための関数名、変数、および引数のセットです。 remix-ideを使用してブラウザでイーサリアムスマートコントラクトを操作する場合、Compite-> Details-> Abiタブで簡単に見つけることができます。



大きな問題は、ページのリロード時またはテーブル間の切り替え時の単一の時点でのゲーム状態の決定であり、まだ部分的に残っています。 このような問題が要素的に解決されるサーバーではなく、ブロックチェーンで作業していることを忘れないでください;ここから情報を取得することは非常にエキゾチックです。 対戦相手の参加、動きの開始、動きの完了など、これらの14のゲームイベントすべてに常に耳を傾けます。 また、現在のスコア、ラウンド開始時間などのゲーム情報のリクエストを常に送信しています。 さらに、ゲームの状態のほとんどは、1つのイベントや1つの変数ではなく、いくつかのイベントをいくつかの変数(ブロックチェーンからの直接データ取得の結果)で一度に重ね合わせることによって決定されます。 たとえば、ゲームが2回以上勝利し、3回目のラウンドで秘密鍵を送信するときが来た場合です。 ゲームが開始され、対戦相手が接続し、3ラウンドが通過し、暗号化された動きが両側から送信されたという事実を追跡する必要があります。 この時点でページをリロードしてみて、ブロックチェーンとのすべての関係を再度復元する必要があります。 情報の各要求または受信は非同期に発生します。 遅延は互いに重なり合っているため、アプリケーションの実行はサーバーバージョンよりも大幅に遅くなります。



しかし、ゲームのスマートコントラクトに戻ります。 ゲームデータの保存方法、クライアント側とのやり取りに使用するイベント、テーブルの作成方法については既に説明しました。 次に、ムーブの暗号化について説明します。 可能な旅行オプションは、1-石、2-はさみ、3-紙の3つだけです。 プレーヤーが自分の動きを選択すると、すべてクライアントで開始されます。 カードの選択が満たすスクリプトは乱数を生成します。これをシード(以降、Sidと呼びます)と呼びます。 Sidは、テーブル番号に関連してCookieに保存され、Cookieとゲームセッションで使用されるまで保存されます。 プレイヤーが自分の動きを決定するときに初めて、シードが使用されます。石、はさみ、または紙を選択します。 プレーヤーが選択した数字1、2、または3がそれに追加されます。 結果は、sha3メソッドによってハッシュされます。 重要な点:web3.sha3()メソッドは文字列でのみ機能します。 クライアント側では、結果の数値を文字列に簡単に変換し、ハッシュして変数bit1またはbit2のスマートコントラクトに送信します。



 function setBet(uint id, bytes32 bet) public { require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 ); //   if(rooms[id].startTime + 5 minutes > now) { if(msg.sender == rooms[id].player1) { rooms[id].res2 = 5; rooms[id].bit1 = bet; } else if(msg.sender == rooms[id].player2) { rooms[id].res1 = 5; rooms[id].bit2 = bet; } if(rooms[id].bit1 != 0x0 && rooms[id].bit2 != 0x0) { SeedOpened(id); BetsFinished(id); } BetsAdd(msg.sender , id); } else { Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id); } }
      
      





setBet関数は、主に両側での移動を保証します。 両方のプレイヤーが動き、記録されるまで、ゲームプレイは続行できません。 同時に、彼女はタイマーの遵守を監視し、移動する可能性が非常に高いかどうかの必要な初期チェックを実行します。 移動が行われると、関数はクライアントに信号を送信します:SeedOpenedおよびBetsFinisfed。 これらの信号を受け取ったクライアントは、「ショーカード」と呼ばれるようにプレーヤーに提供します。つまり、移動をハッシュする前に、ランダムに生成した合計の前の数字の最初の部分をスマートコンタクトに送信します。 「カードの開示」への同意を確認すると、プレーヤーはこの番号をスマートコントラクトの別の機能-setSeedに送信します。



 function setSeed(uint256 id, uint256 seed) public { require( rooms[id].bit2 != 0x0 && rooms[id].bit1 != 0x0 ); require(msg.sender == rooms[id].player1 || msg.sender == rooms[id].player2 ); //   if(rooms[id].startTime + 5 minutes > now) { if(msg.sender == rooms[id].player1) decodeHash1(id, seed); else if(msg.sender == rooms[id].player2) decodeHash2(id, seed); } else { Result(rooms[id].player1, rooms[id].res1, id); Result(rooms[id].player2, rooms[id].res2, id); } }
      
      





次に、受信したシードをスマートコントラクトdecodeHash1およびdecodeHash2の内部関数に転送します



 function decodeHash1(uint id, uint seed) internal { uint e1 = seed + 1; bytes32 bitHash1a = keccak256(uintToString(e1)); uint e2 = seed + 2; bytes32 bitHash1b = keccak256(uintToString(e2)); uint e3 = seed + 3; bytes32 bitHash1c = keccak256(uintToString(e3)); if(rooms[id].bit1 == bitHash1a) rooms[id].res1 = 1; if(rooms[id].bit1 == bitHash1b) rooms[id].res1 = 2; if(rooms[id].bit1 == bitHash1c) rooms[id].res1 = 3; Result(rooms[id].player1, rooms[id].res1, id); // return res1; }
      
      





これで暗号化サイクルが完了します。 この関数は、イーサリアム内部でのみクライアントについて既に説明したハッシュ手順を再現し、各プレイヤーに対して3回実行します-3つの可能な移動で。 次に、イーサリアムのハッシュ結果とクライアントからの既存のハッシュ結果との通常の比較が行われます。 対応することで、プレイヤーがどのような動きをしたかが明確になります。 開示中にプレーヤーから取得したキーを宣言または保存するのではなく、それらの代わりに、各プレーヤーのres変数にデコード結果を0〜4の数値としてすぐに記録します。



この段階では、別の興味深いニュアンスがあります。 判明したとおり、web3 jsライブラリのsha3メソッドに対応する標準関数solidity keccak256は、入力が数値ではなく文字列の場合にのみ適切な結果を提供します。 Keccak256では数値を操作できますが、クライアントweb3.sha3()は文字列のみを受け入れるため、keccak256は入力で文字列も受け取る必要があります。 そして、堅実性に関する数値の文字列への変換は、javascriptほど単純ではありません。 これを行うには、追加の内部関数uintToString()を記述します。 純粋とマークすることは、この関数がスマートコントラクトのメモリステータスに何らかの影響を与える権利を持たないことを意味します。読み取りと書き込み ここにそのようなニュアンスがあります。



 function uintToString(uint i) internal pure returns (string){ // bytes memory bstr = new bytes; if (i == 0) return "0"; uint j = i; uint length; while (j != 0){ length++; j /= 10; } bytes memory bstr = new bytes(length); uint k = length - 1; while (i != 0){ bstr[k--] = byte(48 + i % 10); i /= 10; } return string(bstr); }
      
      





最後に、winRes()内部勝者決定機能によってゲームループが完了します。 ゲームのすべての可能な結果が含まれています。つまり、引き分けにより、現在のラウンドのリプレイが始まります。 ラウンドまたは試合の勝者を決定します。 したがって、最終および中間の勝利の概念が表示されます。 この機能は、プレイヤーの1人がアクションを取る時間がない状況を理解し、現在のラウンドで負けたプレイヤーをカウントします。 両方のプレーヤーが本来よりも長い間アイドル状態になっている状況では、現在のアカウントに関係なくテーブルが閉じます。 この場合、ゲームは停止し、すべてが元の状態に戻ります。



最終コード
 function winRes(uint id) internal { require(rooms[id].res1 > 0 || rooms[id].res2 > 0); address win = 0x0; if(rooms[id].res1 == 1 && rooms[id].res2 == 2) win = rooms[id].player1; if(rooms[id].res1 == 1 && rooms[id].res2 == 3) win = rooms[id].player2; if(rooms[id].res1 == 2 && rooms[id].res2 == 1) win = rooms[id].player2; if(rooms[id].res1 == 2 && rooms[id].res2 == 3) win = rooms[id].player1; if(rooms[id].res1 == 3 && rooms[id].res2 == 1) win = rooms[id].player1; if(rooms[id].res1 == 3 && rooms[id].res2 == 2) win = rooms[id].player2; if(rooms[id].res1 == 4 && rooms[id].res2 != 4 ) win = rooms[id].player2; if(rooms[id].res2 == 4 && rooms[id].res1 != 4 ) win = rooms[id].player1; if(rooms[id].res1 == 5 && rooms[id].res2 != 5 ) win = rooms[id].player2; if(rooms[id].res2 == 5 && rooms[id].res1 != 5 ) win = rooms[id].player1; if((rooms[id].res2 == 4 && rooms[id].res1 == 4 ) || (rooms[id].res2 == 5 && rooms[id].res1 == 5 )) revertRoom(id); else { // -    if(win == 0x0) { replay(id); OneMoreGame(id); } else { //   if( win == rooms[id].player1 ) rooms[id].c1 += 1; if( win == rooms[id].player2 ) rooms[id].c2 += 1; //    n-      if( rooms[id].counter > 1 && rooms[id].c1 < rooms[id].counter && rooms[id].c2 < rooms[id].counter ) { ScoreChanged(id, rooms[id].c1, rooms[id].c2); replay(id); OneMoreGame(id); } else { //          ScoreChanged(id, rooms[id].c1, rooms[id].c2); if( rooms[id].bet > 0 ) { rewardWin(win, id); } Winner(win, id); closeRoom(id); } } } }
      
      







ゲームアルゴリズムはゼロから作成されたため、このオプションは一連の実験と調整の成功した結果です。 私たちは自分自身としか比較できないので、どれほど効果的かつ最適であるかを言うのは困難です。 アルゴリズムの以前の4つのバージョンと比較して、これは数倍効率的です。 おそらくもっと多くの可能な最適化ソリューションがありますが、これは将来の問題です。



All Articles