Unity 내일배움캠프 TIL 0816 (3) | C# 기초 | 스네이크 게임 | 블랙잭 게임 | 근데 이제 맨땅에 구현해버린

cheeseonrose·2023년 8월 16일
0

Unity 내일배움캠프

목록 보기
12/89
post-thumbnail

3주차 과제!!!
갑자기 게임 만들라고 해서 당황함

스네이크 게임

첫 번째 과제는 스네이크 게임 만들기다

규칙

  • 처음 뱀의 몸통 길이는 3
  • 별을 먹을 때마다 몸통 길이가 1씩 늘어남
  • 뱀의 머리가 자신의 몸통이나 벽에 닿으면 게임 종료

고려할 점

  1. 뱀이 이동하는 것을 어떻게 표현할 것인가
  2. 게임보드 업데이트와 방향키 입력을 어떻게 동시에 처리할 것인가

구현

  • 뱀의 이동 좌표값 계산
    • bfs 문제 풀 때 dx, dy 사용했던 것처럼 코드를 작성했다.
    • 현재 방향값(dir)에 따라 뱀을 이동시키고, 자기 자신의 몸통이나 벽에 닿으면 반복문을 끝내도록 하였다.
    static int[] dx = { 0, 1, 0, -1 };
     static int[] dy = {1, 0, -1, 0 };
     static int dir = 0;      // → : 0, ↓ : 1, ← : 2, ↑ : 3
    while (!isGameOver)
    {
    	// 게임 보드 업데이트
     	updateGameBoard(gameBoard);
         
       	// 뱀이 다음에 이동할 좌표값
       	int nextX = curX + dx[dir];
       	int nextY = curY + dy[dir];
       
       	// 벽에 닿거나 뱀의 몸통에 닿으면 게임 종료
       	if (nextX < 0 || nextY < 0 || nextX >= GAME_BOARD_SIZE || nextY >= GAME_BOARD_SIZE ||
       		gameBoard[nextX, nextY] == SNAKE)
       	{
       		Console.WriteLine("게임 종료!");
         	Console.WriteLine("뱀 길이 : " + snakeLength);
         	Console.WriteLine("먹은 별의 개수 : " + star);
         	isGameOver = true;
       	}
       	else
       	{
       		moveSnake(nextX, nextY, gameBoard);
       	}
       
       	curX = nextX;
       	curY = nextY;
       
       	Thread.Sleep(100);
    }
  • 게임보드 업데이트와 방향키 입력 동시 처리
    • 게임보드 업데이트는 MainThread에서, 방향키 입력은 새 Thread를 만들어서 처리했다.
    • 둘 다 while문 이용
       // 방향키 입력을 받는 Thread 생성
        Thread thread = new Thread(() => getDirection());
        thread.Start();
      static void getDirection()
      {
      	ConsoleKeyInfo input;
          
          while (true)
          {
          	input = Console.ReadKey();
              
              switch (input.Key)
              {
              	case ConsoleKey.RightArrow:
                  	dir = 0;
                  	break;
                  case ConsoleKey.DownArrow:
                  	dir = 1;
                  	break;
                  case ConsoleKey.LeftArrow:
                  	dir = 2;
                  	break;
                  case ConsoleKey.UpArrow:
                  	dir = 3;
                  	break;
                  case ConsoleKey.Escape:
                  	dir = 4;
                  	Environment.Exit(0);
                  	break;
              }
          }    
       }

전체 코드

internal class Program
{ 
	const int GAME_BOARD_SIZE = 10;
    const int EMPTY = 0;
    const int SNAKE = 1;
    const int STAR = 2;
    const int FIRST_X = 5;
    const int FIRST_Y = 3;
    const int SPEED = 70;
    
    static int[] dx = { 0, 1, 0, -1 };
    static int[] dy = {1, 0, -1, 0 };
    static Queue<int[]> queue = new Queue<int[]>();   // 뱀 몸통 좌표를 담을 큐
    static int star_x = 5;  // 별의 x 좌표
    static int star_y = 7;  // 별의 y 좌표
    static int dir = 0;      // → : 0, ↓ : 1, ← : 2, ↑ : 3
    static int snakeLength = 3;
    static int star = 0;
    
