본문 바로가기

programming/내가 만들고 싶어서 만든 것!

[최종본] C++를 활용한 오목게임 만들기 - AI모드의 OOP적 설계

반응형

1. 개요

 

 드디어 AI(?)모드를 환성하였다. 말이 AI모드지 사실상 1인 플레이 모드이다. 아주 간단한 판별 알고리즘을 사용하였다. 다행이도 오목에 관련된 알고리즘이 설명되어있는 블로그를 찾았다. 이 블로그를 참고하여 OOP적으로 설계된 AI모드의 오목게임을 만들어보았다. 다음은 내가 참고한 블로그 글의 주소이다.

 

참고자료: https://ku-hug.tistory.com/2

 

오목 AI/인공지능의 간단한 원리 및 코드

대학교 1학년때 C언어 교수님이 프로그램을 만들어 제출하라해서 만든 프로그램... 조잡하다ㅠㅠ  인공지능보단 여러가지 패턴에 가중치를 설정해서 가중치가 가장 낮은 곳에 돌을 놓는 프로

ku-hug.tistory.com

 기존에 만들어둔 Concave클래스를 상속받았기에 AI모드 역할을 하는 ConcaveAI의 기능은 몇 되지 않는다. 가중치를 설정하고 그 가중치에 따라 착수를 하는 코드가 대표적이다. 따라서 이 코드를 이해하려면 Concave Class를 꼭 본 후, ConcaveAI Class를 봐주길 바란다. 다음은 Concave Class에 대해 설명된 블로그 글이다.

 

Concave Class: apape1225.tistory.com/55

 

C++를 활용한 오목게임 만들기 - PvP모드의 OOP적 설계

1. 개요  방학 때, 들은 특강을 계기로 만들어 보았던 오목 프로그램. 학기가 지나고 같은 특강을 똑같이 신청하게 되었다. 이유는 Class에 대한 문법은 알아도 이를 활용한 OOP적인 설계는 하지 못

apape1225.tistory.com

 

2. 설계방법

 

 "방법을 알면 그것을 그대로 코드로 옮길 수 있어야 한다." 내가 프로그래밍을 공부할 때 생각하는 나만의 방향성이다. 이 목표를 이루기 위해선 보다 많은 기능을 알고 보다 다양한 사고를 알아야 할 것이다. 내가 이번에 배운 STL과 같은 것이 해당된다고 생각하며 나름 뿌듯한 느낌으로 코드를 작성했다.

 

 ConacaveAI와 ConcavePvP의 가장 큰 차이점은 AI모드에서는 사용자가 자신이 흑돌인지, 백돌인지를 선택할 수 있어야 한다는 것이다. 이를 구현하기 위해 state라는 변수를 만들어 AI가 자신의 돌인지 사용자의 돌인지를 판단하여 가중치를 설정할 수 있게 구성하였다.

 

 가중치 설정방법은 의외로 간단하였다. 물론 위에 블로그의 방법을 사용하여 그런 것일 수도 있다. 그러나 Concave클래스에서 승패를 판별하는 메소드인 checkWin()메소드를 조금 변형하였더니, 손쉽게 해결되었다. checkWin()은 5개의 연속된 돌을 찾는 메소드라면 나는 이를 약간만 변형하여 3개의 연속된 돌을 찾으면 되기 때문이다. 다음은 소스코드이다.

 

3. 소스코드

 

 일단 ConcaveAI 클래스를 보면서 구성을 설명해보겠다.

 

- ConcaveAI.h

 

#pragma once
#include "Concave.h"
#include "InfoWindow.h"
#include <conio.h>

class ConcaveAI : Concave, InfoWindow
{
private:
	array<array<int, SIZE>, SIZE> weight;
	bool player;
	bool turn = true;
	const char *black;
	const char *white;
	bool state;
	void moveDir();
	void setWeight();
	void setAI();
	void setX(int x) { posX = x; }
	void setY(int y) { posY = y; }
	void printWeight();

public:
	ConcaveAI();
	~ConcaveAI();
	void playConcave();
};

 

 playConcave()메소드는 말그대로 게임이 실행되는 메소드이다. printWeight()는 가중치를 출력하는 메소드이다. 그렇다. 나는 아이다라는 아주 훌륭한 디버깅툴이 있어도 저런 방식으로 디버깅을 진행한다...... 다음은 간단한 메소드들의 설명이다.


  • moveDir() : 사용자로부터 키를 입력받아 화면에서 커서의 위치를 이동하는 메소드
  • setWeight() : 가중치를 설정하는 메소드
  • setAI() : 가중치를 바탕으로 AI가 착수할 위치를 판별하는 메소드
  • setX(), setY() : setAI()메소드에서 사용하기 위해 작성하였다. 원하는 위치로 posX와 posY의 값을 설정하게 한다. (posX와 posY는 Concave의 변수이다.)

