* 본문은 Microsoft Visual Studio 2022 버전 17.6.3를 사용하였으므로 다른 버전의 비쥬얼 스튜디오나 다른 C언어 작동 프로그램으로 작동할 경우 오류가 생길 수 있음을 양지해주세요.
1학기 과목에서 학습한 챕터는 구조체와 포인터..까지였으나 (C언어 익스프레스 개정 4판에서 15장까지)
배열만을 주로 사용했다. 나중에 더 고차원적인 코딩을 할 수 있게 된다면, 포인터와 배열 (둘의 관계가 밀접하므로)을
적절히 사용하여 결과물을 만들 수 있을 것이라고 기대된다.
1. 프로그램의 개요
이 프로그램은 오목 프로그램이다. 단, 규칙 규현의 문제로 인해 오목, 혹은 육목을 완성하면 이기는 방식으로 되어있다. (가로, 세로, 대각선 방향으로 연속 5개, 6개가 완성되면 종료하는 방식이고, 종료을 위한 배열 조건을 이중 for문을 이용해서 배열에 저장한다.)결과적으로 3.3, 4.4, 장목이 허용되므로 이는 프리룰에 기반하는 것이라고 할 수 있다. 더 수준높은 알고리즘을 구현한다면 렌주룰(3.3, 4.4, 장목이 허용되지 않는 룰) 또는 소시로프-8룰(렌주룰에서 진화된 것이다. 흑은 필승법이 언제나 존재하기 때문에 초반 몇 수를 흑이 정해준 범위 내에서 백이 골라준다.)을 구현할 수 있을 것이다. 또한 사용자의 클릭에 반응하는 GUI도 설계 가능할 것으로 기대된다.
제목은 ‘2인용’ 오목 프로그램이지만, 시작시에 한 가지 선택지가 더 등장하는데, 바로 ‘AI와 두기’이다. 이는 난수를 0~9까지 발생시켜 행과 열에 각각 대입시킨다. 말은 AI와 두기이지만 그저 무작위 대입 수준이다.
필자의 프로그래밍 여건상 GUI를 적용시킬 수는 없기 때문에 x,y좌표를 입력하여 (행, 열과는 반대다.) 착수하는 방식을 사용한다.
2. 블록 다이어그램 (플로우차트)
3. 소스코드와 그 설명
일단 소스코드를 나열하고 설명하기 전에 말할 것은, 코드가 굉장히 길다(..) 무려 380줄이나 되는데 필자의 축약 능력이 부족해서일지도 모른다. 이렇게 긴 코드를 작성한건 처음이기에 그 쪽에 의의를 두고 있다.
printgame(), 초기 19x19 오목 틀 생성 함수 (line 309-389)
void printgame() {
int j, k, l;
// 가로줄 숫자 표시
printf(" ");
for (l = 0; l < 9; l++) {
printf("%d ", l + 1);
}
for (l = 9; l < 19; l++) {
printf("%d", l + 1);
}
printf("\n");
for (j = 0; j < 19; j++) {
// 세로줄 숫자 표시
if (j < 9) {
printf("%d ", j + 1);
}
if (j >= 9) {
printf("%d", j + 1);
}
// 오목판 생성
if (j == 0) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'┌';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┐';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┬';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j == 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'└';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┘';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┴';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j != 0 && j != 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'├';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┤';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┼';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
}
}
일단 살펴보기 전에, 사진을 통해 행과 열의 개념을 정립해보자.
j는 행을 뜻하는 변수고, k는 열을 뜻하는 변수가 된다. (l은 흔히 말하는 i같은 단순 반복을 위한 변수다.)
좌표상 j는 y좌표고 k는 x좌표다.
line 310~320 : 가로줄(행) 숫자 표시
int j, k, l;
// 가로줄 숫자 표시
printf(" ");
for (l = 0; l < 9; l++) {
printf("%d ", l + 1);
}
for (l = 9; l < 19; l++) {
printf("%d", l + 1);
}
printf("\n");
일단 처음 2번째 글자까지는 행과 열이 교차하는 지점이므로 공백을 두 칸 정도 띄어준다.
그리고 l이 0부터 8이 될 때까지 l+1을 출력해주어서 1, 2, 3, ..., 9를 뒤에 공백 하나 넣어서 출력해준다.
그리고 10부터는 이미 두 글자여서 공백이 필요없으므로 l이 9부터 18이 될 때까지 l+1인 10, 11, 12, ..., 19를 공백 없이 출력한다.
line 322~329 : 세로줄(열) 숫자 표시
for (j = 0; j < 19; j++) {
// 세로줄 숫자 표시
if (j < 9) {
printf("%d ", j + 1);
}
if (j >= 9) {
printf("%d", j + 1);
}
일단 세로줄 표시는 오목판과 함께 생성되기 때문에, 공백이 필요 없다. (화면상 둘째줄부터 표시)
j=0부터 18까지 (1행부터 19행까지) 진행되는데, 9보다 작으면 (10행 미만이면) 공백을 포함해 출력하고
9보다 크거나 같으면 (10행 이상이면) 공백을 포함하지 않고 출력한다.
line 322, 331~389 : 본격적인 오목판 생성
for (j = 0; j < 19; j++) {
// ... (세로줄 표시 코드 생략)
// 오목판 생성
if (j == 0) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'┌';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┐';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┬';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j == 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'└';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┘';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┴';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j != 0 && j != 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
s[j][k] = L'├';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
s[j][k] = L'┤';
wprintf(L"%lc", s[j][k]);
}
else {
s[j][k] = L'┼';
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
}
잘 보면, 배열에 대입하고 그 배열을 호출하여 출력하는데 wprint, L, %lc가 사용되는걸 알 수 있다.
(물론 main 함수 맨 앞에 추가되는 요소가 더 있다.)
이는 그냥 특수문자를 대입해버리면 문자가 깨져버리기 때문에, 이런 식으로 작성해줘야 하는 것이다.
j가 0(1행)일 때, k가 0(1열)이면 ┌를 배열에 저장하고 출력하고 공백을 뒤에 붙인다.
k가 18(19열)이면 ┐를 배열에 저장하고 출력한다.
그 외의 열(2~18열)이면 ┬를 출력하고 공백을 뒤에 붙인다. 그리고 줄을 바꾼다.
그러므로 1행은 ┌ ┬ ┬ ┬ ┬....┬ ┬ ┬ ┐와 같이 출력된다.
j가 18(19행)일 때, k가 0(1열)이면 └를 배열에 저장하고 출력하고 공백을 뒤에 붙인다.
k가 18(19열)이면 ┘를 배열에 저장하고 출력한다.
그 외의 열(2~18열)이면 ┴를 출력하고 공백을 뒤에 붙인다. 그리고 줄을 바꾼다.
그러므로 19행은 └ ┴ ┴ ┴ ┴...┴ ┴ ┴ ┘와 같이 출력된다.
j가 0도 18도 아니라면(1,19행이 아니라면),
k가 0(1열)이면 ├를 배열에 저장하고 출력하고 공백을 뒤에 붙인다.
k가 18(19열)이면 ┤를 배열에 저장하고 출력한다.
그 외의 열(2~18열)이면 ┼를 출력하고 공백을 뒤에 붙인다. 그리고 줄을 바꾼다.
그러므로 19행은 ├ ┼ ┼ ┼ ┼... ┼ ┼ ┼ ┤와 같이 출력된다.
이로써 행과 열의 번호도 매기고, 특수문자를 이용해 판도 만듦으로써, 19x19 오목판이 생성되었다.
editgame(), 19x19 오목 틀 수정 함수 (line 227- 299)
void editgame() {
int j, k, l;
// 가로줄 숫자 표시
printf(" ");
for (l = 0; l < 9; l++) {
printf("%d ", l + 1);
}
for (l = 9; l < 19; l++) {
printf("%d", l + 1);
}
printf("\n");
for (j = 0; j < 19; j++) {
// 세로줄 숫자 표시
if (j < 9) {
printf("%d ", j + 1);
}
if (j >= 9) {
printf("%d", j + 1);
}
// 오목판 표시
if (j == 0) {
for (k = 0; k < 19; k++) {
if (k == 0) {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
wprintf(L"%lc", s[j][k]);
}
else {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j == 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
wprintf(L"%lc", s[j][k]);
}
else {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
if (j != 0 && j != 18) {
for (k = 0; k < 19; k++) {
if (k == 0) {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
else if (k == 18) {
wprintf(L"%lc", s[j][k]);
}
else {
wprintf(L"%lc", s[j][k]);
printf(" ");
}
}
printf("\n");
}
}
}
앞서 설명한 printgame()를 베이스로 저장된 함수다. printgame()은 최초 1회에만 구성시에만 필요하다. 왜냐하면 이미 배열에 문자들이 저장 되었기 때문에 ┼ 등의 문자를 다시 배열에 중복 대입할 필요가 없다. 그 대신에 배열을 다시 불러오는 것이 효율적이다. 위 editgame()함수는 기존 printgame()함수에서 ┼ 등을 출력하는 함수를 제거하고, 오직 배열만을 불러온다.
또한, 나중에는 흑돌, 백돌을 ┼ 등의 문자 대신에 배열에 대입한다. printgame()함수를 그대로 사용한다면 기껏 배열에 등록한 흑돌이 다시 ┼로 바뀌어 버리는 불상사가 발생할 수도 있다.
whowin(), 누가 이겼는지 판단하는 함수 (line 149-225)
void whowin() {
int m, n;
// 흑돌: 가로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[m][n] == L'●' && s[m][n + 1] == L'●' && s[m][n + 2] == L'●' && s[m][n + 3] == L'●' && s[m][n + 4] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
// 흑돌: 세로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[n][m] == L'●' && s[n + 1][m] == L'●' && s[n + 2][m] == L'●' && s[n + 3][m] == L'●' && s[n + 4][m] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
// 흑돌: 좌->우 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[n][m] == L'●' && s[n + 1][m + 1] == L'●' && s[n + 2][m + 2] == L'●' && s[n + 3][m + 3] == L'●' && s[n + 4][m + 4] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
// 흑돌: 우->좌 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 4; n < SIZE; n++) {
if (s[n][m] == L'●' && s[n - 1][m + 1] == L'●' && s[n - 2][m + 2] == L'●' && s[n - 3][m + 3] == L'●' && s[n - 4][m + 4] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
// 백돌: 가로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[m][n] == L'○' && s[m][n + 1] == L'○' && s[m][n + 2] == L'○' && s[m][n + 3] == L'○' && s[m][n + 4] == L'○') {
printf("\n게임이 종료 되었습니다. 백의 승리!");
exit(0);
}
}
}
// 백돌: 세로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[n][m] == L'○' && s[n + 1][m] == L'○' && s[n + 2][m] == L'○' && s[n + 3][m] == L'○' && s[n + 4][m] == L'○') {
printf("\n게임이 종료 되었습니다. 백의 승리!");
exit(0);
}
}
}
// 백돌: 좌->우 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[n][m] == L'○' && s[n + 1][m + 1] == L'○' && s[n + 2][m + 2] == L'○' && s[n + 3][m + 3] == L'○' && s[n + 4][m + 4] == L'○') {
printf("\n게임이 종료 되었습니다. 백의 승리!");
exit(0);
}
}
}
// 백돌: 우->좌 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 4; n < SIZE; n++) {
if (s[n][m] == L'○' && s[n - 1][m + 1] == L'○' && s[n - 2][m + 2] == L'○' && s[n - 3][m + 3] == L'○' && s[n - 4][m + 4] == L'○') {
printf("\n게임이 종료 되었습니다. 백의 승리!");
exit(0);
}
}
}
}
코드의 주석을 보면 알 수 있듯이, 우리가 배열에 등록해야 할 승리 조건은 다음과 같다.
(1) 가로 5개 이상 연속 완성
(2) 세로 5개 이상 연속 완성
(3) 대각선 좌->우 5개 이상 연속 완성
(4) 대각선 우->좌 5개 이상 연속 완성
5개 '이상'이므로 5개부터 19개까지 연속되는 경우를 다 찾아야할까? 아니다.
5개 이상 연속 완성 여부를 구할 때는, 연속 5개인지 판단하기만 하면 된다.
즉, 다음과 같은 상황을 가정해보자.
i) 5개 완성 여부 판단
OOOOO
5개가 연속이므로 당연히 연속이다.
ii) 6개 완성 여부 판단
OOOO O
중간에 하나가 비어있다. 그리고 나머지 하나를 채워보자.
OOOOOO
6개 연속이 되었다! 그러므로 1,2,3,4,5의 경우와, 2,3,4,5,6의 경우가 역시 5개 연속이다.
그러므로 6개 연속인지를 판단하는 코드를 넣지 않아도 5개 연속임을 판단하는 코드만으로
5~19개 연속임을 판단할 수 있는 것이다.
(당연히 프리룰로 진행하니까 가능한 것이고, 흑이 장목(육목 이상)을 두지 못하는 렌주룰에서는 좀 더 복잡해질 것이다.)
그렇다면 이제, 위의 전제를 토대로 모든 5개 연속의 경우의 수를 구해주면 된다.
흑돌, 백돌 코드 모두 나열되어있지만 특수문자만 다르기 때문에 흑돌만 알아보겠다.
line 152~160 : 가로줄 5개 연속 완성 여부
// 흑돌: 가로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[m][n] == L'●' && s[m][n + 1] == L'●' && s[m][n + 2] == L'●' && s[m][n + 3] == L'●' && s[m][n + 4] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
우선, 앞서 말했었지만 SIZE를 define한 것이 여기서 빛을 발한다. for 문에서의 활용이 아주 간편하다.
가로줄 5개 연속 완성 여부를 판단하기 위해서, 첫번째 행만 본다면
[0][0] [0][1] [0][2] [0][3] [0][4] 부터 [0][14] [0][15] [0][16] [0][17] [0][18]까지 가능하다. 즉 0~14까지 진행되므로
기존 SIZE인 18보다 4 작다. 그래서 n(열)이 SIZE-4가 되는 것이다. 부등호가 '<'인 것은 인덱스가 0부터 시작되므로 SIZE가 애초에 마지막 인덱스보다 1 크다는 것에서 기인한다.
행은 19행까지 있으므로 이것이 18번 m(행) 인덱스까지 진행되면 된다. 역시 마지막 m(행) 인덱스가 SIZE인 19보다 1 작기 때문에 부등호는 '<'다.
즉 나열해 보자면..
[0][0] [0][1] [0][2] [0][3] [0][4] … [0][14] [0][15] [0][16] [0][17] [0][18]
.
.
.
[18][0] [18][1] [18][2] [18][3] [18][4] … [18][14] [18][15] [18][16] [18][17] [18][18]
의 형식으로 정리된다.
만약 위 조건이 만족되면 exit(0)로 실행이 종료된다.
line 152~160 : 세로줄 5개 연속 완성 여부
// 흑돌: 세로 줄 5개 완성 여부
for (m = 0; m < SIZE; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[n][m] == L'●' && s[n + 1][m] == L'●' && s[n + 2][m] == L'●' && s[n + 3][m] == L'●' && s[n + 4][m] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
세로줄은 가로줄과 매우 유사하다. 그저 아까의 배열에서 90도 회전한 꼴(행과 열이 반전됨)이라고 생각하면 된다.
즉 나열해 보자면..
[1][0] [2][0] [3][0] [4][0] [5][0] … [14][0] [15][0] [16][0] [17][0] [18][0]
.
.
.
[0][18] [1][18] [2][18] [3][18] [4][18] … [14][18] [15][18] [16][18] [17][18] [18][18]
의 형식으로 정리된다.
line 170~178 : 좌->우 대각선 5개 연속 완성 여부
// 흑돌: 좌->우 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE - 4; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[m][n] == L'●' && s[m + 1][n + 1] == L'●' && s[m + 2][n + 2] == L'●' && s[m + 3][n + 3] == L'●' && s[m + 4][n + 4] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
대각선부터는 비교적 어렵다. 쉽게 y=-x에 해당하는 대각선을 배치해준다고 생각하면 된다.
이번에 SIZE-4를 둘 다 적용시킨 이유는, 배열 인덱스의 최댓값이 배열의 크기보다 크면 안되기 때문이다.
가로 세로의 경우에는 배열에 모두 m만 있었지만 여기는 m+4까지 있으므로 가로 세로에서 n+4까지 있어 n에 SIZE-4를 적용시킨 것처럼, 대각선 상에는 m+4까지 있으므로 m에도 SIZE-4를 적용시켜 준다.
나열해보면,
[0][0] [1][1] [2][2] [3][3] [4][4] … [0][14] [1][15] [2][16] [3][17] [4][18]
.
.
.
[18][0] [18][1] [18][2] [18][3] [18][4] … [18][14] [18][15] [18][16] [18][17] [18][18]
의 형식으로 정리된다.
line 179~187 : 우->좌 대각선 5개 연속 완성 여부
// 흑돌: 우->좌 대각선 줄 5개 완성 여부
for (m = 0; m < SIZE - 4; m++) {
for (n = 0; n < SIZE - 4; n++) {
if (s[m][n + 4] == L'●' && s[m + 1][n + 3] == L'●' && s[m + 2][n + 2] == L'●' && s[m + 3][n + 1] == L'●' && s[m + 4][n] == L'●') {
printf("\n게임이 종료 되었습니다. 흑의 승리!");
exit(0);
}
}
}
우->좌 대각선을 다룰 때 가로<->세로 변환같이 그냥 행과 열을 반전시킨다고 생각할 수도 있는데, 이번에는 출발점이 좀 다르다.
가로 세로는 x=0, 1,... y=0,-1,...으로 진행되므로 y = -x에 대해 대칭이다. (배열에서 y = -1을 1로 가져가고 있으므로 사실상 y = x에 대한 대칭이다. 그러므로 서로 교환이 가능한 것이다.)
하지만, 이 때는 y = -x에 대해 대칭이 아니므로 행과 열을 반전시키는 것이 통하지 않는다.
그러므로 [0][4] [1][3] [2][2] [3][1] [4][0]으로 시작해서 1씩 늘려가야 한다. (1씩 늘려가는 과정(for문)은 좌->우와 동일하다.)
나열해보면,
[0][4] [1][3] [2][2] [3][1] [4][0] ... [0][18] [1][17] [2][16] [3][15] [4][14]
.
.
.
[14][4] [15][3] [16][2] [17][1] [18][0] ... [14][18] [15][17] [16][16] [17][15] [18][14]
로 정리된다.
여기까지 오목 프로그램의 사용자 정의함수에 대해 알아봤고,
나머지 내용은 하 편에서 마저 다루도록 하겠다.
이어서 하 편 보기
https://eatstar.tistory.com/22
기말과제 - C언어로 오목 프로그램 만들기 (下-전체적 프로그램 진행)
https://eatstar.tistory.com/21 기말과제 - C언어로 오목 프로그램 만들기 (上- 헤더 파일과 사용자 정의함수) * 본문은 Microsoft Visual Studio 2022 버전 17.6.3를 사용하였으므로 다른 버전의 비쥬얼 스튜디오나
eatstar.tistory.com
전체 소스코드
https://eatstar.tistory.com/23
기말과제 - C언어로 오목 프로그램 (전체 소스코드)
* 본문은 Microsoft Visual Studio 2022 버전 17.6.3를 사용하였으므로 다른 버전의 비쥬얼 스튜디오나 다른 C언어 작동 프로그램으로 작동할 경우 오류가 생길 수 있음을 양지해주세요. 해설은 다음 링크
eatstar.tistory.com
'프로그래밍 project > C언어' 카테고리의 다른 글
기말과제 - C언어로 오목 프로그램 (전체 소스코드) (1) | 2023.06.18 |
---|---|
기말과제 - C언어로 오목 프로그램 만들기 (下-전체적 프로그램 진행) (0) | 2023.06.18 |