👾 내일배움캠프/🎮 TIL & WIL

내일배움캠프 (휴일) TIL - Tic Tac Toe (틱택토 게임) C#

리리핸 2023. 8. 15. 23:42

어제 집중을 너무 못해서 오늘로 과제가 미뤄졌다.

 

이번 과제는 틱택토 게임 만들기인데, 해본 적 없는 과제라 재밌었다. (야구게임, 홀짝문제, 어쩌구저쩌구 등 어떤 언어든지 반복적으로 등장하는 과제들이 있는데, 언어 찍먹 유목민에게는 조금 지루한 과제들이다.)

 

암튼 코드로 그림 그려보기?

가보자고

 

[기본 세팅]

 

string[,] board = new string[3, 3];

string inputX;
string inputY;
int x;
int y;

int computerX;
int computerY;

bool isRunning = true;
string winStatus = "";
int playCount = 0;

 

  • 우선 보드를 만들기 위해 다차원 배열을 생성했다. 처음에는 int 배열로 생성했으나, 보드의 모양과 보드에 기록된 내용을 출력하는 데는 배열의 값이 string 형태인 것이 훨씬 깔끔하게 돼서 바꾸었다.
  • 그리고 플레이어로부터 입력받을 좌표 inputXinputY, 입력된 좌표를 배열의 인덱스로 쓰기 위한 int 형태의 변수 x, y도 선언했다.
  • computerXcomputerY컴퓨터 차례에 랜덤으로 뽑을 좌표가 될 친구들이다.
  • isRunning은 보드에서 가로 혹은 세로, 대각선의 값이 일치할 때 게임을 중단시키기 위한 변수이고, winStatus는 누가 이겼는지 판별한 후 출력되는 문구를 위한 변수이다.
  • playCount는 이긴 사람 없이 칸이 다 찰 경우를 대비해서 플레이 횟수를 측정하는 변수

 


 

[보드 그리기]

 

void PrintBoard()
{
    Console.WriteLine();
    Console.WriteLine("     1   2   3   x");
    Console.WriteLine();

    for (int i = 0; i < 3; i++)
    {
        Console.Write(" " + (i + 1) + "  ");
        for (int j = 0; j < 3; j++)
        {
            if (board[j, i] == null)
            {
                Console.Write("   ");
            }
            else
            {
                Console.Write(" {0} ", board[j, i]);
            }
            if (j < 2)
            {
                Console.Write("|");
            }
            
        }
        Console.WriteLine();
        if (i < 2)
        {
            Console.WriteLine("    -----------");
        }
    }

    Console.WriteLine();
    Console.WriteLine(" y");
}

 

  • 다음으로 보드 출력을 위한 메서드를 만들었다. 
  • 중간 중간의 Console.WriteLine();은 그냥 콘솔창에서의 가독성을 높이기 위해 빈 줄을 삽입한 것
  • Console.WriteLine("     1   2   3   x");는 표 위에 x좌표를 표시하기 위한 코드 (이미지 참고)
  • for문 안의 Console.Write(" " + (i + 1) + "  ");와 마지막의 Console.WriteLine(" y");는 각 행 앞에 y좌표를 표시하기 위한 코드 (이미지 참고)
  •  for (int i = 0; i < 3; i++)은 각 마다 반복되는 행위를 위한 반복문, for (int j = 0; j < 3; j++)은 각 마다 반복되는 행위를 위한 반복문으로, i == 0일 때 (첫 번째 행) j == 0, 1, 2 (각 열)의 과정을 거친 후 다음 행으로 넘어간다. 
  • j for문 안에 있는 if (j < 2) 조건은 마지막 열이 아닐 경우 열을 구분하는 세로 선을 그려내기 위함
  • i for문의 if (i < 2) 또한 마지막 행이 아닐 경우 구분선을 그려넣기 위함
  • 배열 요소의 초기값은 null인데, 플레이어가 선택하거나 컴퓨터에 의해 랜덤하게 뽑힌 좌표는 각각 "O"와 "X"값이 할당되는 구조이다. (아래 기술된 메서드들) 이에 따라 null일 때는 빈 칸을 그리고, 값이 있을 때는 해당 값을 그리게 된다.

 

