Logica_InteligenciaArtificial.js

import { turnoJugador } from "./Turno.js";
import { EventBus } from "../EventBus.js";
import { Eventos } from "../Events.js";

/**
 * Clase que implementa la Inteligencia Artificial para el jugador J2.
 * Gestiona la toma de decisiones, cálculo de pesos tácticos y ejecución de movimientos.
 * @class InteligenciaArtificial
 * @memberof Logica
 */
export default class InteligenciaArtificial {
    /**
     * Constructor de la Inteligencia Artificial
     * @param {Tablero} tablero - Tablero de juego
     * @param {TableroGrafico} tableroGrafico - Representación gráfica del tablero
     * @param {Equipo} equipoIA - Equipo controlado por la IA (J2)
     * @param {Phaser.Scene} escena - Escena de Phaser
     * @param {number} acciones - Número de acciones por turno
     * @param {Turno} turno - Instancia del sistema de turnos
     */
    constructor(tablero, tableroGrafico, equipoIA, escena, acciones, turno) {
        this.tablero = tablero;
        this.tableroGrafico = tableroGrafico;
        this.equipoIA = equipoIA;
        this.escena = escena;
        
        /**
         * Número de acciones disponibles por turno
         * @type {number}
         */
        this.acciones = acciones;
        /**
         * Contador de turnos transcurridos (usado para priorizar artillería)
         * @type {number}
         */
        this.contadorArtilleria = 0;
        /**
         * Referencia al sistema de turnos
         * @type {Turno}
         */
        this.turno = turno;

        // Suscripción a eventos de cambio de turno
        EventBus.on(Eventos.CHANGE_TURN, () => {
            this.contadorArtilleria++;
            if (turnoJugador == 'J2') {
                // Añadir delay para permitir que restTablero() termine
                setTimeout(() => this.TurnoIA(), 100);
            }
        });
    }

