juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

ai.sh (14873B)


      1 #!/usr/bin/env bash
      2 # ============================================================================
      3 #  lib/ai.sh  --  La inteligencia artificial: jugar contra la computadora.
      4 # ============================================================================
      5 #
      6 # Esta es la TERCERA fuente de jugadas (ademas de "vos en el teclado" y "otra
      7 # persona por red"): la computadora. No la programamos nosotros (seria carisimo y
      8 # bash es demasiado lento para pensar jugadas); usamos un motor externo muy fuerte
      9 # llamado Stockfish.
     10 #
     11 # COMO LE HABLAMOS A STOCKFISH:
     12 #   Stockfish entiende el "protocolo UCI": le escribimos ordenes de texto por su
     13 #   entrada y nos contesta por su salida. Por ejemplo, le mandamos la posicion y
     14 #   "go" (pensa), y nos responde "bestmove e2e4" (la mejor jugada). Lo arrancamos
     15 #   como un "coprocess" (un programa en paralelo al nuestro), igual que ncat para
     16 #   la red. En modo IA no hay red, asi que nunca corren los dos coprocesos juntos.
     17 #
     18 # El binario de Stockfish es una descarga opcional (ver bin/get-stockfish.sh).
     19 # Este archivo (la "integracion") siempre viene con el juego. Usa board.sh (get_fen).
     20 
     21 # Variables del motor. AI_R/AI_W/AI_PID son los canales para hablarle y su numero
     22 # de proceso (igual que NET_* para la red).
     23 AI_R=""; AI_W=""; AI_PID=""
     24 AI_COLOR=""                 # el color que juega la maquina (vos jugas LOCAL_COLOR)
     25 EVAL_SCORE=0                # ultimo puntaje calculado (centipawns, punto de vista blancas)
     26 AI_SKILL=6                  # nivel de habilidad 0-20 (vacio si limitamos por Elo)
     27 AI_ELO=""                   # fuerza en Elo 1320-3190 (se usa en vez del nivel, opcional)
     28 AI_MOVETIME=500             # milisegundos que el motor "piensa" cada jugada
     29 AI_LABEL="Medium"           # nombre de la dificultad, para mostrar al empezar
     30 AI_SPECTATE_DELAY=1         # segundos entre jugadas en modo espectador ("step" = manual)
     31 
     32 # _stockfish_path: encontrar el binario del motor, o no imprimir nada si no esta.
     33 # Lo busca en este orden de preferencia:
     34 #   1) la variable de entorno $CHESS_STOCKFISH (si apunta a algo ejecutable);
     35 #   2) el binario que baja get-stockfish.sh, en ./engine/stockfish;
     36 #   3) un "stockfish" instalado en el sistema (en el PATH).
     37 _stockfish_path() {
     38     if [[ -n "${CHESS_STOCKFISH:-}" && -x "$CHESS_STOCKFISH" ]]; then
     39         printf '%s' "$CHESS_STOCKFISH"; return
     40     fi
     41     local bundled="${SCRIPT_DIR:-.}/engine/stockfish"
     42     if [[ -x "$bundled" ]]; then printf '%s' "$bundled"; return; fi
     43     command -v stockfish 2>/dev/null
     44 }
     45 
     46 # ai_available: tenemos el motor disponible? (verdadero si _stockfish_path encontro algo).
     47 ai_available() { [[ -n "$(_stockfish_path)" ]]; }
     48 
     49 # ai_send: mandarle una orden al motor (escribir una linea en su canal de entrada).
     50 ai_send() { printf '%s\n' "$1" >&"$AI_W"; }
     51 
     52 # _ai_wait: leer lineas del motor hasta encontrar una que coincida con el patron
     53 # $1 (por ejemplo "uciok" o "bestmove *"). Devuelve esa linea, o 1 si el motor se
     54 # cerro. Sirve para "esperar" una respuesta puntual ignorando el resto.
     55 _ai_wait() {
     56     local line
     57     while IFS= read -r line <&"$AI_R"; do
     58         # shellcheck disable=SC2254
     59         case "$line" in $1) printf '%s' "$line"; return 0 ;; esac
     60     done
     61     return 1                                   # el motor cerro la comunicacion
     62 }
     63 
     64 # ai_init: arrancar el motor y configurarle la dificultad. Devuelve 1 si no hay motor.
     65 ai_init() {
     66     local sf; sf="$(_stockfish_path)"; [[ -n "$sf" ]] || return 1
     67     coproc AI { "$sf"; }                       # arrancamos Stockfish en paralelo
     68     AI_R=${AI[0]}; AI_W=${AI[1]}               # bash tambien deja el PID en AI_PID
     69     ai_send "uci"; _ai_wait "uciok" >/dev/null || return 1   # saludo UCI inicial
     70     # Configuramos la fuerza: o por Elo (si AI_ELO esta puesto) o por nivel de habilidad.
     71     if [[ -n "$AI_ELO" ]]; then
     72         ai_send "setoption name UCI_LimitStrength value true"
     73         ai_send "setoption name UCI_Elo value $AI_ELO"
     74     else
     75         ai_send "setoption name Skill Level value $AI_SKILL"
     76     fi
     77     ai_send "isready"; _ai_wait "readyok" >/dev/null || return 1   # esperamos que este listo
     78     return 0
     79 }
     80 
     81 # _parse_bestmove: de una linea "bestmove <jugada> [ponder <otra>]" sacar la
     82 # jugada (la segunda palabra). "set -- $1" parte el texto en palabras y las pone
     83 # en $1, $2, ...; nosotros queremos $2.
     84 _parse_bestmove() { set -- $1; printf '%s' "$2"; }
     85 
     86 # ai_move: la fuente de jugadas. Le pasamos al motor la posicion actual y le
     87 # pedimos que juegue; devolvemos su jugada (o nada si el motor murio, que el bucle
     88 # del juego interpreta como una salida).
     89 ai_move() {
     90     ai_send "position fen $(get_fen)"
     91     ai_send "go movetime $AI_MOVETIME"
     92     local line score=0
     93     while IFS= read -r line <&"$AI_R"; do
     94         case "$line" in
     95             "info "*"score cp "*)
     96                 local s="${line##*score cp }"; score="${s%% *}" ;;
     97             "info "*"score mate "*)
     98                 local m="${line##*score mate }"; m="${m%% *}"
     99                 (( m > 0 )) && score=9900 || score=-9900 ;;
    100             "bestmove "*)
    101                 # Normalizamos a punto de vista blancas y guardamos para la barra.
    102                 [[ "$TURN" == black ]] && score=$(( -score ))
    103                 EVAL_SCORE=$score
    104                 _parse_bestmove "$line"; return 0 ;;
    105         esac
    106     done
    107     return 1
    108 }
    109 
    110 # ai_close: apagar el motor. Le pedimos que termine ("quit") y por las dudas
    111 # matamos su proceso.
    112 ai_close() {
    113     [[ -n "$AI_W"   ]] && ai_send "quit" 2>/dev/null
    114     [[ -n "$AI_PID" ]] && kill "$AI_PID" 2>/dev/null
    115     AI_R=""; AI_W=""; AI_PID=""
    116 }
    117 
    118 # ai_pick_level: elegir la dificultad con un menu (fzf). Deja en AI_SKILL/AI_ELO/
    119 # AI_MOVETIME/AI_LABEL la configuracion. Los presets usan el "Skill Level" 0-20 de
    120 # Stockfish; "Custom" usa una fuerza en Elo.
    121 ai_pick_level() {
    122     local sel
    123     sel=$(printf '%s\n' \
    124         "Easy     beginner (Skill 1)" \
    125         "Medium   casual (Skill 6)" \
    126         "Hard     strong club (Skill 13)" \
    127         "Max      full strength" \
    128         "Custom   set an Elo (1320-3190)" \
    129       | fzf --reverse --border --height=40% --with-nth=1.. \
    130             --prompt="Difficulty: " --header="  How strong should the computer play?")
    131     # "${sel%% *}" se queda con la primera palabra de lo elegido.
    132     case "${sel%% *}" in
    133         Easy)   AI_SKILL=1;  AI_ELO=""; AI_MOVETIME=300;  AI_LABEL="Easy" ;;
    134         Hard)   AI_SKILL=13; AI_ELO=""; AI_MOVETIME=800;  AI_LABEL="Hard" ;;
    135         Max)    AI_SKILL=20; AI_ELO=""; AI_MOVETIME=1500; AI_LABEL="Max" ;;
    136         Custom) _ai_custom_elo ;;
    137         *)      AI_SKILL=6;  AI_ELO=""; AI_MOVETIME=500;  AI_LABEL="Medium" ;;   # por defecto
    138     esac
    139 }
    140 
    141 # _ai_custom_elo: preguntar una fuerza en Elo por teclado y dejarla lista. Si lo
    142 # que escriben no es un numero valido, usa 1500; y lo limita al rango que soporta
    143 # Stockfish (1320 a 3190).
    144 _ai_custom_elo() {
    145     local elo
    146     read -r -p "  Elo (1320-3190) [1500]: " elo </dev/tty
    147     [[ "$elo" =~ ^[0-9]+$ ]] || elo=1500
    148     (( elo < 1320 )) && elo=1320
    149     (( elo > 3190 )) && elo=3190
    150     AI_SKILL=""; AI_ELO=$elo; AI_MOVETIME=1000; AI_LABEL="Elo $elo"
    151 }
    152 
    153 # ============================================================================
    154 #  Analisis post-partida
    155 # ============================================================================
    156 
    157 # _ai_eval: evalua la posicion actual con Stockfish a la profundidad $1 (default 12).
    158 # Devuelve una linea "SCORE BESTMOVE", donde SCORE esta en centipawns desde el
    159 # punto de vista de las BLANCAS (positivo = blancas ganan) y BESTMOVE en notacion UCI.
    160 _ai_eval() {
    161     local depth="${1:-12}"
    162     ai_send "position fen $(get_fen)"
    163     ai_send "go depth $depth"
    164     local line score=0 bestmove="0000"
    165     while IFS= read -r line <&"$AI_R"; do
    166         case "$line" in
    167             "info "*"score cp "*)
    168                 local s="${line##*score cp }"; score="${s%% *}" ;;
    169             "info "*"score mate "*)
    170                 local m="${line##*score mate }"; m="${m%% *}"
    171                 (( m > 0 )) && score=9900 || score=-9900 ;;
    172             "bestmove "*)
    173                 bestmove=$(_parse_bestmove "$line"); break ;;
    174         esac
    175     done
    176     # Normalizamos a punto de vista blancas: SF devuelve el puntaje desde quien mueve,
    177     # asi que si mueven las negras, negamos el valor.
    178     [[ "$TURN" == black ]] && score=$(( -score ))
    179     printf '%s %s\n' "$score" "$bestmove"
    180 }
    181 
    182 # _fmt_cp: formatea centipawns como "+1.25" o "-0.50". Mate se muestra como "+M"/"−M".
    183 _fmt_cp() {
    184     local s=$1
    185     (( s >=  9900 )) && { printf '+M'; return; }
    186     (( s <= -9900 )) && { printf '-M'; return; }
    187     local sign abs
    188     (( s >= 0 )) && sign="+" || sign="-"
    189     abs=$(( s < 0 ? -s : s ))
    190     printf '%s%d.%02d' "$sign" $(( abs / 100 )) $(( abs % 100 ))
    191 }
    192 
    193 # _classify_loss: clasifica la perdida de centipawns del jugador en una etiqueta.
    194 # "loss" es positivo cuando el jugador hizo algo suboptimo.
    195 _classify_loss() {
    196     local loss=$1
    197     if   (( loss <   0 )); then printf 'Best'
    198     elif (( loss <  50 )); then printf 'Good'
    199     elif (( loss < 100 )); then printf '?! Inaccuracy'
    200     elif (( loss < 200 )); then printf '?  Mistake'
    201     else                        printf '?? Blunder'; fi
    202 }
    203 
    204 # analyze_game: analiza la partida en PGN_SAN/PGN_STARTFEN con Stockfish.
    205 # Muestra la clasificacion de cada jugada y un resumen al final.
    206 # Requiere el motor ya iniciado (ai_init). $1 = profundidad (default 12).
    207 analyze_game() {
    208     local depth="${1:-12}"
    209     local -a san=("${PGN_SAN[@]}")
    210     (( ${#san[@]} == 0 )) && { echo "  No moves to analyze."; return; }
    211 
    212     echo ""
    213     printf '  Analyzing %d moves at depth %d — this may take a moment...\n' \
    214         "${#san[@]}" "$depth"
    215     echo ""
    216     printf '  %-10s %-10s %-18s %s\n' "Move" "Played" "Assessment" "Eval"
    217     echo "  ──────────────────────────────────────────────────"
    218 
    219     POSCOUNT=(); setup_fen "$PGN_STARTFEN"; POSCOUNT["$(position_key)"]=1
    220 
    221     local score_before score_after best_uci best_san result
    222     local wb=0 wm=0 wi=0   # blunders/mistakes/inaccuracies de blancas
    223     local bb=0 bm=0 bi=0   # idem negras
    224 
    225     result=$(_ai_eval "$depth")
    226     score_before="${result%% *}"
    227 
    228     local i
    229     for (( i=0; i<${#san[@]}; i++ )); do
    230         local mover="$TURN"
    231         local san_move="${san[$i]}"
    232         best_uci="${result##* }"
    233 
    234         # Convertimos la mejor jugada a SAN antes de mover (el tablero sigue igual).
    235         if [[ "$best_uci" != "0000" ]]; then
    236             best_san=$(move_to_san "$best_uci" 2>/dev/null) || best_san="$best_uci"
    237         else
    238             best_san=""
    239         fi
    240 
    241         # Aplicamos la jugada real.
    242         local actual_uci; actual_uci=$(san_to_uci "$san_move") || { echo "  Parse error: $san_move"; break; }
    243         apply_move "$actual_uci"
    244 
    245         # Evaluamos la posicion resultante.
    246         result=$(_ai_eval "$depth")
    247         score_after="${result%% *}"
    248 
    249         # Perdida desde el punto de vista del jugador que movio:
    250         #   Blancas quieren que el puntaje SUBA; negras quieren que BAJE.
    251         local loss
    252         if [[ "$mover" == white ]]; then
    253             loss=$(( score_before - score_after ))
    254         else
    255             loss=$(( score_after - score_before ))
    256         fi
    257 
    258         local label; label=$(_classify_loss "$loss")
    259         local eval_str; eval_str=$(_fmt_cp "$score_after")
    260 
    261         # Numero de jugada para mostrar.
    262         local movenum=$(( i / 2 + 1 ))
    263         local who; [[ "$mover" == white ]] && who="${movenum}." || who="${movenum}..."
    264 
    265         # Sugerencia de la mejor jugada (solo si el movimiento fue suboptimo y distinto).
    266         local hint=""
    267         [[ "$loss" -ge 50 && -n "$best_san" && "$best_san" != "$san_move" ]] && hint="  (best: $best_san)"
    268 
    269         printf '  %-10s %-10s %-18s %s%s\n' "$who" "$san_move" "$label" "$eval_str" "$hint"
    270 
    271         # Contadores de errores.
    272         case "$label" in
    273             *Blunder*)    [[ "$mover" == white ]] && (( wb++ )) || (( bb++ )) ;;
    274             *Mistake*)    [[ "$mover" == white ]] && (( wm++ )) || (( bm++ )) ;;
    275             *Inaccuracy*) [[ "$mover" == white ]] && (( wi++ )) || (( bi++ )) ;;
    276         esac
    277 
    278         score_before=$score_after
    279     done
    280 
    281     echo "  ──────────────────────────────────────────────────"
    282     printf '  White: %d blunder(s), %d mistake(s), %d inaccuracy(ies)\n' "$wb" "$wm" "$wi"
    283     printf '  Black: %d blunder(s), %d mistake(s), %d inaccuracy(ies)\n' "$bb" "$bm" "$bi"
    284     echo ""
    285 }
    286 
    287 # ai_offer_analysis: al final de la partida, le ofrece al jugador un analisis
    288 # post-partida. Arranca Stockfish si no esta corriendo. Llama analyze_game.
    289 ai_offer_analysis() {
    290     (( ${#PGN_SAN[@]} == 0 )) && return
    291     ai_available || return
    292 
    293     local ans
    294     ans=$(printf '%s\n' \
    295         "yes  Analyze this game with Stockfish (depth 12)" \
    296         "no   No thanks" \
    297       | fzf --reverse --border --height=25% --with-nth=1.. \
    298             --prompt="Analysis: " --header="  Game over — would you like a post-game analysis?")
    299     [[ "${ans%% *}" != yes ]] && return
    300 
    301     local need_close=0
    302     if [[ -z "${AI_PID:-}" ]]; then
    303         printf '  Starting Stockfish...\n'
    304         ai_init || { echo "  Could not start Stockfish."; return; }
    305         need_close=1
    306     fi
    307 
    308     analyze_game 12
    309 
    310     (( need_close )) && ai_close
    311     read -r -p "  Press Enter to exit..." _ </dev/tty
    312 }
    313 
    314 # ai_pick_spectate_speed: elegir la velocidad de avance en modo espectador. Deja
    315 # AI_SPECTATE_DELAY en segundos, o en "step" para avance manual con cualquier tecla.
    316 ai_pick_spectate_speed() {
    317     local sel
    318     sel=$(printf '%s\n' \
    319         "fast    ~0.3s between moves" \
    320         "normal  ~1s between moves" \
    321         "slow    ~2s between moves" \
    322         "step    press any key to advance" \
    323       | fzf --reverse --border --height=35% --with-nth=1.. \
    324             --prompt="Speed: " --header="  How fast should moves be shown?")
    325     case "${sel%% *}" in
    326         fast) AI_SPECTATE_DELAY=0 ;;
    327         slow) AI_SPECTATE_DELAY=2 ;;
    328         step) AI_SPECTATE_DELAY=step ;;
    329         *)    AI_SPECTATE_DELAY=1 ;;
    330     esac
    331 }
    332 
    333 # ai_pick_color: elegir de que color juega la persona (con un menu). Deja
    334 # LOCAL_COLOR (vos) y AI_COLOR (la maquina, el color contrario).
    335 ai_pick_color() {
    336     local sel
    337     sel=$(printf '%s\n' \
    338         "White    you move first" \
    339         "Black    the computer moves first" \
    340         "Random   let the coin decide" \
    341       | fzf --reverse --border --height=30% --with-nth=1.. \
    342             --prompt="Your color: " --header="  Which side do you play?")
    343     case "${sel%% *}" in
    344         Black)  LOCAL_COLOR=black ;;
    345         Random) (( RANDOM % 2 )) && LOCAL_COLOR=white || LOCAL_COLOR=black ;;   # moneda al aire
    346         *)      LOCAL_COLOR=white ;;
    347     esac
    348     AI_COLOR=$(other_color "$LOCAL_COLOR")
    349 }