반응형

minimax 속도를 개선하기 위해 다음과 같은 변경사항을 적용하였다.

1. 빈 자리를 검색할 때, 이미 돌이 놓여진 위치 주변에 있는 빈 자리만 반환.

2. AI가 돌을 놓았을 때, 승리 조건이 만족되는 위치라면 즉시 돌을 놓는다.

3. 플레이어가 돌을 놓았을 때, 승리 조건이 만족되는 위치라면 AI가 그 위치에 돌을 즉시 놓는다.

 

1번 빈 자리 검색 시 아래 그림과 같이 돌이 놓여진 주변 빈자리만 검색하여 반환되도록 한다.

반환된 빈 자리는 불필요한 트리 탐색을 줄여 속도가 개선된다.

빈 자리 검색시 반환되는 위치

 

#
# FIAR
# Created by netcanis on 2023/10/07.
#
# minimax
# alpha beta pruning
# 속도 개선 

import pygame
import random
import numpy as np
import sys


BOARD_SIZE = 9  # (7,9,13,15,19)
CELL_SIZE = 40
MARGIN = 20
CLICK_RADIUS = CELL_SIZE // 2

BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL = 3


class FIAR:
    def __init__(self):
        self.init_game()
    
    def init_game(self):
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))

        self.font = pygame.font.Font(None, CELL_SIZE-4)

    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER
        
        self.draw_board(self.screen)
        pygame.display.flip()
         
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells
    
    # 수가 놓여진 자리의 주변 빈 자리만 반환
    def find_adjacent_empty_cells(self):
        empty_cells = []
        for row, col in self.find_empty_cells():
            for dr in [-1, 0, 1]:
                for dc in [-1, 0, 1]:
                    new_row, new_col = row + dr, col + dc
                    if 0 <= new_row < BOARD_SIZE and 0 <= new_col < BOARD_SIZE and self.board[new_row][new_col] != 0:
                        empty_cells.append((row, col))
                        break
        return empty_cells
    
    def check_winner(self, board, row, col):
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True

        return False
    
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_adjacent_empty_cells())
        self.make_move(row, col, player)
        return row, col

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player
            
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            
            print(f"[{self.sequence}] <{player}> {row},{col}")
            
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        if self.is_board_full(self.board):
            return 0
        
        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)

        if depth >= DIFFICULTY_LEVEL:
            return 0
        
        if is_maximizing:# AI
            best_score = -float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_adjacent_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score
    
    # 아래 우선순위에 따라 즉시 처리할 자리는 minimax 알고리즘 없이 즉시 수를 놓아 속도 개선
    # 1.만약 AI가 놓으면 게임을 이기는 자리라면 즉시 수를 놓는다. 
    # 2.만약 Player가 놓으면 게임을 이기는 자리라면 AI는 그 곳에 수는 놓는다.
    def make_optimal_move(self, player):
        # 0. The first one is random.
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)
        
        empty_cells = self.find_adjacent_empty_cells()

        # 1. ai turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
            
        # 2. player turn: 5
        for row, col in empty_cells: 
            self.board[row][col] = -player
            if self.check_winner(self.board, row, col):
                self.board[row][col] = 0
                return (row, col)
            self.board[row][col] = 0
        
        return None
        
    def find_best_move(self, player):
        
        print(f"[{self.sequence+1}] <{player}> Calculating...")
        
        optimal_move = self.make_optimal_move(player)
        if optimal_move != None:
            return optimal_move
        
        alpha = -float('inf')
        beta = float('inf')

        best_move = None
        best_score = -float('inf') if player == AI else float('inf')

        empty_cells = self.find_adjacent_empty_cells()
        for index, (row, col) in enumerate(empty_cells):
            self.board[row][col] = player
            is_maximizing = False if player == AI else True
            score = self.minimax(0, is_maximizing, alpha, beta, row, col)
            self.board[row][col] = 0
            
            if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)
            
            percentage = (index / len(empty_cells)) * 100
            print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")
            
        return best_move
    
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit() 
                            sys.exit()
                    else:
                        popup = False
                        break
            
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (128, 0, 128))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)
            
            pygame.display.flip()
    
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):# draw horizontal lines
            pygame.draw.line(screen, BLACK, 
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN), 
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):# draw vertical lines
                if row == 0:
                    pygame.draw.line(screen, BLACK, 
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN), 
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)
                
                seq_no = self.sequences[row][col] 
                if seq_no != 0:
                    if seq_no == self.sequence: 
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over and self.turn_player == PLAYER:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()
                    
                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                        
                        self.draw_board(self.screen)
                        pygame.display.flip()
                            
                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

    
if __name__ == "__main__":
    game = FIAR()
    game.reset_game()
    
    while True:
        while not game.game_over:
            game.handle_events()
        
        game.show_message("Play Again? (y/n)", is_exit=True)

 

속도는 다소 빨라지긴 했다.

3x3을 만들면 이미 진 것을 예감한 것인지 AI는 그 때부터 엉뚱한 수를 놓는다.  

마치 포기한 것처럼...

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

tic tac toe 게임 머신러닝에 이어 오목 게임을 만들어 보자.

아래와 같은 순서로 총 5회로 구성하였다.

Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

 

9x9보드에서 minimax알고리즘을 사용하여 간단하게 구현해 보았다.

참고로 3x3을 막고있지 않다. 

DIFFICULTY_LEVEL 값을 높이면 좀더 잘 두지만 속도는 느려진다. 반대로 값을 낮추면  속도는 빨라지지만 잘 두지는 못한다.

가끔씩 엉뚱한 수를 두는 이유는 DIFFICULTY_LEVEL 제한으로 minimax트리검색이 중간에 중단되어 마지막으로 중단된 위치에 수를 놓기 때문이다. 해당 속도문제를 속도 최적화 1차, 속도 최적화 2차를 통해 개선하도록 하겠다.

# 필요한 모듈 임포트
import pygame
import random
import numpy as np
import sys

