71  체스 게임 (Chess)

고전적인 보드 게임인 체스를 처음부터 끝까지 코딩해 봅시다. 보드를 구성하고, 각 기물(폰, 나이트, 비숍, 룩, 퀸, 킹)의 이미지를 사용하여 화면을 꾸밉니다. 모든 기물의 이동 규칙을 정확하게 코딩하고, 잘못된 수(Invalid Moves)를 둘 수 없도록 철저히 검증해야 합니다.

체스는 규칙이 상당히 많고 복잡하기 때문에 세심한 설계가 필요합니다. 캐슬링(Castling), 앙파상(En passant), 프로모션(Promotion)과 같은 특수 규칙들까지 완벽하게 구현하는 것이 목표입니다.

71.1 주요 개발 포인트

  • 8x8 보드 표현: 2차원 리스트 또는 배열을 사용하여 체스판의 상태를 관리합니다.
  • 기물 이동 로직: 각 기물의 독특한 이동 범위를 계산하고 경로가 막혔는지 확인합니다.
  • 특수 규칙 구현: 캐슬링, 앙파상, 폰의 승진(Promotion) 조건을 체크합니다.
  • 체크 및 체크메이트 판정: 현재 킹이 공격받고 있는지, 더 이상 피할 곳이 없는지 판단합니다.
  • 턴 관리: 백과 흑이 번갈아 가며 수를 두는 시스템을 구축합니다.

71.2 Python 구현 예시 (보드 초기화 및 출력)