출력된 보드 모습. x y 좌표를 표시. (컴퓨터가 먼저 랜덤한 좌표를 찍은 모습이기 때문에 (3, 2) 자리에 X가 있음)

 

 


 

[컴퓨터의 선택]

 

void ComputerSelect()
{
    do
    {
        computerX = new Random().Next(0, 3);
        computerY = new Random().Next(0, 3);

    } while (board[computerX, computerY] == "O" || board[computerX, computerY] == "X");

    board[computerX, computerY] = "X";

    PrintBoard();
    ++ playCount;
}

 

  • 컴퓨터의 선택은 Random().Next()를 통해 랜덤한 값을 받도록 했다. 
  • 해당 수를 인덱스로 갖는 board[] 배열의 요소가 "O"나 "X"값을 가지면 랜덤한 값을 뽑는 실행을 반복하도록 했다 (do while문 활용). 즉, 이미 플레이어나 컴퓨터에 의해 선점된 자리가 선택되면 다시 뽑아서 겹치지 않는 인덱스를 선택할 수 있도록 했다.
  • 반복을 탈출한 (겹치지 않는) 인덱스가 선택되면 해당 요소에 "X"값을 넣어 컴퓨터에 의해 선택된 자리임을 표시하게 했다. (위의 사진에서는 X가 표시된 자리인 (2, 1)을 컴퓨터가 먼저 뽑은 것)

 


 

[플레이어의 입력 받기]

 

void PlayerInput()
{
    Console.WriteLine();

    do
    {
        Console.Write("x 좌표를 입력하세요 : ");
        inputX = Console.ReadLine();

    } while (!int.TryParse(inputX, out x) || int.Parse(inputX) < 1 || int.Parse(inputX) > 3);

    x = int.Parse(inputX) - 1;

    do
    {
        Console.Write("y 좌표를 입력하세요 : ");
        inputY = Console.ReadLine();

    } while (!int.TryParse(inputY, out y) || int.Parse(inputY) < 1 || int.Parse(inputY) > 3);

    y = int.Parse(inputY) - 1;


    if (board[x, y] != null)
    {
        Console.WriteLine("이미 선택된 칸입니다.");
        PlayerInput();

    }
    else
    {
        board[x, y] = "O";
        PrintBoard();
        ++ playCount;
    }

}

 

  • 코드가 본격적으로 드러워지기 시작했다. 그냥 봤을 때 더럽다는 건 알겠는데 뭘 어떻게 손대야 할지 모르겠다...
  • Console.ReadLine()으로 얻은 string 데이터를 inputX와 inputY에 저장했다. 
  • 우선 do while문을 통해 x와 y값을 각각 받도록 했다. 형식에 맞지 않는 (int로 변환되지 않거나 변환했을 때 1~3이 아닌 경우) 데이터가 주어지면 올바른 형태로 입력될 때까지 반복한다.
  • 조건이 맞다고 판단되면 inputX와 inputY를 int로 변환하여 각각 x, y에 저장했다. 이 때 플레이어 입장에서의 좌표는 1~3이지만, 배열의 인덱스는 0부터 시작하므로 1씩 빼주었다.
  • 플레이어나 컴퓨터에 의해 이미 선점된 칸을 입력하면 다시 입력값을 받도록 하기 위해 재귀메서드를 활용해보았다. 위험하다고 알고 있기 때문에 이게 괜찮은 코드인지는 모르겠다.. 일단은 문제가 없어 보인다.
  • 입력받은 좌표에 문제가 없으면 해당 좌표에 해당하는 배열 요소는 "O"값을 갖게 된다.

 

문제가 없을 경우 왼쪽과 같이 보드에 "O"가 그려져 표시되고, 컴퓨터가 이미 선점한 자리의 좌표를 입력하면 다시 입력하도록 한다. 형식에 맞지 않는 값을 입력한 경우 오른쪽의 결과.

 


 

[판별]

 