    static void Main(string[] args)
    {
    	int[,] gameBoard = new int[GAME_BOARD_SIZE, GAME_BOARD_SIZE];
        
        initSnake(gameBoard);
        gameBoard[star_x, star_y] = STAR;
        
        Console.Title = "Snake Game";
        Console.WriteLine("Welcome!");
        
        Thread.Sleep(1000);
        
        int curX = FIRST_X;
        int curY = FIRST_Y;
        bool isGameOver = false;
        
        // 방향키 입력을 받는 Thread 생성
        Thread thread = new Thread(() => getDirection());
        thread.Start();
        
        while (!isGameOver)
        {
        	// 게임 보드 업데이트
            updateGameBoard(gameBoard);
            
            // 뱀이 다음에 이동할 좌표값
            int nextX = curX + dx[dir];
            int nextY = curY + dy[dir];
            
            // 벽에 닿거나 뱀의 몸통에 닿으면 게임 종료
            if (nextX < 0 || nextY < 0 || nextX >= GAME_BOARD_SIZE || nextY >= GAME_BOARD_SIZE ||
            	gameBoard[nextX, nextY] == SNAKE)
            {
            	Console.WriteLine("게임 종료!");
                isGameOver = true;
                Environment.Exit(0);
            }
            else
            {
            	moveSnake(nextX, nextY, gameBoard);
            }
            
            curX = nextX;
            curY = nextY;
            
            Thread.Sleep(SPEED);
        }
    }
    
    // 처음 뱀의 위치를 보드에 그림
    static void initSnake(int[,] gameBoard)
    {
    	gameBoard[5, 1] = SNAKE;
        gameBoard[5, 2] = SNAKE;
        gameBoard[5, 3] = SNAKE;
        
        queue.Enqueue(new int[] { 5, 1 });
        queue.Enqueue(new int[] { 5, 2 });
        queue.Enqueue(new int[] { 5, 3 }); 
    } 
    
    // 게임 보드 업데이트 
    static void updateGameBoard(int[,] gameBoard)
    {
    	Console.SetCursorPosition(0, 0);    // 보드를 (0,0)부터 그리기 위해 커서 좌표 이동
        Console.WriteLine("ESC : 종료");
        gameBoard[star_x, star_y] = STAR;   // 게임 보드 위에 별 좌표 설정
        for (int i = 0; i < GAME_BOARD_SIZE; i++)
        {
        	for (int j = 0; j < GAME_BOARD_SIZE; j++)
            {
            	int curBoard = gameBoard[i, j];
                if (curBoard == EMPTY)
                {
                	Console.Write("□"); // 빈공간
                } else if (curBoard == SNAKE)
                {
                	Console.Write("■"); // 뱀
                } else if(curBoard == STAR)
                {
                	Console.Write("★"); // 별
                }
            }
            Console.WriteLine();
        }
        
        Console.WriteLine();
        Console.WriteLine("뱀 길이 : " + snakeLength);
        Console.WriteLine("먹은 별의 개수 : " + star);
   }

	// 방향키 입력을 받아서 뱀의 이동 방향 바꾸기
    static void getDirection()
    {
    	ConsoleKeyInfo input;
        
        while (true)
        {
        	input = Console.ReadKey();
            
            switch (input.Key)
            {
            	case ConsoleKey.RightArrow:
                	dir = 0;
                	break;
                case ConsoleKey.DownArrow:
                	dir = 1;
                	break;
                case ConsoleKey.LeftArrow:
                	dir = 2;
                	break;
                case ConsoleKey.UpArrow:
                	dir = 3;
                	break;
                case ConsoleKey.Escape:
                	dir = 4;
                	Environment.Exit(0);
                	break;
            }
        }    
    }
    
    // 다음 좌표로 뱀 이동시키기 (이동할 x  좌표, 이동할 y 좌표, 게임보드)
    static void moveSnake(int x, int y, int[,] gameBoard)
    {
    	// 다음에 이동할 좌표가 빈칸일 때
        if (gameBoard[x, y] == EMPTY)
        {  
        	int[] last = queue.Dequeue();   // 뱀의 꼬리 부분 한칸 없애기
            gameBoard[last[0], last[1]] = EMPTY;
        } 
        // 다음에 이동할 좌표가 별일 때
        else if (gameBoard[x, y] == STAR)
        {
        	snakeLength++;  // 뱀의 몸 길이 증가
            star++;     // 먹은 별 개수 증가
            
            // 빈칸 중에서 임의로 골라 별 좌표 업데이트
            do
            {
            	star_x = new Random().Next(0, GAME_BOARD_SIZE);
            	star_y = new Random().Next(0, GAME_BOARD_SIZE);
            } while (gameBoard[star_x, star_y] == SNAKE);            
        }
        
        gameBoard[x, y] = SNAKE;    // 다음 이동할 좌표를 뱀으로 바꿔줌
        queue.Enqueue(new int[] { x, y });  // 뱀 몸통 업데이트
    }
}

