본문 바로가기
포트폴리오/Project_1

가위바위보 게임 개선하기(hand encoding)

by StelthPark 2021. 12. 13.

1. 개발 내용

 

1. 가위바위보게임에서 방장(originator)이 createRoom을 할 때 미리 낸 패를 숨길 수 있다.

2. 이후에 joinRoom으로 방에 참여한 참가자(taker)의 패도 숨길 수 있다.

3. 방장이 낸 배팅금액과 같은 금액을 참가자가 배팅해야 입장 할  수있다.

4. use~함수를 이용해서 방장과 참여자 둘다 자신이 낸 진짜 패를 해독하여 저장시킨다.

5. 둘다 해독하여 완벽히 저장되어야 compare를 통해 payout이 가능하다.

 

작성 전 선언조건

enum Hand {
    rock,
    paper,
    scissors
  }

  enum PlayerStatus {
    STATUS_WIN,
    STATUS_LOSE,
    STATUS_TIE,
    STATUS_PENDING
  }

  enum GameStatus {
    STATUS_NOT_STRTED,
    STATUS_STARTED,
    STATUS_COMPLETE,
    STATUS_ERROR
  }

  struct Player {
    address payable addr; 
    uint256 playerBetAmount; 
    bytes32 hand;
    PlayerStatus playerStatus;
    uint result;
    uint count;
  }

  struct Game {
    Player originator; 
    Player taker; 
    uint256 betAmount; 
    GameStatus gameStatus; 
  }

  mapping(uint256 => Game) rooms; 
  uint256 roomLen = 0;

1. Hand는 rock, paper, scissors로 가능하며 0,1,2로 구분한다.

2. Player는 전송가능한 주소인 payable로써 주소, 배팅금액, 암호화 된 Hand, 플레이어상태, 진짜 hand, 진짜hand를 저장시켰는지 여부로 구성된다.

3. Game은 방장, 참여자, 배팅금액합계, 게임 상태로 구성된다.

4. rooms는 방목록으로 키는 uint256의 방번호로 값은 Game 구조를 받는다.

5. roomLen은 방의 번호로 사용되며 createRoom에 의해 방이 만들어질때마다 +1하여 방번호를 올려 사용하게된다.

 

 

 

keccak256을 사용하여 패와 임의문자키를 입력받아 암호화한다.

  function keccak(uint256 _hand, string memory _key) public pure returns(bytes32) {
    return keccak256(abi.encodePacked(_hand, _key));
  }

keccak256(abi.encodePacked(인자1,인자2));에서 인자1은 숨길 패, 인자2는 임의문자키를 입력하면 암호화된 bytes32의 값이 반환된다. 이 반환 된값을 createRoom의 _hand인자로 전달해주게 된다. 한마디로 hand를 암호화하여 만들어주는 함수이다.

 

keccack

 

 

방 생성

 function createRoom(bytes32 _hand) public payable returns (uint256 roomNum)
  {
    rooms[roomLen] = Game({
      betAmount: msg.value,
      gameStatus: GameStatus.STATUS_NOT_STRTED,
      originator: Player({
        hand: _hand,
        addr: payable(msg.sender),
        playerStatus: PlayerStatus.STATUS_PENDING,
        playerBetAmount: msg.value,
        result : 0,
        count : 0
      }),
      taker: Player({
        hand: 0,
        addr: payable(msg.sender),
        playerStatus: PlayerStatus.STATUS_PENDING,
        playerBetAmount: 0,
        result : 0,
        count : 0
      })
    });
    roomNum = roomLen;
    roomLen = roomLen + 1;
  }

인자로 keccak함수로 만들어진 암호화된 hand값을 넣고 방을 생성한다. rooms[roomNum].originator.hand에는 이 암호화된 값이 할당된다.

 

createRoom : 0.001이더를 배팅, paper(임시문자키:apple)를 내어 방을 생성한다

 

 

방 입장

  function joinRoom(uint256 roomNum, bytes32 _hand) public payable {
   require(msg.value == rooms[roomNum].betAmount);
    rooms[roomNum].taker = Player({
      hand: _hand,
      addr: payable(msg.sender),
      playerStatus: PlayerStatus.STATUS_PENDING,
      playerBetAmount: msg.value,
      result : 0,
      count : 0
    });
    rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
  }

참가자 또한 keccak함수로 만든 자신의 암호화된 hand값을 인자로 넣으며 roomNum를 함께 넣어 원하는방으로 접속한다.require문을 통해 방장이 낸 배팅금액과 같아야지만 입장할 수 있다. rooms[roomNum].betAmount에는 방장과 참여자의 총 배팅금액이 저장된다.

 

joinRoom : 0.001이더를 배팅, scissors(임시문자키:banana), 방번호를 내어 방에 입장한다.

 

암호화된 패를 풀어 진짜 패 저장