void Referee()
{
    string winner = "";

    for (int i = 0; i < 3; ++i)
    {
        if (board[i, 0] == board[i, 1] && board[i, 0] == board[i, 2])
        {
            winner = board[i, 0];
        }
        else if (board[0, i] == board[1, i] && board[0, i] == board[2, i])
        {
            winner = board[0, i];
        }
    }
    if (board[0,0] == board[1,1] && board[0,0] == board[2,2])
    {
        winner = board[0, 0];
    }
    else if (board[0, 2] == board[1,1] && board[0,2] == board[2, 0])
    {
        winner = board[0, 2];
    }
    else if (playCount == 9)
    {
    	winner = "?";
    }

    if (winner != "")
    {
        switch (winner)
        {
            case "X":
                winStatus = "You Lose!";
                isRunning = false;
                break;
            case "O":
                winStatus = "You Win!";
                isRunning = false;
                break;
            case "?":
            	winStatus = "Tied!";
                winStatus = "You Win!";
                isRunning = false;
            default:
                break;
        }

        Console.WriteLine();
        Console.WriteLine(winStatus);
    }

 

  • 플레이어의 실행과 컴퓨터의 실행에 따른 보드 그리기를 할 완료는 준비되었다. 이제 플레이어든 컴퓨터든 실행을 하고 나면 세 칸이 같은 값으로 이어지진 않았는지 판단할 메서드를 만들 차례다.
  • 만들긴 했는데 진짜 심각하게 더럽다. 나중에 다른 사람들 코드 구경하러 가야지
  • winner라는 문자열 변수를 만들어 세 칸이 같은 값으로 이어진 경우 그 내용이 무엇인지 (누가 이겼는지) 판별하도록 했다.
  • 안의 내용은... 읽어보면 이해 가능한 내용들이지만 진짜 읽기 싫게 생겼다. 어떻게 이렇게 더럽게 쓰지?
    • 암튼 첫 for문은 0~2까지 반복하여 열이 같거나 행이 같은 줄이 있는지 확인하는 코드
    • 그 아래의 if와 else if는 대각선의 경우를 고려한 코드
    • 마지막 조건문과 그 안의 switch문은 게임 종료 여부 및 출력 문구를 결정. "X" 한 줄 혹은 "O"한 줄이 있다고 판단되면 isRunning 값을 false로 바꾸어 게임을 종료시킨다(아래쪽의 게임 실행에서 확인). (winner != "")이라는 조건은 꼭 안 넣어도 될 것 같지만 (어차피 default에 걸려 별 실행을 하지 않기 때문) 그냥 확실하게 하고 싶어서 빈칸들을 한 줄로 인식하는 경우를 제외시켰다.
    • playCount가 9일 경우에도 게임이 종료되도록. 문구는 "Tied!"

 


 

[ 게임 실행 ]

 

while (isRunning == true)
{
    ComputerSelect();
    Referee();
    if (isRunning == false)
    {
        break;
    }
    PlayerInput();
    Referee();
    if (isRunning == false)
    {
        break;
    }
}

 

  • isRunning의 초기값은 true이다. Referee()에 의해 "X" 한 줄 혹은 "O" 한 줄이 있다고 판단될 경우 false로 값이 바뀐다. 그렇기 때문에 먼저 한 줄이 완성되기 전까지는 해당 구문을 반복한다.
  • 컴퓨터가 먼저 선택하고 플레이어가 이어 선택하도록 했다. 각 실행 사이에 Referee()를 넣어 칸을 선택하는 행위가 일어날 때마다 게임 종료 여부를 판단하도록 했다. 이 때 Referee() 내부에서 isRunning이 false로 바뀌면 while구문이 종료될 수 있도록 break를 걸었다. isRunning이 false로 바뀐다 하더라도 while구문은 남은 내용을 다 실행하고 끝나기 때문

 


 

[결과물 및 후기]

 

  • 터미널 창이 이상해서 마지막 결과 화면을 스크롤하지 않으면 제대로 뜨지 않는 문제가 있지만, 아무튼 코드는 잘 돌아간다.
  • 플레이어가 입력한 직후에 컴퓨터의 선택이 출력되니 함께 플레이한다는 느낌이 들지 않는다는 점이 아쉽다. 시간 텀을 주고 "컴퓨터 선택 중..."과 같은 문구를 출력하면 더 재미있을 것 같다.
  • 코드 깔끔하게 짜는 법 알아오기... 다른 분들의 코드 많이 구경하러 다녀야겠다.