다음은 메소드들의 코드이다.

 

- ConcaveAI.cpp

 

#include "ConcaveAI.h"

ConcaveAI::ConcaveAI()
{
	for (int i = 0; i < SIZE; i++) {
		for (int j = 0; j < SIZE; j++)
			weight[i][j] = 0;
	}
	printInfo();
	printBord();
	printfMessage("흑돌은 0, 백돌은 1를 눌러주세요!",getX(),getY());
	int buff;
	buff = getch();
	if (buff == '1') {
		player = false;
		black = "AI의 차례입니다.";
		white = "player의 차례입니다.";
	}
	else {
		player = true;
		black = "player의 차례입니다.";
		white = "AI의 차례입니다.";
	}
		
}

ConcaveAI::~ConcaveAI()
{
}

void ConcaveAI::moveDir() {
	while (true) {
		int dir;
		dir = getch();

		switch (dir) {

		case 'w': moveX(-1);
			break;
		case 's': moveX(1);
			break;
		case 'a': moveY(-1);
			break;
		case 'd': moveY(1);
			break;
		case 'h': return;

		default:
			printfMessage("올바른 키를 입력해주세요!", getY() * 2, getX());
		}
		gotoXY(getY() * 2, getX());
	}
}

void ConcaveAI::setWeight() {
	int buff;
	if (getColor())
		weight[getX()][getY()] = 255;
	else
		weight[getX()][getY()] = 256;
	if (state) {
		buff = -1;
	}
	else {
		buff = 1;
	}

	if (checkRange(getX()-1, getY()-1) && weight[getX() - 1][getY() - 1] < 255)
		weight[getX()-1][getY()-1] += buff;

	if (checkRange(getX() - 1, getY()) && weight[getX() - 1][getY()] < 255)
		weight[getX() - 1][getY()] += buff;

	if (checkRange(getX() - 1, getY() + 1) && weight[getX() - 1][getY() + 1] < 255)
		weight[getX() - 1][getY() + 1] += buff;

	if (checkRange(getX(), getY() - 1) && weight[getX()][getY() - 1] < 255)
		weight[getX()][getY() - 1] += buff;

	if (checkRange(getX(), getY() + 1) && weight[getX()][getY() + 1] < 255)
		weight[getX()][getY() + 1] += buff;

	if (checkRange(getX() + 1, getY() - 1) && weight[getX() + 1][getY() - 1] < 255)
		weight[getX() + 1][getY() - 1] += buff;

	if (checkRange(getX() + 1, getY()) && weight[getX() + 1][getY()] < 255)
		weight[getX() + 1][getY()] += buff;

	if (checkRange(getX() + 1, getY() + 1) && weight[getX() + 1][getY() + 1] < 255)
		weight[getX() + 1][getY() + 1] += buff;


	for (int i = 0; i < SIZE; i++) {			//3개 연속의 돌을 검사하는 코드
		for (int j = 0; j < SIZE; j++) {
			if (weight[i][j] > 254) {
				int buff = weight[i][j];
				int countStone = 1;

				if (checkRange(i + 1, j) && weight[i + 1][j] == buff && 
					checkRange(i + 2, j) && weight[i + 2][j] == buff) {

					if (checkRange(i - 1, j) && weight[i - 1][j] < 255)
						weight[i - 1][j] += state ? -30 : 30;

					if (checkRange(i + 3, j) && weight[i + 3][j] < 255)
						weight[i + 3][j] += state ? -30 : 30;
				}

				if (checkRange(i, j + 1) && weight[i][j + 1] == buff &&
					checkRange(i, j + 2) && weight[i][j + 2] == buff) {

					if (checkRange(i, j - 1) && weight[i][j - 1] < 255)
						weight[i][j - 1] += state ? -30 : 30;

					if (checkRange(i, j + 3) && weight[i][j + 3] < 255)
						weight[i][j + 3] += state ? -30 : 30;
				}

				if (checkRange(i + 1, j + 1) && weight[i + 1][j + 1] == buff &&
					checkRange(i + 2, j + 2) && weight[i + 2][j + 2] == buff) {

					if (checkRange(i - 1, j - 1) && weight[i - 1][j - 1] < 255)
						weight[i - 1][j - 1] += state ? -30 : 30;

					if (checkRange(i + 3, j + 3) && weight[i + 3][j + 3] < 255)
						weight[i + 3][j + 3] += state ? -30 : 30;
				}

				if (checkRange(i + 1, j - 1) && weight[i + 1][j - 1] == buff &&
					checkRange(i + 2, j - 2) && weight[i + 2][j - 2] == buff) {

					if (checkRange( i - 1, j + 1) && weight[i - 1][j + 1] < 255)
						weight[i - 1][j + 1] += state ? -30 : 30;

					if (checkRange(i + 3, j - 3) && weight[i + 3][j - 3] < 255)
						weight[i + 3][j - 3] += state ? -30 : 30;
				}
			}
		}
	}
}

