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

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 }