ches.sh (30311B)
1 #!/usr/bin/env bash 2 # ============================================================================ 3 # ches.sh -- El PUNTO DE ENTRADA del juego de ajedrez en bash. 4 # ============================================================================ 5 # 6 # Este es el archivo que se ejecuta para jugar (./ches.sh). Es "delgado": casi no 7 # tiene logica propia. Lo que hace es: cargar las librerias de lib/, preparar el 8 # modo de juego elegido, y correr el bucle principal de la partida (la funcion 9 # play()). Toda la inteligencia esta repartida en lib/. 10 # 11 # El motor de reglas no sabe de donde viene cada jugada: puede venir del selector 12 # visual (fzf), de otra persona por red, o de la computadora (Stockfish). Eso es 13 # lo que mantiene la red y la IA separadas de las reglas. Ver lib/ para el detalle. 14 # 15 # Formas de usarlo: 16 # ./ches.sh abre el menu interactivo 17 # ./ches.sh hotseat dos personas en esta misma compu 18 # ./ches.sh ai contra la computadora (necesita Stockfish) 19 # ./ches.sh host <port> hacer de servidor (jugas blancas) 20 # ./ches.sh join <host> <port> unirse a una partida (jugas negras) 21 # ./ches.sh server <host> <port> jugar via un relay (ver bin/chess-server.sh) 22 # ./ches.sh resume <file> retomar una partida guardada (ver saves/) 23 # ./ches.sh fen "<FEN>" empezar desde un FEN pegado 24 # ./ches.sh pgn <file> reproducir/seguir una partida desde un PGN 25 # ./ches.sh rejoin <host> <port> reconectarse a una partida en curso 26 # ./ches.sh --selftest correr las pruebas automaticas 27 # ./ches.sh --help mostrar esta ayuda 28 29 # Averiguamos la carpeta donde vive este archivo (ver explicacion en 30 # bin/get-stockfish.sh). La usamos para cargar las librerias con rutas absolutas. 31 SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" 32 # "source" carga otro archivo de bash dentro de este, como si lo pegaramos aca. 33 # El ORDEN importa: cada uno usa cosas de los anteriores. 34 source "$SCRIPT_DIR/lib/board.sh" # el tablero y las coordenadas 35 source "$SCRIPT_DIR/lib/movegen.sh" # jugadas posibles de cada pieza 36 source "$SCRIPT_DIR/lib/rules.sh" # reglas, jaques, fin de partida 37 source "$SCRIPT_DIR/lib/notation.sh" # traduccion a notacion SAN (y PGN) 38 source "$SCRIPT_DIR/lib/ai.sh" # la computadora (Stockfish) 39 source "$SCRIPT_DIR/lib/ui.sh" # dibujar el tablero y los menus 40 source "$SCRIPT_DIR/lib/protocol.sh" # mensajes y conexion de red (bajo nivel) 41 source "$SCRIPT_DIR/lib/transport.sh" # de donde viene cada jugada + la red 42 source "$SCRIPT_DIR/lib/saves.sh" # guardar/cargar/importar/exportar 43 44 # Ruta absoluta a este script. La vista previa del selector (en ui.sh) la necesita 45 # para volver a llamarse a si misma. "export" hace que la vean los subprocesos. 46 export CHESS_BIN="$(readlink -f "$0")" 47 48 # Carpeta donde se guardan y se leen las partidas pausadas. 49 SAVE_DIR="$SCRIPT_DIR/saves" 50 51 # Cargamos la configuracion de ayudas visuales (SHOW_EVAL_BAR, SHOW_OPENING). 52 # Tiene que ir DESPUES de fijar SAVE_DIR porque _SETTINGS_FILE depende de el. 53 settings_load 54 55 # usage: muestra la ayuda. Truco: en vez de repetir el texto, lo SACA del propio 56 # archivo. "sed -n '4,20p'" imprime las lineas 4 a 20 (el bloque de comentarios de 57 # arriba), y el segundo "sed" les saca el "# " del principio. 58 usage() { sed -n '15,27p' "$CHESS_BIN" | sed 's/^# \{0,1\}//'; } 59 60 # --- Historial de jugadas (lo que se muestra al costado del tablero) -------- 61 HIST_FULLMOVE=0 # numero de jugada completa actual 62 HIST_LINE="" # jugada de las blancas pendiente (cuando todavia no movieron las negras) 63 # record_history: anota una jugada en el historial. Las blancas dejan su jugada 64 # "pendiente" (con "..." donde iran las negras); cuando mueven las negras se 65 # completa la linea y se pasa al siguiente numero. 66 record_history() { 67 local mover=$1 mv=$2 68 if [[ "$mover" == white ]]; then 69 HIST_LINE="$mv" 70 HISTORY[$HIST_FULLMOVE]="$HIST_LINE ..." 71 else 72 HISTORY[$HIST_FULLMOVE]="${HIST_LINE:-...} $mv" 73 ((HIST_FULLMOVE++)) 74 HIST_LINE="" 75 fi 76 } 77 78 # --- Retroceso (takeback): "fotos" del estado, una por jugada --------------- 79 # UNDO_STACK es una PILA: una lista donde apilamos una "foto" del estado ANTES de 80 # cada jugada. Para deshacer, sacamos la ultima foto y volvemos a ella. Una foto 81 # guarda: la posicion, los dos relojes, y las variables del historial. (La cuenta 82 # de repeticiones POSCOUNT se maneja aparte, en do_takeback.) Solo se usa si los 83 # retrocesos estan permitidos. 84 declare -a UNDO_STACK=() 85 86 # Registro PGN en vivo (la lista de jugadas en SAN y desde que posicion se 87 # numeran). Se mantiene al dia y se guarda/serializa via saves.sh. 88 declare -a PGN_SAN=() 89 declare -a PGN_UCI=() # las mismas jugadas en notacion UCI (para detectar aperturas) 90 PGN_STARTFEN="$START_FEN" 91 92 # _pgn_result: traduce el resultado interno de la partida al simbolo de resultado 93 # del PGN (desde el punto de vista de las blancas): "1-0" gana blancas, "0-1" gana 94 # negras, "1/2-1/2" tablas, "*" sin terminar. 95 _pgn_result() { 96 case "$1" in 97 checkmate) [[ "$TURN" == white ]] && echo "0-1" || echo "1-0" ;; 98 resign-white|timeout-white) echo "0-1" ;; 99 resign-black|timeout-black) echo "1-0" ;; 100 draw-*|stalemate) echo "1/2-1/2" ;; 101 *) echo "*" ;; 102 esac 103 } 104 105 # _pgn_autosave: va guardando la partida en saves/.current.pgn a medida que se 106 # juega (asi queda en disco como PGN, y sirve de respaldo ante un corte). Por 107 # defecto el resultado es "*" (en curso). 108 _pgn_autosave() { 109 export_pgn "${SAVE_DIR:-saves}/.current.pgn" "${1:-*}" 2>/dev/null || true 110 } 111 112 # _snapshot_push: apila una "foto" del estado actual (antes de mover). Junta todo 113 # en un texto separado por ";", con las listas pegadas por "|". 114 _snapshot_push() { 115 local h p; local IFS='|'; h="${HISTORY[*]}"; p="${PGN_SAN[*]}" 116 UNDO_STACK+=( "$(get_fen);$CLOCK_white;$CLOCK_black;$HIST_FULLMOVE;$HIST_LINE;$h;$p" ) 117 } 118 119 # do_takeback: deshace la ultima jugada. Saca la ultima foto de la pila y restaura 120 # el estado, dejando el turno de quien acababa de mover. Le resta 1 a la cuenta de 121 # repeticiones de la posicion que abandonamos. Devuelve 1 (no hace nada) si la 122 # pila esta vacia. 123 do_takeback() { 124 (( ${#UNDO_STACK[@]} == 0 )) && return 1 125 local curkey; curkey=$(position_key) # la posicion que estamos dejando 126 if [[ -n "${POSCOUNT[$curkey]:-}" ]]; then 127 POSCOUNT[$curkey]=$(( POSCOUNT[$curkey] - 1 )) 128 (( POSCOUNT[$curkey] <= 0 )) && unset 'POSCOUNT[$curkey]' 129 fi 130 # "${UNDO_STACK[-1]}" es el ultimo elemento; "unset ... [-1]" lo saca de la pila. 131 local snap="${UNDO_STACK[-1]}"; unset 'UNDO_STACK[-1]' 132 local fen cw cb hfm hline h p 133 IFS=';' read -r fen cw cb hfm hline h p <<< "$snap" # partimos la foto por ";" 134 setup_fen "$fen" 135 CLOCK_white=$cw; CLOCK_black=$cb 136 HIST_FULLMOVE=$hfm; HIST_LINE="$hline" 137 HISTORY=(); [[ -n "$h" ]] && IFS='|' read -r -a HISTORY <<< "$h" 138 PGN_SAN=(); [[ -n "$p" ]] && IFS='|' read -r -a PGN_SAN <<< "$p" 139 PGN_UCI=("${PGN_UCI[@]:0:${#PGN_SAN[@]}}") # recortamos UCI para que coincida con SAN 140 return 0 141 } 142 143 # --- play(): EL BUCLE PRINCIPAL de la partida ------------------------------- 144 # Aca pasa toda la partida: mientras no termine, pide la jugada a quien 145 # corresponda, la aplica, y repite. Es el corazon que ata todo lo demas. 146 play() { 147 # Arrancamos (o retomamos) desde el estado que dejo listo el transporte: la 148 # posicion inicial para una partida nueva, o una guardada/recibida si se retoma. 149 adopt_game_state 150 151 local result="" 152 while true; do 153 # Primero: la partida ya termino? (mate, ahogado, tablas automaticas...) 154 local st; st=$(game_status) 155 if [[ "$st" != ongoing ]]; then result="$st"; break; fi 156 157 # Medimos el tiempo del que mueve DESDE ACA (el proceso principal): la 158 # fuente de jugadas corre dentro de $( ) y un subproceso no puede cambiar el 159 # reloj del padre. Por eso usamos "SECONDS" (un contador de segundos de 160 # bash) alrededor de la llamada y actualizamos CLOCK_* aca. 161 local mover=$TURN mv t0=$SECONDS 162 if transport_is_local_turn; then 163 # La vista previa del selector ES el tablero, asi que no dibujamos uno 164 # fijo. Limpiamos la pantalla por las dudas: si fzf no puede usar la 165 # "pantalla alternativa", dibuja en linea y quedaria el tablero de 166 # "Waiting..." arriba. Limpiando, garantizamos uno solo. 167 clear 168 mv=$(transport_local_move) 169 elif [[ "$TRANSPORT_MODE" == aivai ]]; then 170 # Modo espectador: Stockfish mueve por los dos bandos. Mostramos el tablero 171 # y esperamos el tiempo elegido (o una tecla en modo "step") antes de pedir 172 # la siguiente jugada. La tecla 'q' termina la partida en cualquier momento. 173 render_board 174 echo " Stockfish (${TURN^^}) thinking..." 175 if [[ "$AI_SPECTATE_DELAY" == step ]]; then 176 printf ' Press any key for next move, q to quit...' >/dev/tty 177 local _sk; read -r -N1 _sk </dev/tty 2>/dev/null 178 echo 179 [[ "$_sk" == q || "$_sk" == Q ]] && { result="quit"; break; } 180 elif (( AI_SPECTATE_DELAY > 0 )); then 181 local _sk 182 if read -r -t "$AI_SPECTATE_DELAY" -N1 _sk </dev/tty 2>/dev/null; then 183 [[ "$_sk" == q || "$_sk" == Q ]] && { result="quit"; break; } 184 fi 185 fi 186 mv=$(ai_move) 187 elif [[ "$TRANSPORT_MODE" == ai ]]; then 188 # La computadora es la fuente de jugadas. Su tiempo de pensar se le 189 # cobra a su reloj con la medicion de SECONDS de mas abajo, igual que a 190 # una persona. 191 render_board 192 echo " Computer is thinking..." 193 mv=$(ai_move) 194 else 195 # La pantalla de espera es interactiva: transport_recv_move hace andar 196 # el reloj y mira el teclado (asi una partida sin tiempo igual puede 197 # gatillar una reconexion manual: una caida dura no produce EOF). 198 render_board 199 local rt="" 200 if (( TC_ENABLED )); then 201 [[ "$mover" == white ]] && rt=$(( CLOCK_white + 2 )) || rt=$(( CLOCK_black + 2 )) 202 fi 203 mv=$(transport_recv_move "$rt") 204 echo 205 fi 206 207 # "mv" trae la jugada, o una palabra de control. Decidimos que hacer: 208 case "$mv" in 209 ""|QUIT) result="quit"; break ;; # salir 210 RESIGN) result="resign-$mover"; break ;; # rendirse 211 CLAIM50) if can_claim_threefold; then result="draw-threefold" 212 else result="draw-50"; fi; break ;; # reclamar tablas 213 TIMEOUT) result="timeout-$mover"; break ;; # se acabo el tiempo 214 DRAWAGREE) result="draw-agreement"; break ;; # tablas acordadas 215 TAKEBACK) do_takeback # deshacer la ultima jugada 216 # contra la maquina, deshacemos tambien SU respuesta, 217 # asi te toca mover de nuevo a vos. 218 [[ "$TRANSPORT_MODE" == ai ]] && do_takeback 219 continue ;; 220 # Reconexion: la maneja el proceso principal (no un subproceso) porque 221 # es el duenio de la pantalla. "continue" vuelve a empezar el bucle. 222 RECONNECT) transport_reconnect auto || { result="quit"; break; }; continue ;; 223 RECONNECT-MANUAL) transport_reconnect manual || { result="quit"; break; }; continue ;; 224 esac 225 226 # Sacamos la "foto" para poder deshacer esta jugada. La tomamos ANTES de 227 # cobrar el tiempo, asi al deshacer tambien se devuelve el tiempo gastado. 228 _snapshot_push 229 230 # Cobramos el tiempo usado y sumamos el incremento; si se acaba aca, fin. 231 if (( TC_ENABLED )); then 232 local elapsed=$(( SECONDS - t0 )) 233 # "local -n clk=..." hace que "clk" sea un APODO (referencia) de la 234 # variable CLOCK_white o CLOCK_black, segun quien mueva. Asi modificamos 235 # el reloj correcto sin repetir codigo. 236 local -n clk="CLOCK_$mover" 237 clk=$(( clk - elapsed )) 238 if (( clk <= 0 )); then result="timeout-$mover"; break; fi 239 clk=$(( clk + TC_INC )) 240 fi 241 242 PGN_SAN+=( "$(move_to_san "$mv")" ) # anotamos la jugada en SAN (antes de aplicarla) 243 PGN_UCI+=( "$mv" ) # idem en UCI (para la deteccion de apertura) 244 apply_move "$mv" 245 record_history "$mover" "$mv" 246 _pgn_autosave # guardamos el PGN en curso 247 done 248 249 _pgn_autosave "$(_pgn_result "$result")" # PGN final, ya con el resultado real 250 251 # Mostramos el tablero final y el mensaje de como termino la partida. 252 render_board 253 case "$result" in 254 checkmate) echo " Game over: checkmate.";; 255 draw-50) echo " Game over: draw claimed (50-move rule).";; 256 draw-threefold) echo " Game over: draw claimed (threefold repetition).";; 257 draw-fivefold) echo " Game over: draw — fivefold repetition.";; 258 draw-75move) echo " Game over: draw — 75-move rule.";; 259 draw-agreement) echo " Game over: draw agreed.";; 260 stalemate|draw-*) echo " Game over: draw.";; 261 resign-*) local who=${result#resign-} winner 262 winner=$(other_color "$who") 263 echo " ${who^^} resigned. ${winner^^} wins.";; 264 timeout-*) local who=${result#timeout-} winner 265 winner=$(other_color "$who") 266 echo " ${who^^} lost on time. ${winner^^} wins.";; 267 quit) echo " Game ended.";; 268 esac 269 echo " Game recorded to ${SAVE_DIR}/.current.pgn" 270 transport_close # cerramos red / motor si los habia 271 272 # Solo ofrecemos analisis si termino una partida real (no una salida abrupta). 273 [[ "$result" == quit ]] || ai_offer_analysis 274 } 275 276 # clock_ticker: actualizador del reloj en vivo del selector. Lo lanza fzf en 277 # segundo plano al abrirse (por eso hereda $FZF_PORT, el puerto de control de fzf). 278 # Cada segundo le manda a fzf un nuevo encabezado con el tiempo. Cuando se acaba, 279 # deja la senial (sentinel) y le dice a fzf que se cierre. Termina cuando fzf ya no 280 # esta (el envio con curl falla). 281 clock_ticker() { 282 local port=$1 283 local deadline=${CHESS_DEADLINE:-0} active=${CHESS_ACTIVE:-white} 284 local sw=${CHESS_CLK_W:-0} sb=${CHESS_CLK_B:-0} sent=${CHESS_SENTINEL:-} 285 local now rem w b hdr 286 while :; do 287 now=$(date +%s); rem=$(( deadline - now )) 288 if [[ "$active" == white ]]; then w=$rem; b=$sb; else w=$sw; b=$rem; fi 289 if (( rem <= 0 )); then 290 [[ -n "$sent" ]] && : > "$sent" # ": > archivo" crea/vacia el archivo (la senial) 291 curl -s -XPOST "localhost:$port" -d "abort" >/dev/null 2>&1 292 return 293 fi 294 hdr=$(_clock_str "$w" "$b" "$active") 295 # "curl" le habla al servidor de control de fzf para cambiar el encabezado. 296 curl -s -XPOST "localhost:$port" -d "change-header($hdr)" >/dev/null 2>&1 || return 297 sleep 1 298 done 299 } 300 301 # _flush_tty: descarta lo que haya quedado tipeado en el teclado. Sin esto, el 302 # Enter que confirma un fzf (o que termina un texto pegado) puede "colarse" en el 303 # SIGUIENTE fzf y elegir su opcion resaltada sin querer. Es seguro llamarlo entre 304 # menus: ahi la persona no esta tipeando nada que nos importe. 305 _flush_tty() { read -r -t 0.1 -N 100000 _ </dev/tty 2>/dev/null || true; } 306 307 # pick_resume_source: elegir desde donde retomar: una partida guardada en saves/ 308 # (mas nuevas primero), o un PGN/FEN pegado. Deja RESUME_FILE, RESUME_PGN o 309 # RESUME_FEN. Devuelve 1 si la persona cancela. 310 pick_resume_source() { 311 RESUME_FILE=""; RESUME_FEN=""; RESUME_PGN="" 312 local paste_fen="✎ Paste a FEN string…" 313 local paste_pgn="✎ Paste a PGN game…" 314 local src 315 _flush_tty 316 # Armamos la lista: las dos opciones de "pegar" arriba, despues los archivos de 317 # saves/ ordenados por fecha (ls -1t = uno por linea, mas nuevo primero). 318 src=$( { printf '%s\n%s\n' "$paste_pgn" "$paste_fen" 319 [[ -d "$SAVE_DIR" ]] && (cd "$SAVE_DIR" && ls -1t 2>/dev/null); } \ 320 | fzf --reverse --border --height=60% --prompt="Resume from: " \ 321 --header=" Pick a saved game, or paste a PGN game / FEN position") 322 [[ -z "$src" ]] && return 1 323 if [[ "$src" == "$paste_pgn" ]]; then 324 printf ' Paste the PGN, then press Enter and Ctrl-D:\n' >/dev/tty 325 local pgn; pgn=$(cat </dev/tty) # "cat </dev/tty" lee todo hasta Ctrl-D 326 [[ -z "$pgn" ]] && return 1 327 if ! load_pgn_string "$pgn"; then 328 printf ' Could not parse that PGN.\n' >/dev/tty; sleep 1; return 1 329 fi 330 RESUME_PGN="$pgn" 331 elif [[ "$src" == "$paste_fen" ]]; then 332 printf ' Paste FEN: ' >/dev/tty 333 local fen; read -r fen </dev/tty 334 [[ -z "$fen" ]] && return 1 335 if ! fen_looks_valid "$fen"; then 336 printf ' That does not look like a FEN.\n' >/dev/tty; sleep 1; return 1 337 fi 338 RESUME_FEN="$fen" 339 else 340 RESUME_FILE="$SAVE_DIR/$src" # eligio un archivo guardado 341 fi 342 return 0 343 } 344 345 # puzzle_mode: muestra puzzles del archivo puzzles.tsv uno por uno. El jugador 346 # elige su jugada con el selector normal; el juego le dice si acerto o fallo y 347 # muestra la solucion. Con fzf elige cual puzzle jugar. 348 puzzle_mode() { 349 local pfile="${SCRIPT_DIR}/puzzles.tsv" 350 [[ -f "$pfile" ]] || { echo "No puzzle file found: $pfile" >&2; return 1; } 351 352 # Construimos la lista de puzzles para el selector. 353 local -a names=() fens=() solutions=() 354 local name fen sol 355 while IFS=$'\t' read -r name fen sol; do 356 [[ "$name" == \#* || -z "$name" ]] && continue 357 names+=("$name"); fens+=("$fen"); solutions+=("$sol") 358 done < "$pfile" 359 360 (( ${#names[@]} == 0 )) && { echo "No puzzles found." >&2; return 1; } 361 362 while true; do 363 # Selector de puzzle. 364 local pick idx 365 pick=$(printf '%s\n' "${names[@]}" \ 366 | fzf --reverse --border --height=60% \ 367 --prompt="Puzzle: " \ 368 --header=$' Choose a puzzle (Esc to exit)\n') 369 [[ -z "$pick" ]] && return 0 370 371 # Buscamos el indice del puzzle elegido. 372 for (( idx=0; idx<${#names[@]}; idx++ )); do 373 [[ "${names[$idx]}" == "$pick" ]] && break 374 done 375 376 # Cargamos la posicion del puzzle. 377 setup_fen "${fens[$idx]}" 378 HISTORY=(); HIST_FULLMOVE=0; HIST_LINE="" 379 ALLOW_TAKEBACK=0; UNDO_STACK=() 380 TC_ENABLED=0; CLOCK_white=0; CLOCK_black=0 381 POSCOUNT=(); POSCOUNT["$(position_key)"]=1 382 PGN_SAN=(); PGN_UCI=(); PGN_STARTFEN="${fens[$idx]}" 383 export CHESS_FEN="${fens[$idx]}" 384 385 # Extraemos las jugadas de la solucion (pueden ser varias: jugada + respuesta). 386 local -a sol_moves=() 387 read -r -a sol_moves <<< "${solutions[$idx]}" 388 local step=0 failed=0 389 390 while (( step < ${#sol_moves[@]} )); do 391 local expected="${sol_moves[$step]}" 392 local is_player_turn=1 393 # Los pasos pares (0, 2, 4…) son del jugador; los impares son la respuesta del rival. 394 (( step % 2 == 1 )) && is_player_turn=0 395 396 if (( is_player_turn )); then 397 # Turno del jugador: usa el selector normal. 398 clear 399 HL_TITLE="Puzzle: ${names[$idx]} — find the best move!" 400 _draw_board; echo "" 401 local mv; mv=$(ui_pick_move) 402 case "$mv" in 403 ""|QUIT|RESIGN) failed=2; break ;; # 2 = salio 404 esac 405 406 # Comparamos la jugada elegida con la esperada (solo origen+destino). 407 if [[ "${mv:0:4}" == "${expected:0:4}" ]]; then 408 PGN_SAN+=("$(move_to_san "$mv")"); PGN_UCI+=("$mv") 409 apply_move "$mv" 410 record_history "$TURN" "$mv" 411 export CHESS_FEN="$(get_fen)" 412 (( step++ )) 413 else 414 failed=1; break 415 fi 416 else 417 # Respuesta del rival (la hacemos automaticamente). 418 render_board 419 echo " Opponent plays ${expected}..." 420 sleep 0.8 421 PGN_SAN+=("$(move_to_san "$expected")"); PGN_UCI+=("$expected") 422 apply_move "$expected" 423 record_history "$TURN" "$expected" 424 export CHESS_FEN="$(get_fen)" 425 (( step++ )) 426 fi 427 done 428 429 # Resultado. 430 render_board 431 case "$failed" in 432 0) echo " Correct! Well done." ;; 433 1) echo " Not quite. The solution was: ${solutions[$idx]}" ;; 434 2) ;; # salio sin resolver 435 esac 436 [[ "$failed" != 2 ]] && read -r -p " Press Enter to continue..." _ </dev/tty 437 done 438 } 439 440 # pgn_viewer: visor paso a paso de una partida PGN. Navega con flechas derecha/izquierda 441 # (o j/k), q para salir. Guarda un snapshot del tablero en cada jugada para poder 442 # ir para atras sin recalcular. 443 pgn_viewer() { 444 local file=$1 445 local pgn_text; pgn_text="$(cat "$file")" 446 load_pgn_string "$pgn_text" || { echo "Could not parse PGN: $file" >&2; return 1; } 447 448 # Guardamos la lista de jugadas en UCI (ya aplicadas por load_pgn_string en GAME_PGN) 449 # y el FEN de inicio. Luego reproducimos desde el principio guardando snapshots. 450 local -a uci_moves=() 451 local startfen="$GAME_STARTFEN" 452 local total="${#GAME_PGN[@]}" 453 454 # Para pasar de SAN a UCI necesitamos reproducir la partida y anotar las jugadas. 455 POSCOUNT=(); setup_fen "$startfen"; POSCOUNT["$(position_key)"]=1 456 local i 457 for (( i=0; i<total; i++ )); do 458 local uci; uci=$(san_to_uci "${GAME_PGN[$i]}") || { echo "Parse error at move $i" >&2; return 1; } 459 uci_moves+=("$uci") 460 apply_move "$uci" 461 done 462 463 # Ahora construimos los snapshots: un array de FENs, uno por posicion (0 = inicio). 464 local -a snaps=() 465 POSCOUNT=(); setup_fen "$startfen"; POSCOUNT["$(position_key)"]=1 466 snaps+=("$(get_fen)") 467 for (( i=0; i<total; i++ )); do 468 apply_move "${uci_moves[$i]}" 469 snaps+=("$(get_fen)") 470 done 471 472 # Bucle de navegacion. 473 local pos=0 # posicion actual (0 = antes de la primera jugada) 474 settings_load 475 while true; do 476 setup_fen "${snaps[$pos]}" 477 # Titulo: numero de jugada y jugada jugada. 478 if (( pos == 0 )); then 479 HL_TITLE="Start position (← → to navigate, q to quit)" 480 else 481 local mn=$(( (pos + 1) / 2 )) color 482 (( pos % 2 == 1 )) && color="White" || color="Black" 483 HL_TITLE="Move ${mn} (${color}): ${GAME_PGN[$((pos-1))]} [${pos}/${total}] (← → / q)" 484 fi 485 render_board 486 487 # Leemos una tecla. Las flechas mandan 3 bytes: ESC [ A/B/C/D. 488 local key esc bracket dir 489 IFS= read -r -s -N1 key </dev/tty 2>/dev/null 490 if [[ "$key" == $'\x1b' ]]; then 491 IFS= read -r -s -N1 -t 0.1 bracket </dev/tty 2>/dev/null 492 IFS= read -r -s -N1 -t 0.1 dir </dev/tty 2>/dev/null 493 if [[ "$bracket" == '[' ]]; then 494 case "$dir" in 495 C) key="right" ;; # flecha derecha 496 D) key="left" ;; # flecha izquierda 497 *) key="" ;; 498 esac 499 else 500 key="" # ESC solo = ignorar 501 fi 502 fi 503 504 case "$key" in 505 right|l|' ') (( pos < total )) && (( pos++ )) ;; 506 left|h) (( pos > 0 )) && (( pos-- )) ;; 507 q|Q) break ;; 508 esac 509 done 510 HL_TITLE="" 511 return 0 512 } 513 514 # main_menu: el menu interactivo que aparece al correr ./ches.sh sin argumentos. La 515 # primera palabra de cada opcion es el modo al que mapea; al elegir uno, pide los 516 # datos que falten (host/puerto) y vuelve a llamar a main(). Repite hasta que se 517 # elige un modo jugable o se sale. 518 main_menu() { 519 # Dos ayuditas para preguntar puerto y direccion por teclado. 520 ask_port() { local p; read -r -p " Port [9000]: " p </dev/tty; echo "${p:-9000}"; } 521 ask_host() { local h; read -r -p " ${1:-Host} address: " h </dev/tty; echo "$h"; } 522 523 while true; do 524 local choice port host 525 choice=$(printf '%s\n' \ 526 "hotseat Two players at this terminal" \ 527 "ai Play vs the computer (Stockfish)" \ 528 "watch Watch the computer play itself (AI vs AI)" \ 529 "puzzle Solve tactical puzzles" \ 530 "host Host a LAN game (peer-to-peer, you play White)" \ 531 "join Join a peer's LAN game (you play Black)" \ 532 "server Play via a LAN relay server" \ 533 "rejoin Reconnect to a game in progress (after a drop)" \ 534 "resume Resume a saved game" \ 535 "theme Change board theme (${CHESS_THEME})" \ 536 "aids Toggle visual aids (eval bar, opening name)" \ 537 "selftest Run the engine self-test" \ 538 "help Show command-line usage" \ 539 "quit Exit" \ 540 | fzf --reverse --border --with-nth=1.. --height=100% \ 541 --prompt="Select: " \ 542 --header=$' ♚ B A S H C H E S S ♚\n (or pass hotseat/host/join/server on the command line)\n') 543 544 # "${choice%% *}" se queda con la primera palabra (el modo elegido). 545 case "${choice%% *}" in 546 hotseat) main hotseat; return ;; 547 ai) main ai; return ;; 548 watch) main watch; return ;; 549 puzzle) puzzle_mode; continue ;; 550 host) port=$(ask_port); main host "$port"; return ;; 551 join) host=$(ask_host); [[ -z "$host" ]] && continue 552 port=$(ask_port); main join "$host" "$port"; return ;; 553 server) host=$(ask_host "Relay server"); [[ -z "$host" ]] && continue 554 port=$(ask_port); main server "$host" "$port"; return ;; 555 rejoin) host=$(ask_host "Survivor's"); [[ -z "$host" ]] && continue 556 port=$(ask_port); main rejoin "$host" "$port"; return ;; 557 theme) ui_pick_theme; continue ;; 558 aids) ui_settings_menu; continue ;; 559 resume) pick_resume_source || continue 560 # Encabezado lindo segun de donde se retoma. 561 local rmode rhdr="${RESUME_PGN:+pasted PGN game}" 562 rhdr="${rhdr:-${RESUME_FEN:-${RESUME_FILE##*/}}}" 563 _flush_tty 564 rmode=$(printf '%s\n' \ 565 "hotseat Continue at this terminal" \ 566 "host Continue over LAN (you host as White)" \ 567 | fzf --reverse --border --height=40% --with-nth=1.. \ 568 --prompt="Resume how: " --header=" ${rhdr}") 569 case "${rmode%% *}" in 570 hotseat) main hotseat; return ;; 571 host) port=$(ask_port); main host "$port"; return ;; 572 *) RESUME_FILE=""; RESUME_FEN=""; RESUME_PGN=""; continue ;; 573 esac ;; 574 selftest) main --selftest ;; # sale via run_selftest 575 help) usage; echo; read -r -p " Press Enter to return to the menu..." _ </dev/tty ;; 576 quit|"") echo " Bye."; exit 0 ;; 577 esac 578 done 579 } 580 581 # main: el "despachante". Mira el primer argumento y arranca lo que corresponda. 582 main() { 583 # "${1:-menu}" usa el 1er argumento, o "menu" si no hay ninguno. 584 case "${1:-menu}" in 585 menu) 586 main_menu; exit 0 587 ;; 588 --selftest) 589 source "$SCRIPT_DIR/lib/selftest.sh" 590 run_selftest; exit $? 591 ;; 592 # Subcomandos OCULTOS: dibujar la vista previa del tablero desde $CHESS_FEN. 593 # Los llama fzf por detras (ver ui.sh); no son para usar a mano. 594 __ppiece) setup_fen "$CHESS_FEN"; ui_preview_piece "$2"; exit 0 ;; 595 __pmove) setup_fen "$CHESS_FEN"; ui_preview_move "$2"; exit 0 ;; 596 # Subcomando oculto: el actualizador del reloj en vivo del selector (ver ui.sh). 597 __clocktick) clock_ticker "$2"; exit 0 ;; 598 --help|-h) usage; exit 0 ;; 599 # Los modos de juego. Algunos validan que vengan los argumentos necesarios 600 # ("$2"/"$3"); si no, muestran la ayuda y salen con error. 601 hotseat) transport_init hotseat ;; 602 ai) transport_init ai ;; 603 watch) transport_init aivai ;; 604 puzzle) puzzle_mode; exit $? ;; 605 host) [[ -n "$2" ]] || { usage; exit 1; }; transport_init host "$2" ;; 606 join) [[ -n "$3" ]] || { usage; exit 1; }; transport_init join "$2" "$3" ;; 607 rejoin) [[ -n "$3" ]] || { usage; exit 1; }; transport_init rejoin "$2" "$3" ;; 608 server) [[ -n "$3" ]] || { usage; exit 1; }; transport_init server "$2" "$3" ;; 609 resume) [[ -n "$2" ]] || { usage; exit 1; } 610 [[ -f "$2" ]] || { echo "No such save file: $2" >&2; exit 1; } 611 RESUME_FILE="$2"; transport_init hotseat ;; 612 fen) [[ -n "$2" ]] || { usage; exit 1; } 613 fen_looks_valid "$2" || { echo "Not a valid FEN: $2" >&2; exit 1; } 614 RESUME_FEN="$2"; transport_init hotseat ;; 615 pgn) [[ -n "$2" ]] || { usage; exit 1; } 616 [[ -f "$2" ]] || { echo "No such PGN file: $2" >&2; exit 1; } 617 pgn_viewer "$2"; exit $? ;; 618 *) usage; exit 1 ;; 619 esac 620 play # despues de preparar el modo, arrancamos la partida 621 } 622 623 main "$@" # finalmente, llamamos a main con todos los argumentos de la linea de comandos