#!/usr/bin/env python3
"""
PARADOXChess: A Non-Singular Chess Engine for Human Play

This engine implements the evaluation algebra defined in paradoxChess_theory_v001.py
and uses non-singular division operators from non_singular_division.txt.
It allows a human to play against the engine via the command line.
"""

import chess
import chess.svg
import math
import random
import time
from typing import Union, Optional, List, Tuple, Dict, Any

# ----------------------------------------------------------------------
#  Non-Singular Operators (from the theory)
# ----------------------------------------------------------------------

PLANCK_EVAL = 1e-6
MATE_PROJECTIVE = 1_000_000      # The "point at infinity" as a finite token
T_EVAP = 100                     # Plies until mate evaporates (50-move rule)
MAX_ENTROPY = 5.0                # Firewall cutoff
PLANCK_MOBILITY = 1e-6           # Minimal mobility to avoid true zero


class UncertainValue:
    """Represents a value smeared by entropy."""
    def __init__(self, mu: float, entropy: float):
        self.mu = mu
        self.H = entropy


class Oscillation:
    """Represents a 2-cycle oscillation (e.g., in zugzwang)."""
    def __init__(self, a: float, b: float):
        self.cycle = (a, b)
        self.phase = 0

    def current(self) -> float:
        val = self.cycle[self.phase % 2]
        self.phase += 1
        return val


class EventHorizonReceipt:
    """Receipt returned at horizon (depth=0)."""
    def __init__(self, fen: str, entropy: float):
        self.fen = fen
        self.entropy = entropy


# ----------------------------------------------------------------------
#  Operator implementations
# ----------------------------------------------------------------------

def singularity_shield(x: float) -> float:
    """⊗ : Planck-scale cutoff – never below PLANCK_EVAL."""
    if abs(x) < PLANCK_EVAL:
        return math.copysign(PLANCK_EVAL, x) if x != 0 else PLANCK_EVAL
    return x


def projective_divide(a: float, b: float) -> float:
    """⨯ : a/b with projective closure: a/0 -> MATE_PROJECTIVE."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return MATE_PROJECTIVE if a > 0 else -MATE_PROJECTIVE
    return a / b


def hawking_radiance(mate_score: float, depth: int) -> float:
    """⨳ : mate score radiates away over depth, evaporates at T_EVAP."""
    if depth >= T_EVAP:
        return 0.0
    return mate_score / (T_EVAP - depth)


def flip_divide(a: float, b: float) -> Union[float, Oscillation]:
    """⊘ : zugzwang oscillation. Returns Oscillation if b near zero."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return Oscillation(a / PLANCK_EVAL, -a / PLANCK_EVAL)
    return a / b


