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 }