# 게임 보드 설정
BOARD_SIZE = 9  # 게임 보드 크기 (9x9)
CELL_SIZE = 40  # 셀 크기
MARGIN = 20  # 보드와 화면 경계의 여백
CLICK_RADIUS = CELL_SIZE // 2  # 클릭 시 지정된 반지름

# 게임 보드의 셀 상태
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GRAY = (100, 100, 100)

# 게임 변수 및 난이도 설정
NUM_ITEMS = (BOARD_SIZE * BOARD_SIZE)
PLAYER = 1
AI = -1

DIFFICULTY_LEVEL = 3  # 게임 난이도 (컴퓨터가 어려운 수를 계산하는 깊이)


# 게임 클래스 정의
class FIAR:
    def __init__(self):
        self.init_game()

    def init_game(self):
        # Pygame 초기화
        pygame.init()
        pygame.display.set_caption("FIAR")
        w = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        h = (BOARD_SIZE-1) * CELL_SIZE + 2 * MARGIN
        self.screen = pygame.display.set_mode((w, h))
        self.font = pygame.font.Font(None, CELL_SIZE-4)

    # 게임 초기화
    def reset_game(self):
        self.board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequences = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=int)
        self.sequence = 0
        self.game_over = False
        self.turn_player = PLAYER
        self.draw_board(self.screen)
        pygame.display.flip()

    # 빈 셀 찾기
    def find_empty_cells(self):
        empty_cells = []
        for row in range(BOARD_SIZE):
            for col in range(BOARD_SIZE):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        random.shuffle(empty_cells)
        return empty_cells

    # 승자 확인
    def check_winner(self, board, row, col):
        # 승자 판별 로직
        def check_direction(dx, dy):
            count = 1
            r, c = row + dx, col + dy
            while 0 <= r < len(board) and 0 <= c < len(board[0]) and board[r][c] == board[row][col]:
                count += 1
                r += dx
                c += dy
            return count

        directions = [(0, 1), (1, 0), (1, 1), (-1, 1)]
        for dx, dy in directions:
            count1 = check_direction(dx, dy)
            count2 = check_direction(-dx, -dy)
            total_count = count1 + count2 - 1
            if total_count == 5:
                return True
        return False

    # 게임 보드가 가득 찼는지 확인
    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    # 무작위로 수를 두는 함수
    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, player)
        return row, col

    # 미니맥스 알고리즘을 사용해 수를 두는 함수
    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    # 수를 두는 함수
    def make_move(self, row, col, player):
        # 게임 보드에 수를 두고 승자를 확인하는 로직
        if self.board[row][col] == 0:
            self.board[row][col] = player
            self.sequence += 1
            self.sequences[row][col] = self.sequence
            if self.check_winner(self.board, row, col):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    # 미니맥스 알고리즘
    def minimax(self, depth, is_maximizing, alpha, beta, row2, col2):
        # 미니맥스 알고리즘으로 최적의 수를 찾는 로직
        if self.is_board_full(self.board):
            return 0

        if is_maximizing:
            if self.check_winner(self.board, row2, col2):
                return -(NUM_ITEMS - depth + 1)
        else:
            if self.check_winner(self.board, row2, col2):
                return (NUM_ITEMS - depth + 1)

        if depth >= DIFFICULTY_LEVEL:
            return 0

        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta, row, col)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    # 최적의 수를 찾는 함수
    def find_best_move(self, player):
        # 최적의 수를 찾는 함수
        if self.sequence == 0:
            row = BOARD_SIZE // 2 + random.randint(-2, 2)
            col = BOARD_SIZE // 2 + random.randint(-2, 2)
            return (row, col)

        alpha = -float('inf')
        beta = float('inf')
        best_move = None
        best_score = -float('inf') if player == AI else float('inf')
        empty_cells = self.find_empty_cells()

        for index, (row, col) in enumerate(empty_cells):
            self.board[row][col] = player
            is_maximizing = False if player == AI else True
            score = self.minimax(0, is_maximizing, alpha, beta, row, col)
            self.board[row][col] = 0

            if (player == AI and score > best_score) or (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)

            percentage = (index / len(empty_cells)) * 100
            print(f"    [{percentage:.1f}%] <{player}> {row},{col} -> {score}")

        return best_move

    # 화면에 메시지 표시
    def show_message(self, message, is_exit=False):
        popup = True
        while popup:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

                if event.type == pygame.KEYDOWN:
                    if is_exit:
                        if event.key == pygame.K_y:
                            self.reset_game()
                            popup = False
                            return
                        elif event.key == pygame.K_n:
                            pygame.quit()
                            sys.exit()
                    else:
                        popup = False
                        break

            # 메시지 텍스트 표시
            text_lines = message.split('\n')
            for i, line in enumerate(text_lines):
                text = self.font.render(line, True, (0, 0, 0))
                text_rect = text.get_rect(topleft=(20, 20 + i * 20))
                self.screen.blit(text, text_rect)

            pygame.display.flip()

    # 게임 보드 그리기
    def draw_board(self, screen):
        screen.fill(GRAY)
        for row in range(BOARD_SIZE):
            pygame.draw.line(screen, BLACK,
                            (0 * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            ((BOARD_SIZE-1) * CELL_SIZE + MARGIN, row * CELL_SIZE + MARGIN),
                            1)
            for col in range(BOARD_SIZE):
                if row == 0:
                    pygame.draw.line(screen, BLACK,
                                    (col * CELL_SIZE + MARGIN, 0 * CELL_SIZE + MARGIN),
                                    (col * CELL_SIZE + MARGIN, (BOARD_SIZE-1) * CELL_SIZE + MARGIN),
                                    1)

                x = col * CELL_SIZE + MARGIN
                y = row * CELL_SIZE + MARGIN

                if self.board[row][col] == PLAYER:
                    pygame.draw.circle(screen, BLACK, (x, y), CLICK_RADIUS)
                elif self.board[row][col] == AI:
                    pygame.draw.circle(screen, WHITE, (x, y), CLICK_RADIUS)

                seq_no = self.sequences[row][col]
                if seq_no != 0:
                    if seq_no == self.sequence:
                        pygame.draw.circle(screen, RED, (x, y), CLICK_RADIUS+1, 1)
                    color = WHITE if self.board[row][col] == PLAYER else BLACK
                    text = self.font.render(f"{seq_no}", True, color)
                    text_rect = text.get_rect(center=(x, y))
                    screen.blit(text, text_rect)

    # 이벤트 처리
    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.game_over = True
            elif event.type == pygame.MOUSEBUTTONDOWN and not self.game_over:
                x, y = pygame.mouse.get_pos()
                row = int(round((y - MARGIN) / CELL_SIZE))
                col = int(round((x - MARGIN) / CELL_SIZE))
                if 0 <= row < BOARD_SIZE and 0 <= col < BOARD_SIZE and self.board[row][col] == 0:
                    self.make_move(row, col, self.turn_player)
                    self.draw_board(self.screen)
                    pygame.display.flip()

                    if self.turn_player == AI:
                        best_move = self.find_best_move(self.turn_player)
                        if best_move is not None:
                            row, col = best_move
                            self.make_move(row, col, self.turn_player)
                            self.draw_board(self.screen)
                            pygame.display.flip()

                    if self.game_over == True:
                        if self.turn_player == 0:
                            self.show_message("Game draw!")
                        else:
                            self.show_message(f"Game Over!\n{'Player' if self.turn_player == PLAYER else 'AI'} wins!")

if __name__ == "__main__":
    game = FIAR()
    game.reset_game()

    while True:
        while not game.game_over:
            game.handle_events()
        
        game.show_message("Play Again? (y/n)", is_exit=True)

 

아래는 게임 실행화면이다.
게임 오버 상태에서 텍스트가 겹치는 오류가 있네.. -_-;;
중요한 문제 아니니 그건 나중에 수정하도록 하겠다.

 

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (1/5) - 기본 구현 (minimax, alpha-beta pruning)

2023.10.27 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (2/5) - 속도 최적화 1차 (minimax 속도 개선)

2023.10.28 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (3/5) - 속도 최적화 2차 (RANDOM모드 추가)

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (4/5) - 훈련 데이터 생성 및 학습

2023.10.29 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

2023.11.03 - [AI,ML, Algorithm] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

 

반응형
블로그 이미지

SKY STORY

,
반응형
# Tic Tac Toe (4/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning
# h5파일 로딩, 게임 GUI.
# 게임 테스트.

import tkinter as tk
from tkinter import messagebox
import random
import numpy as np
import tensorflow as tf
from keras.models import load_model

PLAYER = 1
AI = -1
H5_FILE_NAME = "ttt_model.h5"

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")

        self.init_neural_network()
        self.start_game()

    def init_game(self):
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        self.sequence = 0
        self.game_over = False
        self.turn_player = random.choice([PLAYER, AI])
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_move(r, c, PLAYER),
                )
                self.buttons[row][col].grid(row=row, column=col)

    def find_empty_cells(self):
        empty_cells = []
        for row in range(3):
            for col in range(3):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        return empty_cells

    def check_winner(self, board, player):
        for row in board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(board[row][col] == player for row in range(3)):
                return True
        if all(board[i][i] == player for i in range(3)) or all(board[i][2 - i] == player for i in range(3)):
            return True
        return False

    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def make_move(self, row, col, turn_player):
        if self.board[row][col] == 0:
            self.board[row][col] = turn_player
            self.updateBoardUI(row, col, turn_player)
            
            self.sequence += 1

            if self.check_winner(self.board, turn_player):
                self.game_over = True
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
            else:
                self.turn_player *= -1

    def wait_for_player_move(self):
        player_move_var = tk.IntVar()
        for row in range(3):
            for col in range(3):
                self.buttons[row][col]["command"] = lambda r=row, c=col: player_move_var.set(r * 3 + c)
        self.window.wait_variable(player_move_var)

        player_move = player_move_var.get()
        row = player_move // 3
        col = player_move % 3
        self.make_move(row, col, PLAYER)
    
    def wait_for_player_restart(self):
        response = messagebox.askyesno("Game Over", "Do you want to play again?")
        if response:
            self.start_game()
        else:
            self.window.quit()
            
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'O' if turn_player == 1 else 'X'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()

    def random_move(self, turn_player):
        if self.game_over == True:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, turn_player)
        return row, col

    def init_neural_network(self):
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(27, activation='relu', input_shape=(9,)),
            tf.keras.layers.Dense(9, activation='softmax')
        ])

        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    def predicts(self, input_data):
        if isinstance(input_data, list):
            input_data = np.array(input_data)

        prediction = self.model.predict(input_data.reshape(1, -1))
        sorted_indices = np.argsort(prediction, axis=-1)[:, ::-1]

        index = 0
        for i in sorted_indices[0]:
            if input_data.shape == (9,):
                if input_data[i] == 0:
                    index = i
                    break
            elif input_data.shape == (3, 3):
                row = i // 3
                col = i % 3
                if input_data[row][col] == 0:
                    index = i
                    break

        #max_value = prediction[0, index]
        return index

    def start_game(self):
        self.init_game()
        self.model = load_model(H5_FILE_NAME)
        
        while not self.game_over:
            if self.turn_player == AI:
                next_move = self.predicts(self.board)
                row = next_move // 3
                col = next_move % 3
                self.make_move(row, col, self.turn_player)
            else:
                self.wait_for_player_move()
        
        if self.turn_player == AI:
            messagebox.showinfo("Game Over", "AI wins!")
        elif self.turn_player == PLAYER:
            messagebox.showinfo("Game Over", "Player wins!")
        else:
            messagebox.showinfo("Game Over", "It's a draw!")
        
        self.wait_for_player_restart()
                    
    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

 

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