    /**
     * Ejecuta el turno completo de la IA con delays entre acciones.
     * Proceso:
     * 1. Calcula pesos tácticos de todas las piezas disponibles
     * 2. Prioriza piezas con objetivos claros
     * 3. Ejecuta movimientos usando A* pathfinding
     * 4. Maneja combates cuando se encuentra un enemigo
     * 5. Si no hay objetivos, mueve piezas hacia adelante (columna izquierda)
     */
    async TurnoIA() {
        // Desactivar controles del jugador durante el turno de la IA
        this.tableroGrafico.desactivarInteraccion();
        this.turno.turnoGrafico.desactivarBotonFinalizar();

        // Ejecutar cada acción del turno
        for (let accion = 0; accion < this.acciones; accion++) {
            let bestPieza = null;
            let celdaObjetivo = null;
            let bestPeso = -Infinity;
            let pesos = new Map();
            let camino = [];

            // Calcular peso táctico de todas las piezas disponibles
            for (let pieza of this.equipoIA.piezas) {
                if (pieza.getTipo() === 'Artilleria') {
                    // Artillería solo actúa si puede disparar
                    if (pieza.puedeDisparar()) {
                        let pesoData = pieza.calculaPeso();
                        pesos.set(pieza, pesoData);
                    }
                }
                else if (!pieza.getMovida()) {
                    // Calcular peso de piezas no movidas
                    let pesoData = pieza.calculaPeso();
                    pesos.set(pieza, pesoData);
                }
            }

            // Si no hay piezas disponibles, terminar turno
            if (pesos.size === 0) {
                break;
            }

            // Clasificar piezas según tengan objetivo claro o no
            let piezasConObjetivo = [];
            let piezasSinObjetivo = [];

            for (let [pieza, data] of pesos) {
                if (data.bestCelda !== null) {
                    piezasConObjetivo.push({ pieza, data });
                } else {
                    piezasSinObjetivo.push({ pieza, data });
                }
            }

            // Priorizar piezas con objetivo claro
            if (piezasConObjetivo.length > 0) {
                for (let { pieza, data } of piezasConObjetivo) {
                    // Evitar usar artillería en los primeros 5 turnos
                    if (pieza.getTipo() === 'Artilleria' && this.contadorArtilleria < 5) continue;
                    // Evitar que el comandante se quede en su posición actual
                    if (pieza.getTipo() === 'Comandante' && data.bestCelda === this.tablero.getCelda(pieza.fil, pieza.col)) continue;
                    
                    // Seleccionar la pieza con mayor peso
                    if (data.peso > bestPeso) {
                        bestPeso = data.peso;
                        bestPieza = pieza;
                        celdaObjetivo = data.bestCelda;
                    }
                }
            }
            else {
                // No hay objetivos claros, se manejará posteriormente
                bestPieza = null;
                celdaObjetivo = null;
            }

            // Manejo especial para artillería
            if (bestPieza && bestPieza.getTipo() === 'Artilleria') {
                if (celdaObjetivo && bestPieza.puedeDisparar()) {
                    this.tablero.piezaActiva = bestPieza;
                    EventBus.emit(Eventos.PIECE_SELECTED, bestPieza);

                    let destino = celdaObjetivo.getPosicion();
                    bestPieza.lanzarProyectil(destino.fila, destino.col, this.escena, this.tablero);

                    EventBus.emit(Eventos.PIECE_MOVED, bestPieza, false);

                    await this.esperarDelay(800);
                    continue;
                } else {
                    continue;
                }
            }

            // Calcular camino usando A* para piezas con objetivo
            if (bestPieza && celdaObjetivo) {
                let origen = bestPieza.getPosicion();
                camino = this.aStarPathFinding(
                    bestPieza,
                    this.tablero.getCelda(origen.fila, origen.col),
                    celdaObjetivo
                );

                // Limitar camino a movimientos disponibles
                let movimientosDisponibles = bestPieza.getMovimientos();
                if (camino.length > movimientosDisponibles) {
                    camino = camino.slice(0, movimientosDisponibles);
                }
            }
            else {
                // Sin objetivo claro: mover soldados y caballería hacia la izquierda (hacia J1)
                camino = [];
                for (let [pieza, data] of pesos) {
                    let tipo = pieza.getTipo();

                    if (tipo === 'Soldado' || tipo === 'Caballeria') {
                        let pos = pieza.getPosicion();
                        let movimientosMax = pieza.getMovimientos();
                        let destinoCol = pos.col - movimientosMax;

                        if (destinoCol >= 0) {
                            let celdaDestino = this.tablero.getCelda(pos.fila, destinoCol);

                            if (celdaDestino.estaVacia()) {
                                camino = this.aStarPathFinding(
                                    pieza,
                                    this.tablero.getCelda(pos.fila, pos.col),
                                    celdaDestino
                                );

                                if (camino.length > 0 && camino.length <= movimientosMax) {
                                    bestPieza = pieza;
                                    break;
                                } else {
                                    camino = [];
                                }
                            }
                        }
                    }
                }
            }

            // Ejecutar movimiento por el camino calculado
            if (camino.length > 0 && bestPieza) {
                this.tablero.piezaActiva = bestPieza;
                EventBus.emit(Eventos.PIECE_SELECTED, bestPieza);

                let ultimaCelda = camino[camino.length - 1];
                let hayEnemigoAlFinal = !ultimaCelda.estaVacia() &&
                    ultimaCelda.getPieza().getJugador() !== bestPieza.getJugador();

                // Si hay enemigo al final, detenerse antes para atacar
                let pasosFinal = hayEnemigoAlFinal ? camino.length - 1 : camino.length;

                // Ejecutar movimiento paso a paso
                for (let paso = 0; paso < pasosFinal; paso++) {
                    let siguienteCelda = camino[paso];
                    let destino = siguienteCelda.getPosicion();
                    let origenActual = bestPieza.getPosicion();

                    // Dibujar conquista de territorio
                    this.tableroGrafico.dibujarFragmentoMapa(destino.fila, destino.col, bestPieza.getJugador());

                    // Limpiar celda de origen
                    this.tablero.getCelda(origenActual.fila, origenActual.col).limpiar();

                    // Mover pieza a destino
                    bestPieza.moverse(destino.fila, destino.col);
                    this.tablero.getCelda(destino.fila, destino.col).setContenido(bestPieza);

                    EventBus.emit(Eventos.PIECE_MOVED, bestPieza, false);

                    await this.esperarDelay(300);
                    
                    // Verificar si el comandante llegó a su objetivo defensivo
                    if (bestPieza.getTipo() === 'Comandante' && !hayEnemigoAlFinal) {
                        let posActual = bestPieza.getPosicion();
                        let posObjetivo = celdaObjetivo.getPosicion();
                        if (posActual.fila === posObjetivo.fila && posActual.col === posObjetivo.col) {
                            this.turno.acabarMovimientos();
                            break;
                        }
                    }
                }

                // Ejecutar ataque si hay enemigo al final del camino
                if (hayEnemigoAlFinal && pasosFinal === camino.length - 1) {
                    let celdaEnemiga = ultimaCelda.getPosicion();

                    // Validaciones de seguridad
                    if (!bestPieza || bestPieza.getPosicion() === undefined) {
                        continue;
                    }
                    
                    if (ultimaCelda.estaVacia()) {
                        continue;
                    }

                    // Iniciar combate
                    this.tablero.ataque(celdaEnemiga.fila, celdaEnemiga.col);

                    await this.esperarDelay(300);

                    EventBus.emit(Eventos.ATACK);
                    
                    await this.esperarDelay(1500);
                    
                    EventBus.emit(Eventos.PIECE_MOVED, bestPieza, true);
                }

            } else {
                // No hay movimientos posibles, finalizar turno
                this.turno.acabarMovimientos();
                continue;
            }

            await this.esperarDelay(400);
        }

        // Reactivar controles del jugador
        this.tableroGrafico.activarInteraccion();
        this.turno.turnoGrafico.activarBotonFinalizar();
    }
    

