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

rules.sh (14926B)


      1 #!/usr/bin/env bash
      2 # ============================================================================
      3 #  lib/rules.sh  --  El "cerebro" de las reglas del ajedrez.
      4 # ============================================================================
      5 #
      6 # Este es el archivo mas importante: la AUTORIDAD sobre las reglas. Se encarga de:
      7 #   1) saber si una casilla esta ATACADA por algun color (clave para los jaques);
      8 #   2) filtrar las jugadas pseudo-legales y dejar solo las LEGALES de verdad
      9 #      (las que no dejan a tu propio rey en jaque);
     10 #   3) APLICAR una jugada al tablero, actualizando todo el estado;
     11 #   4) detectar si la partida TERMINO (jaque mate, ahogado, tablas, etc.).
     12 #
     13 # Usa lo de board.sh (el tablero y las coordenadas) y lo de movegen.sh (las
     14 # jugadas posibles de cada pieza).
     15 
     16 # POSCOUNT: cuenta cuantas veces aparecio cada posicion en la partida. Sirve para
     17 # la regla de repeticion (si una posicion se repite, puede haber tablas).
     18 # "declare -A" crea un arreglo ASOCIATIVO: en vez de numerarse 0,1,2..., sus
     19 # casilleros se acceden por un TEXTO (la "huella" de la posicion, ver position_key
     20 # en board.sh). Es como un diccionario: clave -> valor.
     21 declare -A POSCOUNT
     22 
     23 # --- is_square_attacked: esta casilla bajo ataque? --------------------------
     24 # Pregunta: la casilla con indice <idx> (1er argumento), esta atacada por alguna
     25 # pieza del color <by> (2do argumento)? Devuelve exito (0 = "si") o fracaso.
     26 #
     27 # El truco para no revisar TODAS las piezas del rival: nos paramos EN la casilla
     28 # y miramos hacia afuera en cada direccion, preguntando "si fuera un caballo, hay
     29 # un caballo rival a un salto?", "siguiendo la diagonal, hay un alfil o dama?",
     30 # etc. Si encontramos una pieza rival que pueda llegar hasta aca, esta atacada.
     31 is_square_attacked() {
     32     local idx=$1 by=$2
     33     local r=$(( idx / 8 )) c=$(( idx % 8 ))
     34     # Segun el color atacante, definimos como se escribe cada pieza rival
     35     # (mayuscula para blancas, minuscula para negras).
     36     local pawn knight bish rook queen king
     37     if [[ "$by" == white ]]; then
     38         pawn=P knight=N bish=B rook=R queen=Q king=K
     39     else
     40         pawn=p knight=n bish=b rook=r queen=q king=k
     41     fi
     42 
     43     # Ataque de PEON: un peon rival que ataca esta casilla esta una fila "atras"
     44     # (los peones capturan en diagonal hacia adelante, asi que miramos al reves).
     45     local prow
     46     [[ "$by" == white ]] && prow=$(( r + 1 )) || prow=$(( r - 1 ))
     47     if (( prow >= 0 && prow < 8 )); then
     48         local dc nc
     49         for dc in -1 1; do                 # las dos diagonales
     50             nc=$(( c + dc ))
     51             (( nc < 0 || nc > 7 )) && continue
     52             [[ "${BOARD[$(( prow*8+nc ))]}" == "$pawn" ]] && return 0
     53         done
     54     fi
     55 
     56     # Ataque de CABALLO: probamos sus 8 saltos en "L". El arreglo "koff" tiene los
     57     # pares (dr,dc) uno atras del otro: -2 -1, -2 1, -1 -2, ... Por eso el for va
     58     # de a 2 (i+=2) y usa koff[i] y koff[i+1].
     59     local koff=(-2 -1 -2 1 -1 -2 -1 2 1 -2 1 2 2 -1 2 1)
     60     local i nr nc2
     61     for (( i=0; i<16; i+=2 )); do
     62         nr=$(( r + koff[i] )); nc2=$(( c + koff[i+1] ))
     63         (( nr<0 || nr>7 || nc2<0 || nc2>7 )) && continue
     64         [[ "${BOARD[$(( nr*8+nc2 ))]}" == "$knight" ]] && return 0
     65     done
     66 
     67     # Ataque de REY: las 8 casillas pegadas (un rey rival al lado nos ataca).
     68     local kdir=(-1 -1 -1 0 -1 1 0 -1 0 1 1 -1 1 0 1 1)
     69     for (( i=0; i<16; i+=2 )); do
     70         nr=$(( r + kdir[i] )); nc2=$(( c + kdir[i+1] ))
     71         (( nr<0 || nr>7 || nc2<0 || nc2>7 )) && continue
     72         [[ "${BOARD[$(( nr*8+nc2 ))]}" == "$king" ]] && return 0
     73     done
     74 
     75     # Ataques en DIAGONAL (alfil o dama): avanzamos por cada diagonal hasta chocar
     76     # algo. Si lo primero que aparece es un alfil o dama rival, estamos atacados;
     77     # si es cualquier otra pieza, esa direccion queda tapada (break).
     78     local ddir=(-1 -1 -1 1 1 -1 1 1)
     79     local dr dcv
     80     for (( i=0; i<8; i+=2 )); do
     81         dr=${ddir[i]}; dcv=${ddir[i+1]}
     82         nr=$(( r + dr )); nc2=$(( c + dcv ))
     83         while (( nr>=0 && nr<8 && nc2>=0 && nc2<8 )); do
     84             local p="${BOARD[$(( nr*8+nc2 ))]}"
     85             if [[ "$p" != "." ]]; then
     86                 [[ "$p" == "$bish" || "$p" == "$queen" ]] && return 0
     87                 break
     88             fi
     89             nr=$(( nr + dr )); nc2=$(( nc2 + dcv ))
     90         done
     91     done
     92 
     93     # Ataques en RECTA (torre o dama): igual que arriba pero en horizontal/vertical.
     94     local odir=(-1 0 1 0 0 -1 0 1)
     95     for (( i=0; i<8; i+=2 )); do
     96         dr=${odir[i]}; dcv=${odir[i+1]}
     97         nr=$(( r + dr )); nc2=$(( c + dcv ))
     98         while (( nr>=0 && nr<8 && nc2>=0 && nc2<8 )); do
     99             local p="${BOARD[$(( nr*8+nc2 ))]}"
    100             if [[ "$p" != "." ]]; then
    101                 [[ "$p" == "$rook" || "$p" == "$queen" ]] && return 0
    102                 break
    103             fi
    104             nr=$(( nr + dr )); nc2=$(( nc2 + dcv ))
    105         done
    106     done
    107 
    108     return 1   # no encontramos ningun atacante: la casilla NO esta atacada
    109 }
    110 
    111 # find_king: busca el rey del color dado y devuelve su indice (o -1 si no esta).
    112 find_king() {
    113     local color=$1 target i
    114     [[ "$color" == white ]] && target=K || target=k
    115     for (( i=0; i<64; i++ )); do
    116         [[ "${BOARD[$i]}" == "$target" ]] && { echo "$i"; return; }
    117     done
    118     echo -1
    119 }
    120 
    121 # in_check: el rey del color dado esta en jaque? (o sea, su casilla esta atacada
    122 # por el color contrario).
    123 in_check() {
    124     local color=$1 king enemy
    125     king=$(find_king "$color"); enemy=$(other_color "$color")
    126     is_square_attacked "$king" "$enemy"
    127 }
    128 
    129 # --- _apply_raw: mover piezas en el tablero, sin tocar el resto del estado ----
    130 # Esta funcion SOLO modifica el arreglo BOARD (mueve la pieza). No actualiza el
    131 # turno, ni los enroques, ni los contadores. Se usa en dos lugares: dentro de
    132 # apply_move (la version completa), y en el filtro de jugadas legales, donde
    133 # probamos una jugada, miramos si el rey queda en jaque, y deshacemos.
    134 # Maneja los 3 casos especiales: captura al paso, mover la torre en el enroque, y
    135 # la coronacion del peon.
    136 _apply_raw() {
    137     local move=$1
    138     local from=${move:0:2} to=${move:2:2} promo=${move:4:1}   # parto el texto de la jugada
    139     local fi ti; fi=$(alg_to_idx "$from"); ti=$(alg_to_idx "$to")
    140     local piece="${BOARD[$fi]}" pl="${BOARD[$fi]}"
    141     pl="${pl,,}"   # version en minuscula, para preguntar el TIPO sin importar color
    142     local fr=$(( fi/8 )) fc=$(( fi%8 )) tr=$(( ti/8 )) tc=$(( ti%8 ))
    143 
    144     # Captura al paso: un peon va en diagonal a una casilla VACIA. Eso solo pasa
    145     # en la captura al paso, donde el peon capturado esta al costado (misma fila
    146     # de origen, columna de destino). Lo borramos.
    147     if [[ "$pl" == p && $fc -ne $tc && "${BOARD[$ti]}" == "." ]]; then
    148         BOARD[$(( fr*8+tc ))]="."
    149     fi
    150 
    151     # Enroque: el rey se mueve dos columnas. Ademas de mover el rey (mas abajo),
    152     # tenemos que reubicar la TORRE.
    153     if [[ "$pl" == k ]]; then
    154         local diff=$(( tc - fc )); (( diff < 0 )) && diff=$(( -diff ))   # distancia (en valor absoluto)
    155         if (( diff == 2 )); then
    156             if (( tc == 6 )); then   # enroque corto: torre de la columna h a la f
    157                 BOARD[$(( fr*8+5 ))]="${BOARD[$(( fr*8+7 ))]}"; BOARD[$(( fr*8+7 ))]="."
    158             else                     # enroque largo (tc == 2): torre de la a a la d
    159                 BOARD[$(( fr*8+3 ))]="${BOARD[$(( fr*8+0 ))]}"; BOARD[$(( fr*8+0 ))]="."
    160             fi
    161         fi
    162     fi
    163 
    164     BOARD[$fi]="."   # la casilla de origen queda vacia
    165     if [[ -n "$promo" ]]; then
    166         # Coronacion: en el destino ponemos la pieza nueva, con el color correcto
    167         # ("${promo^^}" la pasa a MAYUSCULA para blancas; "${promo,,}" a minuscula).
    168         if [[ "$piece" =~ [A-Z] ]]; then BOARD[$ti]="${promo^^}"; else BOARD[$ti]="${promo,,}"; fi
    169     else
    170         BOARD[$ti]="$piece"   # jugada normal: la pieza va al destino
    171     fi
    172 }
    173 
    174 # _strip_castle: saca un permiso de enroque de la variable CASTLE. "${CASTLE//X/}"
    175 # reemplaza todas las "X" por nada. Si CASTLE queda vacio, lo dejamos en "-".
    176 _strip_castle() {
    177     CASTLE="${CASTLE//$1/}"
    178     [[ -z "$CASTLE" ]] && CASTLE="-"
    179 }
    180 
    181 # --- apply_move: aplicar una jugada COMPLETA, con toda la contabilidad --------
    182 # Esta es la version "de verdad" que se usa al jugar. Ademas de mover la pieza,
    183 # actualiza: derechos de enroque, casilla al paso, el reloj de la regla de 50
    184 # movimientos, el numero de jugada, de quien es el turno, y la cuenta de
    185 # repeticiones. IMPORTANTE: confia en que la jugada es legal (no la valida).
    186 apply_move() {
    187     local move=$1
    188     local from=${move:0:2} to=${move:2:2}
    189     local fi ti; fi=$(alg_to_idx "$from"); ti=$(alg_to_idx "$to")
    190     local piece="${BOARD[$fi]}" pl
    191     pl="${piece,,}"
    192     local target="${BOARD[$ti]}"
    193     local fr=$(( fi/8 )) fc=$(( fi%8 )) tr=$(( ti/8 )) tc=$(( ti%8 ))
    194 
    195     # Averiguamos si fue jugada de peon y/o captura (lo necesita el reloj de 50).
    196     local is_pawn=0 is_capture=0
    197     [[ "$pl" == p ]] && is_pawn=1
    198     [[ "$target" != "." ]] && is_capture=1
    199     # La captura al paso tambien cuenta como captura, aunque la casilla destino
    200     # estaba vacia.
    201     [[ "$pl" == p && $fc -ne $tc && "$target" == "." ]] && is_capture=1
    202 
    203     # Nueva casilla "al paso": solo se habilita cuando un peon avanza DOS casillas.
    204     # En ese caso, la casilla al paso es la del medio del salto.
    205     local new_ep="-"
    206     if [[ "$pl" == p ]]; then
    207         local d=$(( tr - fr )); (( d < 0 )) && d=$(( -d ))
    208         if (( d == 2 )); then new_ep=$(idx_to_alg $(( (fr+tr)/2 * 8 + fc ))); fi
    209     fi
    210 
    211     # Actualizar derechos de enroque: si se movio el rey, ese color pierde ambos
    212     # enroques. Si se movio (o capturaron) una torre de esquina, se pierde el
    213     # enroque de ESE lado. Los indices 56,63,0,7 son las cuatro esquinas.
    214     [[ "$piece" == K ]] && { _strip_castle K; _strip_castle Q; }
    215     [[ "$piece" == k ]] && { _strip_castle k; _strip_castle q; }
    216     case "$fi" in 56) _strip_castle Q;; 63) _strip_castle K;; 0) _strip_castle q;; 7) _strip_castle k;; esac
    217     case "$ti" in 56) _strip_castle Q;; 63) _strip_castle K;; 0) _strip_castle q;; 7) _strip_castle k;; esac
    218 
    219     _apply_raw "$move"   # recien aca movemos las piezas de verdad
    220 
    221     EP="$new_ep"
    222     # Reloj de 50 movimientos: se reinicia a 0 con cada movimiento de peon o
    223     # captura; si no, suma 1. (Al llegar a 100 medias jugadas se pueden pedir tablas.)
    224     if (( is_pawn || is_capture )); then HALFMOVE=0; else HALFMOVE=$(( HALFMOVE + 1 )); fi
    225     # El numero de jugada completa sube despues de que mueven las NEGRAS.
    226     [[ "$TURN" == black ]] && FULLMOVE=$(( FULLMOVE + 1 ))
    227     TURN=$(other_color "$TURN")   # pasa el turno al otro color
    228 
    229     # Anotamos esta posicion en POSCOUNT (sumamos 1 a su cuenta). "${POSCOUNT[k]:-0}"
    230     # dice "el valor guardado en k, o 0 si todavia no hay nada".
    231     local key; key=$(position_key)
    232     POSCOUNT["$key"]=$(( ${POSCOUNT["$key"]:-0} + 1 ))
    233 }
    234 
    235 # --- legal_moves: las jugadas REALMENTE legales -----------------------------
    236 # Toma cada jugada pseudo-legal y descarta las que dejarian a tu propio rey en
    237 # jaque. El metodo: por cada jugada, GUARDAMOS una copia del tablero, la
    238 # aplicamos "en crudo", miramos si nuestro rey quedaria atacado; si NO, la jugada
    239 # es legal y la imprimimos. En cualquier caso, RESTAURAMOS el tablero original.
    240 legal_moves() {
    241     local m saved king_idx enemy
    242     enemy=$(other_color "$TURN")
    243     # "while read m ... done < <(pseudo_moves)" recorre, una por una, las lineas
    244     # que imprime pseudo_moves. El "< <(...)" se llama "process substitution":
    245     # hace que la salida de pseudo_moves se lea como si fuera un archivo.
    246     while IFS= read -r m; do
    247         [[ -z "$m" ]] && continue
    248         saved=("${BOARD[@]}")          # copia de respaldo del tablero entero
    249         _apply_raw "$m"                # probamos la jugada
    250         king_idx=$(find_king "$TURN")
    251         # Si nuestro rey NO queda atacado, la jugada es legal -> la imprimimos.
    252         is_square_attacked "$king_idx" "$enemy" || echo "$m"
    253         BOARD=("${saved[@]}")          # deshacemos (restauramos el respaldo)
    254     done < <(pseudo_moves)
    255 }
    256 
    257 # --- has_insufficient_material: queda material para dar mate? ----------------
    258 # Devuelve "si" (tablas) cuando NINGUN bando tiene material suficiente para dar
    259 # jaque mate (por ejemplo, rey solo contra rey, o rey y alfil contra rey).
    260 has_insufficient_material() {
    261     local i p
    262     local minors=0 wb=0 bb=0 wbsq="" bbsq=""   # contamos piezas "menores" (alfiles/caballos)
    263     for (( i=0; i<64; i++ )); do
    264         p="${BOARD[$i]}"
    265         case "$p" in
    266             P|p|R|r|Q|q) return 1 ;;   # si hay peon, torre o dama: SI hay material
    267             N|n) ((minors++)) ;;       # caballo: suma una pieza menor
    268             # Alfil: suma una menor y anotamos el color de su casilla ((fila+col) par/impar).
    269             B) ((minors++)); ((wb++)); wbsq="$(( (i/8 + i%8) % 2 ))" ;;
    270             b) ((minors++)); ((bb++)); bbsq="$(( (i/8 + i%8) % 2 ))" ;;
    271         esac
    272     done
    273     (( minors <= 1 )) && return 0                        # K, K+A o K+C contra K: tablas
    274     (( wb == 1 && bb == 1 && wbsq == bbsq )) && return 0 # K+A vs K+A con alfiles del mismo color: tablas
    275     return 1
    276 }
    277 
    278 # --- Tablas que se PIDEN (no son automaticas) -------------------------------
    279 # Estas dos no terminan la partida solas: le aparece una opcion a quien mueve
    280 # para reclamarlas, y puede pedirlas o seguir jugando.
    281 #   - Regla de 50 movimientos: 100 medias jugadas sin mover peon ni capturar.
    282 #   - Triple repeticion: la posicion actual ya aparecio 3 o mas veces.
    283 can_claim_draw50()     { (( HALFMOVE >= 100 )); }
    284 can_claim_threefold()  { (( ${POSCOUNT["$(position_key)"]:-0} >= 3 )); }
    285 
    286 # --- game_status: termino la partida? ---------------------------------------
    287 # Imprime uno de estos estados, mirando la posicion del que tiene que mover:
    288 #   ongoing       -> la partida sigue
    289 #   checkmate     -> jaque mate
    290 #   stalemate     -> ahogado (no hay jugadas legales pero el rey NO esta en jaque)
    291 #   draw-fivefold -> tablas automaticas por quintuple repeticion
    292 #   draw-75move   -> tablas automaticas por la regla de 75 movimientos
    293 #   draw-material -> tablas por material insuficiente
    294 # Las tablas que se PIDEN (50 movimientos y triple repeticion) NO se reportan
    295 # aca; sus versiones AUTOMATICAS de FIDE (quintuple repeticion y 75 movimientos)
    296 # si terminan la partida solas.
    297 game_status() {
    298     # "legal_moves | grep -c ." cuenta cuantas lineas (jugadas legales) hay.
    299     local n; n=$(legal_moves | grep -c .)
    300     if (( n == 0 )); then
    301         # Sin jugadas legales: si el rey esta en jaque es mate; si no, es ahogado.
    302         if in_check "$TURN"; then echo checkmate; else echo stalemate; fi
    303         return
    304     fi
    305     local key; key=$(position_key)
    306     (( ${POSCOUNT["$key"]:-0} >= 5 )) && { echo draw-fivefold; return; }   # automatica
    307     (( HALFMOVE >= 150 ))             && { echo draw-75move; return; }     # automatica
    308     has_insufficient_material && { echo draw-material; return; }
    309     echo ongoing
    310 }