이제 학습 데이터를 생성하도록 한다.

플레이어는 랜덤, AI는 minimax 알고리즘을 이용하여 수를 두며

학습 데이터는 AI가 이기거나 비긴 데이터만 저장하도록 하였다.

# Tic Tac Toe (3/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning
# generate training data, csv파일 저장.
# 머신러닝, h5파일 저장.


import os
import tkinter as tk
import random
import numpy as np
import pandas as pd
import tensorflow as tf
from keras.models import Sequential, load_model

NUM_ITEMS = 9
PLAYER = 1
AI = -1

PLAYER_MODE = "RANDOM"
AI_MODE = "MINIMAX"

NUM_EPISODES = 50000
CSV_FILE_NAME = "ttt_training_data.csv"
H5_FILE_NAME = "ttt_model.h5"

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")

        self.episode = 0

        self.init_ML()
        self.learning()

    def init_game(self):
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.sequence = 0
        self.game_over = False
        self.turn_player = random.choice([PLAYER, AI])

    def find_empty_cells(self):
        empty_cells = []
        for row in range(3):
            for col in range(3):
                if self.board[row][col] == 0:
                    empty_cells.append((row, col))
        return empty_cells

    def check_winner(self, board, player):
        for row in board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(board[row][col] == player for row in range(3)):
                return True
        if all(board[i][i] == player for i in range(3)) or all(board[i][2 - i] == player for i in range(3)):
            return True
        return False

    def is_board_full(self, board):
        return all(cell != 0 for row in board for cell in row)

    def random_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = random.choice(self.find_empty_cells())
        self.make_move(row, col, player)
        return row, col

    def minimax_move(self, player):
        if self.game_over:
            return -1, -1
        row, col = self.find_best_move(player)
        self.make_move(row, col, player)
        return row, col

    def make_move(self, row, col, player):
        if self.board[row][col] == 0:
            self.board[row][col] = player

            self.sequence += 1

            if self.check_winner(self.board, player):
                self.game_over = True
                print(f"Game Over! {'Player' if player == PLAYER else 'AI'} wins!")
            elif self.is_board_full(self.board):
                self.game_over = True
                self.turn_player = 0
                print("Game draw!")
            else:
                self.turn_player *= -1

    def find_best_move(self, player):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())

        alpha = -float('inf')
        beta = float('inf')

        best_move = None
        if player == AI:
            best_score = -float('inf')
        else:
            best_score = float('inf')

        for row, col in self.find_empty_cells():
            self.board[row][col] = player
            if player == AI:
                score = self.minimax(0, False, alpha, beta)
            else:
                score = self.minimax(0, True, alpha, beta)
            self.board[row][col] = 0

            if (player == AI and score > best_score) or \
                    (player == PLAYER and score < best_score):
                best_score = score
                best_move = (row, col)

        return best_move

    def minimax(self, depth, is_maximizing, alpha, beta):
        if self.check_winner(self.board, AI):
            return (NUM_ITEMS + 1 - depth)

        if self.check_winner(self.board, PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full(self.board):
            return 0

        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    def updateBoardUI(self, row, col, player):
        pass

    def save_data_to_csv(self, x_data, y_data, file_name):
        x_data_flat = [x.flatten() for x in x_data]
        y_data_flat = [y.flatten() for y in y_data]

        data = {'x_data': x_data_flat, 'y_data': y_data_flat}
        df = pd.DataFrame(data)

        df.to_csv(file_name, index=False)

    def load_data_from_csv(self, file_name):
        df = pd.read_csv(file_name)

        x_data_flat = df['x_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))
        y_data_flat = df['y_data'].apply(lambda x: np.fromstring(x[1:-1], sep=' '))

        x_data = np.array(x_data_flat.to_list())
        y_data = np.array(y_data_flat.to_list())

        return x_data, y_data

    def generate_training_data(self, num_games):
        x_data = []
        y_data = []

        while self.episode < num_games:
            self.init_game()

            x = []
            y = []
            while True:
                row, col = self.get_next_move(self.turn_player)
                x.append(np.array(self.board).flatten())
                y.append(np.eye(9)[row * 3 + col])

                if self.check_winner(self.board, PLAYER):
                    break
                elif self.check_winner(self.board, AI):
                    break
                elif self.is_board_full(self.board):
                    break

            if self.turn_player != PLAYER:
                del x[-1]
                del y[0]
                x_data.extend(x)
                y_data.extend(y)
                self.episode += 1
                print(f"{self.episode}: {self.turn_player} win.")

        return np.array(x_data), np.array(y_data)

    def get_next_move(self, player):
        if (player == AI and AI_MODE == "MINIMAX") or (player == PLAYER and PLAYER_MODE == "MINIMAX"):
            return self.minimax_move(player)
        else:
            return self.random_move(player)

    def init_ML(self):
        # hidden layer : 27 - 각 위치당 3가지 상태(1,-1,0)이고 총 9개의 자리이므로 3x9=27
        self.model = tf.keras.Sequential([
            tf.keras.layers.Dense(27, activation='relu', input_shape=(9,)),
            tf.keras.layers.Dense(9, activation='softmax')
        ])

        self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    def learning(self):
        if os.path.exists(CSV_FILE_NAME) == False:
            x_data, y_data = self.generate_training_data(NUM_EPISODES)

            self.save_data_to_csv(x_data, y_data, CSV_FILE_NAME)
            print(f"{CSV_FILE_NAME} 저장 완료")
        else:
            x_data, y_data = self.load_data_from_csv(CSV_FILE_NAME)

        self.model.fit(x_data, y_data, epochs=100, verbose=1)
        self.model.save(H5_FILE_NAME)

        test_results = self.model.evaluate(x_data, y_data)
        print(f"손실(Loss): {test_results[0]}")
        print(f"정확도(Accuracy): {test_results[1]}")

    def predicts(self, input_data):
        if isinstance(input_data, list):
            input_data = np.array(input_data)

        prediction = self.model.predict(input_data.reshape(1, -1))
        sorted_indices = np.argsort(prediction, axis=-1)[:, ::-1]

        index = 0
        for i in sorted_indices[0]:
            if input_data.shape == (9,):
                if input_data[i] == 0:
                    index = i
                    break
            elif input_data.shape == (3, 3):
                row = i // 3
                col = i % 3
                if input_data[row][col] == 0:
                    index = i
                    break

        #max_value = prediction[0, index]
        return index

    def run(self):
        self.window.mainloop()


if __name__ == "__main__":
    game = TTT()
    game.run()

 

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

반응형
블로그 이미지

SKY STORY

,
반응형

Tic-Tac-Toe와 같은 단순한 게임의 경우 minimax 알고리즘을 사용해도 속도에 그다지 민감하지 않지만

다른게임의 경우 속도가 너무 느려지므로 최적화 처리를 해야한다.

그 래서  alpha-beta pruning를 사용하여 최적화 하였다.

 

Alpha-beta pruning(알파-베타 가지치기)는 미니맥스 알고리즘(Minimax algorithm)과 함께 사용되는 게임 트리 탐색 알고리즘이다. 주로 체스, 오셀로, 틱택토 등의 턴 기반 보드 게임을 포함한 다양한 게임에서 최적의 수를 계산하기 위해 사용된다.

Alpha-beta pruning은 미니맥스 알고리즘의 성능을 향상시키기 위해 개발되었다. 미니맥스 알고리즘은 게임 트리를 완전히 탐색하여 모든 가능한 움직임을 고려하지만 이것은 많은 게임 상황에서 매우 비실용적일 수 있으며, 계산 복잡성이 지나치게 높을 수 있다.

Alpha-beta pruning은 게임 트리의 탐색을 중지하거나 일부 노드를 스킵함으로써 계산 시간을 크게 절약한다. 이것은 최소한의 계산만으로도 최선의 움직임을 찾을 수 있게 해준다. 알파와 베타라는 두 개의 값, 일반적으로 부모 노드에서 현재 노드로 이동할 때 알파(α)와 현재 노드에서 부모 노드로 이동할 때 베타(β)라는 값으로 표시된다.

<알파-베타 가지치기의 작동 방식>

1. 미니맥스 알고리즘과 마찬가지로 게임 트리를 깊이 우선 탐색.
2. 최대 플레이어 노드(현재 플레이어가 최대인 노드)에서 시작하고, 가능한 모든 자식 노드를 탐색. 이 때 알파 값을 최대로 업데이트.
3. 최소 플레이어 노드(상대 플레이어)로 이동하고, 가능한 모든 자식 노드를 탐색. 이 때 베타 값을 최소로 업데이트.
4. 알파 값과 베타 값 사이에 관계를 검사하여 가지치기를 수행. 만약 알파가 베타보다 크거나 같으면 현재 노드의 형제 노드를 스킵.
5. 이 과정을 루트 노드까지 반복하며 최선의 움직임을 결정.

# Tic Tac Toe (2/4)
# Created by netcanis on 2023/09/09.
#
# Minimax
# Alpha–beta pruning


import tkinter as tk
import random
from tkinter import messagebox

NUM_ITEMS = 9
PLAYER = 1
AI = -1

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")
        
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        
        self.sequence = 0
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_human_move(r, c),
                )
                self.buttons[row][col].grid(row=row, column=col)
        
    def find_empty_cells(self):
        return [(row, col) for row in range(3) for col in range(3) if self.board[row][col] == 0]
    
    def check_winner(self, player):
        for row in self.board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(self.board[row][col] == player for row in range(3)):
                return True
        if all(self.board[i][i] == player for i in range(3)) or all(self.board[i][2 - i] == player for i in range(3)):
            return True
        return False
    
    def is_board_full(self):
        return all(cell != 0 for row in self.board for cell in row)
    
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'X' if turn_player == PLAYER else 'O'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()
        
    def make_human_move(self, row, col):
        if self.board[row][col] == 0:
            self.board[row][col] = PLAYER
            self.updateBoardUI(row, col, PLAYER)
            self.sequence += 1
            
            if self.check_winner(PLAYER):
                messagebox.showinfo("Game Over", "Player wins!")
                self.window.quit()
            
            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
            
            self.make_computer_move()
    
    def make_computer_move(self):
        best_move = self.find_best_move()
        if best_move is not None:
            row, col = best_move
            self.board[row][col] = AI
            self.updateBoardUI(row, col, AI)
            self.sequence += 1
            
            if self.check_winner(AI):
                messagebox.showinfo("Game Over", "AI wins!")
                self.window.quit()

            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
    
    def find_best_move(self):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())
        
        alpha = -float('inf')
        beta = float('inf')
        best_move = None
        best_score = -float('inf')
        
        for row, col in self.find_empty_cells():
            self.board[row][col] = AI
            score = self.minimax(0, False, alpha, beta)
            self.board[row][col] = 0
            
            if score > best_score:
                best_score = score
                best_move = (row, col)
        
        return best_move
    
    def minimax(self, depth, is_maximizing, alpha, beta):
        if self.check_winner(AI):
            return (NUM_ITEMS + 1 - depth)
                    
        if self.check_winner(PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full():
            return 0
        
        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False, alpha, beta)
                self.board[row][col] = 0
                best_score = max(best_score, score)
                alpha = max(alpha, best_score)
                if beta <= alpha:
                    break
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True, alpha, beta)
                self.board[row][col] = 0
                best_score = min(best_score, score)
                beta = min(beta, best_score)
                if beta <= alpha:
                    break
            return best_score

    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

 