    /**
     * Implementación del algoritmo A* para encontrar el camino óptimo entre dos celdas.
     * Considera movimientos especiales según el tipo de pieza:
     * - Comandante: movimiento diagonal (heurística Chebyshev)
     * - Caballería: puede saltar piezas aliadas
     * - Otros: solo movimiento cardinal (heurística Manhattan)
     * 
     * @param {Pieza} pieza - Pieza que se moverá
     * @param {Celda} celdaInicio - Celda de origen
     * @param {Celda} celdaDestino - Celda de destino
     * @returns {Array<Celda>} Camino óptimo (array vacío si no hay ruta)
     */
    aStarPathFinding(pieza, celdaInicio, celdaDestino) {
        const MAX_PROFUNDIDAD = 20;
        const openSet = [];
        const closedSet = new Set();
        const cameFrom = new Map();
        const costoMovimiento = new Map();

        const gScore = new Map();
        const fScore = new Map();

        // Inicializar scores
        gScore.set(celdaInicio, 0);
        pieza.getTipo() == 'Comandante' ? fScore.set(celdaInicio, this.diagHeuristic(celdaInicio, celdaDestino))
            : fScore.set(celdaInicio, this.heuristic(celdaInicio, celdaDestino));
        openSet.push(celdaInicio);

        while (openSet.length > 0) {
            // Encontrar celda con menor fScore en openSet
            let currentIndex = 0;
            let current = openSet[0];

            for (let i = 1; i < openSet.length; i++) {
                const candidato = openSet[i];
                const fCurr = fScore.get(current) ?? Infinity;
                const fCand = fScore.get(candidato) ?? Infinity;
                if (fCand < fCurr) {
                    currentIndex = i;
                    current = candidato;
                }
            }

            // Si llegamos al destino, reconstruir camino
            if (current === celdaDestino) {
                return this.reconstruirCamino(cameFrom, current);
            }

            openSet.splice(currentIndex, 1);
            closedSet.add(current);

            const gActual = gScore.get(current) ?? Infinity;

            // Limitar profundidad de búsqueda
            if (gActual > MAX_PROFUNDIDAD) {
                continue;
            }

            // Obtener vecinos según tipo de pieza
            const vecinos = pieza.getTipo() === 'Caballeria'
                ? this.getVecinosConSalto(pieza, current, celdaInicio, gActual)
                : this.getVecinos(pieza, current);

            // Evaluar cada vecino
            for (const vecinoInfo of vecinos) {
                const vecino = vecinoInfo.celda || vecinoInfo;
                const costoExtra = vecinoInfo.costo || 1;

                if (closedSet.has(vecino)) continue;
                if (!this.transitable(vecino, celdaDestino)) continue;

                const expectedG = gActual + costoExtra;
                const gVecino = gScore.get(vecino);

                // Si encontramos un camino mejor, actualizarlo
                if (gVecino === undefined || expectedG < gVecino) {
                    cameFrom.set(vecino, current);
                    gScore.set(vecino, expectedG);
                    costoMovimiento.set(vecino, costoExtra);

                    const h = pieza.getTipo() == 'Comandante' ? this.diagHeuristic(vecino, celdaDestino) : this.heuristic(vecino, celdaDestino);
                    fScore.set(vecino, expectedG + h);

                    if (!openSet.includes(vecino))
                        openSet.push(vecino);
                }
            }
        }
        return [];
    }