class ChessGame:
    """
    체스 게임의 보드 상태와 기본 규칙을 관리합니다.
    """
    def __init__(self):
        # 대문자는 백(White), 소문자는 흑(Black)
        self.board = [
            ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'],
            ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'],
            [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '],
            ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'],
            ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']
        ]
        self.turn = 'White'
        self.castling_rights = {'W': {'K': True, 'Q': True}, 'B': {'k': True, 'q': True}}
        self.en_passant_target = None  # Position (r, c)

    def display(self):
        """
        체스판을 보기 좋게 출력합니다.
        """
        print(f"\n--- {self.turn} 턴 ---")
        print("  a b c d e f g h")
        print("  ----------------")
        for i, row in enumerate(self.board):
            print(f"{8-i}|{' '.join(row)}|{8-i}")
        print("  ----------------")
        print("  a b c d e f g h")

    def to_coords(self, pos_str):
        col = ord(pos_str[0].lower()) - ord('a')
        row = 8 - int(pos_str[1])
        return row, col

    def is_valid_move(self, start_pos, end_pos, check_check=True):
        r1, c1 = self.to_coords(start_pos)
        r2, c2 = self.to_coords(end_pos)
        return self._is_valid_move_coords(r1, c1, r2, c2, check_check)

    def _is_valid_move_coords(self, r1, c1, r2, c2, check_check=True):
        if not (0 <= r1 < 8 and 0 <= c1 < 8 and 0 <= r2 < 8 and 0 <= c2 < 8):
            return False

        piece = self.board[r1][c1]
        target = self.board[r2][c2]

        if piece == ' ' or (self.turn == 'White' and not piece.isupper()) or \
           (self.turn == 'Black' and not piece.islower()):
            return False

        if target != ' ' and piece.isupper() == target.isupper():
            return False

        if not self._validate_piece_move(piece, r1, c1, r2, c2):
            return False

        if check_check:
            original_board = [row[:] for row in self.board]
            original_ep = self.en_passant_target
            original_castling = {k: v.copy() for k, v in self.castling_rights.items()}

            self._execute_move(r1, c1, r2, c2, piece)
            king_pos = self._find_king(self.turn)
            in_check = self._is_under_attack(king_pos[0], king_pos[1], 'Black' if self.turn == 'White' else 'White')

            self.board, self.en_passant_target, self.castling_rights = original_board, original_ep, original_castling
            if in_check: return False

        return True

    def _validate_piece_move(self, piece, r1, c1, r2, c2, include_castling=True):
        dr, dc = r2 - r1, c2 - c1
        p = piece.lower()

        if p == 'p':
            dir = -1 if piece.isupper() else 1
            if dc == 0:
                if dr == dir and self.board[r2][c2] == ' ': return True
                if dr == 2 * dir and r1 == (6 if piece.isupper() else 1) and \
                   self.board[r2][c2] == ' ' and self.board[r1 + dir][c1] == ' ': return True
            elif abs(dc) == 1 and dr == dir:
                if self.board[r2][c2] != ' ' or (r2, c2) == self.en_passant_target: return True
        elif p == 'r':
            if (dr == 0 or dc == 0) and self._is_path_clear(r1, c1, r2, c2): return True
        elif p == 'n':
            if (abs(dr), abs(dc)) in [(1, 2), (2, 1)]: return True
        elif p == 'b':
            if abs(dr) == abs(dc) and self._is_path_clear(r1, c1, r2, c2): return True
        elif p == 'q':
            if (dr == 0 or dc == 0 or abs(dr) == abs(dc)) and self._is_path_clear(r1, c1, r2, c2): return True
        elif p == 'k':
            if abs(dr) <= 1 and abs(dc) <= 1: return True
            if include_castling and dr == 0 and abs(dc) == 2: return self._can_castle(piece, r1, c1, r2, c2)
        return False

    def _is_path_clear(self, r1, c1, r2, c2):
        dr = 0 if r1 == r2 else (1 if r2 > r1 else -1)
        dc = 0 if c1 == c2 else (1 if c2 > c1 else -1)
        r, c = r1 + dr, c1 + dc
        while (r, c) != (r2, c2):
            if self.board[r][c] != ' ': return False
            r, c = r + dr, c + dc
        return True

    def _can_castle(self, piece, r1, c1, r2, c2):
        color, side = ('W', 'K' if c2 > c1 else 'Q') if piece.isupper() else ('B', 'k' if c2 > c1 else 'q')
        if not self.castling_rights[color][side]: return False
        if self._is_under_attack(r1, c1, 'Black' if piece.isupper() else 'White'): return False
        step = 1 if c2 > c1 else -1
        rook_col = 7 if c2 > c1 else 0
        for c in range(min(c1, rook_col) + 1, max(c1, rook_col)):
            if self.board[r1][c] != ' ': return False
        return not self._is_under_attack(r1, c1 + step, 'Black' if piece.isupper() else 'White')

    def _find_king(self, turn):
        target = 'K' if turn == 'White' else 'k'
        for r in range(8):
            for c in range(8):
                if self.board[r][c] == target: return r, c

    def _is_under_attack(self, r, c, attacker_color):
        for r_idx in range(8):
            for c_idx in range(8):
                piece = self.board[r_idx][c_idx]
                if piece != ' ' and ((attacker_color == 'White' and piece.isupper()) or (attacker_color == 'Black' and piece.islower())):
                    if piece.lower() == 'p':
                        dir = -1 if piece.isupper() else 1
                        if r == r_idx + dir and abs(c - c_idx) == 1: return True
                    elif self._validate_piece_move(piece, r_idx, c_idx, r, c, include_castling=False):
                        return True
        return False

    def is_checkmate(self, turn):
        king_pos = self._find_king(turn)
        if not self._is_under_attack(king_pos[0], king_pos[1], 'Black' if turn == 'White' else 'White'):
            return False
        return self._has_no_valid_moves(turn)

    def is_stalemate(self, turn):
        king_pos = self._find_king(turn)
        if self._is_under_attack(king_pos[0], king_pos[1], 'Black' if turn == 'White' else 'White'):
            return False
        return self._has_no_valid_moves(turn)

    def _has_no_valid_moves(self, turn):
        for r1 in range(8):
            for c1 in range(8):
                piece = self.board[r1][c1]
                if piece != ' ' and ((turn == 'White' and piece.isupper()) or (turn == 'Black' and piece.islower())):
                    for r2 in range(8):
                        for c2 in range(8):
                            if self._is_valid_move_coords(r1, c1, r2, c2, check_check=True):
                                return False
        return True

    def move_piece(self, start_pos, end_pos):
        """
        기물을 이동시키고 턴을 넘깁니다. 규칙을 검증합니다.
        """
        if not self.is_valid_move(start_pos, end_pos):
            print(f"잘못된 이동: {start_pos} -> {end_pos}")
            return False

        print(f"기물 이동: {start_pos} -> {end_pos}")
        r1, c1 = self.to_coords(start_pos)
        r2, c2 = self.to_coords(end_pos)
        self._execute_move(r1, c1, r2, c2, self.board[r1][c1])
        self.turn = 'Black' if self.turn == 'White' else 'White'

        king_pos = self._find_king(self.turn)
        in_check = self._is_under_attack(king_pos[0], king_pos[1], 'Black' if self.turn == 'White' else 'White')

        if self.is_checkmate(self.turn):
            print(f"체크메이트! {'White' if self.turn == 'Black' else 'Black'} 승리.")
        elif self.is_stalemate(self.turn):
            print("스테일메이트! 무승부.")
        elif in_check:
            print(f"체크! {self.turn}의 킹이 공격받고 있습니다.")

        return True

    def _execute_move(self, r1, c1, r2, c2, piece):
        target_piece = self.board[r2][c2]
        if target_piece == 'R':
            if r2 == 7 and c2 == 7: self.castling_rights['W']['K'] = False
            elif r2 == 7 and c2 == 0: self.castling_rights['W']['Q'] = False
        elif target_piece == 'r':
            if r2 == 0 and c2 == 7: self.castling_rights['B']['k'] = False
            elif r2 == 0 and c2 == 0: self.castling_rights['B']['q'] = False

        if piece.lower() == 'p' and (r2, c2) == self.en_passant_target: self.board[r1][c2] = ' '
        self.board[r2][c2], self.board[r1][c1] = piece, ' '
        self.en_passant_target = ((r1 + r2) // 2, c1) if piece.lower() == 'p' and abs(r2 - r1) == 2 else None
        if piece == 'P' and r2 == 0: self.board[r2][c2] = 'Q'
        if piece == 'p' and r2 == 7: self.board[r2][c2] = 'q'
        if piece.lower() == 'k' and abs(c2 - c1) == 2:
            rook_col, target_col = (7, 5) if c2 > c1 else (0, 3)
            self.board[r2][target_col], self.board[r2][rook_col] = self.board[r2][rook_col], ' '
        if piece == 'K': self.castling_rights['W']['K'] = self.castling_rights['W']['Q'] = False
        elif piece == 'k': self.castling_rights['B']['k'] = self.castling_rights['B']['q'] = False
        elif piece == 'R':
            if r1 == 7 and c1 == 7: self.castling_rights['W']['K'] = False
            elif r1 == 7 and c1 == 0: self.castling_rights['W']['Q'] = False
        elif piece == 'r':
            if r1 == 0 and c1 == 7: self.castling_rights['B']['k'] = False
            elif r1 == 0 and c1 == 0: self.castling_rights['B']['q'] = False

if __name__ == "__main__":
    game = ChessGame()
    game.display()
    
    # 예시 이동
    game.move_piece("e2", "e4")
    game.display()