반응형
블로그 이미지

SKY STORY

,
반응형

신경망 알고리즘을 이용하여 tic-tac-toe게임을 구현해 보고자 한다. 

전체 4회에 걸쳐 업로드 할 예정이며 순서는 다음과 같다.

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

아래 코드는 AI의 수를 계산하기 위해 minimax알고리즘을 사용하였다.

Minimax는 게임 이론 및 인공 지능 분야에서 사용되는 의사결정 알고리즘 중 하나이다. 주로 두 플레이어 간의 게임에서 최적의 움직임을 찾는 데 사용된다. Minimax 알고리즘은 상대방과 자신의 이익 또는 손해를 고려하여 다음 움직임을 결정하는 방법을 제공한다.

<Minimax 알고리즘의 작동 방식>

1. 게임 트리 생성: 현재 게임 상태에서 가능한 모든 움직임을 고려하여 게임 트리를 생성한다. 트리의 각 레벨은 게임의 한 턴을 나타내고, 각 노드는 해당 턴에서 가능한 상태를 나타낸다.

2. 평가 함수: 게임 트리의 말단 노드(게임 종료 상태)에 도달하면 각 상태를 평가한다. 이 평가는 게임에서 어떤 플레이어가 이길 확률 또는 게임 상태의 가치를 나타내는 값이다.

