상세 컨텐츠

본문 제목

오목 AI를 만들어보자! [1편] - 오목 판 만들기

개발일지/오목AI

by 망고멍토 2024. 8. 11. 23:57

본문

728x90

이제 본격적으로 코딩을 할 것이다.

 

오늘 할 것은 오목 엔진을 만드는 것이다. 오목 엔진을 만들기 위해 오목에 대해 알아보자.

 

오목은 15x15의 판에서 진행되는 게임이다. 플레이어는 흑과 백 두명으로, 먼저 자신의 돌로 5개를 잇따라 놓는 사람이 이기는 간단한 게임이다. 세부적인 규칙은 다양하지만, 일반적으로 흑의 유리함을 줄이기 위해 '렌주룰'을 사용한다. 흑이 렌주룰에서 3-3, 4-4, 4-3-3, 4-4-3, 장목(6목 등)이 금지된다. 사실 이렇게 흑에 제한을 두어도, 공격권이 흑에게 먼저 있기 때문에 흑이 더 유리한 게임이 된다.

 

오목에 대해서는 이정도만 알아보고, 이제 pygame을 이용해서 실제 게임을 만들어보자.

import pygame, sys, win32con, win32gui, os
from pygame import gfxdraw

pygame.init()


def wndProc(oldWndProc, draw_callback, hWnd, message, wParam, lParam):
    if message == win32con.WM_SIZE:
        draw_callback()
        win32gui.RedrawWindow(hWnd, None, None, win32con.RDW_INVALIDATE | win32con.RDW_ERASE)
    return win32gui.CallWindowProc(oldWndProc, hWnd, message, wParam, lParam)

class draw:       
    def rrect(surface, rect, color, radius=0.4):
        rect = pygame.Rect(rect)
        color = pygame.Color(*color)
        alpha = color.a
        color.a = 0
        pos = rect.topleft
        rect.topleft = 0,0
        rectangle = pygame.Surface(rect.size,pygame.SRCALPHA)
        circle = pygame.Surface([min(rect.size)*3]*2,pygame.SRCALPHA)
        pygame.draw.ellipse(circle,(0,0,0),circle.get_rect(),0)
        circle = pygame.transform.smoothscale(circle,[int(min(rect.size)*radius)]*2)
        radius = rectangle.blit(circle,(0,0))
        radius.bottomright = rect.bottomright
        rectangle.blit(circle,radius)
        radius.topright = rect.topright
        rectangle.blit(circle,radius)
        radius.bottomleft = rect.bottomleft
        rectangle.blit(circle,radius)

        rectangle.fill((0,0,0),rect.inflate(-radius.w,0))
        rectangle.fill((0,0,0),rect.inflate(0,-radius.h))

        rectangle.fill(color,special_flags=pygame.BLEND_RGBA_MAX)
        rectangle.fill((255,255,255,alpha),special_flags=pygame.BLEND_RGBA_MIN)
        return surface.blit(rectangle,pos)

