티스토리 뷰
학원 과제로 7포커를 만들면서 어려움을 많이 느꼈다.
유니티라면 필요한 기능이나 함수를 써서 쉽게 만들 수 있을 것 같았는데
콘솔앱으로만 만들려고 하니 막막했다.
이게 C#의 기본은 없고 유니티의 각종 편의 기능만 써와서 그런것 같아 느낀게 컸다...
코드를 일일히 리뷰하는 건 어렵고,
만들면서 남기고 싶은 부분만 정리해본다.
1. 나는 포커룰을 모르잖아
족보만 대충 알았지 백스트레이트, 마운틴 이런거 몰라서...
게임 룰을 모르니 못만들었던게 큰거같다.
공부해보려고 애니팡 포커 시작했는데 초기자금 300억 받고 첫판에 파산한거 ㄹㅇ 실화?
카드 구조체인지 클래스인지 부터 만들어본다.
카드 게임을 하려면 카드가 있어야 하니까.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _20240516_Seven_Poker
{
public enum Suit
{
clubs = 0,
hearts = 1,
diamonds = 2,
spades = 3,
discard,
faceDown,
}
public enum Value
{
discard = 0,
faceDown = 1,
two = 2,
three = 3,
four = 4,
five = 5,
six = 6,
seven = 7,
eight = 8,
nine = 9,
ten = 10,
jack = 11,
qween = 12,
king = 13,
ace =14
}
//|? ||? ||♠ ||♠ |
//| ? || ? || 10 || 7 |
//| ?|| ?|| ♠|| ♠|
class Card
{
string[] numbers = new string[15] { " X", " ?", " 2", " 3", " 4", " 5", " 6", " 7", " 8", " 9", "10", " J", " Q", " K", " A", };
string[] shapes = new string[6] { "♣", "♡", "◇", "♠", " X", " ?" };
public Suit suit { get; set; }
public Value value { get; set; }
public bool isOpened = false;
public string GetNumber() //카드의 번호 글자를 반환합니다.
{
return numbers[(int)value];
}
public string GetShape() //카드의 번호 글자를 반환합니다.
{
return shapes[(int)suit];
}
public void SetFlipDown() //카드가 뒤집어진 상태. 비공개
{
suit = Suit.faceDown;
value = Value.faceDown;
}
public void Discard() //카드가 뒤집어진 상태. 비공개
{
suit = Suit.discard;
value = Value.discard;
}
}
}
enum 형을 사용하여 카드의 Value 와 무늬를 int 배열처럼 사용했다.
여기서 2번부터 시작하려니 허전해서
카드가 뒤집어졌을 때와 버려졌을 때 써볼까 해서 ? 와 X 도 추가했다. (결국 안 썼다)
이제 스트레이트(연속된 숫자의 나열)와
플러시(같은 무늬 5장),
페어(Value가 같은 카드의 짝)를 검출하는 코드가 필요하다.
여기서 많은 시간을 썼는데,
검색해서 찾아봤더니 대부분 5장을 사용하는 기본 포커를 상정하고 만든게 대부분이라
아... 왜 과제로 굳이 7포커를 만들어오라 했는지 알겠더라...
일반적으로 검출을 편하게 하기 위해선
카드 6장으로된 배열의 정렬이 필요하다.
크기순이라던가 모양순서말이다.
이때 크기순이란 카드의 Value 순으로 정렬하는 것이고
모양순서는 Enum 에서 정의한 클로버는 0, 하트는 1, 다이아는 2, 스페이드는 3 이라는 Index Number를 기준으로 하는 것이다.
카드 문양의 우선 순위가 스 > 다 > 하 > 클 인점을 고려하여 Enum을 만든 것이다.
나중에 (int)형으로 캐스트하여 쓰기 위해서.
2. 족보 검출 알고리즘
내가 정말 머리가 나쁜가? 싶었던 부분...
코드를 긁어와 쓰기는 싫고, 이해하고 쓰고 싶었는데 정렬 알고리즘이 이해가 안되는 것이다 ㅠㅠ
void Bubble_Sort(int arr[], int len) {
int i, j, tmp;
for (i = 0; i < len - 1; ++i) {
for (j = 0; j < len - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
아무튼 이해는 했는데 결국 나무위키 코드를 따라 쓴 수준이라 큰 성과는 없던 것같다.
그래서 카드 패를 내림차순으로 정렬하는데 성공하면,
그 다음 부터는 족보 검출이 쉬워진다.
7포커는 총 7장을 받지만 4장째에 한 장을 버리므로 마지막엔 6장을 가지고 족보를 검출해야한다.
그렇다면 내림차순으로 연속된 카드 5장의 검출을 위해선 최대 2번만 확인하면 된다.
이중 for문을 사용해서 말이다.
for (int i = 0; i < 3; i++)
{
int strCount = 1;
bool isFlsh = true;
for (int j = i + 1; j < 6; j++)
{
if (((int)hands[i].value - strCount).Equals((int)hands[j].value))
{
strCount++;
if (isFlsh && hands[i].suit != hands[j].suit)
{
isFlsh = false;
}
}
}
}
첫 번째 카드와 두 번째 카드를 기준으로 패를 두 번 검사한다.
내림차순으로 정리했으니 맨 앞의 두 장이 가장 큰 수일테고
자신의 뒤로 연속된 4장이 더 필요하다.
자기 뒤로 4장이 더 있을 수 있는 카드는 앞의 두 장이니까 i 가 1일 때 동안만 반복하며
i는 또 자기 뒤로 5장, 또는 4장만 검사하도록 한다.
그런데!
for (int i = 0; i < 3; i++)
말한 것과 달리 세바퀴 검사하도록 되어있다.
이것은 앞의 스트레이트가 완성되지 않았을 경우
혹시나 백스트레이트가 만들어졌을 상황까지 고려해야한다는 것이다.
사실 스트레이트 검사를 한 후
새로 백스트레이트 검사를 하기위해 오름차순 정렬을 하면 됐지만
한 번의 for문으로 검사를 하려한 결과였다.
public void StraightCheck()
{
HandsSortingByNumber();
for (int i = 0; i < 3; i++)
{
int strCount = 1;
bool isFlsh = true;
for (int j = i + 1; j < 6; j++)
{
if (((int)hands[i].value - strCount).Equals((int)hands[j].value))
{
strCount++;
if (isFlsh && hands[i].suit != hands[j].suit)
{
isFlsh = false;
}
}
}
//검사하는 카드가 5일때 5의 앞쪽에 a가 있는지 확인
if (i != 0 && (hands[i].value).Equals(Value.five))
for (int j = 0; j < i; j++)
{
if ((hands[j].value).Equals(Value.ace))
{
strCount++;
if (isFlsh && hands[i].suit != hands[j].suit)
{
isFlsh = false;
}
break;
}
}
//스트레이트가 완성되지 않았으면 검출 무의미
if (strCount < 5)
{
if (i.Equals(2)) return;
continue;
}
//로티플
if (hands[i].value.Equals(Value.ace) && isFlsh)
{
score = 1200 + (int)hands[i].suit;
Console.WriteLine($"{name}님의 로얄 스트레이트 플러쉬!!!!");
return;
}
else if (hands[i].value.Equals(Value.five) && isFlsh)
{
score = 1100 + (int)hands[i].suit;
Console.WriteLine($"{name}님이 백 스트레이트 플러쉬 완성했습니다!");
return;
}
else if (isFlsh)
{
score = 1000 + 5 * (int)hands[i].value + (int)hands[i].suit; ;
Console.WriteLine($"{name}님이 스트레이트 플러쉬 완성했습니다!");
return;
}
else if (hands[i].value.Equals(Value.ace))
{
score = 600;
Console.WriteLine($"{name}님이 마운틴 완성했습니다!");
return;
}
else if (hands[i].value.Equals(Value.five))
{
score = 500;
Console.WriteLine($"{name}님이 백 스트레이트 완성했습니다!");
return;
}
else
{
score = 400 + 5 * (int)hands[i].value + (int)hands[i].suit;
Console.WriteLine($"{name}님이 스트레이트 완성했습니다!");
return;
}
}
}
검출을 위한 ' for (int i = 0; i < 3; i++) ' 문 안에서,
첫 번째 for문 ' for (int j = i + 1; j < 6; j++) ' 은 자신이 만난 연결되는 카드 수를 카운팅하며,
연결된 카드를 만났을 때 같은 문양이라면 Bool 값이 true로 유지되지만 다른 문양을 만나면 false로 만들어 버리게 했다.
이후 i 값을 유지한 채, i 를 기준으로 족보를 계산한다.
여기서 아까 백스트레이트의 설명을 부연하자면,
i 가 5일 때 검사를 마치면 int strCount 는 2 까지 만난 경우 최대 4인 상태일 것이다.
이때 5 앞의 카드를 검사하여 A 가 있다면 그 손 패는 백스트레이트인 것이다!
if (i != 0 && (hands[i].value).Equals(Value.five))
카드 5가 손의 첫 번째 카드가 아닌 경우에만 백스트레이트가 될 수 있을 것이다.
if ((hands[j].value).Equals(Value.ace))
물론 첫 번쨰 카드는 A 여야 한다.
if (isFlsh && hands[i].suit != hands[j].suit)
여기서 그 A가 무늬까지 같다면...?
그런 과정을 거치면 나올 수 있는 검출 값은 두 가지이다.
int strCount = 1;
bool isFlsh = true;
카운트가 5이상이라면 다섯 장이 연결된 카드라는 것이고, (=스트레이트 조건 충족)
bool 값이 true 라면 만나는 연결된 카드마다 무늬가 같았다는 것이다. (=플러쉬 조건 충족)
if (hands[i].value.Equals(Value.ace) && isFlsh)
{
score = 1200 + (int)hands[i].suit;
Console.WriteLine($"{name}님의 로얄 스트레이트 플러쉬!!!!");
return;
}
else if (hands[i].value.Equals(Value.five) && isFlsh)
{
score = 1100 + (int)hands[i].suit;
Console.WriteLine($"{name}님이 백 스트레이트 플러쉬 완성했습니다!");
return;
}
else if (isFlsh)
{
score = 1000 + 5 * (int)hands[i].value + (int)hands[i].suit; ;
Console.WriteLine($"{name}님이 스트레이트 플러쉬 완성했습니다!");
return;
}
else if (hands[i].value.Equals(Value.ace))
{
score = 600;
Console.WriteLine($"{name}님이 마운틴 완성했습니다!");
return;
}
else if (hands[i].value.Equals(Value.five))
{
score = 500;
Console.WriteLine($"{name}님이 백 스트레이트 완성했습니다!");
return;
}
else
{
score = 400 + 5 * (int)hands[i].value + (int)hands[i].suit;
Console.WriteLine($"{name}님이 스트레이트 완성했습니다!");
return;
}
이것을 기준으로 다음 if 문 분기에서 충족한 조건에 따라 패의 족보를 검출하면 되는 것이다.
public void FlushCheck()
{
for (int i = 0; i < 2; i++)
{
int flshCount = 0;
for (int j = 0; j < 6; j++)
{
if (hands[i].suit.Equals(hands[j].suit))
flshCount++;
}
if (flshCount > 4)
{
if (score < 700)
score = 700 + (int)(hands[i].suit);
Console.WriteLine($"{name}님이 플러쉬를 완성했습니다!");
return;
}
}
}
여기서 플러쉬만 따로 구현했다.
스트레이트의 코드는 연결된 카드일 경우 무늬가 같은지를 따지는 경우라
그냥 플러쉬의 조건과는 다르기 때문이다.
이런 검사를 하고나서도 더 상위 족보가 완성되지 않았다면 그 아래 페어카드를 검사해야할 것이다.
//페어카드 검사는 이전 결과가 800점 이하일 경우 실행 (풀하우스의 800점보다 높으면 실행할 필요없음) 최대 900점(포카드)
public void PairCheck()
{
for (int i = 0; i < 6; i++)
{
int count = 0;
for (int j = 0; j < 6; j++)
{
if ((int)(hands[i].value)==(int)(hands[j].value))
count++;
}
switch (count)
{
//포카드
case 4:
Console.WriteLine($"{name}님의 포카드!");
score = 900 + (int)hands[i].value;
//포카드가 나왔다면 이번 검출로 나올 수 있는 가장 높은 점수이므로 여기서 검사를 종료한다.
return;
case 3:
// 이전에 트리플이 지금 트리플보다 작은 경우
if (300 < score && score < 300 + (int)hands[i].value)
//새 트리플 점수 입력
score = 300 + (int)hands[i].value;
// 이전에 이미 페어가 나온 경우
else if ((100 < score) && (score < 200))
{
score = 800 + (int)hands[i].value;
return; //풀하우스로 검사 종료
}
// 트리플이 처음 만들어진 조합인 경우
else
score = 300 + (int)hands[i].value;
break;
case 2:
// 이전에 페어가 나왔다면
if (100 < score && score < 200)
if (score < 100 + (int)hands[i].value)
score = 200 + (int)hands[i].value;
else score += 100;
// 이전에 트리플이 나온 경우
else if ((300 < score) && (score < 400))
{
Console.WriteLine($"{name}님의 풀하우스!");
score += 500;
return; //검사 종료
}
// 페어가 처음 만들어진 경우
else if(score < 100)
score = 100 + (int)hands[i].value;
break;
}
}
}
이번엔 6장의 카드가 모두 돌아가며 자신과 같은 카드 수를 카운팅한다.
자신을 포함해서 셀 것이다.
그러면 같은 카드가 4장일 때, 3장일 때, 2장일 때의 결과가 나올 것이다.
public void highCardCheck()
{
//내림차순 정렬
HandsSortingBySuit();
score = (int)hands[0].value * 5 + (int)hands[0].suit; // 최소 10점 ~ 최대 73점
Console.WriteLine($"{name}님의 하이카드!! ( {hands[0].GetShape()} {hands[0].GetNumber()} ) 점");
}
이러고도 족보가 완성되지 않았으면 마지막으로,
가장 높은 손패 하나를 자신의 하이카드로 만들어 상대 패와 겨루면 된다.
이때 Enum을 int 형으로 캐스트하여 각 카드의 진짜 (숫자와 문양의 우선도를 모두 계산한...) Value 를 계산한다.
무늬의 열거형 Enum 값이 클로버부터 스페이드까지 0에서 3까지의 값을 가진다.
카드의 Value에서 일의 자리 값을 무늬에게 주는 것이다.
그리고 2부터 A가 2에서 14까지의 크기를 가지므로 20부터 140까지 백의 자리와 십의 자리를 숫자에게 주는 것이다.
숫자 x 10 + 무늬
이러면 하이카드에 할당한 족보 점수는 최대 143점이 될 것이다.
그러면 다음 원페어의 점수는 최소 200점에서 출발해야 하는데
숫자가 딱 안 떨어지는 거 같아 10대신 5을 곱했다.
무늬 값이 0에서 3까지니까 숫자에 곱할 값은 3보다 크면 된다.
이러면
하이카드 : 10 ~ 73점
페어 : 100 + 페어 카드의 숫자
투페어 : 200 + 더 높은 페어 쪽 숫자
트리플 : 300 + 트리플 카드의 숫자
스트레이트 : 400 + 5장 중 높은 카드의 숫자
백스트레이트 : 500 + A의 무늬 값
마운틴 : 600 + A의 무늬 값
플러쉬 : 700 + 카드의 숫자
풀하우스 : 800 + 트리플 카드의 숫자
포카드 : 900 + 카드의 숫자
스트레이트 플러쉬 : 1000 + 무늬 값
백스트레이트 플러쉬 : 1100 + A의 무늬 값
로얄 스트레이트 플러쉬 : 1200 + A의 무늬 값
이렇게 족보를 점수화하여 우선 순위를 정할 수 있다.
이러면 나중에 최종 Hand Score만 가지고 어떤 숫자나 무늬로 족보를 완성했는지 역산도 가능하다.
'게임 개발 > 게임 개발 프로젝트' 카테고리의 다른 글
학원 수업 ) 길 찾기 알고리즘 (1) | 2024.06.11 |
---|---|
C# 콘솔로 포켓몬스터 게임 만들기 (완결) (0) | 2024.06.06 |
#003 뱀 게임 (0) | 2024.05.01 |
#002 지뢰찾기 만들기 (0) | 2024.04.30 |
#001 영화 '테트리스'를 보았다. (3) | 2024.04.27 |