저번글과 아마 거의 동일한 글이 될 것이다.
가위/바위/보 게임이 어떻게 작성되었는지를 다시 살펴보는 글이 될 것이고
이를 통해서 복습을 할 예정이다.
우리는 열거형이라는 것을 사용한다.
왜냐하면 개발자가 실수로 다른 값을 배정할 수도 있기 때문이다.
열거형으로 선언해둔 인자들만 사용 가능하게 만들어서
오류를 줄이기 위해서 사용을 한다.
순서대로 낼수 있는 경우의 수 - 플레이어 승리 유무 - 게임 진행도 를 열거형으로 선언을 하였다.
enum Hand {
rock, paper, scissors
}
enum PlayerStatus{
STATUS_WIN, STATUS_LOSE, STATUS_TIE, STATUS_PENDING
}
enum GameStatus {
STATUS_NOT_STARTED, STATUS_STARTED, STATUS_COMPLETE, STATUS_ERROR
}
우리는 플레이어의 상태와 게임방의 상태를 만들어 줘야 할 필요가 있다.
일단 플레이어들은 낼수 있는 경우의 수, 지갑의 주소, 승리유무, 금액의 데이터를 기본적으로 가지고 있어야 하며
게임방의 상태는 총 배팅된 금액, 게임 진행 상태, 반장 플레이어, 상대방 플레이어 가 필요할 것이다.
struct Player {
Hand hand;
address payable addr;
PlayerStatus playerStatus;
uint256 playerBetAmount;
}
struct Game {
uint256 betAmount;
GameStatus gameStatus;
Player originator;
Player taker;
}
우리는 플레이어의 상태와 게임방의 상태를 만들었다.
그러면 이제 mapping
을 활용하여 키-값 의 형태로 방을 만들고 다른 함수에 적용할 modifier
들을 설정해 주어야 한다.
mapping(uint => Game) rooms;
uint roomLen = 0;
modifier isValidHand (Hand _hand) {
require((_hand == Hand.rock) || (_hand == Hand.paper) || (_hand == Hand.scissors));
_;
}
modifier isPlayer (uint roomNum, address sender) {
require(sender == rooms[roomNum].originator.addr || sender == rooms[roomNum].taker.addr);
_;
}
rooms같은 경우에는 게임 방을 의미한다.
isValidHand
는 낼수 있는 경우의 수가 정확하게 들어왔는지를 확인하는 것이다.
isPlayer
는 방에 참여하고 있는 사용자가 게임을 종료시키는지를 확인하는 것이다.
기본적인 검증, 구조체, 게임방 등등은 설정을 완료하였으니 게임방을 만드는 함수를 살펴보자.
function createRoom (Hand _hand) public payable isValidHand(_hand) returns (uint 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 = roomLen+1;
}
일단 누구나 방을 만들수 있게 public
로 선언을 하였고
payable
를 적용하여 이더를 받을수 있는 함수로 만들어 주었다.
또한 isValidHand
를 통해 먼저 적절한 경우의 수가 들어오는지를 확인한다.
그리고 방을 만들게 되면 roomNum
을 return해주게 만들어 주었다.
일단 방을 만드는 것이기 떄문에 이전에 mapping
으로 선언해둔 rooms를 활용을 하였다.
키값은 roomLen
이 될 것이며 값은 Game
구조체가 될 것이다.
기본적으로 아직 방을 만드는 반장밖에 없기 떄문에 반장의 값만 해주면 된다.
이 부분에서 taker를 설정을 한다.
기존에 방을 만든 주체는 자신의 경우의 수를 미리 설정해 두었고
상대방은 이제 게임방에 입장을 하면서 자신의 경우의 수를 제시한다.
function joinRoom(uint roomNum, Hand _hand) public payable isValidHand( _hand) {
rooms[roomNum].taker = Player({
hand: _hand,
addr: payable(msg.sender),
playerStatus: PlayerStatus.STATUS_PENDING,
playerBetAmount: msg.value
});
rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
compareHands(roomNum);
}
마찬가지로 payable
, public
, isValidHand
를 적용을 해주었다.
이번에 들어오는 인자들은 방번호에 맞는 taker를 설정을 한다.
총 배팅금액에 taker의 금액을 합쳐주어야 하기 떄문에 갱신을 해주며
이후 compareHands(roomNum)
을 실행시켜 방번호를 인자로 넘겨 주게 된다.
앞서 두 선수가 모두 입장을 하고 경우의 수를 냈기 떄문에
이후에는 누가 이겼는지를 판단해야 한다.
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;
if (taker == originator){ //draw
rooms[roomNum].originator.playerStatus = PlayerStatus.STATUS_TIE;
rooms[roomNum].taker.playerStatus = PlayerStatus.STATUS_TIE;
}
else if ((taker +1) % 3 == originator) { // originator wins
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;
}
}
일단 계산을 해주기 위해서 값을 변환을 해주어야 한다.
우리는 경우의 수를 열거형으로 선언을 하였기 떄문에 이를 uint8
로 변환을 해주는 것이다.
해당 게임방의 진행상태도를 갱신해 준뒤
누가 승리자 인지를 파악한다.
(taker/ originator +1) % 3
이 부분은 별다른 것이 아니라 단순히 누가 승리자인지를 보는 것인다.
우리는 두 사용자가 배팅한 금액을 승리자에게 전송을 해주어야 한다.
이떄 무승부면 다시 배팅한 금액만큼 자신이 가져가게 되고 승자가 결정된 경우에는 승자에게 배팅된 금액이 주어질 것이다.
function payout(uint roomNum) public payable isPlayer(roomNum, msg.sender) {
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;
}
이 함수를 작동시키는 주체는 방에 참여한 사용자중 한명이어야 하기 떄문에
isPlayer
을 조건으로 설정해 두어야 한다.
transfer
를 활용하여 배팅된 금액을 사용자들에게 돌려 줄수가 있다.
처음에는 일단 무승부를 확인하고 이후에는 누가 이겼는지를 판단하게 된다.
이 과정이 끝나게 되면 이제 게임방 상태를 갱신한뒤에 게임을 마무리 하는 것이다.
다시 한번 살펴보면서 어떤 순서로 로직이 작성되었는지를 확인하였다.
흐름 순서 자체는 어렵지 않다.
하지만 아직 솔리디티에 많이 익숙하지 않아서인지 혼자서 작성을 하게 된다면 정말 오랜시간이 걸릴것같다..ㅠㅠ
계속해서 연습을 해 볼 것이고 그러다보면 언젠가는 익숙해 지지 않을까 싶다~~!!