2025-05-10

ai06_5 NalMok

#import pdb

import sys

import random

from PyQt5.QtWidgets import QApplication, QWidget, QMessageBox, QPushButton

from PyQt5.QtGui import QPainter, QColor, QPen

from PyQt5.QtCore import Qt, QTimer, QPoint


BOARD_SIZE = 19

CELL_SIZE = 40

STONE_RADIUS = CELL_SIZE>>1


EMPTY = 0

BLACK = 1

WHITE = 2

#보드 바탕색

BOARD_COLOR = QColor(222, 184, 135)

# 나이트 이동 방향

NALMOK_DIRECTIONS = [(1, 2),(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2),  (2, -1), (2, 1)]



class NalmokGame(QWidget):

    def __init__(self):

        super().__init__()

        self.setWindowTitle("날목")

        self.setFixedSize(BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE +50)


        self.board = [[EMPTY for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]

        self.move_history = []

        self.game_over_flag = False

        self.winning_line = [] # 승리한 5개 돌의 좌표를 저장할 리스트


        self.last_black_move = None

        self.last_white_move = None


        undo_button = QPushButton("Undo", self)

        undo_button.move(10, self.height() - 40)

        undo_button.clicked.connect(self.undo_move)


        # 컴퓨터가 흑으로 시작

        center = BOARD_SIZE // 2

        self.place_stone(center, center, BLACK)

        self.turn = WHITE


        self.show()


    def paintEvent(self, event):

        qp = QPainter()

        qp.begin(self)

        self.draw_board(qp)

        self.draw_stones(qp)

        self.draw_winning_line(qp) # 승리한 돌을 표시하는 함수 호출

        qp.end()


    def draw_winning_line(self, qp):

        if self.winning_line:

            qp.setPen(QPen(QColor(0, 255, 0), 3)) # 녹색으로 강조

            for x, y in self.winning_line:

                cx = STONE_RADIUS + x * CELL_SIZE

                cy = STONE_RADIUS + y * CELL_SIZE

                qp.drawEllipse(cx - STONE_RADIUS // 2, cy - STONE_RADIUS // 2, STONE_RADIUS, STONE_RADIUS)



    def draw_board(self, qp):

        qp.setBrush(BOARD_COLOR)

        qp.drawRect(0, 0, self.width(), self.height())


        qp.setPen(QPen(Qt.black, 1))

        for i in range(BOARD_SIZE):

            qp.drawLine(STONE_RADIUS, STONE_RADIUS + i * CELL_SIZE,

                            STONE_RADIUS + (BOARD_SIZE - 1) * CELL_SIZE, STONE_RADIUS + i * CELL_SIZE)

            qp.drawLine(STONE_RADIUS + i * CELL_SIZE, STONE_RADIUS,

                            STONE_RADIUS + i * CELL_SIZE, STONE_RADIUS + (BOARD_SIZE - 1) * CELL_SIZE)


        star_points = [(3, 3), (3, 9), (3, 15), (9, 3), (9, 9), (9, 15), (15, 3), (15, 9), (15, 15)]

        for x, y in star_points:

            cx = STONE_RADIUS + x * CELL_SIZE

            cy = STONE_RADIUS + y * CELL_SIZE

            qp.setBrush(BLACK)

            qp.drawEllipse(cx-3, cy-3, 6, 6)


    def draw_stones(self, qp):

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] != EMPTY:

                    qp.setBrush(QColor(0, 0, 0) if self.board[y][x] == BLACK else QColor(255, 255, 255))

                    qp.drawEllipse(

                        STONE_RADIUS + x * CELL_SIZE - STONE_RADIUS,

                        STONE_RADIUS + y * CELL_SIZE - STONE_RADIUS,

                        STONE_RADIUS * 2,

                        STONE_RADIUS * 2

                    )


        last = self.last_black_move if self.turn == WHITE else self.last_white_move

#       pdb.set_trace()

        if last:

            x, y = last

            cx = STONE_RADIUS + x * CELL_SIZE

            cy = STONE_RADIUS + y * CELL_SIZE

            qp.setPen(QPen(BOARD_COLOR, 2))

            qp.drawRect(cx-1, cy-1, 3, 3)


    def mousePressEvent(self, event):

        if self.game_over_flag or self.turn != WHITE:

            return


        x = int((event.x() - STONE_RADIUS + CELL_SIZE / 2) // CELL_SIZE)

        y = int((event.y() - STONE_RADIUS + CELL_SIZE / 2) // CELL_SIZE)


        if 0 <= x < BOARD_SIZE and 0 <= y < BOARD_SIZE and self.board[y][x] == EMPTY:

            self.place_stone(x, y, WHITE)

            if self.check_win(x, y, WHITE, True): # 승리 시 winning_line 업데이트

                self.update()

                self.game_over("사용자(백) 승리!")

                return


            self.turn = BLACK

            self.update()

            QTimer.singleShot(1, self.computer_move)


    def place_stone(self, x, y, stone):

        self.board[y][x] = stone

        self.move_history.append((x, y, stone))

        if stone == BLACK:

            self.last_black_move = (x, y)

        else:

            self.last_white_move = (x, y)


    def undo_move(self):

        if len(self.move_history) >= 2:

            for _ in range(2):

                x, y, stone = self.move_history.pop()

                self.board[y][x] = EMPTY

                if stone == BLACK:

                    self.last_black_move = None

                else:

                    self.last_white_move = None

            self.turn = WHITE

            self.winning_line = [] # Undo 시 승리선 초기화

            self.update()


    def check_win(self, x, y, stone, update_winning_line=False):

        winning_stones = []

        for dx, dy in NALMOK_DIRECTIONS:

            line = [(x, y)]

            count = 1

            for dir in [1, -1]:

                for i in range(1, 5):

                    nx, ny = x + dx * i * dir, y + dy * i * dir

                    if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == stone:

                        line.append((nx, ny))

                        count += 1

                    else:

                        break

            if count >= 5:

                if update_winning_line:

                    self.winning_line = line

                return True

        return False


    def should_block_nalmok(self, x, y, stone, length):

        for dx, dy in NALMOK_DIRECTIONS:

            count = 0

            blocked = 0

            for dir in [1, -1]:

                temp_count = 0

                temp_blocked = 0

                for i in range(1, length):

                    nx = x + dx * i * dir

                    ny = y + dy * i * dir

                    if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE:

                        cell = self.board[ny][nx]

                        if cell == stone:

                            temp_count += 1

                        elif cell == EMPTY:

                            continue

                        else:

                            temp_blocked += 1

                            break

                    else:

                        temp_blocked += 1

                        break

                if dir == 1:

                    count += temp_count

                    blocked += temp_blocked

                else:

                    count += temp_count

                    blocked += temp_blocked

            if count == length -1 and blocked < 2:

                # Check if placing a stone at (x, y) would complete the sequence

                placed = False

                for dir in [1, -1]:

                    nx_check = x + dx * length * dir

                    ny_check = y + dy * length * dir

                    if 0 <= nx_check < BOARD_SIZE and 0 <= ny_check < BOARD_SIZE:

                        if self.board[ny_check][nx_check] == stone:

                            continue # Already blocked on this side

                    else:

                        continue # Already blocked on this side


                    temp_board = [row[:] for row in self.board]

                    temp_board[y][x] = stone

                    win = False

                    check_count = 1

                    for check_dir in [1, -1]:

                        for i in range(1, 5):

                            cx, cy = x + dx * i * check_dir, y + dy * i * check_dir

                            if 0 <= cx < BOARD_SIZE and 0 <= cy < BOARD_SIZE and temp_board[cy][cx] == stone:

                                check_count += 1

                            else:

                                break

                    if check_count >= 5:

                        win = True


                if count == length - 1 and blocked < 2:

                    can_extend = False

                    for dir in [1, -1]:

                        nx_extend = x + dx * length * dir

                        ny_extend = y + dy * length * dir

                        if 0 <= nx_extend < BOARD_SIZE and 0 <= ny_extend < BOARD_SIZE and self.board[ny_extend][nx_extend] == EMPTY:

                            can_extend = True

                            break

                    if can_extend or blocked == 0:

                        return True

        return False


    def evaluate_board(self, stone):

        score = 0

        opponent = WHITE if stone == BLACK else BLACK

        # Evaluate score for the given stone

        for length in range(2, 5):

            for y in range(BOARD_SIZE):

                for x in range(BOARD_SIZE):

                    if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, stone, length):

                        if length == 4:

                            score += 1000

                        elif length == 3:

                            score += 100

                        elif length == 2:

                            score += 10


        # Evaluate score for the opponent to subtract potential threats

        for length in range(2, 5):

            for y in range(BOARD_SIZE):

                for x in range(BOARD_SIZE):

                    if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, opponent, length):

                        if length == 4:

                            score -= 900  # Less priority to block than to create own 4

                        elif length == 3:

                            score -= 90

                        elif length == 2:

                            score -= 9

        return score


    def computer_move(self):

        if self.game_over_flag:

            return


        # 우선순위 1: 컴퓨터 승리 수 (5개)

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY:

                    self.place_stone(x, y, BLACK)

                    if self.check_win(x, y, BLACK, True): # 승리 시 winning_line 업데이트

                        self.update()

                        self.game_over("컴퓨터(흑) 승리!")

                        return

                    self.board[y][x] = EMPTY

                    self.move_history.pop()


        # 우선순위 2: 컴퓨터가 만들 수 있는 열린 4

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, BLACK, 4):

                    self.place_stone(x, y, BLACK)

                    self.turn = WHITE

                    self.update()

                    return


        # 우선순위 3: 상대방의 즉각적인 승리 차단 (5개)

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY:

                    self.place_stone(x, y, WHITE)

                    if self.check_win(x, y, WHITE, True): # 승리 시 winning_line 업데이트

                        self.board[y][x] = EMPTY

                        self.move_history.pop()

                        self.place_stone(x, y, BLACK)

                        self.turn = WHITE

                        self.update()

                        return

                    self.board[y][x] = EMPTY

                    self.move_history.pop()


        # 우선순위 4: 상대 열린 4 차단

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, WHITE, 4):

                    self.place_stone(x, y, BLACK)

                    self.turn = WHITE

                    self.update()

                    return


        # 우선순위 5: 컴퓨터 공격 - 열린 3 만들기

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, BLACK, 3):

                    self.place_stone(x, y, BLACK)

                    self.turn = WHITE

                    self.update()

                    return


        # 우선순위 6: 상대 열린 3 차단

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY and self.should_block_nalmok(x, y, WHITE, 3):

                    self.place_stone(x, y, BLACK)

                    self.turn = WHITE

                    self.update()

                    return



        # 우선순위 8: 평가 기반 최적 수

        best_score = -float('inf')

        best_move = None

        center = BOARD_SIZE // 2

        search_range = 5  # 중앙 근처 탐색으로 연산량 감소

        for y in range(max(0, center - search_range), min(BOARD_SIZE, center + search_range)):

            for x in range(max(0, center - search_range), min(BOARD_SIZE, center + search_range)):

                if self.board[y][x] == EMPTY:

                    self.board[y][x] = BLACK

                    score = self.evaluate_board(BLACK) - self.evaluate_board(WHITE)

                    self.board[y][x] = EMPTY

                    if score > best_score:

                        best_score = score

                        best_move = (x, y)

        if best_move:

            x, y = best_move

            self.place_stone(x, y, BLACK)

            self.turn = WHITE

            self.update()

            return


        # 대체 수: 최근 돌 근처 나이트 이동 (더 안전한 수)

        possible_moves = []

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == BLACK:

                    for dx, dy in NALMOK_DIRECTIONS:

                        nx, ny = x + dx, y + dy

                        if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == EMPTY:

                            possible_moves.append((nx, ny))


        if possible_moves:

            move = random.choice(possible_moves)

            self.place_stone(move[0], move[1], BLACK)

            self.turn = WHITE

            self.update()

            return


        # 정말 둘 곳이 없다면 랜덤으로

        empty_cells = []

        for y in range(BOARD_SIZE):

            for x in range(BOARD_SIZE):

                if self.board[y][x] == EMPTY:

                    empty_cells.append((x, y))

        if empty_cells:

            move = random.choice(empty_cells)

            self.place_stone(move[0], move[1], BLACK)

            self.turn = WHITE

            self.update()

            return



    def game_over(self, message):

        self.game_over_flag = True

        self.update() # 마지막 돌을 포함한 승리선을 표시하기 위해 update 호출

        QMessageBox.information(self, "게임 종료", message)

        self.close()


if __name__ == "__main__":

    app = QApplication(sys.argv)

    game = NalmokGame()

    sys.exit(app.exec_())