function useOriginator(uint256  _roomNum, uint256 _hand, string memory _secretKey) public {
    require(msg.sender == rooms[_roomNum].originator.addr);
    require(rooms[_roomNum].originator.hand == keccak(_hand,_secretKey));
    rooms[_roomNum].originator.result = _hand;
    rooms[_roomNum].originator.count = 1;
  }

    function useTaker(uint256  _roomNum, uint256 _hand, string memory _secretKey) public {
    require(msg.sender == rooms[_roomNum].taker.addr);
    require(rooms[_roomNum].taker.hand == keccak(_hand,_secretKey));
    rooms[_roomNum].taker.result = _hand;
    rooms[_roomNum].taker.count = 1;
  }

 

각 함수는 방장과 참여자가 자신이 암호화한 패만을 진짜 패로 풀수있으며 진짜 패가 나오면 자신의 result로 저장시킨다. 또한 compare함수에서 방장과 참여자가 해당 함수를 모두 실행했을때 실행 될 수 있도록 count를 올린다. 자신의 hand에 저장된 암호화 값과 keccak함수를 통해 자신이 낸 패, 임시문자키를 넣어 일치한다면 저장시키게 된다.

 

use~ : hand와 임시문자키 apple를 받아 keccak로 비교하고 originator의 진짜 패를 저장

 

 

진짜 패를 비교하고 결과를 산출하여 토큰몰수

  function compareHands(uint256 roomNum) private {
    require(rooms[roomNum].taker.count==1 && rooms[roomNum].originator.count ==1);
    uint8 originator = uint8(rooms[roomNum].originator.result);
    uint8 taker = uint8(rooms[roomNum].taker.result);

    rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED;

    if (taker == originator) {

      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
    } else if ((taker + 1) % 3 == originator) {
  
      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
    } else if ((originator + 1) % 3 == taker) {
  
      rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
      rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
    } else {
  
      rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
    }
  }
  
modifier isPlayer(uint256 roomNum, address sender) {
    require(
      sender == rooms[roomNum].originator.addr ||
        sender == rooms[roomNum].taker.addr
    );
    _;
  }

  function payout(uint256 roomNum) public payable
    isPlayer(roomNum, msg.sender)
  {
    compareHands(roomNum);
    //playerStatus에 따라 전송처리
}

최종적으로 토큰을 몰수하는 payout함수는 방장과 참가자의 주소만 실행 할수 있도록 isPlayer로  제어하였다.

isPlayer를 통과한뒤 comare함수를 통해 비교하여 전송처리를 하게된다. compare함수에서 방장과 참가자 둘다 자신의 진짜 패를 업데이트해야 비교하고 전송 할 수 있게 된다.

 

payout : 참가자 계정이 가위로 승리하여 토큰을 전송 받았다.

 

2. 개발 회고

 

Keep: 방장이 방을 만들고 참가자가 참여할때 배팅금액이 서로 다르면 안된다고 생각하여 require로 제어하였다. 방장이 낸 패말고도 참여자가 낸 패도 스스로가 keccak함수를 이용하여 암호화된 hand를 발급하고 그것을 이용해 참여 할 수  있도록하여 최대한 서로간의 패가 보이지 않도록 작성하였다.

 

Problem: use~ 함수를 사용하여 각자 진짜 패를 저장 시킬때 트랜잭션에 input에서 어떤 값이 였는지 확인 할 수있다. 하지만 이미 payout을 할때 compare요소로 낸 값은 변경 할 수없으므로 결과에는 영향이 없다. 

문제가 있다. 바로 use~함수를 방장과 참여자중 누가먼저 누를것인가, 먼저 누르게 되면 자신이 뭘 냈는지 볼 수는 있다. 결과는 바뀌지 않았지만 내가 졌구나 라는 판단을 할 수 있게 되고 선량하지 않은 패배자라면 상대방이 먼저 use~ 함수를 사용해 결과를 보여주면 자신은 use 함수를 사용하지 않고 그대로 두게 되면 승자는 payout을 정상적으로 할 수 없고 배팅금은 계속 잠겨 있게 된다.

 

Try: 악의적인 사용자가 자신이 졌다는 결과를 확인하더라도 payout이 이루어질수 있도록 구성해봐야겠다 .첫번째로 졌을시에도 배팅금의 일부를 조금은 돌려 받을 수 있게 작성하는것이다. 그렇다면 한 푼도 못받는것보다 조금이라도 받기위해 payout조건을 맞춰주게 될것이다. 두번째로 코드적으로 결과를 완벽히 숨기는것이다.

 

 

3. 작동 영상

 

4. 코드 및 주소

 

Github : https://github.com/Parkstelth/rock-paper-scissors-Game

 

GitHub - Parkstelth/rock-paper-scissors-Game

Contribute to Parkstelth/rock-paper-scissors-Game development by creating an account on GitHub.

github.com

 

Contract : https://ropsten.etherscan.io/address/0xcE083bf910D6ff6bD07eA7559Da83c8AFaB440cF

 

RPS | 0xcE083bf910D6ff6bD07eA7559Da83c8AFaB440cF

The Contract Address 0xcE083bf910D6ff6bD07eA7559Da83c8AFaB440cF page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

ropsten.etherscan.io

 

댓글