3. Minimax 알고리즘: 게임 트리를 위에서 아래로 (루트에서 리프로) 탐색하면서, 각 레벨에서 최대화(Maximize) 노드와 최소화(Minimize) 노드를 번갈아 가며 선택한다. 최대화 노드에서는 가능한 움직임 중에서 가장 큰 가치를 선택하고, 최소화 노드에서는 가능한 움직임 중에서 가장 작은 가치를 선택한다.

4. 최선의 움직임 결정: 루트 노드까지 도달하면 최종적으로 최선의 움직임을 결정한다. 최대화 노드에서 최대 가치를 가진 움직임이 최적의 움직임이 된다.

# Tic Tac Toe (1/4)
# Created by netcanis on 2023/09/09.
#
# Minimax


import tkinter as tk
import random
from tkinter import messagebox

NUM_ITEMS = 9
PLAYER = 1
AI = -1

class TTT:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("TTT")
        
        self.board = [[0 for _ in range(3)] for _ in range(3)]
        self.buttons = [[None for _ in range(3)] for _ in range(3)]
        
        self.sequence = 0
        
        for row in range(3):
            for col in range(3):
                self.buttons[row][col] = tk.Button(
                    self.window,
                    text=' ',
                    font=("Helvetica", 24),
                    height=1,
                    width=1,
                    command=lambda r=row, c=col: self.make_human_move(r, c),
                )
                self.buttons[row][col].grid(row=row, column=col)
        
    def find_empty_cells(self):
        return [(row, col) for row in range(3) for col in range(3) if self.board[row][col] == 0]
    
    def check_winner(self, player):
        for row in self.board:
            if all(cell == player for cell in row):
                return True
        for col in range(3):
            if all(self.board[row][col] == player for row in range(3)):
                return True
        if all(self.board[i][i] == player for i in range(3)) or all(self.board[i][2 - i] == player for i in range(3)):
            return True
        return False
    
    def is_board_full(self):
        return all(cell != 0 for row in self.board for cell in row)
    
    def updateBoardUI(self, row, col, turn_player):
        self.buttons[row][col]["text"] = 'X' if turn_player == PLAYER else 'O'
        self.buttons[row][col]["state"] = "disabled"
        self.window.update()
        
    def make_human_move(self, row, col):
        if self.board[row][col] == 0:
            self.board[row][col] = PLAYER
            self.updateBoardUI(row, col, PLAYER)
            self.sequence += 1
            
            if self.check_winner(PLAYER):
                messagebox.showinfo("Game Over", "Player wins!")
                self.window.quit()
            
            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
            
            self.make_computer_move()
    
    def make_computer_move(self):
        best_move = self.find_best_move()
        if best_move is not None:
            row, col = best_move
            self.board[row][col] = AI
            self.updateBoardUI(row, col, AI)
            self.sequence += 1
            
            if self.check_winner(AI):
                messagebox.showinfo("Game Over", "AI wins!")
                self.window.quit()

            if self.is_board_full():
                messagebox.showinfo("Game Over", "It's a draw!")
                self.window.quit()
    
    def find_best_move(self):
        if self.sequence <= 1:
            return random.choice(self.find_empty_cells())
        
        best_move = None
        best_score = -float('inf')
        
        for row, col in self.find_empty_cells():
            self.board[row][col] = AI
            score = self.minimax(0, False)
            self.board[row][col] = 0
            
            if score > best_score:
                best_score = score
                best_move = (row, col)
        
        return best_move
        
    def minimax(self, depth, is_maximizing):
        if self.check_winner(AI):
            return NUM_ITEMS + 1 - depth
                    
        if self.check_winner(PLAYER):
            return -(NUM_ITEMS + 1 - depth)

        if self.is_board_full():
            return 0
        
        if is_maximizing:
            best_score = -float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = AI
                score = self.minimax(depth + 1, False)
                self.board[row][col] = 0
                best_score = max(best_score, score)
            return best_score
        else:
            best_score = float('inf')
            for row, col in self.find_empty_cells():
                self.board[row][col] = PLAYER
                score = self.minimax(depth + 1, True)
                self.board[row][col] = 0
                best_score = min(best_score, score)
            return best_score

    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    game = TTT()
    game.run()

 

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (1/4) - minimax

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성

