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 }