Arquitectura del movimiento y la persistencia visual: el Pacman y la máscara binaria.
Análisis del script de presentación ASCII de los videos de Entropía binaria para 2026.
En la programación de animaciones en terminal, mover un objeto de un punto A a un punto B es trivial; lo verdaderamente complejo surge cuando ese objeto debe interactuar con el entorno, modificándolo (permanentemente) a su paso. En el caso del script que estamos estudiando, el desafío es doble: el personaje debe recorrer la pantalla en un patrón de zigzag preciso y, al mismo tiempo, "ir comiéndose" al planeta para revelar un logotipo oculto debajo. Para lograr esto, no podemos pensar en el movimiento y en el dibujo como procesos separados; ambos dependen de un mapa de memoria compartido.
Para poder comprender el movimiento, primero debemos observar cómo se gestionan los límites. El Pacman no se mueve libremente; está confinado a un bucle lógico. En cada ciclo del procesador, su coordenada horizontal varía según una variable de dirección. Sin embargo, para lograr el efecto de escritura en zigzag, el script evalúa constantemente si el personaje ha superado los márgenes predefinidos a la izquierda o a la derecha del planeta. Cuando esto ocurre, suceden tres cosas simultáneamente: la dirección se invierte (se multiplica por -1), la coordenada vertical disminuye para subir un renglón, y el personaje se reposiciona en el borde opuesto. Este comportamiento cíclico asegura que el personaje barra toda la superficie del objeto central sin necesidad de programar cada giro manualmente.
Pero la magia real del script reside en cómo "borra" el planeta sin borrar el logo. Aquí es en donde entra en juego el concepto de matriz asociativa "array asociativo". En BASH, una matriz (array) común, se indiza (indexa) con números (0, 1, 2,...), pero un array asociativo nos permite usar cadenas de texto como índices. El script declara una variable llamada visitados. Cada vez que el Pacman se mueve, no solo cambia su posición en pantalla, sino que calcula qué coordenadas del planeta está tocando en ese momento. Luego, registra esas coordenadas exactas en el array asociativo usando el formato "fila,columna" como clave y asignándole un valor de "1". Es como si el Pacman fuera dejando un rastro de migajas invisibles en la memoria del ordenador, marcando "aquí estuve".
El paso final ocurre en el motor de renderizado. Cuando el script se dispone a dibujar el planeta línea por línea, no imprime ciegamente los caracteres del planeta. En su lugar, para cada celda de la cuadrícula, consulta el array visitados (de ahí su nombre). El programa se hace una pregunta simple: "¿ha pasado el Pacman por la coordenada Y,X actual?". Si la respuesta es no (el valor no existe o es 0), el script dibuja el caracter correspondiente al planeta (tierra o agua). Pero si la respuesta es sí (el valor es 1), el script ignora el planeta y busca en su lugar qué caracter corresponde a esa misma posición en el sprite del logotipo de Entropía binaria. De esta forma, no estamos realmente "borrando" nada; estamos alternando dinámicamente entre dos capas de imágenes (la capa del planeta y la capa del logo) utilizando la posición histórica del Pacman como una máscara de recorte en tiempo real.
Disección del código: implementación del motor de movimiento y revelado.
A continuación, he extraído y aislado la lógica exacta que controla el movimiento en zigzag, la detección de colisión (máscara) y el renderizado condicional.
Nota: para que este código sea funcional de manera independiente sin necesidad de descargar los archivos de texto externos (arte_ascii/...), he sustituido la función de carga de archivos por la definición manual de dos sprites simples (un cuadrado de "Planeta" y un cuadrado de "Logo") y un Pacman básico. El resto de la lógica (movimiento, array visitados, actualizar_mascara y el bucle de dibujo) es textualmente la del script original.
#!/bin/bash
c_borde_planeta="\e[34m"
c_tierra_planeta="\e[33m"
c_agua_planeta="\e[36m"
c_pacman="\e[1;33m"
c_texto="\e[1;37m"
# Colores logo.
c_logo_1_relleno="\e[38;5;52m" # Marrón oscuro.
c_logo_1_fondo="\e[38;5;220m" # Dorado.
# Ajustes de posición planeta y logo.
ajuste_planeta_y=0; ajuste_planeta_x=0
ajuste_logo_y=0; ajuste_logo_x=0
limite_pacman_izq=19
limite_pacman_der=5
vel_movimiento=50000000 # Velocidad pacman comiendo.
vel_anim=150000000 # Animación boca (150ms).
dir_arte="arte_ascii"
if [[ ! -d "$dir_arte" ]];then echo "Error: Falta '$dir_arte'";exit 1;fi
tput civis;tput rmam;clear # rmam = desactivar auto-margen.
trap "tput cnorm;tput smam;echo -e '\e[0m';clear;exit" SIGINT
c_reset="\e[0m"
# Estado inicial.
estado_juego=0
filas_visitadas=0
set_color_logo=1
texto_pac=""
declare -A visitados
# CARGA DE SPRITES DESDE ARCHIVOS DE TEXTO PLANO.
cargar_sprite() {
local archivo="$dir_arte/$1"
local -n ref_arr=$2; local -n ref_ancho=$3; local -n ref_alto=$4
if [[ ! -f "$archivo" ]];then echo "Error: falta '$archivo'";tput cnorm;exit 1;fi
mapfile -t ref_arr < "$archivo"
ref_alto=${#ref_arr[@]}
ref_ancho=0
for linea in "${ref_arr[@]}";do
len=${#linea}
if [[ $len -gt $ref_ancho ]];then ref_ancho=$len;fi
done
}
declare -a arte_planeta arte_logo pac_cerrado pac_abierto_izq pac_abierto_der
cargar_sprite "planeta" arte_planeta ancho_planeta alto_planeta
cargar_sprite "logo_entropía" arte_logo ancho_logo alto_logo
cargar_sprite "pacman_cerrado" pac_cerrado ancho_pac alto_pac
cargar_sprite "pacman_abierto_izq" pac_abierto_izq tmp_w tmp_h
cargar_sprite "pacman_abierto_der" pac_abierto_der tmp_w tmp_h
# POSICIONAMIENTO INICIAL.
ancho_term=$(tput cols);alto_term=$(tput lines)
offset_x=$(( ((ancho_term - ancho_planeta) / 2) + ajuste_planeta_x ))
offset_y=$(( ((alto_term - alto_planeta) / 2) + ajuste_planeta_y ))
logo_rel_x=$(( ((ancho_planeta - ancho_logo) / 2) + ajuste_logo_x ))
logo_rel_y=$(( ((alto_planeta - alto_logo) / 2) + ajuste_logo_y ))
pac_x=$(( offset_x + ancho_planeta + 2 ))
pac_y=$(( offset_y + alto_planeta - alto_pac ))
pac_dir=-1;estado_pac=0;idx_texto=0
# LÓGICA DE MÁSCARA (EL BORRADO)
actualizar_mascara() {
local -n ref_sprite_actual
if [[ $estado_pac -eq 0 ]];then ref_sprite_actual=pac_cerrado
elif [[ $pac_dir -eq 1 ]];then ref_sprite_actual=pac_abierto_der
else ref_sprite_actual=pac_abierto_izq
fi
local rel_pac_x=$((pac_x - offset_x))
local rel_pac_y=$((pac_y - offset_y))
for ((py=0; py<alto_pac; py++));do
local linea_sprite="${ref_sprite_actual[py]}"
for ((px=0; px<ancho_pac; px++));do
local char_en_sprite="${linea_sprite:px:1}"
if [[ "$char_en_sprite" != " " ]] && [[ -n "$char_en_sprite" ]];then
local ly=$((rel_pac_y + py))
local lx=$((rel_pac_x + px))
if [[ $ly -ge 0 ]] && [[ $ly -lt $alto_planeta ]] && [[ $lx -ge 0 ]] && [[ $lx -lt $ancho_planeta ]];then
visitados["$ly,$lx"]=1
fi
fi
done
done
}
# MOTOR GRÁFICO (RENDERIZADO).
dibujar_escena() {
buffer="\e[H\e[2J"
# PLANETA / LOGO Eb.
local c_relleno=$c_logo_1_relleno
local c_fondo=$c_logo_1_fondo
for ((i=0;i<alto_planeta;i++));do
local pantalla_y=$((offset_y + i))
buffer+="\e[${pantalla_y};${offset_x}H"
local linea_planeta="${arte_planeta[i]}"
local idx_fila_logo=$((i - logo_rel_y))
local linea_logo=""
if [[ $idx_fila_logo -ge 0 ]] && [[ $idx_fila_logo -lt $alto_logo ]];then linea_logo="${arte_logo[$idx_fila_logo]}";fi
local linea_construida=""
for ((j=0; j<ancho_planeta; j++)); do
# LÓGICA DE VISIBILIDAD.
local mostrar_logo=0
if [[ "${visitados["$i,$j"]}" == "1" ]];then mostrar_logo=1;fi
if [[ $mostrar_logo -eq 1 ]];then
# MODO "LOGO".
local idx_col_logo=$((j - logo_rel_x))
if [[ $idx_fila_logo -ge 0 ]] && [[ $idx_fila_logo -lt $alto_logo ]] && [[ $idx_col_logo -ge 0 ]] && [[ $idx_col_logo -lt $ancho_logo ]];then
local char="${linea_logo:idx_col_logo:1}"
if [[ "$char" == "#" ]];then linea_construida+="${c_fondo}█${c_reset}"
elif [[ "$char" == " " ]] || [[ -z "$char" ]];then linea_construida+=" "
else linea_construida+="${c_relleno}█${c_reset}"
fi
else
linea_construida+=" "
fi
else
# MODO "PLANETA".
local char="${linea_planeta:j:1}"
if [[ -z "$char" ]]; then char=" ";fi
if [[ "$char" == "█" || "$char" == "#" ]];then linea_construida+="${c_tierra_planeta}${char}${c_reset}"
elif [[ "$char" == "-" || "$char" == "=" || "$char" == ":" || "$char" == "." ]];then linea_construida+="${c_borde_planeta}${char}${c_reset}"
else linea_construida+="${c_agua_planeta}${char}${c_reset}";fi
fi
done
buffer+="$linea_construida"
done
# PACMAN.
local -n ref_p
if [[ $estado_pac -eq 0 ]];then ref_p=pac_cerrado
elif [[ $pac_dir -eq 1 ]];then ref_p=pac_abierto_der
else ref_p=pac_abierto_izq;fi
local j=0
for pline in "${ref_p[@]}";do
local py=$((pac_y + j))
local linea_transparente="${pline// /\\e[1C}"
if [[ $py -gt 0 ]] && [[ $py -lt $alto_term ]];then
buffer+="\e[${py};${pac_x}H${c_pacman}${linea_transparente}${c_reset}"
fi
((j++))
done
echo -ne "$buffer"
}
# BUCLE PRINCIPAL.
ultimo_tiempo=$(date +%s%N)
temp_mov=0
temp_anim=0
while true;do
ahora=$(date +%s%N)
delta=$((ahora - ultimo_tiempo))
ultimo_tiempo=$ahora
# LÓGICA DE MOVIMIENTO (ESTADO 0).
if [[ $estado_juego -eq 0 ]];then
temp_mov=$((temp_mov + delta))
if [[ $temp_mov -ge $vel_movimiento ]];then
pac_x=$((pac_x + pac_dir))
actualizar_mascara
# Límites zigzag.
if [[ $pac_dir -eq -1 ]] && [[ $pac_x -le $((offset_x - limite_pacman_izq)) ]];then
pac_dir=1;pac_y=$((pac_y - 4));pac_x=$((offset_x - limite_pacman_izq))
fi
if [[ $pac_dir -eq 1 ]] && [[ $pac_x -ge $((offset_x + ancho_planeta + limite_pacman_der)) ]];then
pac_dir=-1;pac_y=$((pac_y - 4));pac_x=$((offset_x + ancho_planeta + limite_pacman_der))
fi
# FIN DEL RECORRIDO (Salida del script).
if [[ $pac_y -le $((offset_y + 2)) ]] && [[ $pac_dir -eq 1 ]] && [[ $pac_x -ge $((offset_x + ancho_planeta)) ]];then
tput cnorm;echo -e "\e[H\e[2J\e[0m";exit 0
fi
temp_mov=0
dibujar_escena
fi
fi
# Acumular tiempo para la animación.
temp_anim=$((temp_anim + delta))
# Si pasó el tiempo suficiente, cambiar estado (0 a 1, o 1 a 0, etc).
if [[ $temp_anim -ge $vel_anim ]];then
estado_pac=$((1 - estado_pac))
temp_anim=0
fi
sleep 0.001 # Descanso de CPU para no recargarla innecesariamente.
done
Análisis detallado: cómo funciona el motor de borrado del planeta y revelado del logotipo.
Antes de ejecutar el código extraído en la sección anterior, es imperativo aclarar un requisito físico. Aunque la lógica del script es autónoma, sus dependencias son los archivos de gráficos ASCII. Para que el programa funcione y no acuse errores del tipo "falta el directorio arte_ascii, o falta el archivo pacman", debes tener una carpeta llamada arte_ascii en el mismo directorio en donde guardes el script. Dentro de ella, deben existir los siguientes archivos de texto plano que contienen los dibujos: planeta, logo_entropía, pacman_cerrado, pacman_abierto_izq y pacman_abierto_der. Sin estos archivos, las matrices de memoria estarán vacías y el script no tendrá nada que dibujar.
Una vez cubierto este requisito, pasemos al análisis técnico de cómo BASH, un lenguaje pensado para administrar sistemas, es llevado aquí a comportarse como un excelente motor de videojuegos.
1. La carga de recursos por referencia.
El script comienza definiendo colores y variables, pero la primera pieza de ingeniería interesante es la función cargar_sprite. En lugar de escribir el código de carga cinco veces (una para cada archivo), se utiliza una función general. Lo notable aquí es el uso de local -n. Esto crea una "nameref" (referencia por nombre), que actúa como un puntero. Cuando llamamos a cargar_sprite "planeta" arte_planeta ..., la función no recibe una copia del array, sino que apunta directamente a la variable global arte_planeta. El comando mapfile -t es el encargado de leer el archivo de texto línea por línea y volcarlo directamente dentro del array. Sin este comando, tendríamos que utilizar, por ejemplo, bucles while read que son mucho más lentos.
2. El sistema de coordenadas relativas.
Para centrar el planeta en la pantalla, el script calcula el offset_y y offset_x basándose en el tamaño actual de la terminal (tput cols). Sin embargo, el reto matemático está en el logotipo. El logo de "Entropía binaria" es algo más pequeño que el planeta y debe aparecer centrado dentro de él. Para esto se calculan logo_rel_y y logo_rel_x. Estas variables almacenan la diferencia de posición entre la esquina superior izquierda del planeta y la esquina superior izquierda del logo. Este cálculo es fundamental para que más adelante sepamos exactamente qué píxel del logo corresponde a qué píxel del planeta.
3. El corazón del efecto: declare -A visitados.
Esta línea es muy importante, porque al declarar visitados como un array asociativo (-A), estamos creando una memoria no dependiente de índices numéricos consecutivos (0, 1, 2...), sino de claves de texto. El script utiliza este array como una "máscara de recorte". No guardamos el dibujo final, sino simplemente un registro de coordenadas "Y,X" por las que el Pacman ha pasado. Si la clave "5,10" existe en el array, significa que esa celda ya fue "comida".
4. La función actualizar_mascara.
Cada vez que el Pacman se mueve un paso, se llama a esta función. Su trabajo no es dibujar, sino calcular colisiones. El script recorre el cuadrado pequeño que forma el cuerpo del Pacman. Si el Pacman tiene un píxel sólido en una posición, el script traduce esa posición local a la posición global del planeta. La línea visitados["$ly,$lx"]=1 es la que "marca" el territorio. Nótese que no borramos el planeta en este punto; simplemente anotamos en nuestra libreta virtual que esa coordenada específica ya ha sido visitada.
5. El renderizado condicional (dibujar_escena).
Aquí es en donde ocurre la magia visual. El script no dibuja el logo y luego le pega el "planeta comido parcialmente" encima: esto posiblemente causaría parpadeos que el ojo humano sería capaz de advertir. En su lugar, decide píxel por píxel qué debe mostrar, componiendo una imagen que será la que se espere ver (planeta mezclado con logo de manera inteligente y razonable. El bucle anidado recorre todas las filas (i) y columnas (j) del área del planeta. En cada iteración, se hace una pregunta crítica: if [[ "${visitados["$i,$j"]}" == "1" ]]; ...
- Si la respuesta es SÍ (1): significa que el Pacman ya pasó por ahí. El script ignora el array del planeta y mira el array arte_logo. Utilizando las coordenadas relativas calculadas al principio, extrae el color y el caracter del logo y lo añade al búfer.
/ Si la respuesta es NO (vacío): significa que el terreno está intacto. El script ignora el logo y extrae el carácter del array arte_planeta, aplicándole los colores de tierra o agua.
Este enfoque permite que el logo aparezca "detrás" del planeta a medida que este es borrado, creando una sensación de profundidad y capas.
6. El bucle de física y el movimiento zigzag.
Finalmente, el bucle while true maneja el tiempo. Se usa la técnica de "Delta tiempo" (ahora - ultimo_tiempo) para controlar la velocidad. Esto asegura que el juego corra a la misma velocidad independientemente de la potencia de la CPU que esté ejecutando el script, cosa que no sucede con "sleep". El movimiento en zigzag se logra monitoreando la variable pac_x.
Cuando pac_dir es -1 (izquierda) y se llega al limite_pacman_izq, el script cambia pac_dir a 1 (derecha) y resta 4 a pac_y.
Al restar a pac_y, el personaje sube visualmente porque en la terminal, la fila 0 está arriba. Este cambio de dirección y altura crea el patrón de barrido que permite limpiar todo el mapa automáticamente hasta que la coordenada Y del Pacman supera la parte superior del planeta, momento en el cual el script detecta el fin del juego y termina, así como termina este artículo :D

0 Comentarios:
Publicar un comentario