    /**
     * Obtiene los vecinos de una celda considerando el salto de caballería.
     * La caballería puede saltar piezas aliadas en su primer movimiento.
     * 
     * @param {Pieza} pieza - Pieza de caballería
     * @param {Celda} celda - Celda actual
     * @param {Celda} celdaInicio - Celda de inicio del pathfinding
     * @param {number} gActual - Distancia desde el inicio
     * @returns {Array<Object>} Array de {celda, costo}
     */
    getVecinosConSalto(pieza, celda, celdaInicio, gActual) {
        const pos = celda.getPosicion();
        const fila = pos.fila;
        const col = pos.col;
        const res = [];

        // Direcciones cardinales
        const direcciones = [
            { df: -1, dc: 0 },  // arriba
            { df: 1, dc: 0 },   // abajo
            { df: 0, dc: -1 },  // izquierda
            { df: 0, dc: 1 }    // derecha
        ];

        for (const dir of direcciones) {
            const nf = fila + dir.df;
            const nc = col + dir.dc;

            // Verificar límites del tablero
            if (nf < 0 || nf >= this.tablero.filas || nc < 0 || nc >= this.tablero.columnas)
                continue;

            const vecinoCelda = this.tablero.getCelda(nf, nc);

            if (vecinoCelda.estaVacia()) {
                res.push({ celda: vecinoCelda, costo: 1 });
            }
            else if (!vecinoCelda.estaVacia() && vecinoCelda.getPieza().getJugador() !== pieza.getJugador()) {
                // Puede atacar enemigos
                res.push({ celda: vecinoCelda, costo: 1 });
            }
            else if (celda === celdaInicio && gActual === 0 && pieza.getSaltoCaballeria()) {
                // Desde posición inicial, puede saltar aliado
                const nf2 = nf + dir.df;
                const nc2 = nc + dir.dc;

                if (nf2 >= 0 && nf2 < this.tablero.filas && nc2 >= 0 && nc2 < this.tablero.columnas) {
                    const celdaSalto = this.tablero.getCelda(nf2, nc2);

                    if (celdaSalto.estaVacia() ||
                        (!celdaSalto.estaVacia() && celdaSalto.getPieza().getJugador() !== pieza.getJugador())) {
                        res.push({ celda: celdaSalto, costo: 2 });
                    }
                }
            }
        }

        return res;
    }