void ConcaveAI::setAI() {

	int buffX = 0, buffY = 0;
	int min = weight[buffX][buffY];

	for (int i = 0; i < SIZE; i++) {
		for (int j = 0; j < SIZE; j++) {
			if (weight[i][j] < min) {
				buffX = i;
				buffY = j;
				min = weight[i][j];
			}
		}
	}

	setX(buffX);
	setY(buffY);
	gotoXY(getY() * 2, getX());
}

void ConcaveAI::playConcave() {
	gotoXY(0, 0);
	while (true) {
		if (getColor()) {
			printfMessage(black, getX(), getY());
			state = getColor() == player;
			if (state) {
				moveDir();
			}
			else {
				setAI();
			}
		}
		else {
			printfMessage(white, getX(), getY());
			state = getColor() == player;
			if (state) {
				moveDir();
			}
			else {
				setAI();
			}
		}
		if (getColor())
			printf("○");
		else
			printf("●");
		changeBord(getX(), getY(), getColor());
		if (checkWin()) {
			if (getColor())
				printfMessage("흑돌이 승리하였습니다.", getY() * 2, getX());
			else
				printfMessage("백돌이 승리하였습니다.", getY() * 2, getX());
			return;
		}

		setWeight();
		reverseColor();
	}
}

void ConcaveAI::printWeight() {

	gotoXY(40, 40);
	for (int i = 0; i < SIZE; i++) {
		for (int j = 0; j < SIZE; j++) {
			printf("%4d ", weight[i][j]);
		}
		printf("\n");
	}

}

 

3. 결어

 

 OOP적인 설계를 경험하려면 이정도의 프로그램은 작성해야한다고 생각했으며 설계 과정에서 개인적인 즐거움이 느껴졌기에 여기까지 완성한 것 같다. 그러나 아직도 "OOP적인 설계를 하였다!" 라는 만족감은 얻지 못하였다. 내가 사용한 것은 결국 '상속'밖에 없기 때문이다. 연산자 재정의, 오버로드, 가상함수와 같은 훌륭한 OOP적 기능을 사용하지 못했다.

 

 내가 작성한 것은 말만 AI이지 사실은 1인모드이다. 그것도 초급 난이도라고 할 수 있다. 진정한 오목 AI는 트리 자료구조를 사용해 최소최대 알고리즘, 가지치기 알고리즘을 구현하여 만들 수 있다. 하지만 나는 절대적인 값을 설정하는 방법도 아직 이해하지 못하였다. 트리라는 자료구조는 학교의 '자료구조' 강의와 혼자 알고리즘을 독학하며 만난 이진탐색트르 같은 것들이지 최소최대, 가지치기와 같은 고급알고리즘은 공부하지 못했다.

 

 프로젝트를 진행하면서 나의 한계를 많이 느꼈다. 그래도 방학이라는 긴 기간동한 나름 할 일이 생겨 기쁘기도 하다. 최소최대알고리즘, 가지치기알고리즘을 공부하여 이 블로그에 제대로된 AI 오목을 올렸으면 좋겠다.

반응형