이제 본격적으로 코딩을 할 것이다.
오늘 할 것은 오목 엔진을 만드는 것이다. 오목 엔진을 만들기 위해 오목에 대해 알아보자.
오목은 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 이용)
아무튼 작동하는 오목 판을 만들었다. 검은색 돌이 먼저 두게 되며, 클릭할 때 마다 순서가 바뀐다.
다만, 아직 오목 판에 렌주룰을 적용하지 않았다. 이걸 어떻게 해야할지 고민을 좀 해야하므로 다음 글에 써보도록 하겠다!
오목 AI를 만들어보자! [2편] - 오목 알고리즘 (미완) (0) | 2024.08.13 |
---|---|
오목 AI를 만들어보자! [0편] (0) | 2024.08.09 |