수도코드
가위바위보 컨트랙트는 다음의 퍼블릭 함수를 가지고 있다.
createRoom
: 가위바위보 게임을 하기 위한 방을 만든다.joinRoom
: 만들어진 방에 참가한다.checkTotalPay
: 만들어진 방들의 총 배팅 금액을 확인한다.paout
: 게임을 마친다. 게임의 결과에 따라 배팅 금액을 송금한다.가위바위보 컨트랙트는 다음과 같이 진행된다.
createRoom
을 호출한다.crateRoom
은 새로운 방을 만들고, 방의 번호를 리턴한다.joinRoom
을 호출한다.joinRoom
은 참가자를 방에 참여시킨다.joinRoom
은 방장과 참가자의 가위/바위/보 값을 확인하고 해당 방의 승자를 설정한다.checkTotalPay
함수를 호출한다.payout
함수를 호출한다.사용자와 게임 구조체 생성
0.8.7
를 사용한다.RPS
이다.payable
키워드를 사용해 송금이 가능하다는 것을 명시한다.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
}
게임에서 각 플레이어의 중소와 배팅 금액을 알고 있어야 한다.
따라서 플레이어 구조체는 다음과 같이 작성할 수 있다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
struct Player {
address payable addr; // 주소
uint256 playerBetAmount; // 배팅 금액
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
enum Hand { // 가위, 바위, 보 값에 대한 enum
rock, paper, scissors
}
struct Player {
address payable addr; // 주소
uint256 playerBetAmount; // 배팅 금액
Hand hand; // 플레이어가 낸 가위, 바위, 보 값
}
}
또한 플레이어는 게임의 결과에 따른 상태가 있을 것이다. 상태에는 "대기중", "승리", "비김", "패배" 총 4가지의 상태가 있고 그 외에는 없으므로 역시 enum
을 사용한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
enum Hand { // 가위, 바위, 보 값에 대한 enum
rock, paper, scissors
}
enum PlayerStatus { // 플레이어의 상태
STATUS_WIN, STATUS_LOSE, STATUS_TIE, STATUS_PENDING
}
struct Player {
address payable addr; // 주소
uint256 playerBetAmount; // 배팅 금액
Hand hand; // 플레이어가 낸 가위, 바위, 보 값
PlayerStatus playerStatus; // 사용자의 현 상태
}
}
컨트랙트에는 게임을 진행하는 여러 방(room)이 있으며, 각 방은 모두 같은 형식을 가지고 있다. 방에는 방을 만든 방장 정보, 방에 참여한 참가자 정보, 총 배팅 금액이 있다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
// ...
struct Game {
Player originator; // 방장 정보
Player taker; // 참여자 정보
uint256 betAmount; // 총 배팅 금액
}
mapping(uint => Game) rooms; // rooms[0], rooms[1] 형식으로 접근할 수 있다. 각 요소는 game 구조체 형식이다.
uint roomLen = 0; // rooms의 키 값이다. 방이 생성될 때마다 1씩 올라간다.
}
각 게임은 방장이 방을 만들어둔 상태일 수도 있고, 참여자가 참여하여 게임 결과가 나온 상태일 수도 있고, 게임 결과에 따라 배팅 금액을 분배한 상태일 수도 있다. 또는 게임 중간에 에러가 발생할 수도 있다.
게임의 상태는 위의 네가지 상태만 있어야 하니 enum
으로 지정한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
// ...
enum GameStatus { // 게임의 상태
STATUS_NOT_STARTED, STATUS_STARTED, STATUS_COMPLETE, STATUS_ERROR
}
struct Game {
Player originator; // 방장 정보
Player taker; // 참여자 정보
uint256 betAmount; // 총 배팅 금액
GameStatus gameStatus; // 게임의 현 상태
}
}
createRoom
- 게임 생성하기
createRoom
은 게임을 생성한다. 게임을 생성한 방장은 자신이 낼 가위/바위/보 값을 인자로 보내고, 배팅 금액은 msg.value
로 설정한다. 여기서 msg.value
는 솔리디티에 정의된 글로벌 변수이므로 함수를 사용할 때 입력받지만, 함수 내에서는 파라미터로 설정할 필요가 없다.
contract RPS {
// ...
function createRoom (Hand _hand) public payable { // 배팅 금액을 설정하기 때문에 payable 키워드 사용
// 게임을 만들고 나면, 해당 게임의 방 번호를 반환
}
}
Game
구조체의 인스턴스를 만든다.
betAmount
: 아직 방장만 있기 때문에 방장의 배팅 금액을 넣는다.gameStatus
: 아직 시작하지 않은 상태이기 때문에 GameStatus.STATUS_NOT_STARTED
값을 넣는다.originator
: Player
구조체의 인스턴스를 만들어, 방장의 정보를 넣어준다.taker
: Player
구조체 형식의 데이터로 초기화되어야 하기 때문에 addr
에는 방장의 주소를 hand
에는 Hand.rock
으로 할당해준다.이렇게 만든 Game
인스턴스를 room[roomLen]
에 할당한다.
contract RPS {
// ...
function createRoom (Hand _hand) public payable returns (uint roomNum) { // 베팅금액을 설정하기 때문에 payable 키워드 사용, 변수 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({ // will change
hand: Hand.rock,
addr: payable(msg.sender),
playerStatus: PlayerStatus.STATUS_PENDING,
playerBetAmount: 0
})
});
roomNum = roomLen; // roomNum은 리턴된다. 현재 방 번호를 roomNum에 할당시켜 반환
roomLen = roomLen+1; // 다음 방 번호를 설정
}
}
modifier isValidHand (Hand _hand) {
require((_hand == Hand.rock) || (_hand == Hand.paper) || (_hand == Hand.scissors));
_;
}
function createRoom (Hand _hand) public payable isValidHand(_hand) returns (uint roomNum) {
// ...
}
joinRoom
- 방에 참가하기
joinRoom
은 기존에 만들어진 방에 참가한다.
참가자는 참가할 방 번호와 자신이 낼 가위/바위/보 값을 인자로 보내고, 배팅 금액은 msg.value
로 설정한다. 마찬가지로, isValidHand
함수 제어자도 사용한다.
contract RPS {
// ...
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
});
// 참가자가 참여하면서 게임의 배팅 금액이 추가되었기 때문에, Game 인스턴스의 betAmount 역시 변경해준다.
rooms[roomNum].betAmount = rooms[roomNum].betAmount + msg.value;
}
}
joinRoom
함수가 끝나는 시점에서, 방장과 참가자가 모두 가위/바위/보 값을 냈기 때문에 게임의 승패를 확인할 수 있다. 게임의 결과에 따라 게임의 상태와 참여자들의 상태를 업데이트 하는 함수 compareHands()
를 작성해준다. 게임의 결과는 joinRoom
이 완료된 시점에서 확인할 수 있기 때문에 joinRoom
함수의 맨 마지막에 compareHands()
함수를 호출해준다. 이 함수는 인자로 게임의 결과를 확인할 방 번호를 받는다.
contract RPS {
// ...
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(); // 게임 결과 업데이트 함수 호출
}
function compareHands(uint roomNum) private {
// ...
}
}
compareHands
를 작성하기 전에, enum Hand
를 보면 각 값은 순서에 따라 0부터 숫자가 매겨진다. 방장과 참가자가 가지고 있는 값은 0(rock), 1(paper), 2(scissors) 중 하나이다. 1(paper)은 0(rock)을 이기고, 2(scissors)는 1(paper)를 이기고, 0(rock)은 2(scissors)를 이긴다.
즉, 상대방의 값 x와 나의 값 y에 대해 다음의 조건이 만족하면 자신이 이긴 것이다.
(x + 1) % 3 == y
따라서 방장이 참가자를 이긴 상황을 코드로 작성하면 다음과 같다.
if ((takerHand + 1) % 3 == originatorHand) {
// originator Win!
}
이제 compareHands
함수를 작성해보자.
먼저, 해당 게임의 방장과 참가자의 가위/바위/보 값은 enum
값이기 떄문에 정수형으로 바꿔준다. 또한 게임을 본격적으로 비교하기 때문에, 게임의 상태를 GameStatus.STATUS_STARTED
로 변경한다.
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) { // 비긴 경우
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;
}
}
checkTotalPay - 방마다 배팅 금액 확인하기
각 방마다 배팅이 얼마나 결렸는지 확인할 수 있는 함수이다.
방 번호를 인자로 받아, 해당 방마다 얼마씩 배틍 금액이 걸려있는지 확인할 수 있다. 유저들은 뱉이 금액이 높은 혹은 적은 방을 찾은 후 자유롭게 선택하여 참여할 수 있다. 컨트랙트에 있는 금액을 보기만 하기 위해 솔리디티에 내장되어 있는 view
함수를 사용하고, transfer
함수는 다음과 같이 사용하였다.
contract RPS {
// ...
function checkTotalPay(uint roomNum) public view returns(uint roomNumPay) {
return rooms[roomNum].betAmount;
}
}
payout- - 배팅 금액 송금하기
payout
함수는 방 번호를 인자로 받아, 게임 결과에 따라 배팅 금액을 송금하고, 게임을 종료한다. 컨트랙트에 있는 금액을 송금하기 위해서는 솔리디티에 내장되어 있는 transfer
함수를 사용한다. transfer
함수는 다음과 같이 사용할 수 있다.
ADDRESS.transfer(value) // ADDRESS로 value 만큼 송금한다.
가위/바위/보 컨트랙트에서는 비긴 경우에는 자신의 배팅 금액을 돌려받고, 이긴 경우네는 전체 배팅금액을 돌려받는다.
function payout(uint roomNum) public payable {
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; // 게임이 종료되었으므로 게임 상태 변경
}
여기서 생각해야 될 점은, payout
함수를 실행하는 주체는 방장 또는 참가자여야 한다는 점이다. 따라서 payout
을 실행하기 전에 해당 함수를 실행하는 주체가 방장 또는 참가자인지 확인하는 함수 isplayer
함수를 만든다.
isplayer
는 방 번호와 함수를 호출한 사용자의 주소를 받는다. 그리고 사용자의 주소가 방장 또는 참가자의 주소와 일치하는지 확인한다.
modifier isPlayer (uint roomNum, address sender) {
require(sender == rooms[roomNum].originator.addr || sender == rooms[roomNum].taker.addr);
_;
}
function payout(uint roomNum) public payable isPlayer(roomNum, msg.sender) {...}
전체 코드
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract RPS {
constructor () payable {}
enum Hand { // 가위, 바위, 보 값에 대한 enum
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 { // player structure
address payable addr; // 주소
uint256 playerBetAmount; // 베팅 금액
Hand hand;
PlayerStatus playerStatus; // 사용자의 현 상태
}
struct Game {
Player originator; // 방장 정보
Player taker; // 참여자 정보
uint256 betAmount; // 총 베팅 금액
GameStatus gameStatus; // 게임의 현 상태
}
mapping(uint => Game) rooms; // rooms[0], rooms[1] 형식으로 접근할 수 있으며, 각 요소는 Game 구조체 형식
uint roomLen = 0; // rooms의 키 값, 방이 생성될 때마다 1씩 올라간다.
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);
_;
}
function createRoom (Hand _hand) public payable isValidHand(_hand) returns (uint roomNum) { // 베팅금액을 설정하기 때문에 payable 키워드 사용, 변수 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({ // will change
hand: Hand.rock,
addr: payable(msg.sender),
playerStatus: PlayerStatus.STATUS_PENDING,
playerBetAmount: 0
})
});
roomNum = roomLen; // roomNum은 리턴된다. 현재 방 번호를 roomNum에 할당시켜 반환
roomLen = roomLen+1; // 다음 방 번호를 설정
}
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); // 게임 결과 업데이트 함수 호출
}
function checkTotalPay(uint roomNum) public view returns(uint roomNumPay) {
return rooms[roomNum].betAmount;
}
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; // 게임이 종료되었으므로 게임 상태 변경
}
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) { // 비긴 경우
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;
}
}
}