Inhalt

Aktueller Ordner: chessteg_modular/engine
⬅ Übergeordnet

core.py

"""
Chessteg Core Engine Module - KORRIGIERTE VERSION (Behebt IndexError)
"""

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

# Füge den aktuellen Pfad zum Python-Pfad hinzu für relative Imports
sys.path.append(os.path.dirname(__file__))

# Konstanten für bessere Lesbarkeit
WHITE = 1
BLACK = -1
EMPTY = 0
DUMMY = 100

# Figuren-Typen
PAWN = 1
KNIGHT = 4
BISHOP = 3
ROOK = 5
QUEEN = 9
KING = 99

# Figuren-Symbole für die Darstellung (Hier in der Engine, um sie unabhängig zu machen)
PIECE_SYMBOLS = {
    1: '♙',   # Weißer Bauer
    -1: '♟',  # Schwarzer Bauer
    4: '♘',   # Weißer Springer
    -4: '♞',  # Schwarzer Springer
    3: '♗',   # Weißer Läufer
    -3: '♝',  # Schwarzer Läufer
    5: '♖',   # Weißer Turm
    -5: '♜',  # Schwarzer Turm
    9: '♕',   # Weiße Dame
    -9: '♛',  # Schwarze Dame
    99: '♔',  # Weißer König
    -99: '♚', # Schwarzer König
    EMPTY: ' '
}

# Import der Komponenten (relative Imports in einer echten Modulstruktur)
try:
    from move_generation import MoveGenerator
    from evaluation import PositionEvaluator
    from search import SearchAlgorithm
    from rules import ChessRules
except ImportError:
    print("WARNING: Relative imports failed. Using dummy components.")
    class DummyComponent:
        def __init__(self, *args): pass
        def generate_moves(self, *args): return []
        def evaluate_position(self): return 0
        def computer_move(self): return None
        
    MoveGenerator = DummyComponent
    PositionEvaluator = DummyComponent
    SearchAlgorithm = DummyComponent
    ChessRules = DummyComponent


