saves.sh (15101B)
1 #!/usr/bin/env bash 2 # ============================================================================ 3 # lib/saves.sh -- Guardar, recuperar, e importar/exportar partidas. 4 # ============================================================================ 5 # 6 # Este archivo se ocupa de la PERSISTENCIA: poder pausar una partida y guardarla 7 # en un archivo (en la carpeta saves/) para retomarla mas tarde (util si se te 8 # apaga la notebook en el medio del juego). Un "guardado" anota todo lo necesario 9 # para seguir exactamente donde estabas: la posicion (FEN), el control de tiempo, 10 # los dos relojes, si se permiten retrocesos, y el historial de jugadas. 11 # 12 # El MISMO paquete de datos tambien viaja por la red, asi en una partida en LAN 13 # alcanza con que UN solo lado tenga el guardado: ese lo carga y le manda la 14 # posicion al otro (ver transport.sh). 15 # 16 # Tambien maneja PGN (el formato estandar del ajedrez, ver notation.sh): exportar 17 # la partida para abrirla en lichess/chess.com, e importar una pegando su PGN. 18 19 # --- Las variables GAME_*: el estado "en preparacion" ----------------------- 20 # Antes de empezar a jugar, dejamos la partida lista en estas variables GAME_*. 21 # Despues, la funcion play() (en ches.sh) las copia al motor de verdad. El lider las 22 # completa (partida nueva o cargada de un guardado) y el otro las recibe por red. 23 GAME_FEN="$START_FEN" # la posicion en formato FEN 24 GAME_CW=0 # segundos del reloj de las blancas 25 GAME_CB=0 # segundos del reloj de las negras 26 GAME_TB=0 # se permiten retrocesos? (1 = si) 27 GAME_HFM=0 # numero de jugada para el historial 28 GAME_HLINE="" # jugada de las blancas pendiente cuando le toca a las negras 29 GAME_STARTFEN="$START_FEN" # desde que posicion se cuentan los numeros del PGN 30 declare -a GAME_HIST=() # historial de jugadas (para mostrar al costado) 31 declare -a GAME_PGN=() # jugadas en notacion SAN (para exportar a PGN) 32 33 # game_state_fresh: deja las GAME_* listas para una partida NUEVA. Se llama 34 # despues de elegir el control de tiempo (tc_pick) y los retrocesos (tb_pick), 35 # asi los relojes arrancan con el tiempo elegido. 36 game_state_fresh() { 37 GAME_FEN="$START_FEN" 38 # Si hay reloj, los dos arrancan con el tiempo base; si no, en 0. 39 if (( TC_ENABLED )); then GAME_CW=$TC_BASE; GAME_CB=$TC_BASE; else GAME_CW=0; GAME_CB=0; fi 40 GAME_TB=$ALLOW_TAKEBACK 41 GAME_HFM=0; GAME_HLINE=""; GAME_HIST=() 42 GAME_STARTFEN="$START_FEN"; GAME_PGN=() 43 } 44 45 # --- Guardar la partida en un archivo --------------------------------------- 46 # save_game escribe el estado ACTUAL del juego a un archivo, en formato 47 # "clave=valor" (una cosa por linea). Es facil de leer para una persona y para 48 # el programa. 49 save_game() { 50 local file=$1 51 mkdir -p "$(dirname "$file")" # crea la carpeta saves/ si no existe ("dirname" = la carpeta del archivo) 52 # Las llaves { ... } agrupan varios printf, y el "> $file" del final manda 53 # TODO lo que imprimen esos comandos juntos al archivo (lo crea/sobreescribe). 54 { 55 printf 'fen=%s\n' "$(get_fen)" 56 printf 'tc=%s\n' "$(tc_encode)" # control de tiempo (ver transport.sh) 57 printf 'clock_white=%s\n' "$CLOCK_white" 58 printf 'clock_black=%s\n' "$CLOCK_black" 59 printf 'takeback=%s\n' "$ALLOW_TAKEBACK" 60 printf 'startfen=%s\n' "$PGN_STARTFEN" 61 printf 'hist_fullmove=%s\n' "$HIST_FULLMOVE" 62 printf 'hist_line=%s\n' "$HIST_LINE" 63 local h 64 for h in "${HISTORY[@]}"; do printf 'hist=%s\n' "$h"; done # una linea por jugada del historial 65 local s 66 for s in "${PGN_SAN[@]}"; do printf 'pgn=%s\n' "$s"; done # una linea por jugada en SAN 67 } > "$file" 68 } 69 70 # --- Cargar la partida desde un archivo ------------------------------------- 71 # load_game lee el archivo y completa las GAME_*. Tambien acepta un archivo que 72 # sea SOLO un FEN suelto (sin "clave=valor"): lo toma como posicion de arranque 73 # sin reloj ni historial. 74 load_game() { 75 local file=$1 76 # "grep -q '='" busca el simbolo "=" en el archivo (modo silencioso). Si NO 77 # lo encuentra (el "!" invierte el resultado), es un archivo de FEN pelado. 78 if ! grep -q '=' "$file"; then 79 GAME_FEN="$(head -n1 "$file")" # "head -n1" toma solo la primera linea 80 TC_ENABLED=0; TC_BASE=0; TC_INC=0 81 GAME_CW=0; GAME_CB=0; GAME_TB=0; GAME_HFM=0; GAME_HLINE=""; GAME_HIST=() 82 GAME_STARTFEN="$GAME_FEN"; GAME_PGN=() 83 return 84 fi 85 GAME_HIST=(); GAME_PGN=(); GAME_TB=0; GAME_STARTFEN="" 86 local key val 87 # Leemos linea por linea. "IFS='='" parte cada linea en lo de antes y lo de 88 # despues del "=": eso queda en "key" (clave) y "val" (valor). 89 while IFS='=' read -r key val; do 90 case "$key" in 91 fen) GAME_FEN="$val" ;; 92 tc) tc_decode "$val" ;; 93 clock_white) GAME_CW="$val" ;; 94 clock_black) GAME_CB="$val" ;; 95 takeback) GAME_TB="$val" ;; 96 startfen) GAME_STARTFEN="$val" ;; 97 hist_fullmove) GAME_HFM="$val" ;; 98 hist_line) GAME_HLINE="$val" ;; 99 hist) GAME_HIST+=("$val") ;; # "+=" agrega al final del arreglo 100 pgn) GAME_PGN+=("$val") ;; 101 esac 102 done < "$file" # "< $file" hace que el while lea del archivo 103 # Guardados viejos no tenian "startfen": asumimos que arrancaron del comienzo. 104 [[ -z "$GAME_STARTFEN" ]] && GAME_STARTFEN="$START_FEN" 105 } 106 107 # load_fen_string: arrancar desde un FEN pegado por la persona. Esa posicion, sin 108 # reloj y sin historial. 109 load_fen_string() { 110 GAME_FEN="$1" 111 TC_ENABLED=0; TC_BASE=0; TC_INC=0 112 GAME_CW=0; GAME_CB=0; GAME_TB=0; GAME_HFM=0; GAME_HLINE=""; GAME_HIST=() 113 GAME_STARTFEN="$1"; GAME_PGN=() 114 } 115 116 # fen_looks_valid: un chequeo SIMPLE de que un FEN pegado no sea cualquier cosa. 117 # No valida todo, solo rechaza basura evidente antes de intentar usarlo. 118 fen_looks_valid() { 119 # "${1%% *}" se queda con lo de ANTES del primer espacio (las piezas). 120 # "${1#* }" se queda con lo de DESPUES del primer espacio (el resto). 121 local placement=${1%% *} rest=${1#* } 122 [[ "$1" == *" "* ]] || return 1 # tiene que tener al menos un espacio 123 [[ "$rest" == w* || "$rest" == b* ]] || return 1 # el turno tiene que ser w o b 124 # "tr -cd '/'" deja SOLO las barras "/" del texto; "wc -c" las cuenta. Un FEN 125 # valido tiene 7 barras (separan las 8 filas). 126 [[ "$(tr -cd '/' <<<"$placement" | wc -c)" == 7 ]] 127 } 128 129 # --- Serializacion para la red (todo el estado en UNA linea) ---------------- 130 # Para mandar el estado por red lo convertimos a una sola linea de texto. Usamos 131 # ";" para separar los campos, y "|" para separar los elementos dentro de las 132 # listas (historial y SAN). Ni el FEN ni esas listas usan ";" o "|", asi que 133 # despues podemos volver a separar sin confusiones. El historial va ULTIMO porque 134 # es "lo que sobra" al final. 135 _state_encode() { 136 # "local IFS='|'" hace que, al expandir un arreglo con [*], los elementos se 137 # peguen con "|" en el medio. Asi "GAME_HIST[*]" queda "jug1|jug2|jug3". 138 local hist pgn; local IFS='|'; hist="${GAME_HIST[*]}"; pgn="${GAME_PGN[*]}" 139 printf '%s;%s;%s;%s;%s;%s;%s;%s;%s' \ 140 "$GAME_FEN" "$GAME_CW" "$GAME_CB" "$GAME_HFM" "$GAME_HLINE" "$GAME_TB" \ 141 "$GAME_STARTFEN" "$pgn" "$hist" 142 } 143 144 # _state_decode: lo inverso. Recibe esa linea y vuelve a llenar las GAME_*. 145 _state_decode() { 146 local p=$1 147 # Truco para ir "comiendo" campos de a uno: 148 # "${p%%;*}" -> lo de ANTES del primer ";" (el campo actual) 149 # "${p#*;}" -> lo de DESPUES del primer ";" (lo que queda por procesar) 150 GAME_FEN="${p%%;*}"; p="${p#*;}" 151 GAME_CW="${p%%;*}"; p="${p#*;}" 152 GAME_CB="${p%%;*}"; p="${p#*;}" 153 GAME_HFM="${p%%;*}"; p="${p#*;}" 154 GAME_HLINE="${p%%;*}"; p="${p#*;}" 155 GAME_TB="${p%%;*}"; p="${p#*;}" 156 GAME_STARTFEN="${p%%;*}"; p="${p#*;}" 157 local pgn="${p%%;*}"; p="${p#*;}" 158 # Volvemos a partir las listas por "|" para reconstruir los arreglos. 159 GAME_PGN=(); [[ -n "$pgn" ]] && IFS='|' read -r -a GAME_PGN <<< "$pgn" 160 GAME_HIST=() 161 [[ -n "$p" ]] && IFS='|' read -r -a GAME_HIST <<< "$p" # lo que sobra es el historial 162 } 163 164 # stage_live_state: copia el estado VIVO del juego (lo que esta pasando ahora) a 165 # las variables GAME_*, para poder reenviarlo si alguien se reconecta (ver 166 # transport_reconnect). Es como sacarle una foto a la partida en curso. 167 stage_live_state() { 168 GAME_FEN="$(get_fen)" 169 GAME_CW=$CLOCK_white; GAME_CB=$CLOCK_black 170 GAME_TB=$ALLOW_TAKEBACK 171 GAME_HFM=$HIST_FULLMOVE; GAME_HLINE="$HIST_LINE" 172 GAME_HIST=("${HISTORY[@]}") 173 GAME_STARTFEN="$PGN_STARTFEN"; GAME_PGN=("${PGN_SAN[@]}") 174 } 175 176 # adopt_game_state: lo contrario. Carga las GAME_* en el motor vivo. Se usa al 177 # empezar a jugar (en play()) y al reconectarse, para adoptar el estado que mando 178 # el otro. El historial de repeticiones se reinicia a solo la posicion actual 179 # (es un detalle menor aceptable cuando hay que resincronizar). 180 adopt_game_state() { 181 setup_fen "$GAME_FEN" 182 POSCOUNT=(); POSCOUNT["$(position_key)"]=1 183 CLOCK_white=$GAME_CW; CLOCK_black=$GAME_CB 184 ALLOW_TAKEBACK=$GAME_TB; UNDO_STACK=() 185 HISTORY=("${GAME_HIST[@]}"); HIST_FULLMOVE=$GAME_HFM; HIST_LINE="$GAME_HLINE" 186 PGN_STARTFEN="$GAME_STARTFEN"; PGN_SAN=("${GAME_PGN[@]}") 187 } 188 189 # --- Exportar / importar PGN ------------------------------------------------ 190 # Exportar lee la partida VIVA (la lista PGN_SAN + PGN_STARTFEN, que mantiene 191 # ches.sh). Importar parsea un PGN pegado y lo deja en las GAME_* (como load_fen_string). 192 193 # pgn_movetext: arma el texto de las jugadas con su numeracion, contando desde 194 # PGN_STARTFEN, y termina con el resultado (ej "1-0", "1/2-1/2" o "*" si sigue). 195 pgn_movetext() { 196 local result=$1 197 # Partimos el FEN de inicio en palabras para saber a quien le tocaba y en que 198 # numero de jugada arrancamos (campos 2 y 6 del FEN, indices 1 y 5). 199 local -a sf=( $PGN_STARTFEN ) 200 local side=${sf[1]:-w} num=${sf[5]:-1} out="" i 201 for (( i=0; i<${#PGN_SAN[@]}; i++ )); do 202 if [[ "$side" == w ]]; then 203 out+="${num}. ${PGN_SAN[i]} "; side=b # "5. e4 " 204 else 205 # Si la partida ARRANCA con una jugada de las negras, hay que escribir 206 # "5... " antes de la jugada. 207 (( i == 0 )) && out+="${num}... " 208 out+="${PGN_SAN[i]} "; side=w; (( num++ )) 209 fi 210 done 211 printf '%s%s\n' "$out" "$result" 212 } 213 214 # export_pgn: escribe un PGN estandar de la partida viva a un archivo. Los textos 215 # entre corchetes son "tags" estandar del formato PGN (van en ingles a proposito, 216 # son parte del formato). Si no arrancamos de la posicion inicial, agregamos los 217 # tags [SetUp]/[FEN] para que el PGN se pueda reproducir igual. 218 export_pgn() { 219 local file=$1 result=${2:-*} 220 mkdir -p "$(dirname "$file")" 221 { 222 printf '[Event "Bash Chess"]\n[Site "LAN"]\n' 223 printf '[Date "%s"]\n[Round "-"]\n' "$(date +%Y.%m.%d)" # "date" pone la fecha de hoy 224 printf '[White "White"]\n[Black "Black"]\n' 225 printf '[Result "%s"]\n' "$result" 226 [[ "$PGN_STARTFEN" != "$START_FEN" ]] && printf '[SetUp "1"]\n[FEN "%s"]\n' "$PGN_STARTFEN" 227 printf '\n' 228 pgn_movetext "$result" 229 } > "$file" 230 } 231 232 # export_fen: escribe la posicion actual como un FEN pelado a un archivo, y ademas 233 # la imprime (para poder copiarla y pegarla en otro lado). 234 export_fen() { 235 local file=$1 fen; fen="$(get_fen)" 236 mkdir -p "$(dirname "$file")" 237 printf '%s\n' "$fen" > "$file" 238 printf '%s' "$fen" 239 } 240 241 # _pgn_body: limpia un PGN dejando SOLO las jugadas (en SAN), separadas por 242 # espacios. Saca: los tags entre corchetes, los comentarios entre llaves, las 243 # variantes entre parentesis, los numeros de jugada y el resultado final. 244 _pgn_body() { 245 set -f # apaga la expansion de comodines: el "*" del resultado NO debe 246 # transformarse en una lista de archivos. (Esta funcion corre dentro 247 # de un $( ), asi que "set -f" solo afecta a esta copia, no al resto.) 248 # "sed" edita texto. Aca, con tres ordenes separadas por ";": 249 # s/\{[^}]*\}//g borra todo lo que este entre llaves { ... } 250 # s/\([^()]*\)//g borra lo que este entre parentesis ( ... ) 251 # /^[[:space:]]*\[/d borra las lineas que empiezan con "[" (los tags) 252 local t; t=$(printf '%s\n' "$1" | sed -E 's/\{[^}]*\}//g; s/\([^()]*\)//g; /^[[:space:]]*\[/d') 253 local tok out="" 254 for tok in $t; do 255 # "${tok##*.}" saca todo hasta el ultimo ".", asi "12." o "12..." quedan 256 # vacios (eran solo numeros de jugada) y "12.e4" quedaria "e4". 257 case "$tok" in *.*) tok="${tok##*.}" ;; esac 258 [[ -z "$tok" ]] && continue 259 # Descartamos el resultado ("1-0", etc.) y los NAG (codigos tipo "$1"). 260 case "$tok" in '1-0'|'0-1'|'1/2-1/2'|'*'|\$*) continue ;; esac 261 out+="$tok " 262 done 263 set +f # volvemos a prender la expansion de comodines 264 printf '%s' "$out" 265 } 266 267 # pgn_looks_valid: chequeo simple de que un PGN pegado tenga al menos algo que 268 # parezca una jugada, antes de intentar reproducirlo. 269 pgn_looks_valid() { 270 local body; body=$(_pgn_body "$1") 271 [[ "$body" =~ [a-hKQRBNO] ]] # tiene alguna letra de columna o de pieza? 272 } 273 274 # load_pgn_string: reproduce un PGN pegado, jugada por jugada, y deja el resultado 275 # en las GAME_* (posicion final, historial, lista SAN, posicion de inicio). 276 # Respeta el tag [FEN] si la partida arranca de una posicion no estandar. Devuelve 277 # 1 si alguna jugada no es legal en su momento (PGN invalido). 278 load_pgn_string() { 279 local text=$1 startfen="$START_FEN" fenline 280 # Buscamos un tag [FEN "..."] para saber desde donde arranca la partida. 281 fenline=$(printf '%s\n' "$text" | grep -iE '^\[FEN ' | head -n1) 282 if [[ -n "$fenline" ]]; then startfen=${fenline#*\"}; startfen=${startfen%%\"*}; fi 283 284 # Ponemos el tablero en la posicion de arranque y reseteamos los contadores. 285 POSCOUNT=(); setup_fen "$startfen"; POSCOUNT["$(position_key)"]=1 286 GAME_PGN=(); GAME_HIST=(); GAME_HFM=0; GAME_HLINE="" 287 local body tok uci mover 288 body=$(_pgn_body "$text") 289 for tok in $body; do 290 # Cada "tok" es una jugada en SAN. La convertimos a UCI buscando entre las 291 # legales. Si no coincide ninguna, el PGN es invalido: devolvemos 1. 292 uci=$(san_to_uci "$tok") || return 1 293 mover=$TURN 294 GAME_PGN+=( "$(move_to_san "$uci")" ) # guardamos la jugada en SAN "canonico" 295 apply_move "$uci" 296 # Reconstruimos el historial con el mismo formato que usa el juego. 297 if [[ "$mover" == white ]]; then 298 GAME_HLINE="$uci"; GAME_HIST[$GAME_HFM]="$GAME_HLINE ..." 299 else 300 GAME_HIST[$GAME_HFM]="${GAME_HLINE:-...} $uci"; ((GAME_HFM++)); GAME_HLINE="" 301 fi 302 done 303 GAME_FEN="$(get_fen)"; GAME_STARTFEN="$startfen" 304 GAME_CW=0; GAME_CB=0; GAME_TB=0 305 TC_ENABLED=0; TC_BASE=0; TC_INC=0 306 return 0 307 }