2024.04.08 - [프로그래밍/강화학습 (RL)] - 틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 4월 과제]
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 4월 과제]
2024.03.28 - [프로그래밍/강화학습 (RL)] - 틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 3월 과제] 틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 3월 과제]저번 백준 풀이 과제에 이어
olzl07.tistory.com
저번 달에는 연구에 필요한 이론적 배경들에 대해 공부해보았습니다.
이번 달에 해야할 일은 크게 2가지가 있습니다.
- 연구에 필요한 데이터를 수집하여 정리하고 각 속성에 대해 설명하기
- 4월의 배경지식을 활용한 알고리즘 설계(프로그래밍이 아님, 순서도, 의사코드 등 다양하게 활용)
이렇게 2가지인데, 저는 강화학습을 진행할 예정이라 딱히 수집할 데이터가 없으므로 그냥 순서도 짜고 플레이어 2명이서 틱택토를 플레이 할 수 있는 환경을 구축해보도록 하겠습니다. (개발하지 말라고 돼있었지만 저는 환경 개발 안 하면 할 게 없습니다...)
순서도
환경을 구축하기에 앞서, 어떤 방식으로 코드를 짤지를 순서도를 통해 알아보겠습니다.
먼저, 기본적인 틱택토 플레이 코드의 순서도는 다음과 같습니다.
클래스 구상
그럼 이제 이 순서도대로 코드를 짜기 위해 필요한 것들을 생각해보겠습니다. 일단 당연히 코드는 클래스를 이용하여 객체지향적으로 짜야합니다. 그러면 클래스에 들어가는 메서드 등은 어떤 것이 있어야하고, 이들은 어떤 기능을 해야하는지를 의사코드를 통해 생각해보겠습니다.
1. 초기화 메서드
먼저, 게임 보드를 초기화할 메서드가 필요합니다. 초기화는 클래스를 생성하자마자 해줘야하므로 생성자를 통해 초기화를 진행하면 될 것 같습니다. 초기화 메서드에서는 보드 초기화, 승리 조건 지정, 승자 초기화, 플레이어 초기화 등의 작업을 해주어야 합니다.
이를 의사코드로 나타내면 다음과 같습니다.
보드 초기화
승리 조건 = []
승자 초기화
플레이어 초기화
2. 보드 출력 메서드
현재 보드의 상황을 출력해주는 메서드도 필요합니다. 보드의 요소들을 하나씩 검사하며 0이면 빈칸, 1이면 O, -1이면 X를 출력하게 하면 될 것입니다.
이를 의사코드로 나타내면 다음과 같습니다.
for 3번 반복
for 3번 반복
if 요소가 0
배열에 빈칸 추가
elif 요소가 1
배열에 O 추가
else
배열에 X 추가
배열 출력
3. 착수 메서드
플레이어가 보드 내의 칸에 착수를 하는 메서드도 필요할 것입니다. 플레이어가 착수하고자 하는 칸이 문제가 없는지 확인하고, 문제가 없다면 해당 칸에 착수하게 할 것입니다.
이후, 착수가 끝나면 승자가 있는지 확인해야합니다. 승자가 있다면 승자를 출력하고 게임을 종료하고, 없다면 또 무승부인지를 확인해야합니다. 만약 결과가 무승부라면 무승부를 출력한 뒤 게임을 종료하고, 무승부가 아니라면 게임을 이어나가는 방식으로 진행할 것입니다.
이를 의사코드로 나타내면 다음과 같습니다.
if 착수하고자 하는 칸이 범위를 벗어나거나 이미 참
False 반환
착수하고자 하는 칸에 플레이어의 정보 넣기
플레이어 바꾸기
if 승자가 있다면
if 승자가 1이라면
"Player 1 Win!!" 출력
elif 승자가 2라면
"Player 2 Win!!" 출력
게임 종료
else
비어있는 칸이 없다면
"Draw!!" 출력
게임 종료
플레이어 바꾸기
게임 이어나가기
4. 승자 확인 메서드
앞서 승자가 있는지 확인하는 절차를 거쳤으므로 승자가 있는지를 확인하는 메서드도 짜주어야 합니다. 보드의 요소들 중 1이 들어있는 칸들이 승리 조건을 만족하면 1을 승자로, 2가 들어있는 칸들이 승리 조건을 만족하면 2를 승자로 지정해줄 것입니다. 또, 비어있는 칸이 없다면 승자를 없애 무승부로 만들어줄 것입니다.
이를 의사코드로 나타내면 다음과 같습니다.
for 모든 승리 조건에 대해 반복
if 보드 내의 요소들 중 1들의 배열이 승리 조건을 만족
승자를 1로 지정
함수 종료
elif 보드 내의 요소들 중 -1들의 배열이 승리 조건을 만족
승자를 -1로 지정
함수 종료
if 모든 칸이 참
승자를 없앰
함수 종료
5. 빈 칸 확인 메서드
마지막으로 보드에 비어있는 칸이 있는지를 확인하는 메서드가 필요합니다. 보드 내에 0이 있으면 비어있다고 판단하게 하면 됩니다.
이를 의사코드로 나타내면 다음과 같습니다.
if 보드 안에 0 포함
True 반환
else
False 반환
데이터 파악
틱택토 플레이 환경을 구현하기에 앞서, 어떤 데이터가 입력되고 출력되는지를 자세히 알아보겠습니다. 먼저 저는 틱택토 보드를 9칸의 배열을 활용해 구현할 것입니다. 따라서 입력 데이터는 착수할 위치이고, 이는 배열의 인덱스인 0~8 사이의 정수입니다.
출력되는 데이터는 한 수 한 수 착수할때마다 보드의 상태를 출력할 것이고, 승자가 가려지면 누가 승자인지도 출력할 것입니다. 또, 보드를 볼 때 편하게 볼 수 있게 하기 위해 열과 열 사이를 ' | '로 구분하여 출력할 것입니다. 따라서 예상 출력은 다음과 같은 형태라고 할 수 있습니다.
| | | X |
| | O | |
| | | |
or
Player 2 Win!!
| X | X | X |
| | O | |
| | | O |
환경 구현
이제 본격적으로 앞서 짠 순서도와 의사코드들을 바탕으로 2명의 플레이어가 틱택토를 플레이 할 수 있는 환경을 구축해보겠습니다.
먼저, 클래스를 설정하고 초기화 메서드를 지정해줄 것입니다. 초기화 메서드는 앞서 말했듯이 init 생성자로 만들어 객체가 생성되자마자 실행되게 할 것입니다.
보드를 모두 0으로 초기화해주고, 승리할 수 있는 조건들을 지정해준 뒤 승자를 0, 플레이어를 -1로 초기화해줄 것입니다. (플레이어를 -1로 초기화하는 이유는 보통 틱택토는 X가 먼저 두기 때문입니다)
저는 앞으로 1을 O, -1을 X로 생각하고 프로그래밍을 할 것입니다.
def __init__(self) : # 초기화
self.board = [0] * 9
self.answer = [ # 승리 조건들
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]]
self.winner = 0
self.player = -1
이후 보드를 print하는 코드를 짜주었습니다. 반복문을 이용해 board 내의 모든 요소들을 돌며 요소가 0이면 빈칸, 1이면 O, -1이면 X를 저장하도록 하였고, 가독성을 위해 열과 열 사이를 ' | '로 구분하여 출력되게 하였습니다. ' | '로 구분하는 것을 위해 join 함수를 사용하였습니다.
def print(self) : # 보드 출력
for i in range(3) :
row = [] # 보드 내 요소들 상태 저장 배열
for j in range(3) :
item = self.board[3*i + j]
if item == 0 : # 요소가 비었는지 찼는지 (O or X) 확인
row.append(' ')
elif item == 1 :
row.append('O')
else :
row.append('X')
print(f'| {" | ".join(row)} |')
print('')
다음으론 착수 메서드를 짜보았습니다. 착수할 위치 num을 매개변수로 입력받고, 어차피 학습시킬때 이러한 오류가 날 일은 없겠지만 혹시 모르므로 우선 num이 0 미만 8 초과이거나 num 위치가 이미 차있으면 False를 반환하게 하였습니다.
이후 O와 X를 구분하는 1, -1과 플레이어의 정보 1, -1이 서로 같으므로 착수할 위치의 요소에 현재 플레이어의 값을 저장해주면 됩니다. 이후 플레이어를 변경하였습니다.
착수를 마친 후에는 승자가 있는지 판단하여 승자가 있으면 승자를 출력하고 게임을 종료시켰습니다. 승자가 없다면 모든 칸이 다 찼는지를 확인해 무승부인지를 판단하여 무승부라면 무승부라고 출력하고 게임을 종료하게 했고, 무승부도 아니라면 그냥 게임이 이어지게 하였습니다. 게임이 종료될때는 True,이어질때는 False를 반환하게 하여 게임 종료를 판단하게 만들었습니다.
def move(self, num) : # 수 두기
if num < 0 or num > 8 or self.board[num] != 0 :
return False # 없는 칸에 두려 하거나 이미 차 있는 칸에 두려 함
self.board[num] = self.player # 수 둠
self.player *= -1 # 선수 바꾸기
if self.check_win() : # 이긴 사람이 있나 확인
if self.winner == 1 :
print("Player 1 Win!!")
elif self.winner == -1 :
print("Player 2 Win!!")
return True
else :
if not self.empty() :
print("Draw!!")
return True
self.player *= -1
return False
승자가 있는지를 판단하는 코드도 짜보았습니다. 앞서 지정한 승리 조건들의 모든 조건에 대해 보드의 요소들 중 1 혹은 -1이 전부 그 조건에 들어간다면 승자를 해당 플레이어로 지정하고, 함수를 종료하였습니다. 만약 여기서 승자를 가리지 못한다면 보드가 비어있는지를 확인해서 비어있지 않다면 승자를 None으로 지정해 무승부라고 표기하고 함수를 종료하였습니다. (이 코드 짜면서 all 함수를 처음 써봤는데 이런 함수도 다 쓸데가 있구나~라는 걸 깨닫게 되었습니다.)
def check_win(self) : # 이긴 사람 있나 확인
for condition in self.answer :
if all(self.board[i] == 1 for i in condition) : # 이기는 조건 중 한 경우의 모든 요소가 1이라면 winner를 1로 지정
self.winner = 1
return True
elif all(self.board[i] == -1 for i in condition) : # 이기는 조건 중 한 경우의 모든 요소가 -1이라면 winner를 -1로 지정
self.winner = -1
return True
if not self.empty() :
self.winner = None # 이긴 사람도 없는데 모든 칸이 찼으면 무승부
return True
마지막으로 보드에 빈 칸이 있는지를 확인하는 메서드를 짜겠습니다. 간단하게 리스트 내에 0이 있는지를 판단하여 True 혹은 False를 반환하게 하는 방식으로 구현할 수 있었습니다.
def empty(self) : # 빈 칸이 하나라도 있는지 확인
if 0 in self.board :
return True
else :
return False
최종 코드입니다.
class TicTacToe :
def __init__(self) : # 초기화
self.board = [0] * 9
self.answer = [ # 승리 조건들
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]]
self.winner = 0
self.player = -1
def print(self) : # 보드 출력
for i in range(3) :
row = [] # 보드 내 요소들 상태 저장 배열
for j in range(3) :
item = self.board[3*i + j]
if item == 0 : # 요소가 비었는지 찼는지 (O or X) 확인
row.append(' ')
elif item == 1 :
row.append('O')
else :
row.append('X')
print(f'| {" | ".join(row)} |')
print('')
def move(self, num) : # 수 두기
if num < 0 or num > 8 or self.board[num] != 0 :
return False # 없는 칸에 두려 하거나 이미 차 있는 칸에 두려 함
self.board[num] = self.player # 수 둠
self.player *= -1 # 선수 바꾸기
if self.check_win() : # 이긴 사람이 있나 확인
if self.winner == 1 :
print("Player 1 Win!!")
elif self.winner == -1 :
print("Player 2 Win!!")
return True
else :
if not self.empty() :
print("Draw!!")
return True
self.player *= -1
return False
def check_win(self) : # 이긴 사람 있나 확인
for condition in self.answer :
if all(self.board[i] == 1 for i in condition) : # 이기는 조건 중 한 경우의 모든 요소가 1이라면 winner를 1로 지정
self.winner = 1
return True
elif all(self.board[i] == -1 for i in condition) : # 이기는 조건 중 한 경우의 모든 요소가 -1이라면 winner를 -1로 지정
self.winner = -1
return True
if not self.empty() :
self.winner = None # 이긴 사람도 없는데 모든 칸이 찼으면 무승부
return True
def empty(self) : # 빈 칸이 하나라도 있는지 확인
if 0 in self.board :
return True
else :
return False
간단한 수동 조작을 통해 출력해본 결과는 다음과 같습니다.
test = TicTacToe()
test.print()
test.move(2)
test.print()
test.move(4)
test.print()
test.move(0)
test.print()
test.move(8)
test.print()
test.move(1)
test.print()
| | | |
| | | |
| | | |
| | | X |
| | | |
| | | |
| | | X |
| | O | |
| | | |
| X | | X |
| | O | |
| | | |
| X | | X |
| | O | |
| | | O |
Player 2 Win!!
| X | X | X |
| | O | |
| | | O |
마무리
이로써 플레이어들이 틱택토를 플레이 할 수 있는 환경을 구축해보았습니다. 사실 아직 완벽히 구축한 건 아니고, 입력을 키패드로 받거나 기보를 저장하는 것을 만드는 등 할 일이 좀 남긴 했는데 이는 6월에 좀 더 수정을 해보도록 하겠습니다.
6월과 7월에는 구축한 환경을 수정하고, 알고리즘을 적용해 학습을 실제로 시켜보겠습니다.
'프로그래밍 > 틱택토 강화학습 (TTT-RL)' 카테고리의 다른 글
틱택토 강화학습 (Tik-Tak-Toe RL) - [심화 탐구] (2) | 2024.08.12 |
---|---|
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 7월 과제] (0) | 2024.07.09 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 6월 과제] (0) | 2024.06.29 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 4월 과제] (2) | 2024.04.13 |
틱택토 강화학습 (Tik-Tak-Toe RL) - [정보과학융합탐구 - 3월 과제] (1) | 2024.03.28 |