Unity

C# 블랙잭 만들기

박도치 2023. 11. 9. 12:01

C#을 통한 간단한 블랙잭을 만드는 과정을 담은 내용이다.

 

1. 취지

  • 어디까지나 코드 자랑이 아닌 함께 공부하기 위함이다. 그리고 코드리뷰를 함께 하면서 완전히 나의 것으로 흡수하기 위해 쓰는 내용이다.
  • 코드리뷰에 있어서 취지에 대해 말하자면 SelectMany(SelectMany를 이용한 두 가지 배열을 list에 같이 돌려 다시 담는 내용) 라는 메서드를 이용하여 이러한 방법으로도 할 수 있다는 것을 함께 알아가기 위함이다.
  • 난 어떤식의 코드를 짰고, 설명을 함으로써 흐름파악 및 안보였던 것들을 다시한번 보게끔 하기 위함이다.
  • 어려웠던 점이라던지 힘들었던 부분에 대해서 같이 나누기 위함도 있다.

 

2. 설계

블랙잭의 규칙에 대해서 알아봤고, 이에 맞춘 설계를 하였다.

 

2-1 블랙잭의 핵심 규칙

  • 52장의 카드와 딜러 플레이어가 존재한다.
  • 플레이어와 딜러가 각각 랜덤된 2장의 카드를 가져간다
  • 딜러는 카드의 수가 17이 되면 더 이상 카드를 Hit하지 못한다.
  • 플레이어는 원하는 만큼 카드를 Hit(카드를 먹는 행동)할 수 있고 그러지 않는다면 종료하여 서로의 수를 비교한다.
  • 21이 넘어가면 버스트가 되어 패배하고, 21이 넘지 않는다면 21에 가까운 자가 승리한다.

 

2-1 설계

  • 메인
  • 블랙잭 게임 스타트
  • 카드 준비 및 셔플
  • 초기 카드를 보여줌 (플레이어는 두 장 다 보여주고, 딜러는 한 장은 뒷면, 한 장은 보여준다.)
  • 플레이어가 카드를 Hit 하는 단계 (y를 누르면 카드를 드로우, 아니면 그대로 딜러와 플레이어 값을 계산)
  • 플레이어가 Hit하여 넘칠 경우, 딜러의 승리, 넘치지 않고 n을 눌러 더이상 드로우를 하지 않을 경우 두 카드를 비교하여 21에 가까운 사람이 승리
  • 게임을 계속 플레이하려면 y, 아니면 n으로 게임을 계속 진행할지 말지를 선택

 

3. 코드

 

3-1 메인

static void Main(string[] args)
{
    Console.WriteLine("========BLACK JACK!========");
    Console.WriteLine("블랙잭 게임 스타트!");

    // 게임 스타트, 게임 시작시 PlayBlackJack() 호출, 끝날 경우 계속 플레이하시겠습니까 호출
    while (true)
    {
        PlayBlackJack();

        Console.WriteLine("계속 플레이하시겠습니까? (Y/N)");
        if(Console.ReadLine().ToUpper() != "Y")
        {
            break;
        }
        Console.Clear();
    }
    Console.WriteLine("게임 종료");
}

 

 

main이며 실행시 위 두개의 문장이 나타나고 아래 PalyBlackJack함수가 호출된다.

 

아래는 게임이 끝난 후 나타나는 문장으로 처음에는 if문에 y를 넣고, else에 n을넣어 게임종료를 했는데, 좀 더 편의를 위해 y가 아닌 다른 키를 눌러도 게임이 종료되게끔 다시 코드를짰다.

 

3-1 카드 준비