    /**
     * Heurística de Chebyshev (distancia diagonal).
     * Usada para el Comandante que puede moverse en diagonal.
     * 
     * @param {Celda} a - Celda origen
     * @param {Celda} b - Celda destino
     * @returns {number} Distancia estimada
     */
    diagHeuristic(a, b) {
        const pa = a.getPosicion();
        const pb = b.getPosicion();
        const dx = Math.abs(pa.fila - pb.fila);
        const dy = Math.abs(pa.col - pb.col);
        return Math.max(dx, dy);
    }

    /**
     * Heurística de Manhattan (distancia cardinal).
     * Usada para piezas que solo se mueven en direcciones cardinales.
     * 
     * @param {Celda} a - Celda origen
     * @param {Celda} b - Celda destino
     * @returns {number} Distancia estimada
     */
    heuristic(a, b) {
        const pa = a.getPosicion();
        const pb = b.getPosicion();
        return Math.abs(pa.fila - pb.fila) + Math.abs(pa.col - pb.col);
    }

    /**
     * Determina si una celda es transitable.
     * Una celda es transitable si está vacía, excepto si es el destino final.
     * 
     * @param {Celda} celda - Celda a evaluar
     * @param {Celda} celdaDestino - Destino final del camino
     * @returns {boolean} true si es transitable
     */
    transitable(celda, celdaDestino) {
        if (celda === celdaDestino) return true;
        return celda.estaVacia();
    }

    /**
     * Reconstruye el camino desde el origen hasta el destino.
     * Utiliza el mapa cameFrom generado por A*.
     * 
     * @param {Map} cameFrom - Mapa de predecesores
     * @param {Celda} actual - Celda de destino
     * @returns {Array<Celda>} Camino ordenado desde origen a destino
     */
    reconstruirCamino(cameFrom, actual) {
        const path = [];
        let current = actual;
        while (cameFrom.has(current)) {
            path.push(current);
            current = cameFrom.get(current);
        }
        path.reverse();
        return path;
    }

    /**
     * Obtiene las celdas vecinas válidas de una celda.
     * Para el Comandante incluye diagonales, para otras piezas solo cardinales.
     * 
     * @param {Pieza} pieza - Pieza que se está moviendo
     * @param {Celda} celda - Celda actual
     * @returns {Array<Celda>} Array de celdas vecinas
     */
    getVecinos(pieza, celda) {
        const pos = celda.getPosicion();
        const fila = pos.fila;
        const col = pos.col;
        const res = [];

        // Movimientos cardinales (arriba, abajo, izquierda, derecha)
        if (fila > 0) {
            res.push(this.tablero.getCelda(fila - 1, col));
        }
        if (fila < this.tablero.filas - 1) {
            res.push(this.tablero.getCelda(fila + 1, col));
        }
        if (col > 0) {
            res.push(this.tablero.getCelda(fila, col - 1));
        }
        if (col < this.tablero.columnas - 1) {
            res.push(this.tablero.getCelda(fila, col + 1));
        }
        
        // Movimientos diagonales para el Comandante
        if (pieza.getTipo() == 'Comandante') {
            if (fila > 0 && col > 0) {
                res.push(this.tablero.getCelda(fila - 1, col - 1));
            }
            if (fila > 0 && col < this.tablero.columnas - 1) {
                res.push(this.tablero.getCelda(fila - 1, col + 1));
            }
            if (fila < this.tablero.filas - 1 && col > 0) {
                res.push(this.tablero.getCelda(fila + 1, col - 1));
            }
            if (fila < this.tablero.filas - 1 && col < this.tablero.columnas - 1) {
                res.push(this.tablero.getCelda(fila + 1, col + 1));
            }
        }

        return res;
    }

    /**
     * Función auxiliar para esperar un tiempo determinado.
     * Usada para crear delays visuales entre acciones de la IA.
     * 
     * @param {number} ms - Milisegundos a esperar
     * @returns {Promise} Promesa que se resuelve después del delay
     */
    esperarDelay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}