ui.sh (34547B)
1 #!/usr/bin/env bash 2 # ============================================================================ 3 # lib/ui.sh -- La INTERFAZ: dibujar el tablero y elegir jugadas. 4 # ============================================================================ 5 # 6 # Todo lo que ve y toca la persona vive aca: el tablero a color en la terminal, el 7 # historial al costado, los relojes, los menus, y el selector de jugadas (que usa 8 # fzf, un programa de menus). IMPORTANTE: los textos que ve la persona quedan en 9 # INGLES a proposito (es la interfaz del juego); solo los COMENTARIOS estan en 10 # castellano. 11 # 12 # Sobre los colores en la terminal: se controlan con "codigos de escape ANSI", 13 # unos textos raros como "\e[48;5;180m". No te asustes: son instrucciones para la 14 # terminal del estilo "pinta el fondo de este color". "\e[0m" significa "volve a 15 # lo normal". Usa board.sh, movegen.sh y rules.sh. 16 17 # --- Temas de color del tablero --------------------------------------------- 18 # Cada tema define 6 colores de fondo (codigos ANSI "48;5;N"): 19 # THEME_LIGHT casilla clara 20 # THEME_DARK casilla oscura 21 # THEME_FROM casilla de origen seleccionada 22 # THEME_CAPTURE destino de captura 23 # THEME_TARGET destino de jugada tranquila 24 # THEME_PATH camino de la pieza 25 26 # theme_set: aplica uno de los temas predefinidos. Se llama al arrancar y desde el 27 # menu de temas. El nombre del tema activo se guarda en CHESS_THEME para mostrarlo. 28 # Archivo donde se persiste el tema elegido. Al estar en /tmp con el UID, cada 29 # usuario tiene el suyo. Esto permite que los subprocesos (la vista previa de fzf 30 # y el proceso padre que dibuja el tablero real) compartan el mismo tema aunque 31 # corran en subshells separados y no puedan pasarse variables entre si. 32 _THEME_FILE="${TMPDIR:-/tmp}/chess_theme_${UID:-$(id -u)}" 33 34 CHESS_THEME="classic" 35 theme_set() { 36 # Ignoramos cualquier argumento que no sea un tema conocido: asi un archivo 37 # vacio o corrupto no borra los colores que ya estaban cargados. 38 case "$1" in 39 classic|ocean|night|contrast|colorblind|sunset) : ;; 40 *) return ;; 41 esac 42 CHESS_THEME="$1" 43 printf '%s' "$1" > "$_THEME_FILE" # persiste para que el padre y los subprocesos lo lean 44 case "$1" in 45 classic) 46 # Arena y verde oliva, los colores de madera clasicos. 47 THEME_LIGHT="48;5;180" THEME_DARK="48;5;101" 48 THEME_FROM="48;5;33" THEME_CAPTURE="48;5;160" 49 THEME_TARGET="48;5;71" THEME_PATH="48;5;109" ;; 50 ocean) 51 # Azules: celeste claro y azul marino. 52 THEME_LIGHT="48;5;153" THEME_DARK="48;5;25" 53 THEME_FROM="48;5;220" THEME_CAPTURE="48;5;196" 54 THEME_TARGET="48;5;48" THEME_PATH="48;5;87" ;; 55 night) 56 # Grises oscuros para terminales con fondo negro. 57 THEME_LIGHT="48;5;240" THEME_DARK="48;5;235" 58 THEME_FROM="48;5;24" THEME_CAPTURE="48;5;88" 59 THEME_TARGET="48;5;22" THEME_PATH="48;5;23" ;; 60 contrast) 61 # Alto contraste: blanco y negro puros, resaltados muy vividos. 62 THEME_LIGHT="48;5;231" THEME_DARK="48;5;16" 63 THEME_FROM="48;5;21" THEME_CAPTURE="48;5;196" 64 THEME_TARGET="48;5;46" THEME_PATH="48;5;51" ;; 65 colorblind) 66 # Azul y naranja: distinguibles para deuteranopia/protanopia. 67 THEME_LIGHT="48;5;215" THEME_DARK="48;5;25" 68 THEME_FROM="48;5;226" THEME_CAPTURE="48;5;202" 69 THEME_TARGET="48;5;75" THEME_PATH="48;5;117" ;; 70 sunset) 71 # Naranja y violeta: durazno claro contra violeta medio. Los 72 # resaltados van en azul/verde para no confundirse con el tablero. 73 THEME_LIGHT="48;5;216" THEME_DARK="48;5;97" 74 THEME_FROM="48;5;33" THEME_CAPTURE="48;5;160" 75 THEME_TARGET="48;5;35" THEME_PATH="48;5;109" ;; 76 esac 77 } 78 # Al arrancar: leemos el archivo persistente si existe; si no, el tema por defecto. 79 if [[ -f "$_THEME_FILE" ]]; then 80 theme_set "$(< "$_THEME_FILE")" 81 else 82 theme_set "classic" 83 fi 84 85 # --- Configuracion de ayudas visuales --------------------------------------- 86 # Estas variables controlan si se muestran las "ayudas" opcionales durante la 87 # partida. Se guardan en un archivo para recordarlas entre sesiones. 88 89 _SETTINGS_FILE="${SAVE_DIR:-saves}/.settings" 90 91 SHOW_EVAL_BAR=1 # 1 = mostrar barra de evaluacion de Stockfish 92 SHOW_OPENING=1 # 1 = mostrar el nombre de la apertura en curso 93 94 # settings_load: lee el archivo de configuracion si existe y aplica los valores. 95 settings_load() { 96 [[ -f "$_SETTINGS_FILE" ]] || return 97 local key val 98 while IFS='=' read -r key val; do 99 case "$key" in 100 SHOW_EVAL_BAR) SHOW_EVAL_BAR="$val" ;; 101 SHOW_OPENING) SHOW_OPENING="$val" ;; 102 esac 103 done < "$_SETTINGS_FILE" 104 } 105 106 # settings_save: escribe los valores actuales al archivo. 107 settings_save() { 108 mkdir -p "$(dirname "$_SETTINGS_FILE")" 109 printf 'SHOW_EVAL_BAR=%s\nSHOW_OPENING=%s\n' \ 110 "$SHOW_EVAL_BAR" "$SHOW_OPENING" > "$_SETTINGS_FILE" 111 } 112 113 # ui_settings_menu: menu de toggles. Cada seleccion prende/apaga una ayuda; 114 # se cierra con Escape o eligiendo "back". 115 ui_settings_menu() { 116 while true; do 117 local ev_lbl op_lbl 118 (( SHOW_EVAL_BAR )) && ev_lbl="[ON ]" || ev_lbl="[OFF]" 119 (( SHOW_OPENING )) && op_lbl="[ON ]" || op_lbl="[OFF]" 120 local pick 121 pick=$(printf '%s\n' \ 122 "evalbar ${ev_lbl} Evaluation bar — Stockfish score shown while playing" \ 123 "opening ${op_lbl} Opening name — identify the opening being played" \ 124 "back Done" \ 125 | fzf --reverse --border --height=35% --with-nth=1.. \ 126 --prompt="Aids: " --header=" Visual aids — select to toggle") 127 case "${pick%% *}" in 128 evalbar) (( SHOW_EVAL_BAR )) && SHOW_EVAL_BAR=0 || SHOW_EVAL_BAR=1 129 settings_save; export SHOW_EVAL_BAR ;; 130 opening) (( SHOW_OPENING )) && SHOW_OPENING=0 || SHOW_OPENING=1 131 settings_save; export SHOW_OPENING ;; 132 *) return ;; 133 esac 134 done 135 } 136 137 # ui_pick_theme: abre un selector de tema y aplica el elegido. Se llama desde el 138 # menu de pausa y desde el menu principal. 139 ui_pick_theme() { 140 local pick 141 pick=$(printf '%s\n' \ 142 "classic Wood tones — sand & olive (default)" \ 143 "ocean Cool blues — sky & navy" \ 144 "night Dark greys — low-light terminals" \ 145 "contrast High contrast — pure black & white" \ 146 "colorblind Blue & orange — deuteranopia / protanopia friendly" \ 147 "sunset Orange & violet — peach & purple squares" \ 148 | fzf --reverse --border --height=45% --with-nth=1.. \ 149 --prompt="Theme: " \ 150 --header=" Current theme: ${CHESS_THEME} (Esc to cancel)") 151 [[ -z "$pick" ]] && return 152 theme_set "${pick%% *}" 153 export CHESS_THEME # los subprocesos de la vista previa tambien lo necesitan 154 } 155 156 declare -a HISTORY # historial de jugadas (un elemento por jugada completa) 157 158 # _glyph: devuelve el dibujito (simbolo Unicode) de una pieza. Usamos siempre las 159 # figuras "rellenas" porque las "huecas" se ven mal sobre casillas claras; el 160 # color de cada bando lo da el color de letra (ver _piece_fg), no la figura. 161 _glyph() { 162 # "case" segun la letra. "[Pp]" coincide con P mayuscula o p minuscula (las dos 163 # son peones, de distinto color). Asi un solo caso cubre los dos colores. 164 case "$1" in 165 [Pp]) echo "♙";; 166 [Kk]) echo "♚";; [Qq]) echo "♛";; [Rr]) echo "♜";; 167 [Bb]) echo "♝";; [Nn]) echo "♞";; 168 *) echo " ";; # casilla vacia: un espacio 169 esac 170 } 171 172 # _piece_fg: el color de LETRA de una pieza: blanco brillante para las blancas, 173 # casi negro para las negras. Asi distinguimos los bandos aunque la figura sea igual. 174 _piece_fg() { 175 if is_white "$1"; then echo "\e[38;5;231m" # blanco brillante 176 elif is_black "$1"; then echo "\e[38;5;232m" # casi negro 177 else echo ""; fi 178 } 179 180 # _captured_glyphs: muestra las piezas capturadas de un color. Lo calcula 181 # comparando cuantas piezas DEBERIA haber al inicio contra cuantas hay ahora; la 182 # diferencia son las capturadas. $1 = "white" o "black" (el bando capturado). 183 _captured_glyphs() { 184 local color=$1 185 local -A want # arreglo asociativo: cuantas piezas de cada tipo hay al inicio 186 if [[ "$color" == white ]]; then want=( [P]=8 [N]=2 [B]=2 [R]=2 [Q]=1 ) 187 else want=( [p]=8 [n]=2 [b]=2 [r]=2 [q]=1 ); fi 188 local -A have=() 189 local i p 190 # Contamos cuantas piezas de cada tipo hay AHORA en el tablero. 191 for (( i=0; i<64; i++ )); do p="${BOARD[$i]}"; [[ "$p" != "." ]] && (( have["$p"]++ )); done 192 local out="" k n 193 # "${!want[@]}" recorre las CLAVES del arreglo (los tipos de pieza). 194 for k in "${!want[@]}"; do 195 n=$(( want[$k] - ${have[$k]:-0} )) # cuantas faltan = capturadas 196 while (( n-- > 0 )); do out+="$(_glyph "$k")"; done # un dibujito por cada una 197 done 198 echo "$out" 199 } 200 201 # --- Barra de evaluacion (ayuda opcional, ver SHOW_EVAL_BAR) ---------------- 202 203 # _draw_eval_bar: dibuja una barra horizontal que indica quien esta ganando segun 204 # Stockfish. Los cuadros claros (░) representan la ventaja de blancas y los oscuros 205 # (▓) la de negras. La barra se satura en ±500 centipawns (5 piezas de valor). 206 _draw_eval_bar() { 207 local score=${EVAL_SCORE:-0} 208 # Saturamos en ±500 cp para que la barra no quede siempre en un extremo. 209 local clamped=$score 210 (( clamped > 500 )) && clamped=500 211 (( clamped < -500 )) && clamped=-500 212 # Convertimos a [0..20]: 10 = igualdad. Cada unidad = 50 cp. 213 local white=$(( 10 + clamped / 50 )) 214 (( white < 0 )) && white=0 215 (( white > 20 )) && white=20 216 local black=$(( 20 - white )) 217 218 local bar="" i 219 for (( i=0; i<black; i++ )); do bar+="\e[48;5;232m \e[0m"; done 220 for (( i=0; i<white; i++ )); do bar+="\e[48;5;231m \e[0m"; done 221 222 printf ' Eval %b %s\n' "$bar" "$(_fmt_cp "$score")" 223 } 224 225 # --- Ayudantes del reloj (solo para mostrarlo) ------------------------------ 226 # El estado del reloj vive en transport.sh; estas funciones solo lo formatean. 227 228 # _fmt_clock: pasa segundos a "MM:SS" (minutos:segundos), nunca negativo. 229 # "%02d" significa "numero con al menos 2 digitos, rellenando con cero". 230 _fmt_clock() { 231 local s=$1 232 (( s < 0 )) && s=0 233 printf '%02d:%02d' $(( s / 60 )) $(( s % 60 )) 234 } 235 236 # _clock_str: una linea con los dos relojes; al lado que mueve (3er argumento) le 237 # pone una flechita. Se usa tanto para la linea fija del tablero como para el 238 # encabezado del selector que va contando en vivo (ver clock_ticker en ches.sh). 239 _clock_str() { 240 local w=$1 b=$2 active=$3 241 local wlab="White $(_fmt_clock "$w")" blab="Black $(_fmt_clock "$b")" 242 [[ "$active" == white ]] && wlab="▶ $wlab" 243 [[ "$active" == black ]] && blab="▶ $blab" 244 printf '⏱ %s %s' "$wlab" "$blab" 245 } 246 247 # _status_line: el texto de estado de la posicion (de quien es el turno, si hay 248 # jaque, o como termino la partida). Pregunta el estado a game_status (rules.sh). 249 _status_line() { 250 local st; st=$(game_status) 251 case "$st" in 252 checkmate) echo "CHECKMATE — $(other_color "$TURN") wins";; 253 stalemate) echo "STALEMATE — draw";; 254 draw-fivefold) echo "DRAW — fivefold repetition";; 255 draw-75move) echo "DRAW — 75-move rule";; 256 draw-material) echo "DRAW — insufficient material";; 257 *) if in_check "$TURN"; then echo "${TURN^^} to move — CHECK"; else echo "${TURN^^} to move"; fi;; 258 esac 259 } 260 261 # --- Resaltados del tablero (para la vista previa del selector) ------------- 262 # Cuando elegis una pieza, el selector muestra una vista previa del tablero con 263 # las jugadas posibles pintadas. Estas variables guardan QUE pintar. Las casillas 264 # se guardan por su indice, en una lista con espacios alrededor para poder 265 # preguntar facil si un indice esta adentro. 266 HL_FROM=-1 # casilla de origen elegida (indice) o -1 si ninguna 267 HL_TARGETS=" " # destinos de jugadas tranquilas (sin captura) 268 HL_CAPTURES=" " # destinos de capturas 269 HL_PATH=" " # casillas por las que pasa una pieza que se desliza 270 HL_TITLE="" # titulo de arriba; si esta vacio, usa la linea de estado 271 272 _hl_reset() { HL_FROM=-1; HL_TARGETS=" "; HL_CAPTURES=" "; HL_PATH=" "; HL_TITLE=""; } 273 # _hl_has: el indice $2 esta en la lista $1? (busca " indice " con comodines *). 274 _hl_has() { [[ "$1" == *" $2 "* ]]; } 275 276 # _piece_name: el nombre en ingles de una pieza, segun su letra. 277 _piece_name() { 278 case "${1,,}" in 279 p) echo Pawn;; n) echo Knight;; b) echo Bishop;; 280 r) echo Rook;; q) echo Queen;; k) echo King;; *) echo "";; 281 esac 282 } 283 284 # _is_capture_move: la jugada $1 es una captura (incluyendo al paso) en la 285 # posicion actual? (Es como _move_is_capture de notation.sh, repetida aca para que 286 # la interfaz no dependa de ese archivo.) 287 _is_capture_move() { 288 local m=$1; local frm=${m:0:2} to=${m:2:2} 289 local ti; ti=$(alg_to_idx "$to") 290 [[ "${BOARD[$ti]}" != "." ]] && return 0 291 local fi; fi=$(alg_to_idx "$frm") 292 [[ "${BOARD[$fi],,}" == p && "${frm:0:1}" != "${to:0:1}" && "$to" == "$EP" ]] 293 } 294 295 # --- Nombre de apertura (ayuda opcional, ver SHOW_OPENING) ------------------ 296 297 # _opening_name: busca la coincidencia mas larga entre las jugadas jugadas hasta 298 # ahora (PGN_UCI) y el archivo de aperturas. Imprime el nombre, o nada si no hay 299 # coincidencia. PGN_UCI es el arreglo de jugadas en UCI que mantiene ches.sh. 300 _OPENINGS_FILE="${SCRIPT_DIR:-/home/pancho/ajedrez}/lib/openings.tsv" 301 _opening_name() { 302 [[ -f "$_OPENINGS_FILE" ]] || return 303 (( ${#PGN_UCI[@]} == 0 )) && return 304 305 # Armamos el prefijo de jugadas jugadas como texto (ej: "e2e4 c7c5 g1f3"). 306 local played="${PGN_UCI[*]}" # ${arreglo[*]} pega los elementos con espacios 307 308 local best_name="" best_len=0 309 local code name moves 310 while IFS=$'\t' read -r code name moves; do 311 [[ "$code" == \#* || -z "$code" ]] && continue # saltamos comentarios y vacios 312 local mlen=${#moves} 313 # La secuencia de aperturas tiene que ser un prefijo de lo que jugamos. 314 # Comparamos con "${played:0:mlen}" (los primeros mlen caracteres). 315 if [[ "${played:0:$mlen}" == "$moves" && $mlen -gt $best_len ]]; then 316 # Ademas, el caracter siguiente tiene que ser un espacio o fin: evitamos 317 # que "e2e4" coincida con "e2e4c" (que no existe, pero por las dudas). 318 local next="${played:$mlen:1}" 319 [[ -z "$next" || "$next" == " " ]] && best_name="$name" && best_len=$mlen 320 fi 321 done < "$_OPENINGS_FILE" 322 323 [[ -n "$best_name" ]] && printf '%s' "$best_name" 324 } 325 326 # --- _draw_board: DIBUJAR el tablero ---------------------------------------- 327 # Esta funcion arma el tablero a color con todos los datos. No limpia la pantalla 328 # (de eso se encarga render_board). La vista previa del selector la usa directo. 329 _draw_board() { 330 local title="${HL_TITLE:-$(_status_line)}" # titulo, o la linea de estado si no hay 331 # Si la ayuda de apertura esta prendida, la agregamos al titulo. 332 if (( ${SHOW_OPENING:-1} )) && [[ -z "$HL_TITLE" ]]; then 333 local op; op=$(_opening_name) 334 [[ -n "$op" ]] && title+=" \e[38;5;245m— ${op}\e[0m" 335 fi 336 echo -e " ${title}\t\t MOVE HISTORY" 337 echo " +------------------------+ ------------------" 338 339 # El historial muestra las ultimas 8 jugadas como mucho. 340 local total=${#HISTORY[@]} hstart=0 341 (( total > 8 )) && hstart=$(( total - 8 )) 342 343 local r c 344 for (( r=0; r<8; r++ )); do # cada fila del tablero 345 local row_str="$((8-r)) |" # el numero de fila (8 arriba, 1 abajo) y el borde 346 for (( c=0; c<8; c++ )); do # cada columna 347 local idx=$(( r * 8 + c )) bg piece p fg 348 piece="${BOARD[$idx]}" 349 # Color de fondo del casillero (alternado, como un tablero real). 350 if (( (r + c) % 2 == 0 )); then bg="\e[${THEME_LIGHT}m"; else bg="\e[${THEME_DARK}m"; fi 351 p=$(_glyph "$piece"); fg=$(_piece_fg "$piece") 352 353 # Si esta casilla esta resaltada, le cambiamos el color de fondo. 354 if (( idx == HL_FROM )); then 355 bg="\e[${THEME_FROM}m" 356 elif _hl_has "$HL_CAPTURES" "$idx"; then 357 bg="\e[${THEME_CAPTURE}m" 358 elif _hl_has "$HL_TARGETS" "$idx"; then 359 bg="\e[${THEME_TARGET}m" 360 [[ "$piece" == "." ]] && { p="•"; fg="\e[38;5;231m"; } # punto si esta vacia 361 elif _hl_has "$HL_PATH" "$idx"; then 362 bg="\e[${THEME_PATH}m" 363 [[ "$piece" == "." ]] && { p="·"; fg="\e[38;5;231m"; } 364 fi 365 # Pegamos la celda: fondo + negrita (\e[1m) + color de letra + la figura + reset. 366 row_str+="${bg}\e[1m${fg} ${p} \e[0m" 367 done 368 row_str+="|" 369 370 # A la derecha de cada fila mostramos una linea del historial de jugadas. 371 local move_line="" hidx=$(( hstart + r )) 372 [[ -n "${HISTORY[$hidx]}" ]] && move_line=" $((hidx + 1)). ${HISTORY[$hidx]}" 373 echo -e "${row_str}${move_line}" 374 done 375 376 echo " +------------------------+ ------------------" 377 echo " a b c d e f g h " 378 echo -e " Captured: white $(_captured_glyphs white) black $(_captured_glyphs black)" 379 380 # Contador de la regla de 50 movimientos (jugadas completas sin peon ni captura). 381 local hm50=$(( HALFMOVE / 2 )) 382 local c50=" 50-move rule: ${hm50}/50" 383 if (( HALFMOVE >= 100 )); then c50+=" \e[1;38;5;231m\e[48;5;71m DRAW CLAIMABLE \e[0m"; fi 384 echo -e "$c50" 385 386 # Aviso de que se pueden pedir tablas por triple repeticion. (En la pantalla 387 # real, POSCOUNT esta cargado; en la vista previa del selector no, asi que ahi 388 # no aparece. No pasa nada, es solo un aviso.) 389 if can_claim_threefold; then 390 echo -e " Repetition: \e[1;38;5;231m\e[48;5;71m THREEFOLD — DRAW CLAIMABLE \e[0m" 391 fi 392 393 # Barra de evaluacion (solo si la ayuda esta prendida y hay un puntaje calculado). 394 # En la vista previa del selector EVAL_SCORE no esta disponible, asi que no aparece. 395 if (( ${SHOW_EVAL_BAR:-1} )) && [[ -n "${AI_PID:-}" ]]; then 396 _draw_eval_bar 397 fi 398 399 # El reloj. Solo la pantalla real (render_board) tiene TC_ENABLED prendido; la 400 # vista previa del selector corre en otro proceso con TC_ENABLED=0, asi que ahi 401 # no se dibuja (el reloj que va contando aparece en el encabezado del selector). 402 if (( ${TC_ENABLED:-0} )); then 403 echo -e " $(_clock_str "$CLOCK_white" "$CLOCK_black" "$TURN")" 404 fi 405 } 406 407 # render_board: la version para la pantalla real: limpia la terminal, resetea los 408 # resaltados, dibuja, y deja una linea en blanco. 409 render_board() { 410 # Leemos el tema del archivo por si cambio en un subshell (menu de pausa). 411 # "-s" chequea que el archivo exista Y no este vacio. 412 [[ -s "$_THEME_FILE" ]] && theme_set "$(< "$_THEME_FILE")" 413 clear 414 _hl_reset 415 _draw_board 416 echo "" 417 } 418 419 # --- Vistas previas del selector (las llama ches.sh con subcomandos ocultos) ---- 420 # El selector fzf muestra una "vista previa" al costado. Esa vista la genera otro 421 # proceso que reconstruye la posicion desde la variable CHESS_FEN. 422 423 # ui_preview_piece: vista previa al elegir una PIEZA: resalta todos sus destinos 424 # (capturas en rojo, jugadas tranquilas en verde). 425 ui_preview_piece() { 426 local sq=$1 fi m ti 427 # Si lo elegido no es una casilla (ej: la opcion de reclamar tablas), mostramos 428 # el tablero comun con un titulo explicativo, sin resaltar nada raro. 429 if [[ ! "$sq" =~ ^[a-h][1-8]$ ]]; then 430 _hl_reset 431 HL_TITLE="\e[48;5;71m ♻ \e[0m Claim a draw — threefold repetition or the 50-move rule" 432 _draw_board 433 return 434 fi 435 fi=$(alg_to_idx "$sq") 436 _hl_reset 437 HL_FROM=$fi 438 # Recorremos las jugadas legales que SALEN de esa casilla y las clasificamos. 439 while IFS= read -r m; do 440 [[ "${m:0:2}" == "$sq" ]] || continue 441 ti=$(alg_to_idx "${m:2:2}") 442 if _is_capture_move "$m"; then HL_CAPTURES+="$ti "; else HL_TARGETS+="$ti "; fi 443 done < <(legal_moves) 444 HL_TITLE="$(_piece_name "${BOARD[$fi]}") $sq \e[48;5;71m • \e[0m move \e[48;5;160m ✗ \e[0m capture" 445 _draw_board 446 } 447 448 # ui_preview_move: vista previa de UNA jugada concreta: resalta el origen, el 449 # camino que recorre, y el destino (rojo si es captura). 450 ui_preview_move() { 451 local mv=$1; local frm=${mv:0:2} to=${mv:2:2} 452 local fi ti fr fc tr tc 453 fi=$(alg_to_idx "$frm"); ti=$(alg_to_idx "$to") 454 _hl_reset 455 HL_FROM=$fi 456 fr=$(( fi/8 )); fc=$(( fi%8 )); tr=$(( ti/8 )); tc=$(( ti%8 )) 457 458 # Calculamos el camino (solo para piezas que se deslizan en linea). dr/dc es la 459 # direccion (paso a paso) desde el origen hacia el destino. 460 local dr=0 dc=0 461 (( tr > fr )) && dr=1; (( tr < fr )) && dr=-1 462 (( tc > fc )) && dc=1; (( tc < fc )) && dc=-1 463 # Solo hay "camino" si la jugada es en linea recta o en diagonal perfecta. 464 if (( dr == 0 || dc == 0 )) || (( (tr-fr)*(tr-fr) == (tc-fc)*(tc-fc) )); then 465 local nr=$(( fr+dr )) nc=$(( fc+dc )) 466 while (( nr != tr || nc != tc )); do # marcamos las casillas intermedias 467 HL_PATH+="$(( nr*8+nc )) " 468 nr=$(( nr+dr )); nc=$(( nc+dc )) 469 done 470 fi 471 472 if _is_capture_move "$mv"; then HL_CAPTURES+="$ti "; else HL_TARGETS+="$ti "; fi 473 HL_TITLE="$(_piece_name "${BOARD[$fi]}") $frm → $to" 474 _draw_board 475 } 476 477 # --- Guardar / exportar desde el menu de pausa ------------------------------ 478 # Estas funciones piden un nombre de archivo por teclado y guardan/exportan. Leen 479 # de /dev/tty (el teclado real) porque fzf ya solto la pantalla cuando llegamos aca. 480 481 LAST_SAVE="" 482 _save_prompt() { 483 printf ' Save name [game]: ' >/dev/tty 484 local name; read -r name </dev/tty 485 name=${name:-game}; name=${name// /_} # nombre por defecto "game"; espacios -> "_" 486 local file="${SAVE_DIR:-saves}/${name}.save" 487 save_game "$file" 488 LAST_SAVE="$file" 489 } 490 491 # Exportar la partida hasta ahora a un PGN estandar (para analizar/compartir). 492 LAST_EXPORT="" 493 _export_pgn_prompt() { 494 printf ' PGN file name [game]: ' >/dev/tty 495 local name; read -r name </dev/tty 496 name=${name:-game}; name=${name// /_} 497 local file="${SAVE_DIR:-saves}/${name}.pgn" 498 export_pgn "$file" "*" 499 LAST_EXPORT="$file" 500 } 501 502 # Exportar la posicion actual como un FEN pelado (a un archivo, y se muestra para 503 # poder copiarla). 504 LAST_FEN="" 505 _export_fen_prompt() { 506 printf ' FEN file name [position]: ' >/dev/tty 507 local name; read -r name </dev/tty 508 name=${name:-position}; name=${name// /_} 509 local file="${SAVE_DIR:-saves}/${name}.fen" 510 LAST_FEN="$(export_fen "$file")" 511 } 512 513 # ui_confirm_draw: menu chico de si/no cuando el otro ofrece tablas. Devuelve 0 514 # (acepta) o 1 (rechaza). Es solo interfaz; la red la maneja quien la llama. 515 ui_confirm_draw() { 516 local ans 517 ans=$(printf '%s\n' "no ✗ Decline, keep playing" "yes 🤝 Accept the draw" \ 518 | fzf --reverse --border --height=30% --with-nth=1.. \ 519 --prompt="Draw offer: " --header=" Your opponent offers a draw") 520 [[ "${ans%% *}" == yes ]] 521 } 522 523 # ui_wait_menu: se abre al apretar una tecla mientras esperamos al otro. Devuelve 524 # una palabra de control: "" (seguir esperando), RECONNECT (declarar que el otro 525 # se cayo), SAVE, QUIT o RESIGN. 526 ui_wait_menu() { 527 local act 528 act=$(printf '%s\n' \ 529 "wait ▶ Keep waiting" \ 530 "drop ⇄ Opponent disconnected — wait for reconnect" \ 531 "save 💾 Save game" \ 532 "quit ⏹ Quit (no resign)" \ 533 "resign 🏳 Resign" \ 534 | fzf --reverse --border --height=45% --with-nth=1.. \ 535 --prompt="Waiting: " --header=" Opponent hasn't moved — pick an action") 536 case "${act%% *}" in 537 drop) echo "RECONNECT" ;; 538 save) echo "SAVE" ;; 539 quit) echo "QUIT" ;; 540 resign) echo "RESIGN" ;; 541 *) echo "" ;; 542 esac 543 } 544 545 # _wait_repaint: repinta EN EL LUGAR la unica linea de espera (el reloj del otro 546 # que va contando, o un relojito si no hay tiempo, mas la ayuda del teclado). 547 # "\r" lleva el cursor al inicio de la linea, asi se reescribe encima sin bajar. 548 _wait_repaint() { 549 local active=$1 deadline=$2 line 550 if [[ -n "$deadline" ]]; then 551 local now rem w=$CLOCK_white b=$CLOCK_black 552 now=$(date +%s); rem=$(( deadline - now )); (( rem < 0 )) && rem=0 553 [[ "$active" == white ]] && w=$rem || b=$rem 554 line="$(_clock_str "$w" "$b" "$active")" 555 else 556 line="⏳ Waiting for ${active^^}'s move" 557 fi 558 printf '\r %s (press any key for options) ' "$line" >/dev/tty 559 } 560 561 # ui_confirm_takeback: menu de si/no cuando el otro pide retroceder su jugada. 562 # Devuelve 0 (acepta) o 1 (rechaza). Solo interfaz. 563 ui_confirm_takeback() { 564 local ans 565 ans=$(printf '%s\n' "no ✗ Refuse" "yes ↩ Allow the takeback" \ 566 | fzf --reverse --border --height=30% --with-nth=1.. \ 567 --prompt="Takeback: " --header=" Opponent wants to take back their last move") 568 [[ "${ans%% *}" == yes ]] 569 } 570 571 # --- _escape_menu: el menu de PAUSA (al apretar Escape en el selector) ------- 572 # Deja seguir jugando, ofrecer tablas, pedir un retroceso (si esta activado), 573 # guardar, exportar, salir sin rendirse, o rendirse. Devuelve la palabra de 574 # control que tiene que actuar quien llama ("" = seguir, DRAWOFFER, TAKEBACK, 575 # QUIT o RESIGN). La opcion de retroceder solo aparece si esta permitido y hay una 576 # jugada en la pila de deshacer. 577 _escape_menu() { 578 local status=" Game paused — choose an action" 579 while true; do 580 # Armamos las opciones en un arreglo, agregando algunas solo si corresponde. 581 local -a items=( "play ▶ Keep playing" ) 582 # Ofrecer tablas es una negociacion entre dos; no tiene sentido contra la maquina. 583 [[ "${TRANSPORT_MODE:-}" != ai ]] && items+=( "draw 🤝 Offer draw" ) 584 if (( ${ALLOW_TAKEBACK:-0} )) && (( ${#UNDO_STACK[@]} > 0 )); then 585 items+=( "undo ↩ Request takeback" ) 586 fi 587 items+=( "save 💾 Save game" \ 588 "epgn ⬇ Export PGN (full game)" \ 589 "efen ⬇ Export FEN (this position)" \ 590 "theme 🎨 Change board theme (${CHESS_THEME})" \ 591 "aids ☰ Toggle visual aids (eval bar, opening name)" \ 592 "quit ⏹ Quit (no resign)" "resign 🏳 Resign" ) 593 local act 594 act=$(printf '%s\n' "${items[@]}" \ 595 | fzf --reverse --border --height=60% --with-nth=1.. \ 596 --prompt="Paused: " --header="$status") 597 case "${act%% *}" in 598 play|"") return ;; 599 draw) echo "DRAWOFFER"; return ;; 600 undo) echo "TAKEBACK"; return ;; 601 save) _save_prompt; status=" Saved to ${LAST_SAVE}" ;; 602 epgn) _export_pgn_prompt; status=" Exported PGN to ${LAST_EXPORT}" ;; 603 efen) _export_fen_prompt; status=" Exported FEN: ${LAST_FEN}" ;; 604 theme) ui_pick_theme; status=" Theme: ${CHESS_THEME}" ;; 605 aids) ui_settings_menu; status=" Aids updated" ;; 606 quit) echo "QUIT"; return ;; 607 resign) echo "RESIGN"; return ;; 608 esac 609 done 610 } 611 612 # --- ui_pick_move: ELEGIR una jugada, en dos pasos -------------------------- 613 # 1) elegir una pieza (la vista previa resalta sus jugadas y capturas) 614 # 2) elegir el destino (la vista previa resalta el camino origen->destino) 615 # Devuelve la jugada elegida (con la coronacion ya resuelta), o nada si la persona 616 # cancela en el paso 1 (quien llama lo toma como salir/rendirse). Cancelar en el 617 # paso 2 vuelve al paso 1. 618 ui_pick_move() { 619 local -a moves 620 # "mapfile -t moves < <(...)" mete cada linea que imprime legal_moves como un 621 # elemento del arreglo "moves". 622 mapfile -t moves < <(legal_moves) 623 (( ${#moves[@]} == 0 )) && return # sin jugadas: no hay nada que elegir 624 625 # Exportamos la posicion y el tema para que el proceso de la vista previa los use. 626 export CHESS_FEN; CHESS_FEN="$(get_fen)" 627 export CHESS_THEME 628 local bin="${CHESS_BIN:-$PWD/ches.sh}" 629 630 # --- Cableado del reloj en vivo (solo si hay control de tiempo) --- 631 # Truco lindo: cada etapa de fzf abre un pequenio "servidor de control" 632 # (--listen). Al arrancar, lanza en segundo plano "ches.sh __clocktick", que cada 633 # un segundo repinta el encabezado con el tiempo restante y, si se acaba, deja 634 # una senial (un archivo "sentinel") y cierra el selector. Despues de cada 635 # etapa miramos ese archivo para distinguir "se acabo el tiempo" de "cancele a 636 # mano". Si no hay reloj, todo este bloque no hace nada. 637 local -a topts=() 638 local sentinel="" 639 if (( ${TC_ENABLED:-0} )); then 640 sentinel="${TMPDIR:-/tmp}/chess_to.$$.$RANDOM" # nombre de archivo unico 641 rm -f "$sentinel" 642 local now myrem 643 now=$(date +%s) 644 [[ "$TURN" == white ]] && myrem=$CLOCK_white || myrem=$CLOCK_black 645 # Exportamos datos para que el proceso del reloj sepa cuanto falta. 646 export CHESS_DEADLINE=$(( now + myrem )) 647 export CHESS_ACTIVE="$TURN" 648 export CHESS_CLK_W="$CLOCK_white" CHESS_CLK_B="$CLOCK_black" 649 export CHESS_SENTINEL="$sentinel" 650 topts=( --listen --header-first 651 --header="$(_clock_str "$CLOCK_white" "$CLOCK_black" "$TURN")" 652 --bind "start:execute-silent(bash '$bin' __clocktick \$FZF_PORT &)" ) 653 fi 654 # _timed_out: verdadero si la ultima etapa se cerro porque se acabo el tiempo. 655 _timed_out() { [[ -n "$sentinel" && -e "$sentinel" ]]; } 656 657 # Agrupamos las jugadas por casilla de ORIGEN (para el menu del paso 1). 658 local -A by_src seen 659 local -a srcs=() 660 local m base frm 661 for m in "${moves[@]}"; do 662 base="${m:0:4}" # solo origen+destino (sin la letra de coronacion) 663 [[ -n "${seen[$base]}" ]] && continue # evitamos repetir la misma jugada base 664 seen[$base]=1 665 frm="${base:0:2}" 666 [[ -z "${by_src[$frm]}" ]] && srcs+=("$frm") 667 by_src[$frm]+="$base " 668 done 669 670 while true; do 671 # --- Paso 1: elegir una pieza --- 672 local -a plist=() 673 for frm in "${srcs[@]}"; do 674 # Contamos cuantas jugadas y cuantas capturas tiene esa pieza, para el texto. 675 local cnt=0 caps=0 b 676 for b in ${by_src[$frm]}; do 677 ((cnt++)); _is_capture_move "$b" && ((caps++)) 678 done 679 local extra=""; (( caps > 0 )) && extra=", ${caps} capture$([[ $caps -gt 1 ]] && echo s)" 680 plist+=("$(printf '%s %-6s %d move%s%s' "$frm" \ 681 "$(_piece_name "${BOARD[$(alg_to_idx "$frm")]}")" \ 682 "$cnt" "$([[ $cnt -gt 1 ]] && echo s)" "$extra")") 683 done 684 685 # Si se pueden pedir tablas, agregamos una opcion arriba de todo. (La triple 686 # repeticion tiene prioridad en el texto; play() decide que regla aplica.) 687 local claim_row="" 688 if can_claim_threefold; then claim_row="CLAIM ♻ Claim draw (threefold repetition)" 689 elif can_claim_draw50; then claim_row="CLAIM ♻ Claim draw (50-move rule)"; fi 690 691 local pick 692 # Mostramos el menu: la opcion de tablas (si hay) arriba, despues las piezas 693 # ordenadas. "--preview" hace que fzf muestre el tablero al costado llamando 694 # al subcomando oculto __ppiece con la casilla resaltada ({1}). 695 pick=$( { [[ -n "$claim_row" ]] && printf '%s\n' "$claim_row" 696 printf '%s\n' "${plist[@]}" | sort; } | fzf --reverse --border \ 697 --prompt="Select piece (${TURN}): " --with-nth=1.. "${topts[@]}" \ 698 --preview="bash '$bin' __ppiece {1}" --preview-window=right:62%:border-left) 699 _timed_out && { rm -f "$sentinel"; echo "TIMEOUT"; return; } 700 if [[ -z "$pick" ]]; then # Escape en el paso 1 -> menu de pausa 701 local em; em=$(_escape_menu) 702 case "$em" in 703 QUIT) echo "QUIT"; return ;; 704 RESIGN) echo "RESIGN"; return ;; 705 DRAWOFFER) echo "DRAWOFFER"; return ;; 706 TAKEBACK) echo "TAKEBACK"; return ;; 707 *) continue ;; # seguir jugando 708 esac 709 fi 710 local src="${pick%% *}" 711 [[ "$src" == CLAIM ]] && { echo "CLAIM50"; return; } # eligio reclamar tablas 712 713 # --- Paso 2: elegir el destino --- 714 local -a dlist=() 715 for base in ${by_src[$src]}; do 716 local to="${base:2:2}" mark="move" 717 _is_capture_move "$base" && mark="✗ capture" 718 dlist+=("$(printf '%s %s' "$to" "$mark")") 719 done 720 721 local dpick 722 dpick=$(printf '%s\n' "${dlist[@]}" | sort | fzf --reverse --border \ 723 --prompt="Move ${src} to: " --with-nth=1.. "${topts[@]}" \ 724 --preview="bash '$bin' __pmove ${src}{1}" --preview-window=right:62%:border-left) 725 _timed_out && { rm -f "$sentinel"; echo "TIMEOUT"; return; } 726 [[ -z "$dpick" ]] && continue # cancelar el paso 2: volver al paso 1 727 local dst="${dpick%% *}" 728 local chosen="${src}${dst}" 729 730 # --- Coronacion (si esta jugada tiene variantes de coronar) --- 731 local promos="" 732 for m in "${moves[@]}"; do 733 # Una jugada de 5 letras cuya base coincide es una coronacion; juntamos 734 # las piezas posibles (q/r/b/n). 735 [[ ${#m} == 5 && "${m:0:4}" == "$chosen" ]] && promos+="${m:4:1}" 736 done 737 if [[ -n "$promos" ]]; then 738 local choices="" i ch 739 for (( i=0; i<${#promos}; i++ )); do 740 ch="${promos:$i:1}" 741 case "$ch" in q) choices+="q (Queen)"$'\n';; r) choices+="r (Rook)"$'\n';; 742 b) choices+="b (Bishop)"$'\n';; n) choices+="n (Knight)"$'\n';; esac 743 done 744 local sel 745 sel=$(printf '%s' "$choices" | fzf --height=20% --reverse \ 746 --prompt="Promote to: " --border "${topts[@]}") 747 _timed_out && { rm -f "$sentinel"; echo "TIMEOUT"; return; } 748 [[ -z "$sel" ]] && continue 749 chosen+="${sel:0:1}" # le pegamos la letra de la pieza elegida 750 fi 751 752 echo "$chosen" 753 return 754 done 755 }