class ChesstegEngine:
    """
    Die zentrale Schach-Engine. Verwaltet den Spielzustand, Figuren und die
    Schnittstellen zu den modularen Komponenten (Züge, Bewertung, Suche, Regeln).
    """

    def __init__(self):
        self.board = [EMPTY] * 120 # 10x12 Array, die Ränder sind DUMMY
        self.pieces: List[Dict[str, Any]] = []
        self.white_turn = True
        self.checkmate = False
        self.stalemate = False
        self.next_piece_id = 1 # Eindeutige ID für jede Figur
        self.move_history: List[Dict[str, Any]] = [] # Speichert vergangene Zustände

        # Komponenten initialisieren
        self.move_generator = MoveGenerator(self)
        self.evaluator = PositionEvaluator(self)
        self.search_algorithm = SearchAlgorithm(self)
        self.rules = ChessRules(self) # Regeln müssen vor initialize_pieces initialisiert werden

        self.initialize_board()
        self.initialize_pieces()
        self.synchronize_board_state()
        
    # =========================================================================
    # ZUSTANDSVERWALTUNG
    # =========================================================================

    def initialize_board(self):
        """Setzt das 120-Felder-Board (10x12) mit DUMMY-Rändern auf."""
        # Alle Felder auf EMPTY setzen
        self.board = [EMPTY] * 120 # Indices 0 bis 119

        # KORREKTUR: Ränder setzen
        # 1. Unterer und oberer vollständiger Rand (Rows 0 und 11, jeweils 10 Felder)
        for i in range(10): # i von 0 bis 9 (10 Iterationen)
            # Row 0 (Indices 0 bis 9)
            self.board[i] = DUMMY
            # Row 11 (Indices 110 bis 119)
            self.board[110 + i] = DUMMY
            
        # 2. Linke und rechte Ränder (Spalten 0 und 9) für Rows 1 bis 10
        for i in range(1, 11): # i von 1 bis 10 (10 Iterationen)
            self.board[i * 10] = DUMMY      # Col 0 (z.B. 10, 20, ..., 100)
            self.board[i * 10 + 9] = DUMMY  # Col 9 (z.B. 19, 29, ..., 109)
            
    def initialize_pieces(self):
        """Setzt die Figuren auf die Standard-Anfangsstellung."""
        self.pieces.clear()
        self.next_piece_id = 1
        
        # Initialisierung der Regeln (für Castling Rights etc.)
        self.rules.__init__(self) 

        # Schwarze Figuren (Reihe 9 und 8) - A8=91 bis H8=98; A7=81 bis H7=88
        self._add_piece(ROOK, BLACK, 91)
        self._add_piece(KNIGHT, BLACK, 92)
        self._add_piece(BISHOP, BLACK, 93)
        self._add_piece(QUEEN, BLACK, 94)
        self._add_piece(KING, BLACK, 95)
        self._add_piece(BISHOP, BLACK, 96)
        self._add_piece(KNIGHT, BLACK, 97)
        self._add_piece(ROOK, BLACK, 98)
        for i in range(81, 89):
            self._add_piece(PAWN, BLACK, i)

        # Weiße Figuren (Reihe 2 und 3) - A1=21 bis H1=28; A2=31 bis H2=38
        for i in range(31, 39):
            self._add_piece(PAWN, WHITE, i)
        self._add_piece(ROOK, WHITE, 21)
        self._add_piece(KNIGHT, WHITE, 22)
        self._add_piece(BISHOP, WHITE, 23)
        self._add_piece(QUEEN, WHITE, 24)
        self._add_piece(KING, WHITE, 25)
        self._add_piece(BISHOP, WHITE, 26)
        self._add_piece(KNIGHT, WHITE, 27)
        self._add_piece(ROOK, WHITE, 28)
        
        self.white_turn = True
        self.checkmate = False
        self.stalemate = False
        self.move_history.clear()
        
        self.synchronize_board_state()

    def _add_piece(self, piece_type: int, color: int, position: int):
        """Fügt dem Figuren-Array eine neue Figur hinzu."""
        piece_value = piece_type * color
        symbol_key = piece_value 
        
        new_piece = {
            'id': self.next_piece_id,
            'type': piece_type,
            'color': color,
            'value': piece_value,
            'position': position,
            'captured': False,
            'has_moved': False, 
            'symbol': PIECE_SYMBOLS.get(symbol_key, '?')
        }
        self.pieces.append(new_piece)
        self.next_piece_id += 1

    def synchronize_board_state(self, silent=False):
        """
        Stellt sicher, dass das 120-Felder-Board den aktuellen Positionen
        im `pieces` Array entspricht.
        """
        # 1. Board resetten (ohne DUMMY-Felder zu überschreiben)
        self.initialize_board()

        # 2. Figuren positionieren
        for piece in self.pieces:
            if not piece['captured']:
                position = piece['position']
                piece_value = piece['value']
                
                if self.is_valid_position(position):
                    self.board[position] = piece_value
                else:
                    if not silent:
                        print(f"WARNUNG: Figur ID {piece['id']} an ungültiger Position {position}")
                    piece['captured'] = True 
        
    def get_piece_at(self, position: int) -> Optional[Dict[str, Any]]:
        """Gibt das Figuren-Objekt an einer 10x10 Position zurück."""
        for piece in self.pieces:
            if not piece['captured'] and piece['position'] == position:
                return piece
        return None

    def get_piece_by_id(self, piece_id: int) -> Optional[Dict[str, Any]]:
        """Gibt das Figuren-Objekt anhand der ID zurück."""
        for piece in self.pieces:
            if piece['id'] == piece_id:
                return piece
        return None

    def get_king(self, color: int) -> Optional[Dict[str, Any]]:
        """Gibt das König-Objekt der angegebenen Farbe zurück."""
        for piece in self.pieces:
            if piece['type'] == KING and piece['color'] == color and not piece['captured']:
                return piece
        return None
        
    def is_valid_position(self, position: int) -> bool:
        """Prüft, ob eine Position innerhalb des 8x8 Spielfeldes liegt (21-98)."""
        if 20 < position < 100:
            col = position % 10
            return 1 <= col <= 8
        return False
        
    # =========================================================================
    # ZUG-AUSFÜHRUNG
    # =========================================================================

    def make_move(self, move: Dict[str, Any]) -> bool:
        """Führt einen Zug aus und aktualisiert den Spielzustand - KORRIGIERTE VERSION"""
        
        # 1. Speichere den aktuellen Zustand vor der Ausführung
        self._save_state()

        # 2. Führe den Zug durch (Figuren-Update)
        piece = self.get_piece_at(move['from_pos'])
        if not piece:
            print(f"❌ Keine Figur auf Startposition {self._position_to_notation(move['from_pos'])}")
            self._restore_state() 
            return False

        # 🚨 NEU: Spezielle Zug-Behandlung VOR der normalen Ausführung
        # Rochade
        if move.get('special_type') == 'castling':
            return self._execute_castling_move(move, piece)
        
        # En Passant
        if move.get('special_type') == 'en_passant':
            return self._execute_en_passant_move(move, piece)
        
        # Bauernumwandlung
        if move.get('promotion_piece'):
            return self._execute_promotion_move(move, piece)

        # Normale Zugausführung
        captured_piece = self.get_piece_at(move['to_pos'])
        if captured_piece:
            captured_piece['captured'] = True
            move['captured_piece_id'] = captured_piece['id']
        else:
            move['captured_piece_id'] = None
            
        # Figur an Zielposition bewegen
        piece['position'] = move['to_pos']
        piece['has_moved'] = True 

        # 3. Spezielle Regeln ausführen
        if hasattr(self.rules, 'process_move'):
            self.rules.process_move(move, piece, captured_piece)

        # 4. Board und Zustand synchronisieren
        self.synchronize_board_state()
        self.white_turn = not self.white_turn
        
        # 5. Prüfe auf Schachmatt/Patt
        self._check_game_end()

        return True

    def _execute_castling_move(self, move: Dict[str, Any], king: Dict[str, Any]) -> bool:
        """Führt Rochade aus"""
        rook = self.get_piece_by_id(move['rook_id'])
        if not rook:
            return False
            
        # Bewege König
        king['position'] = move['to_pos']
        king['has_moved'] = True
        
        # Bewege Turm  
        rook['position'] = move['rook_to']
        rook['has_moved'] = True
        
        # Rochaderechte aktualisieren
        self.rules._revoke_castling_rights(king['color'])
        
        self.synchronize_board_state()
        self.white_turn = not self.white_turn
        self._check_game_end()
        return True

    def _execute_en_passant_move(self, move: Dict[str, Any], pawn: Dict[str, Any]) -> bool:
        """Führt en Passant aus"""
        # Bewege Bauer
        pawn['position'] = move['to_pos']
        
        # Schlage gegnerischen Bauer
        captured_pawn_pos = move['capture_pos']
        captured_pawn = self.get_piece_at(captured_pawn_pos)
        if captured_pawn:
            captured_pawn['captured'] = True
            move['captured_piece_id'] = captured_pawn['id']
        
        self.synchronize_board_state()
        self.white_turn = not self.white_turn
        self._check_game_end()
        return True

    def _execute_promotion_move(self, move: Dict[str, Any], pawn: Dict[str, Any]) -> bool:
        """Führt Bauernumwandlung aus"""
        # Normale Bewegung
        captured_piece = self.get_piece_at(move['to_pos'])
        if captured_piece:
            captured_piece['captured'] = True
            move['captured_piece_id'] = captured_piece['id']
        
        # Bewege Bauer zur Umwandlungsposition
        pawn['position'] = move['to_pos']
        
        # Umwandlung durchführen
        promotion_piece_type = move['promotion_piece']
        pawn['type'] = promotion_piece_type
        pawn['value'] = promotion_piece_type * pawn['color']
        pawn['symbol'] = PIECE_SYMBOLS.get(pawn['value'], '?')
        
        self.synchronize_board_state()
        self.white_turn = not self.white_turn
        self._check_game_end()
        return True

    def undo_move(self) -> bool:
        """Macht den letzten Zug rückgängig."""
        if not self.move_history:
            return False

        # Lade den vorherigen Zustand
        previous_state = self.move_history.pop()
        
        self.pieces = previous_state['pieces']
        self.white_turn = previous_state['white_turn']
        self.checkmate = previous_state['checkmate']
        self.stalemate = previous_state['stalemate']
        self.next_piece_id = previous_state['next_piece_id']
        
        # Lade den Zustand der Regeln
        self.rules.en_passant_target = previous_state['rules']['en_passant_target']
        self.rules.castling_rights = previous_state['rules']['castling_rights']
        
        self.synchronize_board_state()
        return True

    def _save_state(self):
        """Speichert den aktuellen Spielzustand in der Historie."""
        current_state = {
            'pieces': copy.deepcopy(self.pieces),
            'white_turn': self.white_turn,
            'checkmate': self.checkmate,
            'stalemate': self.stalemate,
            'next_piece_id': self.next_piece_id,
            'rules': {
                'en_passant_target': self.rules.en_passant_target,
                'castling_rights': copy.deepcopy(self.rules.castling_rights)
            }
        }
        self.move_history.append(current_state)

    def _restore_state(self):
        """Stellt den Zustand aus dem letzten Eintrag in der Historie wieder her."""
        if self.move_history:
            self.pieces = self.move_history[-1]['pieces']
            self.white_turn = self.move_history[-1]['white_turn']
            self.checkmate = self.move_history[-1]['checkmate']
            self.stalemate = self.move_history[-1]['stalemate']
            self.next_piece_id = self.move_history[-1]['next_piece_id']
            self.rules.en_passant_target = self.move_history[-1]['rules']['en_passant_target']
            self.rules.castling_rights = self.move_history[-1]['rules']['castling_rights']
            self.synchronize_board_state()

    # =========================================================================
    # SCHACH/MATT/PATT-PRÜFUNG
    # =========================================================================
    
    def generate_all_moves(self, color: int) -> List[Dict[str, Any]]:
        """Generiert alle legalen Züge für die angegebene Farbe."""
        # Aufruf der korrigierten Methode im MoveGenerator
        return self.move_generator.generate_moves(color)

    def is_king_in_check(self, color: int) -> bool:
        """Prüft, ob der König der gegebenen Farbe im Schach steht."""
        king = self.get_king(color)
        if not king:
            return False 
            
        opponent_color = BLACK if color == WHITE else WHITE
        
        return self.move_generator.is_square_attacked(king['position'], opponent_color)
        
    def _check_game_end(self):
        """Prüft, ob die aktuelle Stellung Schachmatt oder Patt ist."""
        color = WHITE if self.white_turn else BLACK
        
        legal_moves = self.generate_all_moves(color)
        
        if not legal_moves:
            if self.is_king_in_check(color):
                self.checkmate = True
                self.stalemate = False
            else:
                self.checkmate = False
                self.stalemate = True
        else:
            self.checkmate = False
            self.stalemate = False

    def is_game_over(self) -> bool:
        """Prüft, ob das Spiel beendet ist."""
        return self.checkmate or self.stalemate

    def take_snapshot(self):
        """Erstellt eine Momentaufnahme des aktuellen Zustands."""
        return {
            'pieces': copy.deepcopy(self.pieces),
            'white_turn': self.white_turn,
            'checkmate': self.checkmate,
            'stalemate': self.stalemate,
            'board': self.board.copy()
        }

    def restore_snapshot(self, snapshot):
        """Stellt einen Zustand aus einer Momentaufnahme wieder her."""
        self.pieces = snapshot['pieces']
        self.white_turn = snapshot['white_turn']
        self.checkmate = snapshot['checkmate']
        self.stalemate = snapshot['stalemate']
        self.board = snapshot['board']

    def _apply_move_internal(self, move):
        """Wendet einen Zug an, ohne den Spielzustand vollständig zu ändern (für Suchalgorithmus)."""
        piece = self.get_piece_by_id(move['piece_id'])
        if not piece:
            return
            
        # Alte Position leeren
        self.board[piece['position']] = EMPTY
        
        # Geschlagene Figur entfernen
        if move.get('capture_pos'):
            captured_piece = self.get_piece_at(move['capture_pos'])
            if captured_piece:
                captured_piece['captured'] = True
                self.board[move['capture_pos']] = EMPTY
        
        # Figur bewegen
        piece['position'] = move['to_pos']
        self.board[move['to_pos']] = piece['value']
        
        # Promotion behandeln
        if move.get('promotion_piece'):
            piece['type'] = move['promotion_piece']
            piece['value'] = move['promotion_piece'] * piece['color']
            piece['symbol'] = PIECE_SYMBOLS.get(piece['value'], '?')

    # =========================================================================
    # HILFSFUNKTIONEN (NOTATION)
    # =========================================================================

    def _position_to_notation(self, position: int) -> str:
        """Konvertiert interne Position zu algebraischer Notation"""
        files = ['', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', '']
        row = position // 10
        file = position % 10
        if 1 <= file <= 8 and 2 <= row <= 9:
            return f"{files[file]}{row - 1}"
        return "??"

    # =========================================================================
    # EDITOR-FUNKTIONEN
    # =========================================================================

    def editor_place_piece(self, piece_type: int, color: int, position: int) -> bool:
        """Platziert eine Figur auf einem Feld (Editor-Funktion)"""
        if not self.is_valid_position(position):
            return False
        
        # Entferne existierende Figur
        existing_piece = self.get_piece_at(position)
        if existing_piece:
            existing_piece['captured'] = True
        
        # Füge neue Figur hinzu (verwendet _add_piece mit neuer ID)
        self._add_piece(piece_type, color, position)
        
        # Synchronisiere das Board, um die neue Figur anzuzeigen
        self.synchronize_board_state(silent=True)
        return True
    
    def editor_remove_piece(self, position):
        """Entfernt eine Figur vom Brett (Editor-Funktion)"""
        piece = self.get_piece_at(position)
        if piece:
            piece['captured'] = True
            self.synchronize_board_state(silent=True)
            return True
        
        return False
    
    def editor_clear_board(self):
        """Entfernt alle Figuren vom Brett"""
        for piece in self.pieces:
            piece['captured'] = True
        self.synchronize_board_state(silent=True)
    
    def editor_standard_position(self):
        """Setzt die Standard-Anfangsstellung"""
        self.initialize_pieces()