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

transport.sh (24864B)


      1 #!/usr/bin/env bash
      2 # ============================================================================
      3 #  lib/transport.sh  --  De DONDE viene cada jugada (la "fuente de jugadas").
      4 # ============================================================================
      5 #
      6 # Idea central del proyecto: el motor de ajedrez (las reglas) NO sabe ni le
      7 # importa si jugas vos en el teclado, otra persona por red, o la computadora.
      8 # Cada turno, el juego pregunta: "es mi turno?". Si lo es, pide una jugada a la
      9 # interfaz (el menu visual). Si no, espera la jugada del otro lado. El mismo motor
     10 # corre en las dos computadoras; lo unico que cambia es la FUENTE de la jugada.
     11 # Este archivo es justamente esa "fuente de jugadas" y todo lo de la red.
     12 #
     13 #   TRANSPORT_MODE : hotseat | p2p | server | ai   (en que modo estamos jugando)
     14 #   LOCAL_COLOR    : white | black | both   (both = hotseat, manejamos los dos lados)
     15 #
     16 # Usa protocol.sh (mandar/recibir por red) y ui.sh (el menu para elegir jugada).
     17 
     18 TRANSPORT_MODE=hotseat   # por defecto, dos personas en la misma compu
     19 LOCAL_COLOR=both         # por defecto, manejamos ambos colores
     20 
     21 # Datos de la conexion, que recordamos para poder RECONECTAR si se corta la red
     22 # (ver transport_reconnect y el modo "rejoin").
     23 #   NET_ROLE : host | join | server   (que lado de la conexion somos)
     24 #   NET_HOST : direccion del otro / del relay al que nos conectamos
     25 #   NET_PORT : puerto donde escuchamos (host) o al que nos conectamos
     26 #   MY_COLOR : nuestro color, que se mantiene aunque nos reconectemos
     27 NET_ROLE=""
     28 NET_HOST=""
     29 NET_PORT=""
     30 MY_COLOR=""
     31 
     32 # --- El reloj de ajedrez (compartido con ui.sh y con el bucle de ches.sh) ------
     33 #   TC_ENABLED : 1 si hay reloj, 0 si se juega sin tiempo
     34 #   TC_BASE    : tiempo base por jugador, en segundos
     35 #   TC_INC     : incremento "Fischer" que se suma despues de cada jugada
     36 #   CLOCK_*    : segundos que le quedan a cada lado
     37 # El control de tiempo se serializa para la red como un texto: "off" o "BASE:INC".
     38 TC_ENABLED=0
     39 TC_BASE=0
     40 TC_INC=0
     41 CLOCK_white=0
     42 CLOCK_black=0
     43 
     44 # Se permiten retrocesos (deshacer la ultima jugada de comun acuerdo)? Lo elige
     45 # quien arma la partida (con tb_pick) y viaja al otro dentro del estado (GAME_TB).
     46 # El menu de pausa solo ofrece "retroceder" si esto esta activado.
     47 ALLOW_TAKEBACK=0
     48 
     49 # --- tc_pick: elegir el control de tiempo con un menu (fzf) ------------------
     50 # "fzf" es un programa que muestra una lista y deja elegir una opcion con las
     51 # flechas y Enter. Le damos las opciones con "printf ... | fzf". Lo que la persona
     52 # elige queda en "sel". (Los textos quedan en ingles: es la interfaz.)
     53 tc_pick() {
     54     local sel
     55     sel=$(printf '%s\n' \
     56         "1 min" "3 min" "5 min" "10 min" "15 min" \
     57         "3 + 2  (3 min, 2s/move)" "5 + 5  (5 min, 5s/move)" \
     58         "10 + 5  (10 min, 5s/move)" "15 + 10  (15 min, 10s/move)" \
     59         "No time (unlimited)" "Custom..." \
     60       | fzf --reverse --border --height=60% --prompt="Time control: " \
     61             --header=$'  Choose a clock for this game')
     62     # Segun lo elegido, prendemos el reloj y fijamos el tiempo base y el incremento.
     63     case "$sel" in
     64         "1 min")    TC_ENABLED=1; TC_BASE=60;  TC_INC=0 ;;
     65         "3 min")    TC_ENABLED=1; TC_BASE=180; TC_INC=0 ;;
     66         "5 min")    TC_ENABLED=1; TC_BASE=300; TC_INC=0 ;;
     67         "10 min")   TC_ENABLED=1; TC_BASE=600; TC_INC=0 ;;
     68         "15 min")   TC_ENABLED=1; TC_BASE=900; TC_INC=0 ;;
     69         "3 + 2"*)   TC_ENABLED=1; TC_BASE=180; TC_INC=2 ;;
     70         "5 + 5"*)   TC_ENABLED=1; TC_BASE=300; TC_INC=5 ;;
     71         "10 + 5"*)  TC_ENABLED=1; TC_BASE=600; TC_INC=5 ;;
     72         "15 + 10"*) TC_ENABLED=1; TC_BASE=900; TC_INC=10 ;;
     73         "Custom..."*) _tc_custom ;;
     74         *)          TC_ENABLED=0; TC_BASE=0;   TC_INC=0 ;;   # Sin tiempo / cancelado
     75     esac
     76 }
     77 
     78 # tb_pick: elegir si se permiten retrocesos. Deja ALLOW_TAKEBACK en 1 o 0.
     79 tb_pick() {
     80     local sel
     81     sel=$(printf '%s\n' \
     82         "No  — moves are final" \
     83         "Yes — allow takebacks (undo last move by agreement)" \
     84       | fzf --reverse --border --height=30% --prompt="Takebacks: " \
     85             --header=$'  Allow taking moves back this game?')
     86     [[ "$sel" == Yes* ]] && ALLOW_TAKEBACK=1 || ALLOW_TAKEBACK=0
     87 }
     88 
     89 # _tc_custom: control de tiempo a medida. Pregunta minutos e incremento por
     90 # teclado. "read -p" muestra un texto y espera lo que escriban; "</dev/tty" lee
     91 # directo del teclado real.
     92 _tc_custom() {
     93     local mins inc
     94     read -r -p "  Base minutes per side [5]: " mins </dev/tty
     95     read -r -p "  Increment seconds per move [0]: " inc </dev/tty
     96     # "[[ "$mins" =~ ^[0-9]+$ ]]" chequea que sea un numero. Si no, usa el valor
     97     # por defecto (5 minutos / 0 de incremento).
     98     [[ "$mins" =~ ^[0-9]+$ ]] || mins=5
     99     [[ "$inc"  =~ ^[0-9]+$ ]] || inc=0
    100     if (( mins == 0 && inc == 0 )); then TC_ENABLED=0; TC_BASE=0; TC_INC=0; return; fi
    101     TC_ENABLED=1; TC_BASE=$(( mins * 60 )); TC_INC=$inc   # pasamos minutos a segundos
    102 }
    103 
    104 # Convertir el control de tiempo a texto (para la red) y de vuelta.
    105 tc_encode() { if (( TC_ENABLED )); then printf '%s:%s' "$TC_BASE" "$TC_INC"; else printf 'off'; fi; }
    106 tc_decode() {
    107     local tok=$1
    108     # Si es "off", vacio, o no tiene ":", no hay reloj.
    109     if [[ "$tok" == off || -z "$tok" || "$tok" != *:* ]]; then
    110         TC_ENABLED=0; TC_BASE=0; TC_INC=0; return
    111     fi
    112     # "${tok%%:*}" es lo de antes del ":", "${tok##*:}" lo de despues.
    113     TC_ENABLED=1; TC_BASE="${tok%%:*}"; TC_INC="${tok##*:}"
    114 }
    115 
    116 # _push_state_to_peer: el lider (o el sobreviviente de una caida) le manda al otro
    117 # la configuracion + el estado actual. El otro juega el color contrario al nuestro.
    118 _push_state_to_peer() {
    119     net_send "HELLO host $(tc_encode)"
    120     net_send "COLOR $(other_color "$MY_COLOR")"
    121     net_send "STATE $(_state_encode)"
    122 }
    123 
    124 # _adopt_state_from_peer: el que se conecta espera el COLOR y el STATE que manda el
    125 # lider, fija su color, y carga la partida en GAME_*. Devuelve 1 si se corto.
    126 _adopt_state_from_peer() {
    127     local got_color=0 got_state=0 line
    128     # Seguimos esperando hasta tener AMBAS cosas (color y estado).
    129     while (( !got_color || !got_state )); do
    130         line=$(net_recv) || return 1
    131         case "$line" in
    132             "COLOR "*)      LOCAL_COLOR="${line#COLOR }"; got_color=1 ;;
    133             "STATE "*)      _state_decode "${line#STATE }"; got_state=1 ;;
    134             "HELLO host "*) tc_decode "${line#HELLO host }" ;;
    135         esac
    136     done
    137     return 0
    138 }
    139 
    140 # _net_show_ip: imprime la IP de esta maquina en la red (para decirsela al otro).
    141 # "hostname -I" lista las IP; "awk '{print $1}'" se queda con la primera.
    142 _net_show_ip() {
    143     local ip; ip=$(hostname -I 2>/dev/null | awk '{print $1}')
    144     [[ -n "$ip" ]] && echo "  Your IP: ${ip}"
    145 }
    146 
    147 # --- transport_init: preparar la partida segun el modo elegido --------------
    148 # Modos:
    149 #   hotseat                dos personas en la misma compu
    150 #   ai                     contra la computadora (Stockfish)
    151 #   host <puerto>          hacemos de servidor (jugamos blancas)
    152 #   join <host> <puerto>   nos conectamos a un servidor (jugamos negras)
    153 #   server <host> <puerto> jugamos via un relay (el color se negocia)
    154 #   rejoin <host> <puerto> reconectarse a una partida en curso
    155 transport_init() {
    156     local mode=$1; shift
    157     case "$mode" in
    158         hotseat)
    159             TRANSPORT_MODE=hotseat
    160             LOCAL_COLOR=both
    161             _leader_setup                  # reloj + posicion inicial o cargada
    162             ;;
    163         ai)
    164             TRANSPORT_MODE=ai
    165             # Sin el motor instalado no se puede jugar contra la maquina.
    166             if ! ai_available; then
    167                 echo "Stockfish engine not found." >&2
    168                 echo "Install it with:  ./bin/get-stockfish.sh   (or: pacman -S stockfish)" >&2
    169                 exit 1
    170             fi
    171             ai_pick_color                  # vos = LOCAL_COLOR, el motor = AI_COLOR
    172             ai_pick_level                  # dificultad -> nivel/Elo + tiempo de pensar
    173             _leader_setup                  # reloj + retrocesos + posicion
    174             ai_init || { echo "Could not start the engine." >&2; exit 1; }
    175             echo "You are ${LOCAL_COLOR^^} vs Stockfish (${AI_LABEL})."
    176             ;;
    177         aivai)
    178             # Modo espectador: Stockfish juega los dos bandos con el mismo nivel.
    179             TRANSPORT_MODE=aivai
    180             LOCAL_COLOR=none               # ningun bando es "local"; transport_is_local_turn siempre devuelve falso
    181             if ! ai_available; then
    182                 echo "Stockfish engine not found." >&2
    183                 echo "Install it with:  ./bin/get-stockfish.sh   (or: pacman -S stockfish)" >&2
    184                 exit 1
    185             fi
    186             ai_pick_level
    187             ai_pick_spectate_speed
    188             _leader_setup
    189             ai_init || { echo "Could not start the engine." >&2; exit 1; }
    190             echo "AI vs AI — Stockfish (${AI_LABEL}) plays both sides."
    191             ;;
    192         host)
    193             TRANSPORT_MODE=p2p
    194             local port=$1
    195             NET_ROLE=host; NET_PORT=$port; MY_COLOR=white
    196             _leader_setup                  # el host arma la partida para los dos
    197             echo "Waiting for opponent to connect on port ${port}..."
    198             # Arrancamos ncat ESCUCHANDO en el puerto, como un coproceso en paralelo.
    199             coproc NET { ncat -l "$port"; }
    200             NET_R=${NET[0]}; NET_W=${NET[1]}   # guardamos los canales de lectura/escritura
    201             # net_recv falla (EOF) si ncat no pudo "agarrar" el puerto, casi
    202             # siempre porque ya hay otro juego usandolo. NO seguimos de largo hacia
    203             # una partida fantasma para una sola persona: avisamos y cortamos.
    204             if ! net_recv >/dev/null; then  # esperamos el HELLO del que se conecta
    205                 echo "Could not listen on port ${port} — it may already be in use." >&2
    206                 echo "Pick another port, or stop the other game first (see: ss -ltnp | grep ${port})." >&2
    207                 transport_close; exit 1
    208             fi
    209             LOCAL_COLOR=white
    210             _push_state_to_peer
    211             echo "Opponent connected. You are WHITE."
    212             ;;
    213         join)
    214             TRANSPORT_MODE=p2p
    215             local host=$1 port=$2
    216             NET_ROLE=join; NET_HOST=$host; NET_PORT=$port
    217             echo "Connecting to ${host}:${port}..."
    218             coproc NET { ncat "$host" "$port"; }   # ncat CONECTANDOSE al host
    219             NET_R=${NET[0]}; NET_W=${NET[1]}
    220             net_send "HELLO join"
    221             if ! _adopt_state_from_peer; then   # recibimos COLOR + STATE del host
    222                 echo "Could not connect to ${host}:${port}." >&2
    223                 transport_close; exit 1
    224             fi
    225             MY_COLOR=$LOCAL_COLOR
    226             echo "Connected. You are ${LOCAL_COLOR^^}."
    227             ;;
    228         rejoin)
    229             # Reconectarse a un sobreviviente que esta esperando (ver transport_reconnect).
    230             TRANSPORT_MODE=p2p
    231             local host=$1 port=$2
    232             NET_ROLE=join; NET_HOST=$host; NET_PORT=$port
    233             echo "Rejoining game at ${host}:${port}..."
    234             coproc NET { ncat "$host" "$port"; }
    235             NET_R=${NET[0]}; NET_W=${NET[1]}
    236             net_send "HELLO rejoin"
    237             if ! _adopt_state_from_peer; then
    238                 echo "Could not rejoin ${host}:${port}." >&2
    239                 transport_close; exit 1
    240             fi
    241             MY_COLOR=$LOCAL_COLOR
    242             echo "Rejoined. You are ${LOCAL_COLOR^^}."
    243             ;;
    244         server)
    245             TRANSPORT_MODE=server
    246             local host=$1 port=$2
    247             NET_ROLE=server; NET_HOST=$host; NET_PORT=$port
    248             echo "Connecting to relay ${host}:${port}..."
    249             coproc NET { ncat "$host" "$port"; }
    250             NET_R=${NET[0]}; NET_W=${NET[1]}
    251             _server_handshake
    252             MY_COLOR=$LOCAL_COLOR
    253             _server_setup                  # las blancas eligen reloj + estado; las negras reciben
    254             echo "Joined game. You are ${LOCAL_COLOR^^}."
    255             ;;
    256         *)
    257             echo "Unknown transport mode: $mode" >&2; return 1 ;;
    258     esac
    259 }
    260 
    261 # _server_handshake: en modo relay, ningun lado es "el servidor", asi que hay que
    262 # negociar quien juega cada color. Cada uno se inventa un numero al azar y lo
    263 # anuncia hasta escuchar el del otro; el numero mas alto juega blancas.
    264 _server_handshake() {
    265     local myid peerid="" line tries=0
    266     myid=$(( RANDOM * 32768 + RANDOM ))   # un numero grande al azar
    267     while [[ -z "$peerid" ]]; do
    268         net_send "HELLO $myid"
    269         line=$(net_recv_timeout 1) || line=""
    270         case "$line" in
    271             "HELLO "*)
    272                 local pid="${line#HELLO }"
    273                 if [[ "$pid" == "$myid" ]]; then
    274                     myid=$(( RANDOM * 32768 + RANDOM ))   # casualidad: mismo numero, lo cambiamos
    275                 else
    276                     peerid="$pid"
    277                 fi
    278                 ;;
    279         esac
    280         # Si pasamos 300 intentos sin respuesta, nos rendimos.
    281         (( ++tries > 300 )) && { echo "Handshake timed out." >&2; exit 1; }
    282     done
    283     # Nos aseguramos de que el otro tambien nos escuche antes de seguir.
    284     local i; for i in 1 2 3 4 5; do net_send "HELLO $myid"; done
    285     if (( myid > peerid )); then LOCAL_COLOR=white; else LOCAL_COLOR=black; fi
    286 }
    287 
    288 # De donde arranca la partida, segun lo que haya pedido ches.sh antes de transport_init
    289 # (desde el menu de "resume" o los comandos resume/fen/pgn). Si los tres estan
    290 # vacios, es una partida nueva.
    291 RESUME_FILE=""
    292 RESUME_FEN=""
    293 RESUME_PGN=""
    294 
    295 # _leader_setup: arma la partida que va a jugar el lider (el que inicia el hotseat,
    296 # el host, o las blancas en el relay): un PGN pegado, un FEN pegado, un archivo
    297 # guardado, o si no, una partida nueva con reloj. Llena las variables GAME_*.
    298 _leader_setup() {
    299     if [[ -n "$RESUME_PGN" ]]; then
    300         load_pgn_string "$RESUME_PGN"
    301         echo "Started from pasted PGN."
    302     elif [[ -n "$RESUME_FEN" ]]; then
    303         load_fen_string "$RESUME_FEN"
    304         echo "Started from pasted FEN."
    305     elif [[ -n "$RESUME_FILE" ]]; then
    306         load_game "$RESUME_FILE"           # tambien fija el control de tiempo
    307         echo "Resumed from ${RESUME_FILE}."
    308     else
    309         tc_pick
    310         tb_pick
    311         game_state_fresh
    312     fi
    313 }
    314 
    315 # _server_setup (relay): las blancas (las decide el handshake) arman la partida y
    316 # anuncian el tiempo y el estado; las negras se quedan esperando esos datos.
    317 _server_setup() {
    318     if [[ "$LOCAL_COLOR" == white ]]; then
    319         _leader_setup
    320         local i
    321         # Lo mandamos 3 veces por las dudas (en un relay se puede perder algun mensaje).
    322         for i in 1 2 3; do
    323             net_send "TIME $(tc_encode)"
    324             net_send "STATE $(_state_encode)"
    325         done
    326     else
    327         local line tok got_time=0 got_state=0 tries=0
    328         while (( !got_time || !got_state )); do
    329             line=$(net_recv_timeout 2) || { (( ++tries > 120 )) && break; continue; }
    330             case "$line" in
    331                 "TIME "*)  read -r _ tok <<<"$line"; tc_decode "$tok"; got_time=1 ;;
    332                 "STATE "*) _state_decode "${line#STATE }"; got_state=1 ;;
    333             esac
    334         done
    335     fi
    336 }
    337 
    338 # transport_is_local_turn: el que tiene que mover, se controla en ESTA compu?
    339 # Es verdad en hotseat (manejamos los dos), o si el color que mueve es el nuestro.
    340 transport_is_local_turn() {
    341     [[ "$LOCAL_COLOR" == both || "$LOCAL_COLOR" == "$TURN" ]]
    342 }
    343 
    344 # _negotiate_draw: ofrecer tablas y avisar si el otro acepto (0) o rechazo (1).
    345 # En hotseat se le pregunta a la otra persona ahi mismo; en red se manda DRAWOFFER
    346 # y se espera el DRAWYES/DRAWNO. La parte de RED vive aca, no en la interfaz.
    347 _negotiate_draw() {
    348     [[ "$TRANSPORT_MODE" == ai ]] && return 1  # la maquina sigue jugando (la oferta no se ofrece)
    349     if [[ "$LOCAL_COLOR" == both ]]; then
    350         ui_confirm_draw; return $?            # hotseat: preguntamos en esta compu
    351     fi
    352     net_send "DRAWOFFER"
    353     local line
    354     while line=$(net_recv); do
    355         case "$line" in
    356             DRAWYES) return 0 ;;
    357             DRAWNO)  return 1 ;;
    358         esac
    359     done
    360     return 1                                   # el otro desaparecio
    361 }
    362 
    363 # _negotiate_takeback: pedir un retroceso y avisar si el otro acepto (0) o no (1).
    364 # En hotseat (y contra la maquina) se acepta al toque; en red se manda TAKEBACK y
    365 # se espera el TBYES/TBNO.
    366 _negotiate_takeback() {
    367     [[ "$LOCAL_COLOR" == both || "$TRANSPORT_MODE" == ai ]] && return 0   # local: hacelo nomas
    368     net_send "TAKEBACK"
    369     local line
    370     while line=$(net_recv); do
    371         case "$line" in
    372             TBYES) return 0 ;;
    373             TBNO)  return 1 ;;
    374         esac
    375     done
    376     return 1                                   # el otro desaparecio
    377 }
    378 
    379 # --- transport_local_move: nuestra jugada (la elegimos y la mandamos) -------
    380 # Pedimos la jugada a la interfaz (ui_pick_move) y la transmitimos. Devuelve la
    381 # jugada, o una "palabra de control": RESIGN (rendirse), CLAIM50 (reclamar tablas),
    382 # TIMEOUT (se acabo el tiempo), QUIT (salir), DRAWAGREE (tablas acordadas),
    383 # TAKEBACK (retroceso). Si rechazan unas tablas o un retroceso, volvemos a pedir
    384 # una jugada normal (no se pierde el turno).
    385 transport_local_move() {
    386     local m
    387     while :; do                               # ":" es un comando que no hace nada; "while :" = bucle infinito
    388         m=$(ui_pick_move)
    389         case "$m" in
    390             "")        net_send "RESIGN";  echo "RESIGN";  return ;;   # cancelar el menu = rendirse
    391             CLAIM50)   net_send "DRAW50";  echo "CLAIM50"; return ;;
    392             # Se nos acabo el tiempo mientras elegiamos; avisamos al otro asi ve una
    393             # victoria por tiempo y no una desconexion a secas.
    394             TIMEOUT)   net_send "TIMEOUT"; echo "TIMEOUT"; return ;;
    395             # Salida prolija (por ejemplo despues de guardar). No es rendicion: le
    396             # avisamos al otro para que tambien frene.
    397             QUIT)      net_send "QUIT";    echo "QUIT";    return ;;
    398             DRAWOFFER) if _negotiate_draw;     then echo "DRAWAGREE"; return; fi
    399                        continue ;;           # rechazaron -> elegir una jugada de verdad
    400             TAKEBACK)  if _negotiate_takeback; then echo "TAKEBACK";  return; fi
    401                        continue ;;           # rechazaron -> elegir una jugada de verdad
    402             *)         net_send "MOVE $m";  echo "$m";     return ;;   # una jugada normal
    403         esac
    404     done
    405 }
    406 
    407 # --- transport_recv_move: esperar la jugada del otro (pantalla interactiva) --
    408 # Mientras esperamos, la pantalla sigue VIVA: cada ~1 segundo revisamos la red,
    409 # repintamos el reloj, y miramos el teclado (cualquier tecla abre el menu de
    410 # espera). Devuelve la jugada o una palabra de control:
    411 #   RESIGN / CLAIM50 / DRAWAGREE / TAKEBACK / QUIT
    412 #   TIMEOUT           al otro se le acabo el tiempo (solo si pasamos un limite)
    413 #   RECONNECT         se corto la conexion (EOF) -> intentar reconectar solo
    414 #   RECONNECT-MANUAL  la persona declaro que el otro se cayo (sirve para una caida
    415 #                     "dura", que NO genera EOF y por eso no se puede detectar sola)
    416 # <deadline> es el tiempo que le queda al otro (+ un margen); se omite si no hay reloj.
    417 transport_recv_move() {
    418     local deadline_secs=$1 active=$TURN
    419     local epoch="" line rc key
    420     # Si hay limite, calculamos el momento (en segundos desde 1970, con "date +%s")
    421     # en el que se le acabaria el tiempo al otro.
    422     [[ -n "$deadline_secs" ]] && epoch=$(( $(date +%s) + deadline_secs ))
    423     while :; do
    424         # 1. La red: leemos con 1 segundo de limite, asi el bucle no se cuelga y el
    425         #    reloj y el teclado siguen respondiendo.
    426         line=$(net_recv_timeout 1); rc=$?
    427         if (( rc == 0 )); then
    428             # Llego un mensaje: lo procesamos segun que sea.
    429             case "$line" in
    430                 "MOVE "*)  echo "${line#MOVE }"; return ;;
    431                 RESIGN)    echo "RESIGN"; return ;;
    432                 DRAW50)    echo "CLAIM50"; return ;;
    433                 TIMEOUT)   echo "TIMEOUT"; return ;;
    434                 QUIT)      echo "QUIT"; return ;;
    435                 DRAWOFFER) if ui_confirm_draw; then net_send "DRAWYES"; echo "DRAWAGREE"; return
    436                            else net_send "DRAWNO"; fi ;;
    437                 TAKEBACK)  if ui_confirm_takeback; then net_send "TBYES"; echo "TAKEBACK"; return
    438                            else net_send "TBNO"; fi ;;
    439                 "HELLO "*|"TIME "*|"STATE "*|"COLOR "*) : ;;   # ruido suelto del saludo: ignorar
    440             esac
    441         elif (( rc <= 128 )); then
    442             echo "RECONNECT"; return        # EOF: el otro cerro / se cayo su programa
    443         fi
    444         # (rc mayor a 128 = se cumplio el limite de 1 seg: seguimos al reloj y teclado.)
    445 
    446         # 2. El reloj y la caida de bandera (se acabo el tiempo).
    447         if [[ -n "$epoch" ]]; then
    448             (( $(date +%s) >= epoch )) && { echo "TIMEOUT"; return; }
    449         fi
    450         _wait_repaint "$active" "$epoch"   # repintamos la linea del reloj
    451 
    452         # 3. El teclado: cualquier tecla abre el menu de espera. Es la UNICA forma de
    453         #    recuperarse de una caida "dura" (que nunca produce un EOF).
    454         #    "read -t 0.05 -N1" intenta leer 1 tecla con 50ms de paciencia.
    455         if read -r -t 0.05 -N1 key </dev/tty 2>/dev/null; then
    456             case "$(ui_wait_menu)" in
    457                 RECONNECT) echo "RECONNECT-MANUAL"; return ;;
    458                 RESIGN)    net_send "RESIGN"; echo "RESIGN"; return ;;
    459                 QUIT)      net_send "QUIT";   echo "QUIT";   return ;;
    460                 SAVE)      _save_prompt ;;     # guarda y sigue esperando
    461             esac
    462         fi
    463     done
    464 }
    465 
    466 # --- transport_reconnect: volver a armar una conexion que se corto ----------
    467 # Se llama desde el bucle principal. El que sobrevive se vuelve el "punto de
    468 # encuentro" (se pone a escuchar) y reenvia el estado real de la partida; el que
    469 # vuelve lo adopta. En un cortecito pasajero (auto) donde el host sigue vivo, el
    470 # "join" simplemente se reconecta. Devuelve 1 si no lo logra. Imprime el progreso
    471 # en la pantalla (corre en el proceso principal, no dentro de un $( )).
    472 transport_reconnect() {
    473     local mode=$1
    474     transport_close
    475     stage_live_state                       # la posicion actual sera lo que reenviemos
    476     if [[ "$NET_ROLE" == server ]]; then _reconnect_relay; return $?; fi
    477     # Si la persona lo activo a mano, o si somos el host: nos volvemos el punto de
    478     # encuentro. (Si se cayo el host, el "join" sobreviviente se asciende a host,
    479     # porque el que sobrevive siempre tiene el estado real de la partida.)
    480     if [[ "$mode" == manual || "$NET_ROLE" == host ]]; then
    481         _reconnect_as_listener
    482     else
    483         _reconnect_as_dialer               # cortecito pasajero, el host sigue escuchando
    484     fi
    485 }
    486 
    487 # _reconnect_as_listener: nos ponemos a escuchar y le decimos al otro como volver.
    488 _reconnect_as_listener() {
    489     NET_ROLE=host
    490     echo
    491     echo "  Waiting for the other player to reconnect on port ${NET_PORT}."
    492     echo "  On their machine, run:   ./ches.sh rejoin <your-ip> ${NET_PORT}"
    493     _net_show_ip
    494     coproc NET { ncat -l "$NET_PORT"; }
    495     NET_R=${NET[0]}; NET_W=${NET[1]}
    496     net_recv >/dev/null || return 1        # esperamos su HELLO / HELLO rejoin
    497     _push_state_to_peer                    # le reenviamos el estado real
    498     echo "  Reconnected. Resuming."
    499     return 0
    500 }
    501 
    502 # _reconnect_as_dialer: reintentamos conectarnos al host (que sigue vivo).
    503 _reconnect_as_dialer() {
    504     local tries=0
    505     echo
    506     echo "  Connection lost — reconnecting to ${NET_HOST}:${NET_PORT}..."
    507     while :; do
    508         coproc NET { ncat "$NET_HOST" "$NET_PORT"; }
    509         NET_R=${NET[0]}; NET_W=${NET[1]}
    510         net_send "HELLO rejoin"
    511         if _adopt_state_from_peer; then
    512             adopt_game_state; echo "  Reconnected."; return 0
    513         fi
    514         transport_close
    515         (( ++tries > 30 )) && return 1     # despues de 30 intentos, nos rendimos
    516         sleep 1                            # esperamos 1 segundo y reintentamos
    517     done
    518 }
    519 
    520 # _reconnect_relay: en modo relay, volvemos a marcar al relay (que es el punto de
    521 # encuentro), rehacemos el saludo, y el que es blancas reenvia el estado.
    522 _reconnect_relay() {
    523     echo
    524     echo "  Connection lost — re-dialing relay ${NET_HOST}:${NET_PORT}..."
    525     coproc NET { ncat "$NET_HOST" "$NET_PORT"; }
    526     NET_R=${NET[0]}; NET_W=${NET[1]}
    527     _server_handshake || return 1
    528     if [[ "$MY_COLOR" == white ]]; then
    529         _push_state_to_peer
    530     else
    531         _adopt_state_from_peer && adopt_game_state || return 1
    532     fi
    533     echo "  Reconnected."
    534     return 0
    535 }
    536 
    537 # transport_close: cerrar las conexiones al terminar. "kill" le manda una senial
    538 # al proceso para que termine. Cerramos tanto ncat (la red) como el motor (la IA).
    539 transport_close() {
    540     [[ -n "$NET_PID" ]] && kill "$NET_PID" 2>/dev/null
    541     [[ -n "${AI_PID:-}" ]] && ai_close       # apagamos tambien el coproceso del motor
    542     NET_R=""; NET_W=""
    543 }