def uncertain_quotient(a: float, b: float, entropy: float) -> UncertainValue:
    """⊘̷ : smear with entropy when denominator small."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return UncertainValue(MATE_PROJECTIVE if a > 0 else -MATE_PROJECTIVE, entropy)
    return UncertainValue(a / b, entropy)


def event_horizon_divide(fen: str, entropy: float) -> EventHorizonReceipt:
    """∘ : returns a receipt at depth 0."""
    return EventHorizonReceipt(fen, entropy)


def wormhole_quotient(local_eval: float, board: chess.Board) -> float:
    """⨂ : non‑local correlations (castling rights, connected rooks, etc.)."""
    bonus = 0.0
    # Example: castling rights create non‑local entanglement
    if board.has_kingside_castling_rights(chess.WHITE):
        bonus += 0.1
    if board.has_queenside_castling_rights(chess.WHITE):
        bonus += 0.1
    # Rook connections on same rank/file
    for color in [chess.WHITE, chess.BLACK]:
        rooks = board.pieces(chess.ROOK, color)
        for sq1 in rooks:
            for sq2 in rooks:
                if sq1 != sq2 and (chess.square_rank(sq1) == chess.square_rank(sq2) or
                                   chess.square_file(sq1) == chess.square_file(sq2)):
                    bonus += 0.05
    return local_eval + bonus


def novikov_quotient(raw_eval: float, tt_entry: Optional[Dict]) -> float:
    """⨎ : adjust evaluation if transposition table shows a paradox."""
    if tt_entry is not None and 'parity' in tt_entry:
        if tt_entry['parity'] != (raw_eval > 0):
            return raw_eval * 0.95   # gentle consistency adjustment
    return raw_eval


def firewall_divide(branch_eval: Any, branch_entropy: float) -> Any:
    """⊞ : block branches with excessive entropy instead of pruning."""
    if branch_entropy > MAX_ENTROPY:
        return "FIREWALL_BLOCKED"
    return branch_eval


# ----------------------------------------------------------------------
#  PARADOXChess Engine
# ----------------------------------------------------------------------

class PARADOXChess:
    """Non-singular chess engine based on projective evaluation and TSP collapse."""

    def __init__(self, max_depth: int = 4):
        self.max_depth = max_depth
        self.tt = {}            # transposition table (wormhole network)
        self.nodes_evaluated = 0

    # ------------------------------------------------------------------
    #  Evaluation components
    # ------------------------------------------------------------------
    def material_balance(self, board: chess.Board) -> float:
        """Simplified material count."""
        val = 0.0
        for sq in chess.SQUARES:
            piece = board.piece_at(sq)
            if piece:
                v = {chess.PAWN: 1.0, chess.KNIGHT: 3.0, chess.BISHOP: 3.0,
                     chess.ROOK: 5.0, chess.QUEEN: 9.0}.get(piece.piece_type, 0.0)
                val += v if piece.color == chess.WHITE else -v
        return val

    def positional_entropy(self, board: chess.Board) -> float:
        """H(b) : microstate count -> entropy proxy."""
        mobility = len(list(board.legal_moves))
        # tension: number of attacks on centre squares (simplified)
        tension = 0
        for sq in [chess.E4, chess.D4, chess.E5, chess.D5]:
            tension += len(board.attacks(sq))
        return math.log1p(mobility + tension + 1)

    def tactical_complexity(self, board: chess.Board) -> float:
        """Proxy for denominator in general evaluation."""
        # number of checks, captures, and forks (simplified)
        moves = list(board.legal_moves)
        checks = sum(1 for m in moves if board.gives_check(m))
        captures = sum(1 for m in moves if board.is_capture(m))
        return 1.0 + 0.1 * (checks + captures) + 0.01 * len(moves)

    # ------------------------------------------------------------------
    #  Core evaluation function (returns a scalar or receipt)
    # ------------------------------------------------------------------
    def evaluate(self, board: chess.Board, depth: int) -> Union[float, EventHorizonReceipt]:
        """Return a non‑singular evaluation."""
        material = self.material_balance(board)
        mobility = len(list(board.legal_moves))
        entropy = self.positional_entropy(board)

        # Singularity 1: Checkmate
        if board.is_checkmate():
            # Use projective division, then Hawking radiance at higher depth
            raw_mate = projective_divide(material, 0.0)
            return hawking_radiance(raw_mate, depth)

        # Singularity 2: Stalemate / Zugzwang (zero mobility, not mate)
        if mobility == 0:
            osc = flip_divide(material, 0.0)
            if isinstance(osc, Oscillation):
                return osc.current()
            return osc

        # Horizon: depth 0 returns a receipt
        if depth == 0:
            return event_horizon_divide(board.fen(), entropy)

        # General evaluation: uncertain quotient smeared by entropy
        complexity = self.tactical_complexity(board)
        uq = uncertain_quotient(material, complexity, entropy)
        eval_score = uq.mu

        # Wormhole non‑local correlations
        eval_score = wormhole_quotient(eval_score, board)

        # Novikov self‑consistency (using transposition table)
        key = board.zobrist_hash()
        tt_entry = self.tt.get(key)
        eval_score = novikov_quotient(eval_score, tt_entry)

        # Bekenstein bound: cap by entropy * 1e4
        if abs(eval_score) > entropy * 1e4:
            eval_score = math.copysign(entropy * 1e4, eval_score)

        return eval_score

    # ------------------------------------------------------------------
    #  TSP-Collapse search (non‑destructive, uses Firewall)
    # ------------------------------------------------------------------
    def search(self, board: chess.Board, depth: int, alpha: float, beta: float) -> float:
        """Minimax with projective comparison and firewall."""
        self.nodes_evaluated += 1

        # Terminal or horizon
        if board.is_game_over():
            return self.evaluate(board, depth)

        if depth == 0:
            eval_obj = self.evaluate(board, 0)
            if isinstance(eval_obj, EventHorizonReceipt):
                # Receipt: convert to heuristic using its entropy
                return eval_obj.entropy * 10.0 if board.turn == chess.WHITE else -eval_obj.entropy * 10.0
            return eval_obj if isinstance(eval_obj, (int, float)) else 0.0

        # Generate moves
        moves = list(board.legal_moves)
        # Order moves (simple MVV-LVA)
        moves.sort(key=lambda m: self.move_priority(board, m), reverse=True)

        best = -float('inf')
        for move in moves:
            board.push(move)
            branch_entropy = self.positional_entropy(board)

            # Firewall: block chaotic branches, do not delete
            if branch_entropy > MAX_ENTROPY:
                board.pop()
                continue

            child_val = self.search(board, depth - 1, -beta, -alpha)
            board.pop()

            # Projective comparison: treat MATE_PROJECTIVE as larger than any finite
            if self.projective_greater(child_val, best):
                best = child_val

            alpha = max(alpha, best)
            if alpha >= beta:
                break   # beta cutoff (still acceptable, we do not delete branches)

        if best == -float('inf'):
            # No valid moves after firewall? Return evaluation.
            best = self.evaluate(board, depth)
            if isinstance(best, (EventHorizonReceipt, UncertainValue, Oscillation)):
                best = 0.0
        return best

    def move_priority(self, board: chess.Board, move: chess.Move) -> float:
        """Simple MVV-LVA ordering."""
        piece = board.piece_at(move.from_square)
        victim = board.piece_at(move.to_square)
        if victim:
            piece_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                           chess.ROOK:5, chess.QUEEN:9}.get(piece.piece_type, 0)
            victim_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                            chess.ROOK:5, chess.QUEEN:9}.get(victim.piece_type, 0)
            return victim_value - piece_value / 100.0
        return 0.0

    def projective_greater(self, a: Union[float, Any], b: float) -> bool:
        """Comparison on RP¹ – MATE_PROJECTIVE is larger than any finite."""
        if isinstance(a, (Oscillation, EventHorizonReceipt, UncertainValue)):
            return False
        if abs(a) > MATE_PROJECTIVE / 2:
            return a > b
        return a > b

    # ------------------------------------------------------------------
    #  Main move selection
    # ------------------------------------------------------------------
    def get_best_move(self, board: chess.Board, time_limit: float = 1.0) -> chess.Move:
        """Iterative deepening with time control."""
        best_move = None
        start_time = time.time()
        for depth in range(1, self.max_depth + 1):
            best_move = self.iterative_deepening(board, depth)
            if time.time() - start_time > time_limit:
                break
        return best_move

    def iterative_deepening(self, board: chess.Board, depth: int) -> chess.Move:
        """Search at fixed depth and return best move."""
        best_move = None
        best_value = -float('inf')
        moves = list(board.legal_moves)
        # Shuffle to avoid deterministic bias (but preserve ordering somewhat)
        random.shuffle(moves)

        for move in moves:
            board.push(move)
            value = self.search(board, depth - 1, -float('inf'), float('inf'))
            board.pop()

            if self.projective_greater(value, best_value):
                best_value = value
                best_move = move
        return best_move


# ----------------------------------------------------------------------
#  Human vs Engine Interaction
# ----------------------------------------------------------------------

def print_board(board: chess.Board):
    """Print a simple ASCII board with coordinates."""
    print("\n  a b c d e f g h")
    for rank in range(7, -1, -1):
        print(f"{rank+1} ", end="")
        for file in range(8):
            sq = chess.square(file, rank)
            piece = board.piece_at(sq)
            if piece:
                symbol = piece.symbol()
                print(symbol, end=" ")
            else:
                print(".", end=" ")
        print(f" {rank+1}")
    print("  a b c d e f g h\n")


def main():
    engine = PARADOXChess(max_depth=3)   # Depth 3 for reasonable response time
    board = chess.Board()

    print("=" * 50)
    print("PARADOXChess: Non-Singular Chess Engine")
    print("You play as White. Enter moves in algebraic notation (e.g., e2e4 or e4).")
    print("Type 'quit' to exit.")
    print("=" * 50)

    while not board.is_game_over():
        print_board(board)

        # Human move (White)
        if board.turn == chess.WHITE:
            move_str = input("Your move: ").strip().lower()
            if move_str == "quit":
                break
            try:
                # Allow both e2e4 and e4 formats
                if len(move_str) == 2:
                    # pawn move? ambiguous, better to require full
                    move = board.parse_san(move_str)
                else:
                    move = chess.Move.from_uci(move_str)
                if move in board.legal_moves:
                    board.push(move)
                else:
                    print("Illegal move. Try again.")
                    continue
            except Exception:
                print("Invalid format. Use UCI (e2e4) or SAN (e4).")
                continue
        else:
            # Engine move (Black)
            print("Engine is thinking...")
            start = time.time()
            move = engine.get_best_move(board, time_limit=1.5)
            elapsed = time.time() - start
            if move is None:
                print("No legal moves?")
                break
            board.push(move)
            print(f"Engine plays {move.uci()}  (searched {engine.nodes_evaluated} nodes in {elapsed:.2f}s)")
            engine.nodes_evaluated = 0

    # Game over
    print_board(board)
    if board.is_checkmate():
        if board.turn == chess.WHITE:
            print("Checkmate! Engine (Black) wins.")
        else:
            print("Checkmate! You (White) win.")
    elif board.is_stalemate():
        print("Stalemate! Game drawn.")
    elif board.is_insufficient_material():
        print("Insufficient material. Drawn.")
    else:
        print("Game over by other reason.")


if __name__ == "__main__":
    main()