2023.09.12 - [AI,ML, Algorithm] - Tic-Tac-Toe 게임 제작 (4/4) - 머신러닝을 이용한 게임 구현

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (3/4) - 머신러닝 훈련 데이터 생성  (0) 2023.09.12
Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning  (0) 2023.09.12
Simple Neural Network XOR  (0) 2023.08.29
SARSA  (0) 2023.08.28
Q-learning  (0) 2023.08.28
블로그 이미지

SKY STORY

,
반응형

 

# neural network xor
#
# Created by netcanis on 2023/08/22.
#

import numpy as np
from graphviz import Digraph


'''
Input 0 -- w[0] -- Hidden 0 
          \       /  \      
           \     /   w2[0]  
           w[1] /       \    
              \/         Output
              /\        /
           w[2] \    w2[1]
            /    \    /
           /      \  /
Input 1 -- w[3] -- Hidden 1
'''


# XOR 학습 데이터
x_data = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y_data = np.array([[0], [1], [1], [0]])

# 신경망 파라미터 초기화
input_size = 2
hidden_size = 2
output_size = 1
learning_rate = 0.1

# 가중치와 편향 초기화 - 평균이 0이고 표준편자가 1인 랜덤 값으로 행렬을 생성한다. (-1.0 ~ 1.0)
w1 = np.random.randn(input_size, hidden_size)   # 2x2 행렬
b1 = np.random.randn(hidden_size)               # 1차원 배열 (첫번째 은닉층의 편향 벡터. 편향은 각 은닉층 뉴런의 활성화 값을 조정하는 역할.)
w2 = np.random.randn(hidden_size, output_size)  # 2x1 행렬
b2 = np.random.randn(output_size)               # 1차원 배열 (출력층의 편향 벡터. 출력층의 뉴런들을 편향하여 최종 예측 값을 만듬.)