실행 모습



블랙잭 게임

거 블랙잭씨 좀 나와봐유.
스네이크 게임이 더 오래 걸릴 줄 알았는데 스네이크는 천사였던 건에 대하여.
예외처리 지옥 시작

규칙

요약하자면 21에 가까운 숫자를 만드는 사람이 이기는 게임

  1. 카드는 총 52장
    • 스페이드, 하트, 다이아, 클로버
    • A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K
    • A는 1 또는 11
    • J, Q, K는 10
    • 나머지 숫자 카드는 숫자 그대로
  2. 딜러는 플레이어에게 카드 한 장씩을 나눠주고 본인도 카드를 받음
    • 플레이어 카드는 모두 공개
    • 딜러 카드는 1장만 공개
    • 처음에 모든 플레이어와 딜러는 카드 2장씩을 받게 됨
      ※ 이 코드에서는 플레이어 1명(나)과 딜러 1명으로 진행
  3. 플레이어는 6가지 선택을 할 수 있음
    • BlackJack(블랙잭) : 플레이어가 처음 받은 카드 조합이 A + J/Q/K 으로 11 + 10 = 21 인 경우 블랙잭을 할 수 있음
      • 만약 딜러도 블랙잭 상태이면 블랙잭 실패 -> 잃는 금액은 없음
      • 블랙잭 성공일 경우 베팅 금액의 1.5배를 얻음
    • Stand(스탠드) : 지금 플레이어 손에 있는 카드로 승패 여부를 가림
    • Hit(히트) : 플레이어는 1장의 카드를 더 받을 수 있음
      • 히트 1번에 1장씩
      • Stand 선언 전까지 계속 받을 수 있음
      • 만약 히트를 하고 받은 카드로 인해 총합이 21을 초과하면 Bust(버스트)로 패배하게 됨
    • Double Down(더블 다운) : 플레이어가 카드를 딱 1장만 더 받음
      • 베팅 금액만큼 돈을 더 걸어야 함
        (원래 베팅 금액의 2배만큼 더 거는건데 난 베팅 금액만큼 거는걸로 한듯!! 지금 규칙 적다가 깨달은 사실.. 그치만 고치기 귀찮으니까 이대로 진행시켜)
        초기 금액만큼 더 거는거라 지금 코드가 맞음!
    • Split(스플릿) : 플레이어가 가진 카드 2장이 같은 가치를 가질 때, 각각의 카드를 첫 번째 카드로 해서 새로운 카드 2장을 추가로 받을 수 있음
      • 뭔 소리냐면 만약 플레이어 카드가 ◆4랑 ♥4일 때, 카드 2장을 새로 받아서 (◆4, ♠6)과 (♥4, ♣10) 이런 식으로 나눌 수 있다는 얘기
        그러니까 플레이어는 두 개의 핸드(카드 뭉치)를 갖고 플레이하게 되는 것임
        그래서 베팅 금액만큼 돈을 더 걸어야 함 (플레이하는 핸드가 2개가 됐으니까 양쪽에 돈을 거는 것)
      • 근데 여기서 새로 뽑은 카드가 또 가치가 같으면 또 스플릿을 해서 나눠질 수 있음
      • 여기서부터 머리 아파져서 일단 스플릿은 빼기로 함
        게임 만드는 사람 맘이지 뭐 안 그래요? 네?
    • Surrender(서렌더) : 베팅 금액의 절반을 돌려받고 게임을 포기하는 것
  4. 플레이어 플레이가 끝나면 딜러 플레이가 시작됨
    • 딜러 카드 합이 17 이상인 경우 : Stand
      • 소프트 17 어쩌구 하던데 일단 빼고 함
    • 딜러 카드 합이 16 이하인 경우 : Hit
    • 딜러는 Stand나 Hit만 가능
  5. 딜러 플레이가 끝나고 플레이어 카드의 총합과 비교
    • 더 21에 가까운 사람이 승리
    • 21 초과는 패배임

아이고 머리야

