# 0 계기
방학 기간에 심심해서 콘솔에서 돌아가는 yacht dice (야추 다이스)를 만들었다.
주사위 5개를 굴리고 골라서 족보만드는 주사위 포커느낌의 게임이다.
https://youtu.be/D_DdyxvSdpA?feature=shared
침착맨님이 하시는거 보고 재밌겠다 싶긴 했는데, 마침 여러모로 계기가 맞아.. 만들어보았다.

아이디어를 스틸해버렸다
이걸 만들면서
- c++ 익숙해지기
- 최근 객체지향 개념이 감이 잡히기 시작했는데, 좀 써보면서 익숙해지기
- 소켓통신 가능하면 써보기
- 다중 파일 프로그램? 으로 만들어보기
cmake 써보기 (근데 얘는 후술하겠지만 여러 이슈로 버려버림..)
이정도는 해보자고 목표를 잡았고, 개발하는 데 총 6일 정도가 걸렸다. (로컬 구현 3일, 좀 쉬다가 온라인 구현 3일)

지금부터 이놈을 만들면서 생겼던 issue들을 적어보려고 한다.
#1 게임 방향
일단 방향부터 잡았다.
게임 자체가 간단한 카드게임 느낌이라 복잡한 ui가 필요없어 보였고, 따라서 제어 가능한 console ui에서 그래픽을 모두 표현할 수 있는 방법을 찾아보았다.
그리고 기왕이면 Windows, MacOS, Debian(일단 Ubuntu..) 세 곳에서 모두 돌아가는 코드를 쓰고 싶었다.
Windows에서는 windows.h의 api들을 쓰면 되기 때문에 간단하게 문제를 해결할 수 있었다.
SetConsoleTitle("Yacht Game!");
system("mode con:cols=130 lines=40");
void SetColor(Color text, Color back) {
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), text | (back << 4));
}
void gotoxy(int x, int y) {
COORD Cur;
Cur.X = x;
Cur.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), Cur);
}
이런 식으로 간단하게 콘솔 조작이 가능하다.
근데 문제는, console을 조작할 수 있는 방법을 Windows에서 밖에 찾지 못했다..
결국 사실상 Windows에서만 돌아가는 프로그램이 되었고, 처음에는 세 곳에서 돌아가게 하려고 일부러 cmake로 했었는데, 이런 이슈도 있기도 하고, 무엇보다 배포 어케하는지 모르겠어서 결국 유기하고 .sln 써버렸다..
이건 좀 많이 아쉬워서 나중에 방법을 찾으면 개선하고 싶다.
#2 게임 설계
그 후 조금 더 구체적으로 생각해 보았다.
게임 진행은
메인 화면 -> 게임 메뉴 선택 -> 플레이어 이름 입력(player 객체 초기화) -> (13 라운드 * N 명) 번의 턴 진행 -> 점수 비교 후 승자 출력 -> 나가기..?
정도로 생각했다.
player 수를 처음에는 8인 정도..?까지 생각을 했었는데, 화면이 너무 복잡해지는 문제도 있었고, 후에 온라인 기능이나 여러 기능을 만들게 되면 상당히 복잡해질 것 같아, 일단 2인 플레이를 고려하여 만들었다.
게임 모드는 vs Player(local, online), vs CPU 이렇게 세 가지 정도를 생각했다.
vs Player은 다 완성했고, vs CPU는 최선의 선택을 하는 알고리즘 짜서 player2에 집어넣으면 되지 않을까 생각은 든다.
언젠가는 만들겠지..
#3 화면 구성
화면을 어떻게 구성해야 할 지 생각해 보았다.

#4 파일 구성
yacht-client
|- main // 기본 화면 설정, 메뉴화면
|- game // 게임 진행
|- player // 플레이어 관련 구현
|- dice // 주사위 관련 구현
|- interface // 화면 설정 함수 구현
|- global // 여러 파일에서 쓰는 함수 구현
|
yacht-server
|- main // socket으로 데이터 주고받는 기능 구현
대충 이런 구조이다. 물론 처음엔 이렇게 안생겼었고, 점점 만들다보니 이렇게 되었다.
#4-1 순환참조 이슈
멀티파일 프로젝트를 처음 해봐서 생긴 이슈이다.
처음에 A.h가 B.h를 include하고, B.h가 A.h를 include하는 바람에 순환참조 이슈가 발생했고, 에러가 갑자기 몇백개가 나는 대참사가 발생했다..