# 활성화 함수 (시그모이드)
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

# 활성화 함수의 미분
def sigmoid_derivative(x):
    return x * (1 - x)

# 학습 시작
epochs = 10000
for epoch in range(epochs):
    # 순전파 (Forward Propagation)
    z1 = np.dot(x_data, w1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, w2) + b2
    a2 = sigmoid(z2)

    # 오차 계산
    error = y_data - a2

    # 역전파 (Backpropagation)
    delta2 = error * sigmoid_derivative(a2)
    delta1 = np.dot(delta2, w2.T) * sigmoid_derivative(a1)

    # 가중치 업데이트
    w2 += np.dot(a1.T, delta2) * learning_rate
    w1 += np.dot(x_data.T, delta1) * learning_rate

# 예측 함수
def predict(input_data):
    z1 = np.dot(input_data, w1) + b1
    a1 = sigmoid(z1)
    z2 = np.dot(a1, w2) + b2
    a2 = sigmoid(z2)
    return int(np.round(a2)[0])




# -----------------------------------------------------------------------

# TEST

# 학습 완료 후 예측
input_example = np.array([0, 1])
prediction = predict(input_example)
print("Input:", input_example)
print("Prediction:", prediction)



# 레이어 구조도 출력 

# 그래프 생성
graph = Digraph(format='png', engine='dot')
graph.node('Input0', 'Input 0')
graph.node('Input1', 'Input 1')
graph.node('Hidden0', 'Hidden 0')
graph.node('Hidden1', 'Hidden 1')
graph.node('Output', 'Output')
graph.node('Bias1', 'Bias 1')
graph.node('Bias2', 'Bias 2')

# 그래프 속성 설정
graph.attr(rankdir='LR')  # 좌에서 우로 방향 설정

# 입력 노드와 히든 노드 연결
graph.edge('Input0', 'Hidden0', label='w1[0]')
graph.edge('Input0', 'Hidden1', label='w1[1]')
graph.edge('Input1', 'Hidden0', label='w1[2]')
graph.edge('Input1', 'Hidden1', label='w1[3]')

# 히든 노드와 출력 노드 연결
graph.edge('Hidden0', 'Output', label='w2[0]')
graph.edge('Hidden1', 'Output', label='w2[1]')

# 바이어스 노드와 히든 노드 연결
graph.edge('Bias1', 'Hidden0', label='b1[0]')
graph.edge('Bias1', 'Hidden1', label='b1[1]')
graph.edge('Bias2', 'Output', label='b2[0]')


# 그래프 시각화 및 출력
graph.view()

 

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (2/4) - alpha–beta pruning  (0) 2023.09.12
Tic-Tac-Toe 게임 제작 (1/4) - minimax  (0) 2023.09.12
SARSA  (0) 2023.08.28
Q-learning  (0) 2023.08.28
MNIST - TensorFlowLite  (0) 2023.07.19
블로그 이미지

SKY STORY

,

SARSA

개발/AI,ML,ALGORITHM 2023. 8. 28. 09:14
반응형

 

# SARSA
# 
# Created by netcanis on 2023/08/22.
#
# 5x10 크기의 그리드 월드 환경에서 SARSA 알고리즘을 실행하는 간단한 예시입니다. 
# 에이전트는 상, 하, 좌, 우로 움직이며 목표 지점에 도달하는 최적의 경로를 학습
# 매 에피소드마다 이동 경로 및 순서를 실시간으로 출력
# 에피소드 100의 배수마다 이동경로 및 순서 출력
# 경로 횟수가 적을수록 높은 보상
# 에피소드가 증가할수록 탐험보다 활용을 선택하도록 개선.
#
# SARSA (State-Action-Reward-State-Action)는 강화학습의 한 종류로, 상태와 동작의 시퀀스를 고려하여 학습하는 알고리즘입니다. 
# 이 알고리즘은 Q-learning과 비슷하지만, Q-learning이 다음 상태의 최대 Q 값을 사용하는 반면에 SARSA는 다음 상태에서 실제로 
# 선택한 동작에 대한 Q 값을 사용합니다. 이로 인해 더 안정적으로 학습하고, 더 정확한 제어를 할 수 있는 특징이 있습니다.


import numpy as np
import matplotlib.pyplot as plt


