#!/usr/bin/env python3
"""
AleFraJav Engine Arena — plantilla de motor en Python (protocolo AJE v1)

Un motor que compite de verdad, en un fichero y SIN INSTALAR NADA: solo la biblioteca estándar de
Python 3. Elige jugadas al azar entre las legales, así que gana poco — pero juega, y ese es el
punto de partida que debe tener alguien que empieza.

    python3 bot_template.py            # escucha en http://0.0.0.0:5000

Para competir, regístralo en https://alefrajav.iaintelecto.space/arena/desarrolladores con la URL
pública de tu servidor terminada en /aje/move.

Lo único que tienes que cambiar es `elegir_jugada`, al final. Todo lo demás —el protocolo, el PDN y
las reglas— ya está resuelto aquí para que no tengas que resolverlo tú.

────────────────────────────────────────────────────────────────────────────────────────────────
LA PLANTILLA ANTERIOR NO FUNCIONABA, Y CONVIENE SABER POR QUÉ

Hasta el 2026-08-07 esto exponía `/bot/move`, leía `gameSlug` / `boardState` / `timeLimitMs` y
contestaba `{ok, from:{row,col}, to:{row,col}, msSpent}`. Nada de eso es el protocolo:

  · el endpoint es `/aje/move`;
  · la Arena manda `juego`, `posicion` y `msRestantes`, así que `boardState` llegaba SIEMPRE vacío;
  · la respuesta obligatoria es `jugada` (texto PDN) y `msPensados` (número).

Cualquiera que la copiara perdía todas sus partidas en la jugada 1 por respuesta mal formada, y
habría pensado que la culpa era suya. Ver docs/engine-arena/protocolo-aje-v1.md.
────────────────────────────────────────────────────────────────────────────────────────────────
"""

import json
import random
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

NOMBRE = "Python-Starter-Bot"
VERSION = "2.0.0"
JUEGOS = ["checkers-8x8"]

# ── El tablero ──────────────────────────────────────────────────────────────────────────────────
#
# Solo se juegan las 32 casillas oscuras, numeradas del 1 al 32. La casilla 1 está arriba a la
# izquierda, en el lado de las negras; se numera de izquierda a derecha y de arriba abajo.
#
#   negras: salen en 1-12 y mueven primero        blancas: salen en 21-32
#
# Por dentro se usa (fila, columna) con fila 0 arriba. Las negras avanzan hacia filas MAYORES.

LADO_NEGRO, LADO_BLANCO = "B", "W"
AVANCE = {LADO_NEGRO: +1, LADO_BLANCO: -1}
FILA_DE_CORONACION = {LADO_NEGRO: 7, LADO_BLANCO: 0}


def columnas_de(fila):
    """Las cuatro columnas jugables de una fila: las de suma (fila+columna) impar."""
    return [c for c in range(8) if (fila + c) % 2 == 1]


def casilla_a_rc(n):
    fila = (n - 1) // 4
    return fila, columnas_de(fila)[(n - 1) % 4]


def rc_a_casilla(fila, col):
    return fila * 4 + columnas_de(fila).index(col) + 1


def dentro(fila, col):
    return 0 <= fila < 8 and 0 <= col < 8


def leer_posicion(fen):
    """`B:W18,24,K10:B12,16,K22` → (lado que mueve, {(fila,col): (lado, es_dama)})."""
    trozos = fen.strip().split(":")
    lado = trozos[0].strip().upper()
    tablero = {}
    for parte in trozos[1:]:
        color, lista = parte[0].upper(), parte[1:]
        for bruto in filter(None, (x.strip() for x in lista.split(","))):
            dama = bruto.upper().startswith("K")
            tablero[casilla_a_rc(int(bruto[1:] if dama else bruto))] = (color, dama)
    return lado, tablero


# ── Las reglas ──────────────────────────────────────────────────────────────────────────────────
#
# Tres que sorprenden si vienes de otra variante de damas, y que el árbitro aplica sin perdón:
#
#   1. CAPTURAR ES OBLIGATORIO. Si hay una captura disponible, no se puede mover otra cosa.
#   2. Una pieza normal NO captura hacia atrás. Solo las damas.
#   3. Coronar TERMINA el turno. Aunque desde la casilla de coronación hubiera otra captura.


def direcciones(es_dama, lado):
    return [-1, 1] if es_dama else [AVANCE[lado]]


def capturas_desde(tablero, origen, lado, es_dama):
    salidas = []
    for df in direcciones(es_dama, lado):
        for dc in (-1, 1):
            comida = (origen[0] + df, origen[1] + dc)
            destino = (origen[0] + 2 * df, origen[1] + 2 * dc)
            if not dentro(*destino):
                continue
            pieza_comida = tablero.get(comida)
            if pieza_comida and pieza_comida[0] != lado and destino not in tablero:
                salidas.append((destino, comida))
    return salidas