class Display:
    def __init__(self,
                 resolution:tuple[int, int]=(900, 900),
                 ) -> None:
        self.resolution: tuple[int] = resolution
        self.window: pygame.Surface = pygame.display.set_mode(resolution, pygame.DOUBLEBUF|pygame.RESIZABLE)
        self.clock: pygame.Clock = pygame.time.Clock()
        self.plate: list[list[int]] = [[0 for _ in range(15)] for _ in range(15)]
        self.run: bool = True
        self.FPS: int = 60

        pygame.display.set_caption("Delu Omok")

        self.PlateSurf: pygame.Surface = pygame.Surface((800, 800), pygame.SRCALPHA)
        self.PlatePos: tuple[int] = (self.resolution[0]-800)//2, (self.resolution[1]-800)//2
        self.render_plate()

        self.pointing: tuple[int] = None
        self.turn = -1

        pass

    def calc_cord(self, cord:tuple[int]) -> list[int]:
        return list([50+cord[0]*50, 50+cord[1]*50])

    def render_plate(self) -> None:

        self.PlateSurf.fill((221, 191, 166)) # bg color

        for i in range(15): # draw lines
            pygame.draw.line(self.PlateSurf, (0, 0, 0), self.calc_cord((i, 0)), self.calc_cord((i, 14)))
            pygame.draw.line(self.PlateSurf, (0, 0, 0), self.calc_cord((0, i)), self.calc_cord((14, i)))

        pygame.draw.circle(self.PlateSurf, (0, 0, 0), self.calc_cord((3, 3)), 3)
        pygame.draw.circle(self.PlateSurf, (0, 0, 0), self.calc_cord((3, 11)), 3)
        pygame.draw.circle(self.PlateSurf, (0, 0, 0), self.calc_cord((11, 3)), 3)
        pygame.draw.circle(self.PlateSurf, (0, 0, 0), self.calc_cord((11, 11)), 3)

        # render stones
        for y, line in enumerate(self.plate):
            for x, status in enumerate(line):
                # (x, y) -> status
                color: tuple[int]
                pos = self.calc_cord((x, y))
                if (status == 0): continue
                elif (status == 1): color = (255, 255, 255) # white
                else: color = (0, 0, 0) # black

                # draw.aacircle(self.PlateSurf, (128, 128, 128), self.calc_cord((x, y)), 25)
                draw.rrect(self.PlateSurf, [pos[0]-24, pos[1]-24, 48, 48], (128, 128, 128), .99)
                draw.rrect(self.PlateSurf, [pos[0]-23, pos[1]-23, 46, 46], color, .99)
        return

    def render(self) -> None:
        self.window.fill((255, 255, 255))
        self.window.blit(self.PlateSurf, self.PlatePos)

        if (self.pointing):
            pos = self.calc_cord(self.pointing)
            gfxdraw.aacircle(self.window, self.PlatePos[0]+pos[0], self.PlatePos[1]+pos[1], 24, (0, 0, 0))

        pygame.display.update()

        return

    def event(self, events:list[pygame.Event]) -> None:

        for event in events:
            if (event.type == pygame.QUIT):
                pygame.quit()
                sys.exit()

            if (event.type == pygame.MOUSEBUTTONUP):
                if (event.button == 1 and self.pointing):
                    stone = self.plate[self.pointing[1]][self.pointing[0]]
                    if (stone == 0):
                        self.plate[self.pointing[1]][self.pointing[0]] = self.turn
                        self.render_plate()
                        self.turn *= -1

        return

    def update(self) -> None:

        events = pygame.event.get()
        self.resolution = pygame.display.get_window_size()
        self.PlatePos = (self.resolution[0]-800)//2, (self.resolution[1]-800)//2

        mouse = pygame.mouse.get_pos()
        px, py = mouse[0]-self.PlatePos[0]-25, mouse[1]-self.PlatePos[1]-25
        px //= 50
        py //= 50

        if (px < 0 or px > 14 or py < 0 or py > 14):
            self.pointing = None
        else:
            self.pointing = (px, py)

        self.render()
        self.event(events)
        self.clock.tick(self.FPS)

        return

    def loop(self) -> None:
        hwnd = pygame.display.get_wm_info()['window']
        oldWndProc = win32gui.SetWindowLong(hwnd, win32con.GWL_WNDPROC, lambda *args: wndProc(oldWndProc, self.update, *args))

        while (self.run):
            self.update()
            continue

        pygame.quit()
        sys.exit()

        return

if __name__ == '__main__':
    display = Display()
    display.loop()

 

항상 그러하듯 내 코드는 친절하지 않다. 다른 것들은 딱히 설명할 필요가 없어 보이지만, 저 wndProc에 대해 설명해야할 필요가 있을 것 같다. 일단 pygame의 기본 렌더링 방식의 문제점있다. pygame.RESIZEABLE로 크기 조절을 가능해도, 크기 재조정을 할 때 화면이 업데이트가 되지 않는다. 나는 그걸 싫어하기 때문에 wndProc를 이용해 이를 해결한 것이다

.

좌: 기본 | 우: wndProc

(좌: 기본 | 우: wndProc 이용)

 

아무튼 작동하는 오목 판을 만들었다. 검은색 돌이 먼저 두게 되며, 클릭할 때 마다 순서가 바뀐다.

다만, 아직 오목 판에 렌주룰을 적용하지 않았다. 이걸 어떻게 해야할지 고민을 좀 해야하므로 다음 글에 써보도록 하겠다!

728x90
반응형

관련글 더보기