static List<string> InitializeDeck()
{
    string[] ranks = new[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" };
    string[] suits = new[] { "♠", "♥", "◇", "♣" };
    return ranks.SelectMany(rank => suits, (rank, suit) => rank + suit).ToList();
}

 

이전 포스트에서 다뤘던 카드 준비하는 코드이다. 두 개의 배열을 한 곳의 리스트에 다시 담는 역할로 총 52가지 수로 새로운 list에 담아 return해주는 역할이다.

 

3-2 Shuffle()

static string Shuffle(List<string> deck)
{
    Random random = new Random();

    int index = random.Next(0, deck.Count);

    string card = deck[index];

    return card;
}

 

셔플하는 함수로 앞에 준비해둔 카드를 랜덤 값으로 셔플하는 단계이다.

 

매개변수로 이전에 만들어 둔 덱을 받아와서 그 덱의 카운트만큼 랜덤한 index를 받는다.

 

그리고 그 index값을 deck의 배열 위치에 담고 이를 return해주는 식이다.

 

 

3-3 Calculator()

static int Calculator(List<string> handCard)
{
    int totalValue = 0;
    int aceCount = 0;

    foreach(string card in handCard)
    {
        string rank = card.Substring(0, card.Length - 1);

        if (int.TryParse(rank, out int cardValue))
        {
            totalValue += cardValue;
        }
        else if(rank == "A")
        {
            totalValue += 11;
            aceCount++;
        }
        else
        {
            totalValue += 10;
        }
    }

    while(aceCount > 0 && totalValue > 21)
    {
        totalValue -= 10;
        aceCount--;
    }

    return totalValue;
}

 

현재 카드의 값을 계산하는 함수이며, 이는 플레이어 혹은 딜러가 카드를 받았을 때 들어가는 함수이다. 매개변수는 딜러의 덱 리스트 또는 플레이어의 덱 리스트 를 유기적으로 받아와서 계산한다.

 

계산식은 전체 카운트와 에이스카운트로 나눈다. 이는 에이스의 경우 1 또는 11의 점수이기 때문에 에이스 카운트를 따로하여 이후 에이스카드를 먹고 에이스 카운트가 올라갔을 때, 21의 점수가 초과하고, 에이스의 카운트가 올라가 있는 경우에는 전체 점수에서 10을 빼서 에이스를 1로 바꿔준다.

 

예를 들면 3♥ 의 수를 들고있다면 subString으로 ( 3♥ 면 배열에서 2개의 칸을 쓰는 0, 1자리 수) 0부터 배열의 크기 -1 즉, 0번째 자리 하나만 가지고 와서 rank변수에 담은 후 TryParse로 파싱하여 totalvalue에 합쳐주고 만약 rank자리에 A가 들어있다면 11을 합치고, acecount를 늘려 이후 21이 초과하거나 acecount가 존재할 경우 아래에서 totalvalue를 10을 빼주는 식으로 했다.

 

3-4 PlayBlackJack()

static void PlayBlackJack()
{
  List<string> deck = InitializeDeck();
  List<string> playerDeck = new List<string>();
  List<string> dealerDeck = new List<string>();

  // 플레이어 버스트시 false
  bool playerBust = false;

  string seperator = ", ";

  // 카드 나눠주기 단계
  playerDeck.Add(Shuffle(deck));
  dealerDeck.Add(Shuffle(deck));
  playerDeck.Add(Shuffle(deck));
  dealerDeck.Add(Shuffle(deck));


  // 게임 진행
  while(true)
  {
      // 나눠준 패 출력
      Console.WriteLine("플레이어 손패: " + string.Join(seperator, playerDeck));
      Console.WriteLine("딜러 손패: " + dealerDeck[0] + ", [뒷면]");

      // 버스트(21넘길)시 반복문 탈출 
      if (playerBust)
      {
          break;
      }

      Console.WriteLine("카드를 Hit하시겠습니까? (Y/N)");
      string userInput = Console.ReadLine().ToUpper();

      // 플레이어 카드 계산단계
      if (userInput == "Y")
      {
          playerDeck.Add(Shuffle(deck));
          if (Calculator(playerDeck) > 21)
          {
              playerBust = true;
              Console.WriteLine("플레이어 카드 버스트! 21이 넘었습니다!");
          }
      }
      else if(userInput == "N")
      {
          break;
      }else
      {
          Console.Write("잘못된 입력입니다. 다시 입력해주세요!");
      }

      Console.Clear();
  }

  // 딜러 카드 계산단계
  while (Calculator(dealerDeck) < 17)
  {
      dealerDeck.Add(Shuffle(deck));
      
      if(Calculator(dealerDeck) > 21)
        {
            GameOver(playerDeck, dealerDeck);
        }
  }

  Console.Clear();
  Console.WriteLine("플레이어 손패: " + string.Join(seperator, playerDeck));
  Console.WriteLine("딜러 손패: " + string.Join(seperator, dealerDeck));

  //Log
  //foreach (string player in playerDeck)
  //{
  //    Console.WriteLine(player);
  //}

  //foreach (string deeler in dealerDeck)
  //{
  //    Console.WriteLine(deeler);
  //}

  GameOver(playerDeck, dealerDeck);

}

 

앞에서 카드리스트를 만들어 둔 걸 가져온후 셔플을 진행하고, list로 열어둔 player와 dealer에게 각각 카드를 두장 씩 나눠준다. 이후 while문을 통해 루프를 열어주고, player의 패를 두 장 출력, dealer는 한 장만 보여주도록 한다.

 

먼저 player부터 카드를 Hit할지 안할지 선택, 카드를 Hit할 경우 add해주고, 하지 않을 경우 break로 반복문 탈출, 다른 키를 입력할 경우 잘못된 입력이라고 돌려준다.

 

만약 21이 넘어갈 경우 플레이어 카드 버스트라는 문구와 함께 마찬가지로 playerBust함수에 true를 전달하여 반복문을 탈출, 그대로 게임이 종료된다.

 

dealer의 경우 17이전까지 카드를 받아야 하기 때문에 17이 넘어가지 않으면 카드를 드로우한다.

 

아래 줄은 매번 카드를 Hit할 때 마다 카드 손패를 보여주는 역할 및 게임 종료시 패를 다 보여주는 역할을 한다.

 

3-5 GameOver

static void GameOver(List<string> playerDeck, List<string> dealerDeck)
{
    if (Calculator(playerDeck) > 21 && Calculator(dealerDeck) > 21)
    {
        Console.WriteLine("무승부입니다!");
    }
    else if(Calculator(playerDeck) > 21)
    {
        Console.WriteLine("딜러 승리! 플레이어의 카드 버스트로 21이 넘었습니다!");
    }
    else if (Calculator(dealerDeck) > 21)
    {
        Console.WriteLine("플레이어 승리! 딜러의 카드 버스트로 21이 넘었습니다!");
    }
    else if (Calculator(playerDeck) > Calculator(dealerDeck))
    {
        Console.WriteLine("플레이어 승리! 플레이어의 수가 더 21에 가깝습니다!");
    }
    else if (Calculator(playerDeck) < Calculator(dealerDeck))
    {
        Console.WriteLine("딜러 승리! 딜러의 수가 더 21에 가깝습니다!");
    }
    else
    {
        Console.WriteLine("무승부입니다!");
    }
}

 

마지막 게임종료함수이다. 먼저 플레이어덱과 딜러덱 둘 다 21을 초과할 경우 무승부를 줬고, 21넘은 쪽의 반대가 이기게끔 if문을 설계했다. 만약 둘 다 버스트되지 않았을 경우, 숫자가 큰 쪽이 21과 가깝기 때문에 큰 쪽을 승리하게끔 설계했다.

 

3-6 전체 코드

 

using System.Threading.Channels;

namespace _3_10HomeWorkBlackJack
{
    /// <summary>
    /// 블랙 잭
    /// </summary>
    internal class Program
    {
        //카드 덱
        //SelectMany 를 사용, 2 + 4가지모양, 3 + 4가지 모양 ... A +  4가지 모양을 다시 List로 반환하는 코드
        static List<string> InitializeDeck()
        {
            string[] ranks = new[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" };
            string[] suits = new[] { "♠", "♥", "◇", "♣" };
            return ranks.SelectMany(rank => suits, (rank, suit) => rank + suit).ToList();
        }
        
        // 게임 실행 함수
        static void PlayBlackJack()
        {
            List<string> deck = InitializeDeck();
            List<string> playerDeck = new List<string>();
            List<string> dealerDeck = new List<string>();

            // 플레이어 버스트시 false
            bool playerBust = false;

            string seperator = ", ";

            // 카드 나눠주기 단계
            playerDeck.Add(Shuffle(deck));
            dealerDeck.Add(Shuffle(deck));
            playerDeck.Add(Shuffle(deck));
            dealerDeck.Add(Shuffle(deck));


            // 게임 진행
            while(true)
            {
                // 나눠준 패 출력
                Console.WriteLine("플레이어 손패: " + string.Join(seperator, playerDeck));
                Console.WriteLine("딜러 손패: " + dealerDeck[0] + ", [뒷면]");
                
                // 버스트(21넘길)시 반복문 탈출 
                if (playerBust)
                {
                    break;
                }

                Console.WriteLine("카드를 Hit하시겠습니까? (Y/N)");
                string userInput = Console.ReadLine().ToUpper();

                // 플레이어 카드 계산단계
                if (userInput == "Y")
                {
                    playerDeck.Add(Shuffle(deck));
                    if (Calculator(playerDeck) > 21)
                    {
                        playerBust = true;
                        Console.WriteLine("플레이어 카드 버스트! 21이 넘었습니다!");
                    }
                }
                else if(userInput == "N")
                {
                    break;
                }else
                {
                    Console.Write("잘못된 입력입니다. 다시 입력해주세요!");
                }

                Console.Clear();
            }

            // 딜러 카드 계산단계
            while (Calculator(dealerDeck) < 17)
            {
                dealerDeck.Add(Shuffle(deck));
                
                if(Calculator(dealerDeck) > 21)
                {
                    GameOver(playerDeck, dealerDeck);
                }
            }

            Console.Clear();
            Console.WriteLine("플레이어 손패: " + string.Join(seperator, playerDeck));
            Console.WriteLine("딜러 손패: " + string.Join(seperator, dealerDeck));

            //Log
            //foreach (string player in playerDeck)
            //{
            //    Console.WriteLine(player);
            //}

            //foreach (string deeler in dealerDeck)
            //{
            //    Console.WriteLine(deeler);
            //}

            GameOver(playerDeck, dealerDeck);

        }

        // 게임 종료 함수
        static void GameOver(List<string> playerDeck, List<string> dealerDeck)
        {
            if (Calculator(playerDeck) > 21 && Calculator(dealerDeck) > 21)
            {
                Console.WriteLine("무승부입니다!");
            }
            else if(Calculator(playerDeck) > 21)
            {
                Console.WriteLine("딜러 승리! 플레이어의 카드 버스트로 21이 넘었습니다!");
            }
            else if (Calculator(dealerDeck) > 21)
            {
                Console.WriteLine("플레이어 승리! 딜러의 카드 버스트로 21이 넘었습니다!");
            }
            else if (Calculator(playerDeck) > Calculator(dealerDeck))
            {
                Console.WriteLine("플레이어 승리! 플레이어의 수가 더 21에 가깝습니다!");
            }
            else if (Calculator(playerDeck) < Calculator(dealerDeck))
            {
                Console.WriteLine("딜러 승리! 딜러의 수가 더 21에 가깝습니다!");
            }
            else
            {
                Console.WriteLine("무승부입니다!");
            }
        }

        // 카드 덱을 가져와 랜덤한 값으로 셔플해줌
        static string Shuffle(List<string> deck)
        {
            Random random = new Random();

            int index = random.Next(0, deck.Count);

            string card = deck[index];

            return card;
        }

        //계산 함수
        static int Calculator(List<string> handCard)
        {
            int totalValue = 0;
            int aceCount = 0;

            foreach(string card in handCard)
            {
                string rank = card.Substring(0, card.Length - 1);

                if (int.TryParse(rank, out int cardValue))
                {
                    totalValue += cardValue;
                }
                else if(rank == "A")
                {
                    totalValue += 11;
                    aceCount++;
                }
                else
                {
                    totalValue += 10;
                }
            }

            while(aceCount > 0 && totalValue > 21)
            {
                totalValue -= 10;
                aceCount--;
            }

            return totalValue;
        }


        static void Main(string[] args)
        {
            Console.WriteLine("========BLACK JACK!========");
            Console.WriteLine("블랙잭 게임 스타트!");

            // 게임 스타트, 게임 시작시 PlayBlackJack() 호출, 끝날 경우 계속 플레이하시겠습니까 호출
            while (true)
            {
                PlayBlackJack();

                Console.WriteLine("계속 플레이하시겠습니까? (Y/N)");
                if(Console.ReadLine().ToUpper() != "Y")
                {
                    break;
                }
                Console.Clear();
            }
            Console.WriteLine("게임 종료");
        }
    }
}

4. 마무리 및 회고

블랙잭을 만들면서 TryParse라던지 setMany등 새로운 메서드들을 활용하는 것을 공부했고, 처음에 블랙잭 게임 스타트가 잠깐 출력됐다가 이후 사라지는 문제를 해결하지 못하여 튜터님의 힘을 빌린 결과 Console.Clear()의 위치 순서 문제였다. 그리고 계산식 부분은 풀이를 참고했는데 여기서 acecount를 따로 줘서 ace가 있을 때 21 넘어갈 시 acecount를 이용해 점수를 조절하는 부분이 정말 와닿는 부분이었다. 

 

큰 틀이나 규칙을 이용해 흐름 및 자잘한 부분은 직접했지만, 구글이나 주변 사람들의 힘을 많이 받아 아직까지도 부족하다는 점을 많이 느낀다. 하지만, 이를 빌려오는 것 뿐만 아니라 재해석하고, 내 것으로 만드는 것이 더 중요하다고 생각해 포스트를 하게 되었고, 이렇게 코드 리뷰를 하면서 다른사람은 코드를 어떻게 짜는지, 이런 방법도 있다는 것을 얻어갈 수 있게 되는 계기가 되었다.