NalMok
글쓴이: 세벌 / 작성시간: 월, 2025/05/05 - 4:23오후
오목의 변형 게임 날목을 구상하고 PyQt로 만들었어요. 더 개선할 부분 있으면 알려주셔요.
#NalMok V13
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 = 30
STONE_RADIUS = 12
EMPTY = 0
BLACK = 1
WHITE = 2
# 나이트 이동 방향
NALMOK_DIRECTIONS = [(-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2, 1)]
class DraggableButton(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
self.setMouseTracking(True)
self.dragging = False
self.offset = QPoint()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging = True
self.offset = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.dragging:
new_pos = self.mapToParent(event.pos() - self.offset)
self.move(new_pos)
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
self.dragging = False
super().mouseReleaseEvent(event)
class NalmokGame(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("날목")
self.setFixedSize(BOARD_SIZE * CELL_SIZE, BOARD_SIZE * CELL_SIZE)
self.board = [[EMPTY for _ in range(BOARD_SIZE)] for _ in range(BOARD_SIZE)]
self.move_history = []
self.game_over_flag = False
self.last_black_move = None
self.last_white_move = None
undo_button = DraggableButton("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)
qp.end()
def draw_board(self, qp):
qp.setBrush(QColor(222, 184, 135))
qp.drawRect(0, 0, self.width(), self.height())
qp.setPen(QPen(Qt.black, 1))
for i in range(BOARD_SIZE):
qp.drawLine(CELL_SIZE // 2, CELL_SIZE // 2 + i * CELL_SIZE,
CELL_SIZE // 2 + (BOARD_SIZE - 1) * CELL_SIZE, CELL_SIZE // 2 + i * CELL_SIZE)
qp.drawLine(CELL_SIZE // 2 + i * CELL_SIZE, CELL_SIZE // 2,
CELL_SIZE // 2 + i * CELL_SIZE, CELL_SIZE // 2 + (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 = CELL_SIZE // 2 + x * CELL_SIZE
cy = CELL_SIZE // 2 + y * CELL_SIZE
qp.setBrush(QColor(0, 0, 0))
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(
CELL_SIZE // 2 + x * CELL_SIZE - STONE_RADIUS,
CELL_SIZE // 2 + 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
if last:
x, y = last
cx = CELL_SIZE // 2 + x * CELL_SIZE
cy = CELL_SIZE // 2 + y * CELL_SIZE
qp.setPen(QPen(Qt.red, 2))
qp.drawEllipse(cx - 5, cy - 5, 10, 10)
def mousePressEvent(self, event):
if self.game_over_flag or self.turn != WHITE:
return
x = int((event.x() - CELL_SIZE // 2 + CELL_SIZE / 2) // CELL_SIZE)
y = int((event.y() - CELL_SIZE // 2 + 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):
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.update()
def check_win(self, x, y, stone):
for dx, dy in NALMOK_DIRECTIONS:
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:
count += 1
else:
break
if count >= 5:
return True
return False
def should_block_nalmok(self, x, y, stone, length):
for dx, dy in NALMOK_DIRECTIONS:
count = 1
blocked = 0
for i in range(1, length):
nx = x + dx * i
ny = y + dy * i
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE:
cell = self.board[ny][nx]
if cell == stone:
count += 1
elif cell == EMPTY:
continue
else:
blocked += 1
break
else:
blocked += 1
break
if count == length and blocked == 0:
return True
return False
def check_special_attack_pattern(self):
base_patterns = [(-3, -6), (-1, -2), (1, 2), (3, 6)]
all_patterns = []
for dx, dy in base_patterns:
all_patterns.append((dx, dy))
all_patterns.append((-dx, dy))
all_patterns.append((dx, -dy))
all_patterns.append((-dx, -dy))
all_patterns.append((dy, dx))
all_patterns.append((-dy, dx))
all_patterns.append((dy, -dx))
all_patterns.append((-dy, -dx))
all_patterns = list(set(all_patterns))
for y in range(BOARD_SIZE):
for x in range(BOARD_SIZE):
if self.board[y][x] != EMPTY:
continue
for pattern in [base_patterns, [(dy, dx) for dx, dy in base_patterns]]:
found = True
for dx, dy in pattern:
nx, ny = x + dx, y + dy
if not (0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == WHITE):
found = False
break
if found:
return x, y
return None
def evaluate_board(self, stone):
score = 0
for y in range(BOARD_SIZE):
for x in range(BOARD_SIZE):
if self.board[y][x] == stone:
for dx, dy in NALMOK_DIRECTIONS:
count = 1
blocked = 0
for i in range(1, 5):
nx, ny = x + dx * i, y + dy * i
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE:
if self.board[ny][nx] == stone:
count += 1
elif self.board[ny][nx] != EMPTY:
blocked += 1
break
else:
blocked += 1
break
if blocked == 0: # 열린 패턴
if count == 4:
score += 100
elif count == 3:
score += 50
elif count == 2:
score += 10
return score
def computer_move(self):
if self.game_over_flag:
return
# 우선순위 1: 컴퓨터 승리 수
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):
self.update()
self.game_over("컴퓨터(흑) 승리!")
return
self.board[y][x] = EMPTY
self.move_history.pop()
# 우선순위 2: 상대 승리 차단
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):
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()
# 우선순위 3: 특별 공격 패턴 차단
result = self.check_special_attack_pattern()
if result:
x, y = result
self.place_stone(x, y, BLACK)
self.turn = WHITE
self.update()
return
# 우선순위 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
# 우선순위 7: 평가 기반 최적 수
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
# 대체 수: 최근 돌 근처 나이트 이동
base_x, base_y = self.last_black_move if self.last_black_move else (center, center)
for dx, dy in NALMOK_DIRECTIONS:
nx = base_x + dx
ny = base_y + dy
if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == EMPTY:
self.place_stone(nx, ny, BLACK)
self.turn = WHITE
self.update()
return
def game_over(self, message):
self.game_over_flag = True
QMessageBox.information(self, "게임 종료", message)
self.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
game = NalmokGame()
sys.exit(app.exec_())Forums:


날목
#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 = 20 STONE_RADIUS = 9 EMPTY = 0 BLACK = 1 WHITE = 2 # 나이트 이동 방향 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.last_black_move = None self.last_white_move = None undo_button = DraggableButton("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) qp.end() def draw_board(self, qp): qp.setBrush(QColor(222, 184, 135)) qp.drawRect(0, 0, self.width(), self.height()) qp.setPen(QPen(Qt.black, 1)) for i in range(BOARD_SIZE): qp.drawLine(CELL_SIZE // 2, CELL_SIZE // 2 + i * CELL_SIZE, CELL_SIZE // 2 + (BOARD_SIZE - 1) * CELL_SIZE, CELL_SIZE // 2 + i * CELL_SIZE) qp.drawLine(CELL_SIZE // 2 + i * CELL_SIZE, CELL_SIZE // 2, CELL_SIZE // 2 + i * CELL_SIZE, CELL_SIZE // 2 + (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 = CELL_SIZE // 2 + x * CELL_SIZE cy = CELL_SIZE // 2 + y * CELL_SIZE qp.setBrush(QColor(0, 0, 0)) qp.drawEllipse(cx - 2, cy - 2, 4, 4) 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( CELL_SIZE // 2 + x * CELL_SIZE - STONE_RADIUS, CELL_SIZE // 2 + 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 = CELL_SIZE // 2 + x * CELL_SIZE cy = CELL_SIZE // 2 + y * CELL_SIZE qp.setPen(QPen(Qt.green, 2)) qp.drawRect(cx - 1, cy - 1, 2, 2) def mousePressEvent(self, event): if self.game_over_flag or self.turn != WHITE: return x = int((event.x() - CELL_SIZE // 2 + CELL_SIZE / 2) // CELL_SIZE) y = int((event.y() - CELL_SIZE // 2 + 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): 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.update() def check_win(self, x, y, stone): for dx, dy in NALMOK_DIRECTIONS: 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: count += 1 else: break if count >= 5: return True return False def should_block_nalmok(self, x, y, stone, length): for dx, dy in NALMOK_DIRECTIONS: count = 1 blocked = 0 for i in range(1, length): nx = x + dx * i ny = y + dy * i if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE: cell = self.board[ny][nx] if cell == stone: count += 1 elif cell == EMPTY: continue else: blocked += 1 break else: blocked += 1 break if count == length and blocked == 0: return True return False def check_special_attack_pattern(self): base_patterns = [(-3, -6), (-1, -2), (1, 2), (3, 6)] all_patterns = [] for dx, dy in base_patterns: all_patterns.append((dx, dy)) all_patterns.append((-dx, dy)) all_patterns.append((dx, -dy)) all_patterns.append((-dx, -dy)) all_patterns.append((dy, dx)) all_patterns.append((-dy, dx)) all_patterns.append((dy, -dx)) all_patterns.append((-dy, -dx)) all_patterns = list(set(all_patterns)) for y in range(BOARD_SIZE): for x in range(BOARD_SIZE): if self.board[y][x] != EMPTY: continue for pattern in [base_patterns, [(dy, dx) for dx, dy in base_patterns]]: found = True for dx, dy in pattern: nx, ny = x + dx, y + dy if not (0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == WHITE): found = False break if found: return x, y return None def evaluate_board(self, stone): score = 0 for y in range(BOARD_SIZE): for x in range(BOARD_SIZE): if self.board[y][x] == stone: for dx, dy in NALMOK_DIRECTIONS: count = 1 blocked = 0 for i in range(1, 5): nx, ny = x + dx * i, y + dy * i if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE: if self.board[ny][nx] == stone: count += 1 elif self.board[ny][nx] != EMPTY: blocked += 1 break else: blocked += 1 break if blocked == 0: # 열린 패턴 if count == 4: score += 100 elif count == 3: score += 50 elif count == 2: score += 10 return score def computer_move(self): if self.game_over_flag: return # 우선순위 1: 컴퓨터 승리 수 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): self.update() self.game_over("컴퓨터(흑) 승리!") return self.board[y][x] = EMPTY self.move_history.pop() # 우선순위 2: 상대 승리 차단 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): 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 ### # 우선순위 3: 특별 공격 패턴 차단 result = self.check_special_attack_pattern() if result: x, y = result self.place_stone(x, y, BLACK) self.turn = WHITE self.update() return ### # 우선순위 7: 평가 기반 최적 수 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 # 대체 수: 최근 돌 근처 나이트 이동 base_x, base_y = self.last_black_move if self.last_black_move else (center, center) for dx, dy in NALMOK_DIRECTIONS: nx = base_x + dx ny = base_y + dy if 0 <= nx < BOARD_SIZE and 0 <= ny < BOARD_SIZE and self.board[ny][nx] == EMPTY: self.place_stone(nx, ny, BLACK) self.turn = WHITE self.update() return def game_over(self, message): self.game_over_flag = True QMessageBox.information(self, "게임 종료", message) self.close() class DraggableButton(QPushButton): def __init__(self, title, parent): super().__init__(title, parent) self.setMouseTracking(True) self.dragging = False self.offset = QPoint() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.dragging = True self.offset = event.pos() super().mousePressEvent(event) def mouseMoveEvent(self, event): if self.dragging: new_pos = self.mapToParent(event.pos() - self.offset) self.move(new_pos) super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.dragging = False super().mouseReleaseEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) game = NalmokGame() sys.exit(app.exec_())세벌 https://sebuls.blogspot.kr/
댓글 달기