다행히 구글링삽질후에 원인을 발견했고,
헤더파일 새로 만들어서 두 헤더파일이 참조하는 내용을 넣어주어서 순환참조가 발생하지 않게 해결해 주었다.
#5 Class 설계
class는 처음에는 player 정도만 만들면 되겠다고 생각했는데, 생각해보니까 dice도 재사용을 많이 할 것 같아서 두 가지를 class로 구현했다.
class Player {
private:
int _idx = 0;
string _name = "player";
bool _gotBonus = false;
int _score[16] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0 };
// 0~12 follows ranking,
// 13 is subtotal,
// 14 is bonus,
// 15 is total.
public:
void SetPlayer(int idx, string name);
void SetRank(int idx, int score);
int GetRank(int idx);
string GetName();
};
class Dice {
private:
int _value[5] = { 0,0,0,0,0 };
vector<int> _frozenValue;
int _frozenIdx[5] = { -1,-1,-1,-1,-1 };
bool _isChangeable[5] = { 1,1,1,1,1 };
bool _isFrozen[5] = { 0,0,0,0,0 };
public:
bool isDiceChangeable(int idx);
bool isDiceFrozen(int idx);
void SetDiceFrozen(int idx);
int GetDiceValue(int idx);
int GetFrozenValue(int idx);
int GetFrozenIdx(int idx);
void RollDice(); // for local game
void RollDice(SOCKET& serverSock); // for turn user
void RollDice(char val[BUFSIZE]); // for non-turn user
bool SetDiceChangeable(int idx, bool setState);
int GetDiceRank(int idx);
void PrintfrozenDice();
};
처음에는 별 내용 없었는데, 나중에 기능 급조하면서 함수를 막 만들어내가지고 이상한 애들이 좀 많이 생겼다. 핵심만 보자.
dice는 5개가 고정적으로 있기 때문에 값을 관리하는 배열을 int[5]로 만들어주었고, 주사위 상태를 관리하는 배열도 비슷하게 만들어 주었다.
player는 이름, index(몇 번째 player인지 저장), 점수 저장하는 변수가 있다.
이 두 class들은 처음에 game.h에 다 넣어뒀는데 파일이 너무 커져서 각각 dice.h, player.h로 분리하였다.
# 6 턴 설계
턴을 어떻게 진행할지 고민하는 데 있어 시간이 오래 걸렸다.
0. 턴 시작
1. 3번의 기회가 주어지고, 주사위 5개를 굴린다.
2. 주사위를 확인하고 얼릴 주사위를 선택한다. (실제 게임에서는 주사위를 보관한다고 표현하는데, 나는 그냥 얼린다고 했다.)
3. 얼린 주사위를 제외하고 주사위를 다시 굴린다 (기회 1회 차감)
4. 기회가 다 사라질 때까지 1~3 반복
5. 이 주사위 세트를 기록할 족보를 선택한다.
6. 턴 종료
다른 건 다 무난했는데, 2번을 너무 쉽게 생각했다.
처음에는 그냥 단순히 얼릴 주사위를 선택하기만 하면 된다고 생각했는데, 족보를 만드려면 얼릴 순서까지 고려하여 선택할 수 있게 해야 한다!
1 2 3 4 5 // 주사위가 이렇게 나왔다고 가정하자.
-------------------
5 1 3
_ 2 _ 4 _ // 5, 1, 3 순서로 선택
-------------------
5 3
1 2 _ 4 _ // 1을 선택 해제하면 이렇게 선택이 되어야 한다.
bool Dice::SetDiceChangeable(int idx, bool setState) {
if (setState) {
if (_isChangeable[idx] == false) {
_isChangeable[idx] = true;
gotoxy(50 + 5 * idx, 11);
SetColor(WHITE, BLACK);
cout << _value[idx];
_frozenValue.erase(_frozenValue.begin() + _frozenIdx[idx]);
_frozenIdx[idx] = -1;
int frozenIdxCopy[5];
int rank = 0;
copy(begin(_frozenIdx), end(_frozenIdx), begin(frozenIdxCopy));
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (frozenIdxCopy[j] == i) {
_frozenIdx[j] = rank;
rank++;
break;
}
}
}
PrintfrozenDice();
}
else return false;
}
else {
if (_isChangeable[idx] == true) {
_isChangeable[idx] = false;
gotoxy(50 + 5 * idx, 11);
SetColor(DARK_GRAY, BLACK);
cout << _value[idx];
_frozenIdx[idx] = _frozenValue.size();
_frozenValue.push_back(_value[idx]);
PrintfrozenDice();
}
else return false;
}
SetColor(WHITE, BLACK);
return true;
}
어찌어찌 구현은 성공했다. 선택 순서를 vector에서 관리하고, vector에서 값이 해제되면 순위를 재설정하여 다시 저장하고 표시하는 느낌같은데.. 좀 짠지 오래되서 기억이 잘 안나긴 한다.
주석과 클린코드의 중요성을 새삼 느낀다..
# 7 키 씹힘 이슈
처음에 개행 입력을 받을 때 cin.ignore(32467, '\n'); 이런 식으로 작성을 했는데, 키가 계속 씹히는 문제가 발생하였다..
_kbhit()과 _getch()를 이용해 입력을 받고, 분기로 입력값에 따라 처리되게끔 작성하니까 문제가 해결되었다.
main.cpp, game.cpp에서 확인할 수 있다.
# 8 온라인 기능
이게 프로젝트 진행에 있어 가장 막막했던 부분이었다.
일단 TCP 소켓 통신으로 구현하면 될 것 같아 WinSock2를 사용하면 될 것 같았는데, 어떤 방식으로 통신을 하고, 데이터를 주고받을지에 대해 고민을 많이 했던 것 같다.

마인크래프트의 버킷 서버가 생각났고, Player 중 한 명이 server를 열고 여기에 두 Player가 접속하는 방식을 생각하게 되었다.
데이터 전송 방식은 구조체, json 직렬화 등 정말 여러 방식을 생각했지만, 게임이 간단하기 때문에 데이터도 간단하게 주고받는게 낫겠다고 생각하였다.

이런 식으로 32바이트 buf 하나로 주고받는다.. 게임이 간단해서 가능한거지, 좀만 더 복잡해지면 아마 이런 간단한 방식으론 힘들지 않을까 싶다.
# 9 결론
어쨌든.. 이렇게 해서 일단 local, online 두 기능은 구현 완료하였다.
https://youtu.be/Ecm_yOjN9-Y
- YouTube
www.youtube.com
프로젝트는 https://github.com/Fnhid/yacht-game 여기서 확인할 수 있고, Release에서 직접 다운받아 플레이해볼 수도 있다.
지금 코드가 좀 더러운 것 같아서, 좀 쉬고 나서 깔끔하게 코드를 수정해볼 예정이다.
