👾 내일배움캠프/🎮 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 형태인 것이 훨씬 깔끔하게 돼서 바꾸었다.
- 그리고 플레이어로부터 입력받을 좌표 inputX와 inputY, 입력된 좌표를 배열의 인덱스로 쓰기 위한 int 형태의 변수 x, y도 선언했다.
- computerX와 computerY는 컴퓨터 차례에 랜덤으로 뽑을 좌표가 될 친구들이다.
- 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일 때는 빈 칸을 그리고, 값이 있을 때는 해당 값을 그리게 된다.
[컴퓨터의 선택]
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"값을 갖게 된다.
[판별]
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구문은 남은 내용을 다 실행하고 끝나기 때문
[결과물 및 후기]
- 터미널 창이 이상해서 마지막 결과 화면을 스크롤하지 않으면 제대로 뜨지 않는 문제가 있지만, 아무튼 코드는 잘 돌아간다.
- 플레이어가 입력한 직후에 컴퓨터의 선택이 출력되니 함께 플레이한다는 느낌이 들지 않는다는 점이 아쉽다. 시간 텀을 주고 "컴퓨터 선택 중..."과 같은 문구를 출력하면 더 재미있을 것 같다.
- 코드 깔끔하게 짜는 법 알아오기... 다른 분들의 코드 많이 구경하러 다녀야겠다.