# 그리드 월드 환경 설정
# 0: 빈공간, 1: 벽, 2: 목적지, (0,0): 시작위치
grid = np.array([[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
                 [0, 0, 0, 1, 0, 0, 1, 0, 0, 0],
                 [0, 0, 0, 0, 1, 0, 0, 1, 0, 0],
                 [0, 1, 1, 0, 1, 0, 0, 0, 1, 0],
                 [0, 0, 0, 0, 0, 0, 0, 0, 1, 2]])

# 환경 설정
NUM_STATES = np.prod(grid.shape)  # 상태 공간 크기 (5x6=30)
NUM_ACTIONS = 4  # 상, 하, 좌, 우

# hyperparameter
EXPLORATION_PROB = 0.2  # 탐험율 - exploration(탐험)과 exploitation(활용) 사이의 균형을 조절.
LEARNING_RATE = 0.1     # 학습률 - 경사 하강법 등 최적화 알고리즘에서 가중치 업데이트에 사용되는 학습률.
DISCOUNT_FACTOR = 0.99  # 할인율 - 강화 학습에서 미래 보상을 현재 보상보다 얼마나 중요하게 고려할지를 조절하는 요소.
NUM_EPISODES = 1500     # 강화 학습에서 에피소드 수

# Q 함수 초기화
Q = np.zeros((NUM_STATES, NUM_ACTIONS))

# 상태 인덱스 계산 함수
def state_index(state):
    return state[0] * grid.shape[1] + state[1]

# 그래프 초기화
plt.ion()
fig, ax = plt.subplots()
episode_rewards = []

# 시작 위치 설정
start_position = (0, 0)
print("start_position :", start_position) # (0, 0)

# 타겟 위치 설정
target_position = np.argwhere(grid == 2)[0]
print("target_position :", target_position) # (4, 9)


# 그리드 월드 환경 출력 함수
def plot_grid_world(state, path=[]):
    plt.imshow(grid, cmap="gray", interpolation="none", origin="upper")
    plt.xticks([])
    plt.yticks([])
    
    if path:
        path = np.array(path)
        plt.plot(path[:, 1], path[:, 0], marker='o', markersize=4, linestyle='-', color='green')
        
        for i, pos in enumerate(path):
            plt.text(pos[1], pos[0], str(i + 1), ha="center", va="center", color="orange", fontsize=14)
    
    plt.text(state[1], state[0], "A", ha="center", va="center", color="red", fontsize=16)
    plt.text(grid.shape[1] - 1, grid.shape[0] - 1, "B", ha="center", va="center", color="green", fontsize=16)


def choose_action(state, epsilon):
    if np.random.rand() < epsilon:
        return np.random.choice(NUM_ACTIONS)
    return np.argmax(Q[state_index(state)])


# SARSA 알고리즘
for episode in range(NUM_EPISODES):
    print(f"Episode: {episode + 1}/{NUM_EPISODES}")
    
    state = tuple(start_position)
    total_reward = 0
    path_taken = []
    
    # Decaying exploration
    # 에피소드가 증가할 수록 탐험하는 비율을 쇠퇴시킨다.(원래 탐험율의 절반으로 줄인다)
    epsilon = EXPLORATION_PROB + (EXPLORATION_PROB / 2 - EXPLORATION_PROB) * (1.0 - episode / NUM_EPISODES)
    
    # 첫 이동 선택
    action = choose_action(state, epsilon)
    
    while state != tuple(target_position):

        # max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
        if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
            next_state = (max(state[0] - 1, 0), state[1])
        elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
            next_state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
        elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
            next_state = (state[0], max(state[1] - 1, 0))
        elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
            next_state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
        

        # 보상
        # reward = -1 if grid[next_state] == 1 else 1 if grid[next_state] == 2 else 0
        if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
            reward = -(NUM_STATES * 100)
        else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
            reward = -(len(path_taken) + 1)
        
        
        # 다음 이동 선택
        next_action = choose_action(next_state, epsilon)
        
        
        # SARSA
        a1 = Q[state_index(state), action]
        a2 = Q[state_index(next_state), next_action]
        Q[state_index(state)][action] = a1 + LEARNING_RATE * (reward + DISCOUNT_FACTOR * a2 - a1)
        
        
        total_reward += reward
        path_taken.append(state)
        state = next_state
        action = next_action
        
        
        # 에피소드 다음 배수마다 이동경로 및 순서 출력
        if (episode + 1) % 100 == 0:
            plt.figure(2)
            plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
            plt.title(f"Episode : {episode + 1}")
            plot_grid_world(state, path_taken)
            plt.pause(0.01)
            
    episode_rewards.append(total_reward)

    # 에피소드 10의 배수마다 이동경로 및 순서 출력
    if (episode + 1) % 10 == 0:
        plt.figure(1)
        plt.plot(episode_rewards)
        plt.xlabel("Episode")
        plt.ylabel("Total Reward")
        plt.title("Total Reward per Episode")
        plt.show()
        plt.pause(0.01)
    


#----------------------------------------------------------------------------
#
# 결과 출력 
#

# 학습 완료 후 최적 경로 및 순서 출력
state = tuple(start_position) # 시작 위치
total_reward = 0
best_trajectory = [state]  # 최적 경로 및 순서를 저장할 리스트

# 최적 경로 추적
# max와 min 함수는 에이전트가 그리드 월드 환경 내에서 벗어나지 않도록 하기 위해 사용
while state != tuple(target_position):
    action = np.argmax(Q[state_index(state)])
    if action == 0:   # 상 : 현재 행을 1 감소시킴 (위쪽으로 이동)
        state = (max(state[0] - 1, 0), state[1])
    elif action == 1: # 하 : 현재 행을 1 증가시킴 (아래쪽으로 이동)
        state = (min(state[0] + 1, grid.shape[0] - 1), state[1])
    elif action == 2: # 좌 : 현재 열을 1 감소시킴 (왼쪽으로 이동)
        state = (state[0], max(state[1] - 1, 0))
    elif action == 3: # 우 : 현재 열을 1 증가시킴 (오른쪽으로 이동)
        state = (state[0], min(state[1] + 1, grid.shape[1] - 1))
    
    best_trajectory.append(state)
    
    # 보상
    # reward = -1 if grid[next_state] != 1 else -100
    if grid[next_state] == 1: # 벽을 만났을 때 보상 설정
        reward = -(NUM_STATES * 100)
    else: # 경로 횟수가 증가될 수록 횟수 만큼 - 보상 (즉 경로 횟수가 적을 수록 보상이 크다)
        reward = -(len(path_taken) + 1)
    
    total_reward += reward    
    


# 최적 경로 및 순서 출력
plt.figure(3)  # 자동으로 크기 조정됨, 새로운 창으로 출력 
plt.clf() # Matplotlib의 현재 활성화된 그림 창을 지우기
plt.title(f"Total Reward : {total_reward}, Best Trajectory Length : {len(best_trajectory)}")
plot_grid_world(state, best_trajectory)
plt.show(block=True)  # 그래프를 출력한 후 버튼을 누를 때까지 대기        


# 결과 로그 출력 
print("Learned Q-values:")
print(Q)

 

 

반응형

'개발 > AI,ML,ALGORITHM' 카테고리의 다른 글

Tic-Tac-Toe 게임 제작 (1/4) - minimax  (0) 2023.09.12
Simple Neural Network XOR  (0) 2023.08.29
Q-learning  (0) 2023.08.28
MNIST - TensorFlowLite  (0) 2023.07.19
MNIST - Keras  (0) 2023.07.19
블로그 이미지

SKY STORY

,