def cadenas_desde(tablero, origen, lado, es_dama, recorrido):
    """Todas las cadenas completas de captura desde una casilla, como listas de casillas."""
    salidas = capturas_desde(tablero, origen, lado, es_dama)
    if not salidas:
        return [recorrido]

    completas = []
    for destino, comida in salidas:
        siguiente = dict(tablero)
        del siguiente[comida]
        del siguiente[origen]
        corona = not es_dama and destino[0] == FILA_DE_CORONACION[lado]
        siguiente[destino] = (lado, es_dama or corona)
        # Coronar termina el turno: la cadena se corta aquí aunque hubiera más que comer.
        if corona:
            completas.append(recorrido + [destino])
        else:
            completas += cadenas_desde(siguiente, destino, lado, es_dama, recorrido + [destino])
    return completas


def jugadas_legales(fen):
    """Las jugadas legales de la posición, ya en texto PDN: `11-15`, `22x18`, `9x18x27`."""
    lado, tablero = leer_posicion(fen)
    mias = [(rc, p) for rc, p in tablero.items() if p[0] == lado]

    capturas = []
    for origen, (_, es_dama) in mias:
        if capturas_desde(tablero, origen, lado, es_dama):
            for cadena in cadenas_desde(tablero, origen, lado, es_dama, [origen]):
                if len(cadena) > 1:
                    capturas.append("x".join(str(rc_a_casilla(*c)) for c in cadena))
    if capturas:
        return capturas   # capturar es obligatorio

    simples = []
    for origen, (_, es_dama) in mias:
        for df in direcciones(es_dama, lado):
            for dc in (-1, 1):
                destino = (origen[0] + df, origen[1] + dc)
                if dentro(*destino) and destino not in tablero:
                    simples.append(f"{rc_a_casilla(*origen)}-{rc_a_casilla(*destino)}")
    return simples


# ── TU MOTOR VA AQUÍ ────────────────────────────────────────────────────────────────────────────

def elegir_jugada(peticion, legales):
    """
    Devuelve una de `legales`. Esto es lo único que tienes que cambiar.

    `peticion` trae, entre otras cosas, `posicion` (PDN FEN), `msRestantes` y `movimientoNumero`.
    Conecta aquí tu evaluación: minimax, Monte Carlo, una red, lo que sea.
    """
    return random.choice(legales)


# ── El protocolo ────────────────────────────────────────────────────────────────────────────────

class Motor(BaseHTTPRequestHandler):
    def _responder(self, codigo, cuerpo):
        crudo = json.dumps(cuerpo).encode()
        self.send_response(codigo)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(crudo)))
        self.end_headers()
        self.wfile.write(crudo)

    def do_GET(self):
        # Opcional (sección 2.3): sirve para comprobar que el motor sigue vivo.
        if self.path.rstrip("/") == "/aje/info":
            self._responder(200, {"protocolo": 1, "nombre": NOMBRE, "version": VERSION, "juegos": JUEGOS})
        else:
            self._responder(404, {"error": "no encontrado"})

    def do_POST(self):
        if self.path.rstrip("/") != "/aje/move":
            return self._responder(404, {"error": "no encontrado"})

        empezado = time.time()
        largo = int(self.headers.get("Content-Length") or 0)
        try:
            peticion = json.loads(self.rfile.read(largo) or b"{}")
        except ValueError:
            return self._responder(400, {"error": "JSON mal formado"})

        # Rechaza limpiamente lo que no sepas jugar (sección 2.4). Lo que NO debes hacer nunca es
        # contestar una jugada inventada: eso se castiga con la partida entera.
        if peticion.get("juego") not in JUEGOS:
            return self._responder(400, {"error": f"juego no soportado: {peticion.get('juego')}"})

        try:
            legales = jugadas_legales(peticion["posicion"])
        except (KeyError, ValueError, IndexError) as err:
            return self._responder(400, {"error": f"posición ilegible: {err}"})

        if not legales:
            return self._responder(400, {"error": "sin jugadas legales"})

        jugada = elegir_jugada(peticion, legales)
        self._responder(200, {
            "jugada": jugada,
            "msPensados": int((time.time() - empezado) * 1000),
            "info": f"{NOMBRE} v{VERSION} — {len(legales)} jugadas legales",
        })

    def log_message(self, *_):
        pass   # sin ruido en la consola


if __name__ == "__main__":
    print(f"{NOMBRE} v{VERSION} escuchando en http://0.0.0.0:5000/aje/move")
    HTTPServer(("0.0.0.0", 5000), Motor).serve_forever()