고려할 점

  1. 카드 덱에서 나눠준 카드를 어떻게 표현하고 구분할 것인가
  2. 플레이어와 딜러가 들고 있는 카드는 어떻게 표현할 것인가
  3. 예외 처리 예외 처리 예외 처리 예외 처리 예외 처리 예외 처리

구현

  • 카드 덱은 Dictionary를 이용해서 표현했다.
    • 0은 스페이드, 1은 하트, 2는 다이아, 3은 클로버
    • Dictionary<int, bool[]> cardDeck의 형태
    • bool[] 배열은 1~13을 표현해야 하므로 크기는 14로 초기화
    • 예를 들어 ♥5를 나눠줬다면 cardDeck[1][5] = true 로 표시
    const int SPADE = 0;
     const int HEART = 1;
     const int DIAMOND = 2;
     const int CLOVER = 3;
     
     static Dictionary<int, bool[]> cardDeck = new Dictionary<int, bool[]>();
  • 플레이어와 딜러가 들고 있는 카드는 List<int[]> 형태로 저장
    • 이후에 Hit으로 카드를 더 뽑을 수도 있기 때문에 List에 저장했음
    • 예를 들어 현재 플레이어 손의 첫 번째 카드를 알고 싶다면 playerHand[0][0]과 playerHand[0][1]을 조합하면 됨
      • playerHand[n][0] : 카드의 모양(스페이드/하트/다이아/클로버)
      • playerHand[n][1] : 카드의 숫자(A~K)
    // player 손에 있는 카드
     static List<int[]> playerHand = new List<int[]>();
     
     // dealer 손에 있는 카드
     static List<int[]> dealerHand = new List<int[]>();
  • 진심 코드 설명할 엄두가 안 나는데요 .
    이거 안 하면 책장 뒤에서 울고 있을 미래의 내 모습이 보이니까 기록해야지.... 휴...........
  1. 베팅 금액 정하기 -> 이 부분은 설명할게 딱히 없으니 넘어감

  2. 카드 나눠주기

    • 새 카드를 뽑는 함수를 만들어서 처리했다
      • 추후에 카드 뽑을 일이 많아서 따로 함수화했음
      • 플레이어와 딜러 둘 다 카드를 뽑기 때문에 중복되는 부분이 많아서 매개변수로 누가 카드를 뽑는지 받아와서 처리함
    // Hit인 경우 새 카드를 뽑음 (새 카드를 뽑는 사람 player or dealer)
    static void getNewCard(int personType)
    {
    	int shape = new Random().Next(0, 4);
      	int cardNum = new Random().Next(1, 14);
        if (!cardDeck[shape][cardNum])
        {
        	cardDeck[shape][cardNum] = true;
          	if (personType == PLAYER_TYPE)
            {
            	playerHand.Add(new int[] { shape, cardNum });
            } else
            {
            	dealerHand.Add(new int[] { shape, cardNum });
            }
        }
    }
  3. 플레이어 플레이 시작

    • Black Jack
      • 처음에는 플레이어가 블랙잭일 때만 고려해서 분기를 플레이어가 블랙잭이 맞는 경우와 아닌 경우, 플레이어가 블랙잭일 때 딜러가 블랙잭인 경우와 아닌 경우로 나눠서 -1, 0, 1을 리턴했었다.
      • 근데 나중에 카드 총합 비교할 때 딜러가 21인 경우를 걸러내야 해서 블랙잭 함수에 블랙잭 여부를 판단하는 사람 매개변수를 받아오기로 함
        왜 걸러내야 하냐면 A가 1 또는 11로 판단되기 때문에 총합 계산 부분에서 그냥 계산해버리면 딜러 합이 21인데 11로 나오기 때문
    // BlackJack 판단 여부 (BlackJack 여부를 판단 받을 사람 player or dealer)
    static int BlackJack(int personType)
    {
    	if (personType == PLAYER_TYPE)
        {
        	if ((playerHand[0][1] == 1 && playerHand[1][1] >= 10) ||
                (playerHand[0][1] >= 10 && playerHand[1][1] == 1))
            {
            	return 1;
            }
            else
            {
            	return -1;
            }
        } else
        {
        	if ((dealerHand[0][1] == 1 && dealerHand[1][1] >= 10) ||
            	(dealerHand[0][1] >= 10 && dealerHand[1][1] == 1))
            {
            	return 1;
            }
            else
            {
            	return -1;
            }
        }           
    }
    • Stand
      • 플레이어 플레이 종료하고 딜러 플레이 시작
    • Hit
      • 새 카드를 뽑고, 그 카드로 인해 Bust 상태가 되는지 확인
      • 두 함수 다 Hit와 Bust를 체크하는 사람이 누구인지 매개변수로 받아옴
    // Hit인 경우 새 카드를 받고, Bust 여부 판단 (Hit를 한 사람 player or dealer)
    static bool Hit(int personType)
    {
    	getNewCard(personType);
        return isBust(personType);
    }
    
    // Bust 여부 판단 (Bust 여부를 판단 받을 사람 player or dealer)
    static bool isBust(int personType)
    {
    	if (personType == PLAYER_TYPE)
        {
        	int sum = 0;
            foreach (int[] card in playerHand)
            {
            	if (card[1] >= 11) sum += 10;
                else sum += card[1];
            }
            if (sum > 21) return true;
        } else
        {
        	int sum = 0;
            foreach (int[] card in dealerHand)
            {
            	if (card[1] >= 11) sum += 10;
                else sum += card[1];
            }
            if (sum > 21) return true;
        }
       	return false;
    }
    • Double Down
      • 플레이어가 돈을 더 걸 수 있는지 확인
      • 새 카드를 뽑고 Bust 여부 확인
        -> Bust면 플레이어 패배로 게임 종료, 아니면 딜러 플레이 시작
    if (playerMoney - curBetMoney < 0)
     	Console.WriteLine("베팅 금액 부족으로 Double Down 할 수 없습니다. 다시 고르세요.");
     else
     {	
        playerMoney -= curBetMoney;
        curBetMoney *= 2;
     }
     getNewCard(PLAYER_TYPE);    // DoubleDown 선언 후 새 카드를 뽑음
     isPlayerEnd = true;
     if (isBust(PLAYER_TYPE))    // Bust
     {
     	Console.WriteLine("Bust!");               
        isGameOver = true;
     } 
    • Surrender
      • 플레이어에게 베팅 금액의 절반을 돌려준 뒤 바로 게임 종료
  4. 딜러 플레이 시작

    • 플레이어 플레이 이후 isGameOver가 false이면 딜러 플레이를 시작함
    • 딜러 카드 합계 총합을 구한 뒤
      • 21 초과일 때 딜러 패배로 게임 종료
      • 17 이상이면 플레이어와 카드 합 비교
        • 이때 딜러 카드 총합이 딱 21인 경우를 Black Jack 함수로 판단
      • 16 이하이면 Hit 실행
        -> Hit 실행 후 Bust면 딜러 패배로 게임 종료
    int result = -1;    // player, dealer 승패 여부 (2일 때는 무승부)
     while (!isDealerEnd)
     {
    	// 현재 dealer 카드의 합계
        int dealerSum = 0;
        foreach (int[] card in dealerHand)
        {
        	if (card[1] >= 11) dealerSum += 10;
            else dealerSum += card[1];                   
        }
        
        // 21을 초과할 경우 딜러 패배
        if (dealerSum > BLACKJACK)
        {
        	isDealerEnd = true;
            result = PLAYER_TYPE;
        } 
        // 17 ~ 21일 경우 player와 카드 합 비교
        else if (dealerSum >= 17)
        {
        	// dealer 카드 합이 딱 21인 경우 dealer 승리
        	// player가 승리할 수는 없음 (앞에서 블랙잭인 경우를 걸러냈으므로 이 경우 무조건 player 카드 합은 21보다 작기 때문)
        	if (BlackJack(DEALER_TYPE) == 1) result = DEALER_TYPE;
        	else result = compareSum();
        	isDealerEnd = true;
       	 } 
         // 16 이하인 경우
         else
         {
         	// dealer는 히트 카드를 받고 Bust 여부 판단
            if (Hit(DEALER_TYPE))
            {
            	// Bust이면 player 승리
                isDealerEnd = true;
                result = PLAYER_TYPE;
            }
         }
     }

전체 코드

너무 .. 길어서....
여기서 확인.....
GitHub_Black_Jack_Game

실행 모습


블랙잭을 해본 적이 없어서 룰은 좀 다를 수도 있겠지만
아무튼 구현했다는 것에 의미를 두는..네....

??? : 아니 어떻게 스플릿이 없을 수가 있삼?
나 :



낡고 지쳤지만
오늘 하루도 끗!!!!!!!!!!!
마 참 내

0개의 댓글