가위 바위 보 게임을 솔리디티로 작성할 수 있습니다. 플레이어들은 각각 가위, 바위, 보 3가지 옵션 중 하나를 선택해 게임에 참여할 수 있고, 승리한 플레이어는 베팅된 금액을 모두 가져갈 수 있습니다. 가위 바위 보 스마트 컨트랙트 게임은 총 4개의 함수로 구성됩니다.
- createRoom : 게임을 위한 방을 생성합니다.
- joinRoom : 플레이어들은 게임을 위해 방에 참여합니다.
- checkTotalPay : 해당 방에 베팅된 이더(ETH)를 확인합니다.
- payout : 게임을 종료한 후 베팅된 금액을 송금합니다.
스마트 컨트랙트 구조 잡기
스마트 컨트랙트의 기본 요소인 라이센스, pragma version, 컨트랙트를 작성합니다. 가위 바위 보 게임은 이더(ETH)를 베팅한 후 송금받는 기능을 가지므로 payable 키워드를 명시합니다.
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contarct RPS{
constructor() payable{
}
}
플레이어 구조 작성하기
게임에 참여하는 플레이어는 2종류로 구분됩니다. 먼저, 방을 생성하는 플레이어와 참여만 하는 플레이어로 구분해 구조체를 작성합니다.
게임은 플레이어들의 주소를 알아야 하고, 베팅한 금액, 베팅한 조건(가위 바위 보)를 알고 있어야 합니다.
enum Hand{
rock, paper, scissors
}
struct Player{
address payable addr;
uint256 playerBetAmount;
Hand hand;
}
게임에 참여한 플레이어들은 각 단계별로 다른 상태를 가지게 됩니다.
enum PlayerStatus{
STATUS_WIN, STATUS_LOSE, STATUS_TIE, STATUS_PENDING
}
struct Player{
address payable addr;
uint256 playerBetAmount;
Hand hand;
PlayerStatus playerStatus;
}
각 게임은 방장, 참여한 플레이어, 베팅한 금액의 정보를 가집니다. 플레이어들은 uint 변수로 Room을 구분할 수 있어야 합니다. mapping 변수를 선언해주고, 최초 방의 번호는 0으로 지정합니다. 각 게임들 또한 플레이어와 마찬가지로 상태를 가지게 됩니다.
struct Game{
Player originator;
Player taker;
uint256 betAmount;
GameStatus gameStatus;
}
enum GameStatus{
STATUS_NOT_STARTED, STATUS_STARTED, STATUS_COMPLETE, STATUS_ERROR
}
mapping(uint=>Game) rooms;
uint roomLen = 0;
새로운 게임 생성하기
게임을 새로 생성하기 위해서는 createRoom 함수를 사용합니다. 인자로는 가위 바위 보 3가지 조건값 중 하나를 보내줍니다. 베팅 금액은 msg.value(참여자가 트랜잭션으로 보낸 이더)로 지정합니다. 참고로 msg는 솔리디티에 선언되어 있는 전역 변수입니다.
방이 생성되면 새로운 게임을 할당합니다. 게임에는 게임의 상태, 방장, 참여자의 정보가 기록됩니다. 방이 생성된 후 방의 번호가 반환됩니다.
방의 베팅 금액은 일단 방장만 존재하므로 방장의 베팅금액으로 설정합니다. 게임의 상태는 아직 시작이 안된 상태이므로 STATUS_NOT_STARTED로 지정합니다. 참여자는 아직 없으므로 임의의 값(Hand.rock, playerBetAmount : 0)으로 초기화 해줍니다. 방이 생성된 후 방의 갯수를 1 증가합니다.
function createRoom(Hand _hand) public payable returns(roomNum){
rooms[roomLen] = Game({
betAmount = msg.value,
gameStatus : GameStatus.STATUS_NOT_STARTED,
originator : Player({
hand : _hand,
addr : payable(msg.sender),
playerStatus : PlayerStatus.STATUS_PENDING,
playerBetAmount : msg.value
}),
taker : Player({
hand : Hand.rock,
addr : payable(msg.sender),
playerStatus : PlayerStatus.STATUS_PENDING,
playerBetAmount : 0
})
});
roomNum = roomLen;
roomLen++;
}
방을 생성할 때 입력하는 가위 바위 보 외 다른 값이 들어오는 예외 처리를 위해 modifier를 작성합니다.
modifier isValidHand(Hand _hand){
require((_hand == Hand.rock) || (_hand == Hand.paper) || (_hand == Hand.scissors) )
_;
}
modifier는 createRoom함수에서 작동합니다.
function createRoom(Hand _hand) public payable isValidHand(_hand) returns(uint roomNum){
//----
}
joinRoom 방에 참여하기
참여자가 방에 참여하기 위해서는 가위 바위 보 중 하나를 선택하고 베팅 금액을 설정해야 합니다. 베팅 금액은 msg.value로 설정됩니다. 참여자의 가위 바위 보 선택 또한 modifier 검증을 거친 후 실행됩니다.
function joinRoom(uint roomNum, Hand _hand) public payable isValidHand(_hand){
// ---
}
새로운 참여자가 들어오면 게임의 참여자(taker) 항목을 업데이트 합니다. 참여자의 베팅 금액을 반영해 게임의 전체 베팅 금액을 수정합니다.
rooms[roomNum].taker = Game({
hand : _hand,
addr : payable(msg.sender),
playerStatus : PlayerStatus.STATUS_PENDING,
playerBetAmount : msg.value
})
rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
joinRoom 함수가 실행되면 방장의 값과 참여자의 값이 비교되고, 승자와 패자가 구분됩니다. 결과에 따라 플레이어들의 상태를 변경할 수 있는 함수 compareHands 함수가 필요합니다.
function compareHands(uint roomNum) private{
uint8 originator = uint8(rooms[roomNum].originator.hand);
uint8 taker = uint8(rooms[roomNum].taker.hand);
rooms[roomNum].gameStatus = GameStatus.STATUS_STARTED);
}
enum 타입으로 선언된 바위(0), 보(1), 가위(2)는 각 숫자가 매겨지게 되고 이를 통해 x가 y에 대해 승리하는 조건은 다음과 같습니다.
(x+1) % 3 == y
방장과 참여자의 승패 조건값은 3가지 경우로 구분됩니다.
if(taker == originator){
// 비긴 경우
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
}else if((taker + 1) % 3 == originator){
// originator이 이긴 경우
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_WIN;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_LOSE;
}else if((taker + 1) % 3 == originator){
// originator이 패배한 경우
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_LOSE;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_WIN;
}else{
// 예외 경우
rooms[roomNum].gameStatus = GameStatus.STATUS_ERROR;
}
payout 베팅 금액 송금하기
게임이 종료되면 결과값에 따라 베팅금액을 송금해야 합니다. 컨트랙트에 들어있는 이더를 전송하기 위해서는 transfer 함수를 사용합니다.
ADDRESS.transfer(value);
function payout(uint roomNum) public payable isPlayer(roomNum, msg.sender){}
게임의 결과는 3가지 입니다. 비김, 이김, 패배함. 비기는 경우 자신의 베팅 금액을 돌려받고, 이기면 모든 베팅 금액을 돌려받습니다.
// 비긴 경우
if(rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_TIE && rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_TIE){
rooms[roomNum].originator.addr.transfer(rooms[roomNum].originator.playerBetAmount);
rooms[roomNum].taker.addr.transfer(rooms[roomNum].taker.playerBetAmount);
}else{
if(rooms[roomNum].originator.playerStatus == PlayerStatus.STATUS_WIN){
rooms[roomNum].originator.addr.transfer(rooms[roomNum].betAmount);
}else if(rooms[roomNum].taker.playerStatus == PlayerStatus.STATUS_WIN){
rooms[roomNum].taker.addr.transfer(rooms[roomNum].betAmount);
}else{
// 오류가 발생하는 경우 베팅 금액 환불
rooms[roomNum].originator.addr.transfer(rooms[roomNum].originator.playerBetAmount);
rooms[roomNum].taker.addr.transfer(rooms[roomNum].taker.playerBetAmount);
}
rooms[roomNum].gameStatus = GameStatus.STATUS_COMPLETE;
각 방별로 베팅금액 확인하기
생성된 방에는 총 베팅금액이 public 하게 선언되어 있습니다. 따라서 참여자들은 베팅된 금액을 확인하고 더 많은 베팅금액을 찾아 게임을 즐길 수 있습니다. 스마트 컨트랙트에서 단순히 데이터를 읽기만 하는 경우 view 함수를 사용합니다.
function checkTotalPay(uint roomNum) public view returns(uint roomNumPay){
return rooms[roomNum].betAmount;
}
게임 테스트
Remix로 스마트 컨트랙트 배포 후 테스트를 진행합니다. 베팅할 금액과 가위 바위 보 중 하나를 선택해 방을 생성합니다.
새로운 참여자는 기존 방에 참여하기 위해 베팅 금액을 설정하고 가위바위보 중 하나를 선택해 방에 입장합니다.
베팅된 금액이 참여자만큼 증가합니다.
게임이 종료된 후 승자와 패자가 나뉘게 됩니다. 자신이 베팅했던 방과 금액을 입력하고 정산을 하면 정상적으로 결과에 따른 로직이 수행됩니다.
전체 코드 : Github
스마트 컨트랙트 : 이더스캔(0x729cf235068A0Dc12791054dCF8f87D22a7D856B)
'Blockchain' 카테고리의 다른 글
[Blockchain] 이더리움 토큰 발행 ERC-20 이란? (0) | 2022.07.14 |
---|---|
[Blockchain] 가위바위보 스마트 컨트랙트 개선하기 (0) | 2022.07.13 |
[Blockchain] Remixd 사용하는 방법 (0) | 2022.07.12 |
[Blockchain] 이더리움 스마트 컨트랙트 배포하기(Remix) (0) | 2022.07.12 |
[Blockchain] 블록체인 오라클(Oracle) 문제란? (0) | 2022.07.11 |
댓글