여러분, 가끔 친구들과 함께 보드 게임을 즐기기는 하나요? 오늘은 여러분과 함께 대표적인 보드 게임 중 하나인 오목을 파이썬으로 멀티플레이를 구현해보려고 합니다. 오목이란 무엇인지, 어떻게 동작하는지 궁금하시죠? 그럼 바로 시작해볼까요!
오목은 검은 돌과 하얀 돌을 번갈아가며 두어 가로, 세로, 대각선 중 한 방향으로 연속된 다섯 개의 돌을 먼저 놓으면 이기는 게임입니다. 아주 간단한 규칙이지만 깊이 있는 전략이 필요한 게임이죠. 이번에 우리는 파이썬을 활용해서 이러한 오목 게임을 구현해보겠습니다.
본격적으로 구현에 들어가기 전에 구현에 필요한 패키지를 설치해 봅시다. 이번 포스트에서는 pygame을 이용해서 구현했습니다.
$ pip install pygame
게임을 구현하기 위해서는 먼저 게임의 주요 구조를 이해해야 합니다. 오목 게임을 위한 클래스와 메서드를 통해 어떤 방식으로 게임이 진행되는지 살펴봅시다.
오목 게임의 핵심 로직을 담당하는 Omok
클래스를 만들어보겠습니다. 이 클래스는 게임 보드와 현재 플레이어를 관리하고, 플레이어의 차례에 따라 돌을 놓으며, 금수(금지된 수)와 승리 조건을 체크하는 역할을 합니다.
class Omok:
def __init__(self):
self.board = [[0] * 19 for _ in range(19)]
self.current_player = BLACK
self.forbidden_moves = set()
self.directions = [(1, 0), (0, 1), (1, 1), (1, -1)]
board
: 19x19 크기의 게임 보드입니다. 0은 빈 곳, 1은 검은 돌, 2는 하얀 돌을 나타냅니다.current_player
: 현재 차례인 플레이어를 나타냅니다. 검은 돌로 시작합니다.forbidden_moves
: 금지된 수의 위치를 저장하는 셋입니다.directions
: 각 방향을 나타내는 튜플 리스트입니다.플레이어가 돌을 놓는 동작을 구현해보겠습니다. 돌을 놓고, 금수 업데이트, 승리 체크를 진행하는 메서드입니다.
def play(self, row, col):
self.board[row][col] = self.current_player
self.update_forbidden_moves()
if self.check_win(row, col):
return {
"board": self.board,
"forbidden_moves": self.forbidden_moves,
"winner": self.current_player,
"turn": -1,
}
self.current_player = 3 - self.current_player
return {
"board": self.board,
"forbidden_moves": self.forbidden_moves,
"winner": -1,
"turn": self.current_player,
}
위의 메서드는 주어진 위치에 돌을 놓고, 금수와 승리 여부를 체크합니다. 승리 시에는 승리 정보를 리턴하고, 그렇지 않으면 다음 턴을 리턴합니다.
돌을 5개 연속으로 놓았는지를 체크하는 메서드입니다. 4가지 방향을 모두 체크하여 승리 조건을 만족하는지 확인합니다.
def check_win(self, row, col):
for d in self.directions:
count = 1
for i in range(1, 6):
r, c = row + d[0] * i, col + d[1] * i
if 0 <= r < 19 and 0 <= c < 19 and self.board[r][c] == self.current_player:
count += 1
else:
break
for i in range(1, 6):
r, c = row - d[0] * i, col - d[1] * i
if 0 <= r < 19 and 0 <= c < 19 and self.board[r][c] == self.current_player:
count += 1
else:
break
if count == 5:
return True
return False
승리 조건을 체크하는 방식은 각 방향으로 돌을 차례대로 확인하며 5개 연속인 경우를 찾는 것입니다.
이제 오목 게임을 혼자가 아닌 친구와 같이 온라인으로 즐길 수 있도록 네트워크 기능을 추가해보겠습니다. 이를 위해 서버와 클라이언트를 구현할 것입니다.
서버는 두 명의 플레이어를 연결하고 돌을 주고받는 역할을 합니다.
class Server:
def __init__(self, host="localhost", port=12345):
self.host = host
self.port = port
self.socket = None
self.clients = []
self.game = None
self.replay = []
def setup_socket(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind((self.host, self.port))
self.socket.listen(2)
print(f"Server started on {self.host}:{self.port}")
def setup_game(self):
self.game = Omok()
def handle_clients(self):
turn = 0
while True:
data = self.clients[turn].recv(1024)
if not data:
print("no data")
break
data = pickle.loads(data)
row, col = data
result = copy.deepcopy(self.game.play(row, col))
self.replay.append(result)
response = pickle.dumps(result)
for client_socket in self.clients:
client_socket.send(response)
if result["winner"] != -1:
break
turn = 1 - turn
for client_socket in self.clients:
replay_data = pickle.dumps(self.replay)
client_socket.send(replay_data)
client_socket.recv(1024)
client_socket.close()
def run(self):
self.setup_socket()
self.setup_game()
print("Waiting for clients...")
init_data = {
"board": self.game.board,
"forbidden_moves": self.game.forbidden_moves,
"winner": -1,
"turn": 1,
"color": BLACK,
}
while len(self.clients) < 2:
client_socket, client_address = self.socket.accept()
client_socket.send(pickle.dumps(init_data))
self.clients.append(client_socket)
init_data["color"] = WHITE
print("Game started")
self.handle_clients()
서버는 소켓을 설정하고, 두 명의 클라이언트를 연결한 뒤 게임을 진행합니다.
클라이언트는 서버에 접속하여 플레이어의 입력을 받고, 돌을 놓는 역할을 합니다.
class Client:
def __init__(self, host, port):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.connect((host, port))
def receive(self):
print("waiting for data")
receive = self.socket.recv(16384)
print("received data")
return pickle.loads(receive)
def send(self, data):
self.socket.send(pickle.dumps(data))
클라이언트는 서버와 연결을 유지하면서 데이터를 주고받습니다.
게임 플레이어의 움직임은 Player
클래스에서 처리됩니다. 이 클래스는 보드의 상태를 그려주고, 플레이어의 클릭 이벤트를 처리하며 서버와의 데이터를 주고받습니다.
플래이어의 차례와 게임 상황을 제어하는 Player
클래스를 정의해봅시다.
class Player:
board = [[0] * 19 for _ in range(19)]
forbidden_moves = None
winner = None
turn = None
color = None
def __init__(self, client):
self.client = client
self.init_pygame()
self._draw_board()
def init_pygame(self):
pygame.init()
pygame.display.set_caption("Omok")
self.screen = pygame.display.set_mode(BOARD_SIZE, 0, 32)
self.background = pygame.image.load(BACKGROUND).convert()
self.clickable = False
def _draw_board(self):
outline = pygame.Rect(45, 45, 720, 720)
pygame.draw.rect(self.background, BLACK, outline, 3)
for i in range(18):
for j in range(18):
rect = pygame.Rect(45 + (40 * i), 45 + (40 * j), 40, 40)
pygame.draw.rect(self.background, BLACK, rect, 1)
for i in range(3):
for j in range(3):
coords = (165 + (240 * i), 165 + (240 * j))
pygame.draw.circle(self.background, BLACK, coords, 5, 0)
self.screen.blit(self.background, (0, 0))
pygame.display.update()
def _draw_stone(self, row, col, color):
coords = (45 + col * 40, 45 + row * 40)
if color == BLACK:
color_value = (0, 0, 0)
else:
color_value = (255, 255, 255)
pygame.draw.circle(self.screen, color_value, coords, 20, 0)
pygame.display.update()
def run(self):
while True:
self._update_board()
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if self.clickable and event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos
row = int(round(((y - 45) / 40.0), 0))
col = int(round(((x - 45) / 40.0), 0))
if self.board[row][col] == 0 and not (row, col) in self.forbidden_moves:
self.client.send((row, col))
self.clickable = False
def _update_board(self):
data = self.client.receive()
self.board = data["board"]
self.forbidden_moves = data["forbidden_moves"]
self.turn = data["turn"]
self.winner = data["winner"]
self._draw_board()
for row in range(19):
for col in range(19):
if self.board[row][col] != 0:
self._draw_stone(row, col, self.board[row][col])
self.clickable = self.turn == self.color
if self.winner != -1:
self._show_winner()
def _show_winner(self):
font = pygame.font.Font(None, 36)
if self.winner == self.color:
text = font.render("You Win", True, (0, 0, 0))
else:
text = font.render("You Lose", True, (0, 0, 0))
text_rect = text.get_rect(centerx=self.screen.get_width() // 2, top=50)
pygame.draw.rect(self.screen, (255, 255, 255), text_rect)
self.screen.blit(text, text_rect)
pygame.display.update()
이제 플레이어는 서버와 연결 후 게임을 진행하는 동안 각자의 차례를 전달받고, 화면에 돌을 놓으며 게임을 진행합니다.
추가적으로, 게임이 끝난 후 다시 리플레이를 볼 수 있는 기능을 구현해보겠습니다. 리플레이의 데이터는 게임이 진행되는 동안 저장해두고, 나중에 이를 불러와 재생할 수 있도록 합니다.
리플레이 데이터를 불러와서 게임 진행 과정을 재생하는 Replay
클래스를 만들어보겠습니다.
class Replay:
def __init__(self, path):
with open(path, "rb") as f:
self.data = pickle.load(f)
self.pointer = 0
self.init_pygame()
self._draw_board()
def init_pygame(self):
pygame.init()
pygame.display.set_caption("Omok")
self.screen = pygame.display.set_mode(BOARD_SIZE, 0, 32)
self.background = pygame.image.load(BACKGROUND).convert()
def _draw_board(self):
outline = pygame.Rect(45, 45, 720, 720)
pygame.draw.rect(self.background, BLACK, outline, 3)
for i in range(18):
for j in range(18):
rect = pygame.Rect(45 + (40 * i), 45 + (40 * j), 40, 40)
pygame.draw.rect(self.background, BLACK, rect, 1)
for i in range(3):
for j in range(3):
coords = (165 + (240 * i), 165 + (240 * j))
pygame.draw.circle(self.background, BLACK, coords, 5, 0)
self.screen.blit(self.background, (0, 0))
pygame.display.update()
def _draw_stone(self, row, col, color):
coords = (45 + col * 40, 45 + row * 40)
if color == BLACK:
color_value = (0, 0, 0)
else:
color_value = (255, 255, 255)
pygame.draw.circle(self.screen, color_value, coords, 20, 0)
pygame.display.update()
def _update(self):
status = self.data[self.pointer]
self.board = status["board"]
self.forbidden_moves = status["forbidden_moves"]
self._draw_board()
for row in range(19):
for col in range(19):
if self.board[row][col] != 0:
self._draw_stone(row, col, self.board[row][col])
def run(self):
self._update()
while True:
pygame.time.wait(500)
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
self.pointer = max(0, self.pointer - 1)
elif event.key == pygame.K_RIGHT:
self.pointer = min(len(self.data) - 1, self.pointer + 1)
self._update()
리플레이 클래스는 이전의 게임 데이터(replay.pkl
파일)에 접근하여 게임의 진행 상황을 다시 보여줍니다. 게임을 처음부터 끝까지 재생할 수 있도록 키보드 화살표(←,→)를 이용해 이전 턴과 다음 턴을 이동할 수 있습니다.
이제 여러분도 파이썬을 이용해 오목 게임을 직접 구현해볼 수 있습니다. 소켓을 이용해 네트워크 기능을 추가하고, PyGame을 이용해 그래픽을 구현하였습니다. 그리고 마지막으로 리플레이 기능을 통해 게임을 다시 재생해볼 수도 있죠. 다양한 기능을 추가하며 자신만의 오목 게임을 더욱 발전시켜보세요!
프로그래밍 공부와 프로젝트 진행에 도움이 되길 바랍니다. 함께 해주셔서 감사합니다. 다음 포스트에서 또 만나요!
🔥 해당 포스팅은 Dev.POST 도움을 받아 작성되었습니다.