강께르의 개발일지
[C++] 월남뽕 게임 본문
1. 이것은 무엇인가?
- 이 게임은 무엇인가? 월남뽕? 잘못들은 줄 알았다. 이는 카드 게임의 일종이며, 그 룰에 대한 설명은 아래 링크를 대신해 게임에 대한 설명은 줄이도록 하겠다.
https://namu.wiki/w/%EC%9B%94%EB%82%A8%EB%BD%95
2. 요구사항
- 이 게임의 카드는 트럼프 카드로 알려진 그 카드로 사용한다. 총 52장이며, 게임 시작할 때, 무작위로 순서를 잘 섞을 것이다.
- 이 게임은 매 판마다 플레이어에게 판돈을 걸 것인지, 접을 것인지 입력을 받을 것이다. 그 선택에 따라 판돈을 입력하거나, 이를 생략할 수 있다. 생략했을 시, 판돈은 0으로 고정된다.
- 판돈의 최소 금액은 1000원이다.
- 52장의 카드로 이루어진 카드 뭉치에서 3장을 뽑을 것이다. 3장을 뽑아 1번째와 2번째 카드의 숫자 사이에 3번째 카드의 숫자가 있다면, 플레이어의 승리로 판돈을 걸은 만큼 플레이어의 돈이 추가될 것이다.
- 하지만 졌다면 판돈이 그만큼 플레이어의 돈에서 빠져나갈 것이다.
- 그렇게 카드 뭉치에서 카드를 뽑아 더 이상 3장을 뽑을 수 없는 상황까지 게임을 계속한다. 만약 그전에 플레이어의 돈이 1000원 미만이면 카드 뭉치가 남아있더라도 게임을 종료한다.
- 그렇게 해서 끝까지 살아남아 플레이어 돈이 가장 많이 버는 것이 이 게임의 목표이다.
3. 새로 알게 된 점
- 이 게임은 이전의 게임과 달리 상속과 다형성을 사용한 게임이 아니다. 주로 연습하고자 했던 것은 연산자 오버 로딩이다. 이 게임에서 연산자 오버 로딩으로 썼던 것은 Card 클래스에 대한 연산자 오버 로딩이다. Card 연산자는 그 멤버 변수인 m_num를 비교하거나 대입을 필요로 하는 경우가 있는데, 이 경우 Getter, Setter를 사용해서 멤버 변수를 건드리기보다 직관적인 이해를 돕는 연산자 오버 로딩을 사용해 Card 객체끼리의 연산이어도 그 객체의 멤버 변수의 연산이 되도록 하였다. 연산자 오버 로딩을 연습해보는 좋은 게임이었다.
- 카드의 문양만을 저장하는 변수로 char형 변수를 선언했었다. 이는 단일 문자만 저장할 생각이어서 그 이상 공간을 할당하는 것이 필요하지 않기에 char형 변수를 선언했지만, 특수문자는 1바이트 이상의 공간을 필요로 하였기에 1바이트 공간만을 할당하는 char형 변수로 감당할 수 없어 출력할 때? 만 찍히는 것을 확인했다. 그래서 string 객체 변수로 바꿔 선언하고 저장하였다.
4. 클래스에 대해
5. 실행 과정
- MainGame 클래스의 객체가 생성하는 것부터 설명하려고 한다. MainGame 클래스의 객체 mainGame은 GameBoard 클래스의 객체인 gameBoard를 생성하기 위해 생성자를 호출할 것이다.
- GameBoard 클래스의 생성자를 호출하면 판돈을 의미하는 정수형 변수 m_stake와 뽑은 카드 수를 의미하는 정수형 변수 m_drawCnt는 멤버 이니셜라이져를 이용해 0으로 초기화하고 Player 클래스 객체 포인터 변수인 m_player는 동적 할당으로 생성자를 호출한다. 이때 매개변수로 정수 10000을 전달한다.
- Player 클래스의 생성자는 플레이어 소유 돈을 의미하는 m_money를 전달받은 매개변수 정수 10000을 대입하고, 뽑아서 손에 들고 있는 카드를 뜻할 m_hand[]에 대한 생성자를 호출한다.
- Card 클래스의 생성자는 별다른 매개변수를 전달받지 않았을 경우, 디폴트 매개변수로 초기화하도록 해놓았다. 이때, 카드의 숫자를 나타내는 m_num은 0, 카드의 문양을 나타낼 m_art는 ""을 대입한다.
- 이로써 Player 클래스의 생성자 호출이 끝났다. 거슬러 올라가 GameBoard 생성자 호출로 돌아가서 진행한다.
- DeckInit() 함수를 호출한다. 이는 52개의 배열에 한 문양의 13장씩 차례대로 Card 객체를 동적 할당으로 생성하여 대입하는 것을 의미하는데 deckCnt라는 정수형 변수를 만들어 반복문이 돌아가는 동안 모든 m_deck 배열의 인덱스에 동적 할당으로 생성된 Card 객체가 대입될 수 있도록 만든 것이다.
- 동적 할당의 매개변수로 deckCnt % 13과 "◆"과 같은 문양이 "♠", "♥", "♣" 3개가 더 있어 트럼프 카드에는 총 4가지의 문양이 있다. 이 4가지 문양마다 13개의 카드가 필요하다. 52까지 반복문을 통해서 증가될 deckCnt를 이용해 나머지 연산으로 0부터 12의 값을 가진 Card 객체를 생성하게 될 것이다.
- SuffleDeck()은 m_deck 배열을 무작위로 섞는 함수이다. rand() % 52는 0~51 값 중 하나가 정해진다는 의미인데 이를 통해 두 가지 배열 인덱스를 정하고 그 인덱스에 해당되는 Card 객체를 swap해줄 것이다. 이런 과정을 100번 하여 섞었다.
- 이렇게 하여 비로소 GameBoard 클래스의 객체 gameBoard의 생성자가 끝났다. 이를 변수로 하는 mainGame의 함수인 Update()를 호출할 것이다.
- 시작하자마자 조건문을 거칠 것이다. bool값을 반환할 gameBoard.IsEmptyDeck()의 결과값과 gameBoard.GetPlayer().GetMoney() < 1000이라는 조건문에 따라서 반복문을 실행할지 여부를 결정할 것이다.
- IsEmptyDeck()은 m_deck 배열에 뽑을 수 있는 카드가 있냐는 판단을 할 함수이다. gameboard 객체의 멤버 변수인 m_drawCnt는 DrawDeck()이라는 함수를 통해 그 값이 증가될 텐데 매번 반복문을 마치고 IsEmptyDeck()을 호출하여 그 값이 50보다 크면 카드 뭉치에 카드가 없다는 의미로 true를 반환할 것이다.
- 근데 52장인데 왜 50장만 체크하느냐? 이 게임은 3장씩 뽑으면서 진행하는 게임이다. 그래서 적어도 게임을 진행하려면 마지막 카드 뭉치의 카드 장수가 3장은 확보되어있어야 한다. 50장까지 뽑았다는 것 의미는 남은 카드가 51장과 52장이 남아있는 상태라는 것이니 2장밖에 없어 게임은 진행하지 못하게 예외 처리해야 한다.
- gameBoard.GetPlayer().GetMoney() < 1000의 의미는 gameBoard 객체의 멤버 변수 m_player의 멤버 변수 m_money를 반환하라는 함수이다. 즉 플레이어의 소유하고 있는 돈을 알려달라는 의미인데 그 값이 1000보다 적으면 게임하지 못하도록 해야 한다. 나중에 판돈에 대해 이야기하겠지만, 이 게임의 요구사항 중 하나는 판돈을 1000원 이상 걸어야 한다는 것이다.
- 위 두 조건을 만족하고 아래의 조건문을 검사한다. gameBoard.DoBetting() 함수가 호출될 것이다. 이는 플레이어에게 배팅을 할 것인지 말 것인지 메뉴를 제공하고 입력을 받아 반환하는 함수이다. 그에 따라 배팅을 할 것이면 BetStake() 함수가 호출될 것이고, 그것이 아니면 SetStake(0)함수가 호출될 것이다.
- 배팅을 한다고 가정했을 때, gameBoard 클래스의 함수 BetStake()가 호출될 것이다. 현재 돈에 대한 정보를 m_player의 멤버 변수 m_money의 Getter 함수로 알려주고 판돈에 대한 정보를 입력하라고 할 것이다. 입력한다면 gameBoard의 멤버 변수 m_stake에 저장한다. 판돈은 1000원 이상이거나 현재 가진 돈보다 많이 입력할 수 없도록 예외 처리를 한다.
- 만약 배팅을 하지 않고 접었다면? m_stake의 Setter 함수인 SetStake(0)을 호출하여 m_stake에 0을 대입한다. 아무것도 걸지 않은 상태로 만든 것이다.
- 그다음 gameBoard의 멤버 함수 DrawDeck()을 호출한다. 표면적인 의미는 3장의 카드를 이 함수에서 뽑아 Player 객체의 멤버 변수에 대입할 것이다. 반복문을 0부터 3까지 돌려 m_Player->GetCard(idx)에 m_Deck[m_drawCnt++]을 대입할 것이다. 이때 GetCard는 Card의 참조자를 반환해 Lvalue로 대입받을 수 있게 했고, m_drawCnt는 gameBoard 객체의 멤버 변수로 여태까지 뽑은 카드 수를 의미한다. 그래서 값이 계속 증가한다는 것은 이미 거쳐온 배열 인덱스에 관심이 없다는 의미이고 그것은 카드를 버렸다고 퉁칠 것이다.
- 그리고 gameBoard의 멤버 함수 IsWin()을 호출할 것이다. m_player의 m_hand를 참조하여 카드 사이의 값에 들어오는지 판단하고 그에 따른 출력문과 m_player의 m_money를 수정할 것이다.
- 먼저 m_hand의 세 개의 배열 인덱스 중 첫 번째와 두 번째 카드를 의미하는 배열 인덱스를 가지고 그 두 Card 객체의 멤버 변수인 m_num값을 비교할 것이다. 둘 중 누가 더 큰 값인지 가려내야 그에 따른 수식을 앞으로 정의할 수 있을 것이다.
- 플레이어에게 뽑은 카드가 무엇인지 출력해주고, 큰 값을 가진 카드가 무엇인지에 따라 조건문을 저장할 bool 변수 conditionMin과 conditionMax에 다른 조건문을 거치고 그 결과값을 저장한다.
- conditionMin은 세 번째 카드와 위에 비교했을 때 더 작은 값을 가진 카드와 비교하여 세 번째 카드가 큰지, 작은지 bool값을 저장하는 변수이다.
- conditionMax는 세 번째 카드와 위에 비교했을 때 더 큰 값을 가진 카드와 비교하여 세번째 카드가 큰지, 작은지 bool값을 저장하는 변수이다.
- 이 두 개의 bool 변수가 모두 만족한다면 m_money의 Setter 함수를 호출하여 판돈만큼 돈을 추가해 값을 대입해주고,
그렇지 않다면 마찬가지로 Setter 함수를 호출해 판돈만큼 돈을 차감해 값을 대입해준다.
- 여기까지의 반복문을 위에 이야기했던 IsEmptyDeck() 함수를 포함하는 조건문이 false가 나올 때까지 돌린다. 그 조건은 뽑은 카드가 50장이 되거나 플레이어 소유 돈이 1000원 이하가 되는 것이다.
- 반복문이 끝나면 플레이어의 소유 돈에 따른 엔딩 출력문을 출력하고 프로그램을 종료한다.
6. 코드
6-1. Header.h
#pragma once
#include<iostream>
#include<ctime>
#include<Windows.h>
using namespace std;
enum class BET
{
DOBET = 1,
FOLD
};
6-2. MainGame.h
#pragma once
#include"GameBoard.h"
class MainGame
{
private:
GameBoard gameBoard;
public:
void Update();
void Rander();
};
6-3. MainGame.cpp
#include "MainGame.h"
void MainGame::Update()
{
while (!gameBoard.IsEmptyDeck() && !(gameBoard.GetPlayer().GetMoney() < 1000))
{
if ((BET)gameBoard.DoBetting() == BET::DOBET)
{
cout << endl << "이번 판은 배팅을 선택했습니다." << endl;
gameBoard.BetStake();
}
else
{
cout << endl << "이번 판은 접었습니다." << endl;
gameBoard.SetStake(0);
}
gameBoard.DrawDeck();
gameBoard.IsWin();
}
if (gameBoard.GetPlayer().GetMoney() < 1000)
cout << "강원랜드에 다시는 돌아오지 마세요." << endl;
else if (gameBoard.GetPlayer().GetMoney() < 20000)
cout << "좀 치시네요ㅎ" << endl;
else if (gameBoard.GetPlayer().GetMoney() < 50000)
cout << "와 비법 좀 알려주세요." << endl;
}
void MainGame::Rander()
{
gameBoard.PrintDeck();
}
6.4 GameBoard.h
#pragma once
#include"Player.h"
class GameBoard
{
private:
Player* m_player;
Card* m_deck[52];
int m_drawCnt;
int m_stake;
public:
GameBoard();
Player& GetPlayer() { return *m_player; }
void SetStake(int stake) { m_stake = stake; }
void DeckInit();
void SuffleDeck();
void PrintDeck();
void DrawDeck();
void BetStake();
int DoBetting();
void IsWin();
bool IsEmptyDeck();
};
6-5. GameBoard.cpp
#include "GameBoard.h"
GameBoard::GameBoard() : m_drawCnt(0), m_stake(0)
{
m_player = new Player(10000);
DeckInit();
}
void GameBoard::DeckInit()
{
int deckCnt = 0;
for (deckCnt = 0; deckCnt < 13; deckCnt++)
m_deck[deckCnt] = new Card(deckCnt % 13, "◆");
for (deckCnt = 13; deckCnt < 26; deckCnt++)
m_deck[deckCnt] = new Card(deckCnt % 13, "♠");
for (deckCnt = 26; deckCnt < 39; deckCnt++)
m_deck[deckCnt] = new Card(deckCnt % 13, "♥");
for (deckCnt = 39; deckCnt < 52; deckCnt++)
m_deck[deckCnt] = new Card(deckCnt % 13, "♣");
SuffleDeck();
}
void GameBoard::SuffleDeck()
{
srand((unsigned int)time(NULL));
int dest, sour, cnt = 0;
Card* tempCard;
while (cnt < 100)
{
dest = rand() % 52;
sour = rand() % 52;
tempCard = NULL;
tempCard = m_deck[dest];
m_deck[dest] = m_deck[sour];
m_deck[sour] = tempCard;
cnt++;
}
}
void GameBoard::PrintDeck()
{
for (int i = 0; i < 52; i++)
m_deck[i]->PrintCard();
cout << endl;
}
void GameBoard::DrawDeck()
{
for (int hand = 0; hand < 3; hand++)
{
m_player->GetCard(hand) = m_deck[m_drawCnt++];
}
}
void GameBoard::BetStake()
{
cout << "현재 돈 : " << m_player->GetMoney() << endl;
cout << "판돈을 설정해주세요. : ";
cin >> m_stake;
while (m_stake < 1000 || m_player->GetMoney() < m_stake)
{
cout << endl;
cout << "판돈이 적거나 가지고 있는 금액보다 판돈이 많습니다" << endl;
cout << "1000원 이상으로 설정하세요. : ";
cin >> m_stake;
cout << endl;
}
}
int GameBoard::DoBetting()
{
int result = false;
cout << "이번 판에 배팅하시겠습니까?" << endl;
cout << "1. 배팅\t2. 접기" << endl;
cin >> result;
while (!(result > 0 && result < 3))
{
cout << "올바르지 않은 입력입니다." << endl;
cout << "이번 판에 배팅하시겠습니까?" << endl;
cout << "1. 배팅\t2. 접기" << endl;
cin >> result;
cout << endl;
}
return result;
}
void GameBoard::IsWin()
{
bool conditionMin, conditionMax;
bool firstCardBig = m_player->GetCard(0) > m_player->GetCard(1);
cout << endl << "첫번째 카드 : ";
m_player->GetCard(0).PrintCard();
cout << "두번째 카드 : ";
m_player->GetCard(1).PrintCard();
cout << "세번째 카드 : ";
m_player->GetCard(2).PrintCard();
cout << endl;
if (firstCardBig)
{
conditionMin = m_player->GetCard(0) > m_player->GetCard(2);
conditionMax = m_player->GetCard(1) < m_player->GetCard(2);
cout << endl;
m_player->GetCard(0).PrintCard();
cout << " > ";
m_player->GetCard(2).PrintCard();
cout << " > ";
m_player->GetCard(1).PrintCard();
cout << endl;
}
else
{
conditionMin = m_player->GetCard(1) > m_player->GetCard(2);
conditionMax = m_player->GetCard(0) < m_player->GetCard(2);
cout << endl;
m_player->GetCard(1).PrintCard();
cout << " > ";
m_player->GetCard(2).PrintCard();
cout << " > ";
m_player->GetCard(0).PrintCard();
cout << endl;
}
if (conditionMin && conditionMax)
{
cout << "승리!" << endl;
cout << "돈을 " << m_stake << "만큼 따셨습니다." << endl;
m_player->SetMoney(m_player->GetMoney() + m_stake);
}
else
{
cout << "패배..." << endl;
cout << "돈을 " << m_stake << "만큼 잃었습니다." << endl;
m_player->SetMoney(m_player->GetMoney() - m_stake);
}
cout << "현재 돈 : " << m_player->GetMoney() << endl;
cout << endl << endl;
}
bool GameBoard::IsEmptyDeck()
{
bool result = false;
if (m_drawCnt > 50)
{
cout << "남아있는 카드가 3장보다 적습니다." << endl;
cout << "플레이어의 돈 : " << m_player->GetMoney() << endl;
result = true;
}
return result;
}
6-6. Player.h
#pragma once
#include"Card.h"
class Player
{
private:
int m_money;
Card m_hand[3];
public:
Player(int money) : m_money(money)
{ }
int GetMoney() { return m_money; }
void SetMoney(int money) { m_money = money; }
Card& GetCard(int idx) { return m_hand[idx]; }
void PrintHand()
{
for (int hand = 0; hand < 3; hand++)
{
cout << hand + 1 << "번째 패" << endl;
m_hand[hand].PrintCard();
cout << endl;
}
}
};
6-7. Card.h
#pragma once
#include"Header.h"
class Card
{
private:
string m_art;
int m_num;
public:
Card(int num = 0, string art = "") : m_num(num), m_art(art)
{ }
string GetArt() { return m_art; }
int GetNum() { return m_num; }
bool operator>(const Card& card) { return m_num > card.m_num; }
bool operator<(const Card& card) { return m_num < card.m_num; }
bool operator==(const Card& card) { return m_num == card.m_num; }
Card& operator=(const Card* card);
Card& operator=(const Card& card);
void PrintCard();
};
6-8. Card.cpp
#include "Card.h"
Card& Card::operator=(const Card& card)
{
this->m_num = card.m_num;
this->m_art = card.m_art;
return *this;
}
Card& Card::operator=(const Card* card)
{
this->m_num = card->m_num;
this->m_art = card->m_art;
return *this;
}
void Card::PrintCard()
{
cout << "모양 : " << GetArt() << " 숫자 : " << GetNum() << endl;
}
6-9. Main.cpp
#include"MainGame.h"
int main(void)
{
MainGame mainGame;
mainGame.Update();
}
7. 결과
'연습' 카테고리의 다른 글
[C++] 단방향 리스트 구현 (0) | 2021.06.22 |
---|---|
[C++] 상점 기능 구현 (1) | 2021.06.22 |
[C++] 몬스터와 1대3 턴제 전투 게임 (0) | 2021.06.14 |
[C++] 슬라이드 퍼즐 게임 (0) | 2021.06.14 |
[C++] 빙고 게임_미완성 (0) | 2021.06.13 |