Inhalt

Aktueller Ordner: chessteg_modular/engine
⬅ Übergeordnet

move_generation.py

"""
Chessteg Move Generation Module - KORRIGIERTE VERSION MIT DEBUG
Vollständige Zuggenerierung mit allen Schachregeln
"""

import copy
from typing import List, Dict, Any, Optional

# Konstanten für Figurentypen (aus Core Engine)
PAWN = 1
KNIGHT = 4
BISHOP = 3
ROOK = 5
QUEEN = 9
KING = 99
WHITE = 1
BLACK = -1

# Richtungen für 10x10 Board
DIRECTIONS = {
    'N': 10, 'S': -10, 'E': 1, 'W': -1,
    'NE': 11, 'NW': 9, 'SE': -9, 'SW': -11
}

# Springer-Züge (L-Form)
KNIGHT_MOVES = [21, 19, 12, 8, -8, -12, -19, -21]


class MoveGenerator:
    """
    Vollständige Zuggenerierung für alle Figurentypen inklusive spezieller Regeln
    """
    
    def __init__(self, engine):
        self.engine = engine
    
    def generate_moves(self, color: int) -> List[Dict[str, Any]]:
        """
        Generiert alle legalen Züge für eine Farbe inklusive spezieller Züge
        
        Args:
            color: Farbe (1=weiß, -1=schwarz)
            
        Returns:
            List[Dict]: Liste der legalen Züge
        """
        all_moves = []
        
        for piece in self.engine.pieces:
            if not piece['captured'] and piece['color'] == color:
                piece_moves = self.generate_piece_moves(piece)
                all_moves.extend(piece_moves)
        
        # Spezielle Züge hinzufügen
        special_moves = self._generate_special_moves(color)
        all_moves.extend(special_moves)
        
        # Nur legale Züge zurückgeben (ohne Selbstschach)
        legal_moves = [move for move in all_moves if self.is_move_legal(move)]
        
        # DEBUG: Zeige Anzahl der generierten Züge
        # print(f"Generierte legale Züge für {color}: {len(legal_moves)}")
        
        return legal_moves

    def generate_legal_moves(self, color: int) -> List[Dict[str, Any]]:
        """Generiert legale Züge - Alias für generate_moves für Kompatibilität."""
        return self.generate_moves(color)

    def generate_active_moves(self, color: int) -> List[Dict[str, Any]]:
        """Generiert nur aktive Züge (Schläge) für Quiescence Search."""
        all_moves = self.generate_moves(color)
        active_moves = []
        
        for move in all_moves:
            # Schlagzüge und Bauernumwandlungen als "aktiv" betrachten
            if (move.get('capture_pos') or 
                move.get('special_type') == 'en_passant' or
                move.get('promotion_piece')):
                active_moves.append(move)
        
        return active_moves

    def is_move_legal(self, move: Dict[str, Any]) -> bool:
        """
        Prüft, ob ein Zug legal ist (König nicht im Schach nach dem Zug)
        
        Args:
            move: Der Zug im Diktionär-Format
            
        Returns:
            bool: True wenn der Zug legal ist
        """
        piece_id = move['piece_id']
        piece = self.engine.get_piece_by_id(piece_id)
        
        if not piece:
            return False
            
        color = piece['color']
        
        # Führe den Zug temporär aus
        snapshot = self.engine.take_snapshot()
        
        # Verwende die niedrigstufige Methode, um den Zug durchzuführen,
        # ohne die white_turn-Variable zu ändern.
        self.engine._apply_move_internal(move)
        
        is_legal = not self.engine.is_king_in_check(color)
        
        # Mache den Zug rückgängig
        self.engine.restore_snapshot(snapshot)
        
        return is_legal

    def _generate_special_moves(self, color: int) -> List[Dict[str, Any]]:
        """
        Generiert Rochade und Bauernumwandlungen - KORRIGIERTE VERSION
        """
        special_moves = []
        
        # Rochade-Züge
        rules = self.engine.rules
        
        # Kleine Rochade
        if rules.validate_castling(color, 'kingside'):
            if color == WHITE:
                king_pos = 25  # e1
                king_to = 27   # g1
                rook_pos = 28  # h1  
                rook_to = 26   # f1
            else:
                king_pos = 95  # e8
                king_to = 97   # g8
                rook_pos = 98  # h8
                rook_to = 96   # f8
            
            king = self.engine.get_piece_at(king_pos)
            rook = self.engine.get_piece_at(rook_pos)
            
            if king and king['type'] == KING and rook and rook['type'] == ROOK:
                special_moves.append({
                    'piece_id': king['id'],
                    'piece': king,  # 🚨 WICHTIG: Füge piece-Objekt hinzu
                    'type': KING,   # 🚨 WICHTIG: Explizit setzen
                    'color': color, # 🚨 WICHTIG: Explizit setzen
                    'from_pos': king_pos,
                    'to_pos': king_to,
                    'capture_pos': None,
                    'promotion_piece': None,
                    'special_type': 'castling',
                    'rook_id': rook['id'],
                    'rook_from': rook_pos,
                    'rook_to': rook_to
                })
        
        # Große Rochade
        if rules.validate_castling(color, 'queenside'):
            if color == WHITE:
                king_pos = 25  # e1
                king_to = 23   # c1
                rook_pos = 21  # a1  
                rook_to = 24   # d1
            else:
                king_pos = 95  # e8
                king_to = 93   # c8
                rook_pos = 91  # a8
                rook_to = 94   # d8
                
            king = self.engine.get_piece_at(king_pos)
            rook = self.engine.get_piece_at(rook_pos)
            
            if king and king['type'] == KING and rook and rook['type'] == ROOK:
                special_moves.append({
                    'piece_id': king['id'],
                    'piece': king,  # 🚨 WICHTIG
                    'type': KING,   # 🚨 WICHTIG  
                    'color': color, # 🚨 WICHTIG
                    'from_pos': king_pos,
                    'to_pos': king_to,
                    'capture_pos': None,
                    'promotion_piece': None,
                    'special_type': 'castling',
                    'rook_id': rook['id'],
                    'rook_from': rook_pos,
                    'rook_to': rook_to
                })
        
        return special_moves

    def generate_piece_moves(self, piece: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Generiert alle möglichen Züge für eine einzelne Figur (ohne Legalitätsprüfung)
        """
        piece_type = piece['type']
        
        if piece_type == PAWN:
            return self._generate_pawn_moves(piece)
        elif piece_type == ROOK:
            return self._generate_sliding_moves(piece, ['N', 'S', 'E', 'W'])
        elif piece_type == BISHOP:
            return self._generate_sliding_moves(piece, ['NE', 'NW', 'SE', 'SW'])
        elif piece_type == QUEEN:
            return self._generate_sliding_moves(piece, list(DIRECTIONS.keys()))
        elif piece_type == KNIGHT:
            return self._generate_knight_moves(piece)
        elif piece_type == KING:
            return self._generate_king_moves(piece)
            
        return []

    def _create_move(self, piece: Dict[str, Any], to_pos: int, capture_pos: Optional[int] = None, 
                    special_type: Optional[str] = None, promotion_piece: Optional[int] = None) -> Dict[str, Any]:
        """
        Erstellt ein standardisiertes Zug-Diktionär - KORRIGIERTE VERSION
        """
        move_dict = {
            'piece_id': piece['id'],
            'piece': piece,
            'type': piece['type'],        # 🚨 KRITISCH: Figurentyp
            'color': piece['color'],      # 🚨 KRITISCH: Farbe
            'from_pos': piece['position'],
            'to_pos': to_pos,
            'capture_pos': capture_pos,
            'promotion_piece': promotion_piece,
            'special_type': special_type
        }
        
        # Markiere Schlagzüge für Move Ordering
        if capture_pos is not None:
            move_dict['is_capture'] = True
            
        # 🚨 NEU: Setze promotion_type falls promotion_piece vorhanden
        if promotion_piece is not None:
            move_dict['promotion_type'] = promotion_piece
            
        return move_dict

    def _generate_pawn_moves(self, pawn: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Generiert Züge für einen Bauern
        """
        moves = []
        color = pawn['color']
        start_pos = pawn['position']
        
        # Die Richtung, in die der Bauer zieht
        forward = DIRECTIONS['N'] * color
        
        # Position der 1. und 2. Reihe (bezogen auf das 10x10 Board)
        # Die 1. Reihe (aus Benutzersicht) ist die Reihe 2, die 8. Reihe ist die Reihe 9.
        # Weiße Bauern starten auf Reihe 3 (31-38), Schwarze auf Reihe 8 (81-88).
        start_row = 3 if color == WHITE else 8

        # =========================================================================
        # 1. Vorwärtszug (Ein Feld)
        # =========================================================================
        one_step = start_pos + forward
        if self.engine.get_piece_at(one_step) is None:
            
            # KORREKTUR: Eigene Implementierung für Promotions-Prüfung
            if self._is_promotion_rank(one_step, color):
                # Füge alle Promotion-Züge (Dame, Turm, Läufer, Springer) hinzu
                for p_type in [QUEEN, ROOK, BISHOP, KNIGHT]:
                    moves.append(self._create_move(pawn, one_step, promotion_piece=p_type))
            else:
                moves.append(self._create_move(pawn, one_step))

                # =========================================================================
                # 2. Vorwärtszug (Zwei Felder)
                # =========================================================================
                if pawn['position'] // 10 == start_row:
                    two_step = start_pos + 2 * forward
                    if self.engine.get_piece_at(two_step) is None:
                        moves.append(self._create_move(pawn, two_step, special_type='double_pawn_push'))

        # =========================================================================
        # 3. Schlagzüge (Diagonal)
        # =========================================================================
        capture_dirs = [forward + DIRECTIONS['E'], forward + DIRECTIONS['W']]
        
        for capture_dir in capture_dirs:
            target_pos = start_pos + capture_dir
            if not self.engine.is_valid_position(target_pos):
                continue
                
            target_piece = self.engine.get_piece_at(target_pos)
            
            # Normaler Schlagzug
            if target_piece and target_piece['color'] != color:
                if self._is_promotion_rank(target_pos, color):
                    # Promotion-Schlagzug
                    for p_type in [QUEEN, ROOK, BISHOP, KNIGHT]:
                        moves.append(self._create_move(pawn, target_pos, target_pos, promotion_piece=p_type))
                else:
                    moves.append(self._create_move(pawn, target_pos, target_pos))
            
            # En Passant
            elif target_pos == self.engine.rules.en_passant_target:
                # Das geschlagene Bauernfeld ist immer 10 Schritte (eine Reihe) hinter dem Ziel
                captured_pawn_pos = target_pos - forward 
                
                moves.append(self._create_move(pawn, target_pos, captured_pawn_pos, special_type='en_passant'))
                
        return moves

    def _is_promotion_rank(self, position: int, color: int) -> bool:
        """
        Prüft ob eine Position auf der Umwandlungsreihe für die gegebene Farbe liegt
        """
        row = position // 10
        # Weißer Bauer auf 8. Reihe (Row 9) oder schwarzer Bauer auf 1. Reihe (Row 2)
        return (color == WHITE and row == 9) or (color == BLACK and row == 2)

    def _generate_knight_moves(self, piece: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Generiert Züge für einen Springer
        """
        moves = []
        current_pos = piece['position'] # KORREKTUR: Verwende 'position'
        color = piece['color']
        
        for move in KNIGHT_MOVES:
            target_pos = current_pos + move
            if self.engine.is_valid_position(target_pos):
                target_piece = self.engine.get_piece_at(target_pos)
                
                if target_piece is None:
                    # Leeres Feld
                    moves.append(self._create_move(piece, target_pos))
                elif target_piece['color'] != color:
                    # Schlagzug
                    moves.append(self._create_move(piece, target_pos, target_pos))
                    
        return moves

    def _generate_king_moves(self, piece: Dict[str, Any]) -> List[Dict[str, Any]]:
        """
        Generiert Züge für den König (Rochade wird separat in _generate_special_moves behandelt)
        """
        moves = []
        current_pos = piece['position'] # KORREKTUR: Verwende 'position'
        color = piece['color']
        
        for direction in DIRECTIONS.values():
            target_pos = current_pos + direction
            if self.engine.is_valid_position(target_pos):
                target_piece = self.engine.get_piece_at(target_pos)
                
                if target_piece is None:
                    # Leeres Feld
                    moves.append(self._create_move(piece, target_pos))
                elif target_piece['color'] != color:
                    # Schlagzug
                    moves.append(self._create_move(piece, target_pos, target_pos))
                    
        return moves

    def _generate_sliding_moves(self, piece: Dict[str, Any], directions: List[str]) -> List[Dict[str, Any]]:
        """
        Generiert Züge für gleitende Figuren (Dame, Turm, Läufer)
        """
        moves = []
        current_pos = piece['position'] # KORREKTUR: Verwende 'position'
        color = piece['color']
        
        for direction_str in directions:
            direction = DIRECTIONS[direction_str]
            field = current_pos + direction
            
            while self.engine.is_valid_position(field):
                target_piece = self.engine.get_piece_at(field)
                
                if target_piece is None:
                    # Leeres Feld: Zug hinzufügen und weiter in diese Richtung
                    moves.append(self._create_move(piece, field))
                elif target_piece['color'] != color:
                    # Gegnerische Figur: Schlagzug hinzufügen und Schleife beenden
                    moves.append(self._create_move(piece, field, field))
                    break
                else:
                    # Eigene Figur: Blockiert, Schleife beenden
                    break
                    
                field += direction
                
        return moves

    def get_attacked_squares(self, piece: Dict[str, Any]) -> List[int]:
        """
        Gibt eine Liste aller Felder zurück, die von einer bestimmten Figur angegriffen werden.
        Wird für die Schachprüfung verwendet.
        """
        piece_type = piece['type']
        attacked_squares = []
        
        # KORREKTUR: Die Position der Figur ist IMMER 'position', NICHT 'pos'.
        current_pos = piece['position']
        color = piece['color']

        if piece_type == PAWN:
            forward = DIRECTIONS['N'] * color
            # Bauern-Angriffszüge sind diagonal (keine normalen Züge)
            capture_dirs = [forward + DIRECTIONS['E'], forward + DIRECTIONS['W']]
            
            for capture_dir in capture_dirs:
                target_pos = current_pos + capture_dir
                if self.engine.is_valid_position(target_pos):
                    # Nur die Angriffsfelder zurückgeben, unabhängig davon, ob eine Figur dort steht
                    attacked_squares.append(target_pos)
                    
        elif piece_type == KNIGHT:
            for move in KNIGHT_MOVES:
                target_pos = current_pos + move
                if self.engine.is_valid_position(target_pos):
                    attacked_squares.append(target_pos)
                    
        elif piece_type == KING:
            for direction in DIRECTIONS.values():
                target_pos = current_pos + direction
                if self.engine.is_valid_position(target_pos):
                    attacked_squares.append(target_pos)
                    
        elif piece_type in [ROOK, BISHOP, QUEEN]:
            if piece_type == ROOK:
                directions = ['N', 'S', 'E', 'W']
            elif piece_type == BISHOP:
                directions = ['NE', 'NW', 'SE', 'SW']
            else: # QUEEN
                directions = list(DIRECTIONS.keys())
                
            for direction_str in directions:
                direction = DIRECTIONS[direction_str]
                field = current_pos + direction
                
                # Dies ist der Teil, der im Traceback (Zeile 642) den Fehler verursachte, 
                # wenn er in einer Unterfunktion fälschlicherweise 'pos' verwendete.
                # Hier ist die Korrektur: Die Startposition ist current_pos = piece['position'].
                
                while self.engine.is_valid_position(field):
                    attacked_squares.append(field)
                    target_piece = self.engine.get_piece_at(field)
                    
                    # Bei Angriffsgenerierung stoppen wir nur, wenn wir auf eine Figur treffen
                    if target_piece is not None:
                        break
                        
                    field += direction
        
        return attacked_squares
        
    def is_square_attacked(self, position: int, attacker_color: int) -> bool:
        """
        Prüft, ob ein Feld von einer Figur der gegebenen Farbe angegriffen wird
        
        Args:
            position: Zu prüfendes Feld
            attacker_color: Farbe der Angreifer
            
        Returns:
            bool: True wenn Feld angegriffen wird
        """
        for piece in self.engine.pieces:
            if (piece['captured'] or 
                piece['color'] != attacker_color):
                continue
            
            attacked_squares = self.get_attacked_squares(piece)
            if position in attacked_squares:
                return True
        
        return False


# Test des erweiterten MoveGenerators
if __name__ == "__main__":
    print("Testing Extended MoveGenerator...")
    
    from core import ChesstegEngine
    engine = ChesstegEngine()
    move_gen = MoveGenerator(engine)
    
    # Test: Züge für Weiß generieren
    white_moves = move_gen.generate_moves(1)
    print(f"White moves in initial position: {len(white_moves)}")
    
    # Test: Spezielle Züge
    castling_moves = [m for m in white_moves if m.get('special_type') == 'castling']
    print(f"Castling moves available: {len(castling_moves)}")
    
    # Test: Springer-Züge
    knights = [p for p in engine.pieces if p['type'] == 4 and p['color'] == 1]
    for knight in knights:
        knight_moves = move_gen._generate_knight_moves(knight)
        print(f"Knight at {engine._position_to_notation(knight['position'])} moves: {len(knight_moves)}")