Blog

Tipos básicos en Kotlin: Inferencia, números, textos y operaciones

Introducción

En la unidad anterior vimos cómo hacer nuestro primer «Hola Mundo» y cómo usar variables (val y var). Pero nos dejamos un detalle importantísimo en el tintero: ¿qué tipo de información estamos guardando exactamente en esas variables?

En Kotlin, absolutamente todo tiene un tipo. Los tipos son fundamentales porque le dicen al compilador (el «cerebro» que lee tu código) qué puedes y qué no puedes hacer con una variable. Por ejemplo, puedes multiplicar dos números, pero no puedes multiplicar dos palabras, ¿verdad?

Ahora vamos a desgranar los tipos básicos de Kotlin, a aprender a operar con ellos y a descubrir cómo Kotlin nos facilita la vida con su «magia» deductiva.

La inferencia de tipos: Kotlin es muy listo

Si recuerdas la unidad anterior, declarábamos variables así:

var clientes = 10

En ningún momento le dijimos a Kotlin: «Oye, que sepas que clientes es un número entero». Sin embargo, Kotlin lo supo al instante. Esta capacidad se llama Inferencia de Tipos (Type Inference).

Como a la variable clientes le asignamos un 10, Kotlin deduce inmediatamente que su tipo es numérico, concretamente un Int (entero). Gracias a esto, el compilador sabe que puedes realizar operaciones matemáticas con esta variable.

Mira este ejemplo de asignaciones compuestas (una forma abreviada de hacer matemáticas):

fun main() {
    var clientes = 10

    // Vienen 3 clientes más (clientes = clientes + 3)
    clientes += 3  // Ahora hay 13
    
    // Se van 5 clientes
    clientes -= 5  // Ahora hay 8
    
    // El negocio explota y multiplicamos los clientes por 2
    clientes *= 2  // Ahora hay 16
    
    // Dividimos a los clientes en 4 grupos
    clientes /= 4  // Ahora hay 4 en cada grupo

    println(clientes) 
    // Resultado: 4
}

Los tipos básicos de Kotlin: La lista completa

Aunque Kotlin infiere los tipos, a veces querrás (o necesitarás) ser explícito. Para declarar un tipo manualmente, se usan los dos puntos : después del nombre de la variable.

A continuación mostramos los tipos fundamentales.

Números enteros (sin decimales)

  • Int: Es el estándar para números sin decimales. (Ej: val año: Int = 2024)
  • Long: Se usa para números ridículamente grandes. Si un número es demasiado grande para un Int, Kotlin lo convertirá en Long automáticamente. También puedes forzarlo añadiendo una L mayúscula al final. (Ej: val estrellas: Long = 9876543210L)
  • Byte y Short: Se usan en casos muy específicos para ahorrar memoria con números pequeños. Raramente los usarás al empezar.
  • Nota: Kotlin también tiene versiones «Unsigned» (sin signo, es decir, solo positivos) como UInt o ULong marcados con una u (Ej: val puntos: UInt = 100u).

Números con decimales (coma flotante)

  • Double: Es el estándar de Kotlin para números con decimales. Tiene una precisión doble, es decir, admite muchísimos decimales. (Ej: val precio: Double = 19.99)
  • Float: Ocupa menos memoria pero es menos preciso. Para indicarle a Kotlin que quieres un Float y no un Double, debes añadir una f o F al final del número. (Ej: val temperatura: Float = 24.5f)

Booleanos (verdadero o falso)

  • Boolean: Solo puede tener dos valores: true (verdadero) o false (falso). Es la base de la lógica en programación (Ej: val estaEncendido: Boolean = true).

Caracteres y cadenas de texto

  • Char: Representa un único carácter (una sola letra, número o símbolo). Se escribe entre comillas simples ' '. (Ej: val inicial: Char = 'J')
  • String: Representa una cadena de texto (muchos caracteres juntos). Se escribe entre comillas dobles " ". (Ej: val mensaje: String = "¡Hola, mundo!")

Declarar ahora, inicializar después

A veces sabes qué tipo de dato vas a guardar, pero aún no tienes el valor exacto. Kotlin te permite declarar una variable y asignarle su valor más tarde.

Eso sí, en estos casos es obligatorio especificar el tipo explícitamente, porque Kotlin no tiene un valor inicial del que deducirlo:

fun main() {
    // Declaramos la variable especificando el tipo explícitamente, pero sin darle valor
    val d: Int 
    
    // Más adelante en el código, la inicializamos
    d = 3 

    println(d) // Imprime: 3
}

La seguridad de Kotlin frente a errores

¿Qué pasa si intentas imprimir la variable d antes de darle un valor? En otros lenguajes tu programa explotaría (el temido NullPointerException o imprimiría basura de la memoria). Kotlin no te deja hacerlo. El código directamente se pondrá en rojo y no compilará, mostrándote el error: «Variable ‘d’ must be initialized». ¡Un salvavidas enorme!

Ejercicios

Abre tu editor o el Kotlin Playground y pon a prueba lo que acabas de aprender.

El tipado explícito

Kotlin puede inferir los tipos, pero en este ejercicio queremos que seas tú quien los escriba. Modifica el siguiente código para añadir explícitamente (: Tipo) el tipo de dato correcto a cada variable:

fun main() {
    val a = 1000 
    val b = "mensaje de registro"
    val c = 3.14
    val d = 100_000_000_000_000 // Fíjate, los guiones bajos sirven para leer mejor los números grandes
    val e = false
    val f = '\n' // Esto es un carácter especial que representa un "salto de línea"
}

El cofre del tesoro

Crea un programa que simule el oro de un jugador en un videojuego usando una variable mutable (var) llamada monedas:

  1. El jugador empieza con 50 monedas.
  2. Encuentra un cofre mágico y su oro se multiplica por 3. (Usa asignación compuesta *=).
  3. Compra una espada que cuesta 80 monedas. (Usa -=).
  4. Imprime el resultado final: "Tras la aventura, te quedan X monedas".

Detecta el error

El siguiente código tiene un problema y Kotlin se quejará si intentas ejecutarlo. Corrígelo para que funcione y se imprima la edad correctamente.

fun main() {
    val edadUsuario: Int
    println("La edad del usuario es $edadUsuario")
    edadUsuario = 25
}

Cuidado con las divisiones

Si divides dos variables Int (ej: 10 / 3), Kotlin devuelve un número entero (3) y se come los decimales.

Crea dos variables llamadas dividendo (valor 10) y divisor (valor 3). Haz que sean de tipo Double explícitamente para que, al imprimirlas usando una plantilla de cadena, el resultado sea con decimales (3.3333333333333335).

Soluciones a los ejercicios

¡No mires hasta que no te hayas peleado un rato con el código!

El tipado explícito

fun main() {
    val a: Int = 1000 
    val b: String = "mensaje de registro"
    val c: Double = 3.14  // Al tener decimales y no tener 'f', es Double
    val d: Long = 100_000_000_000_000 // Es demasiado grande para ser Int
    val e: Boolean = false // Verdadero o falso
    val f: Char = '\n' // Comillas simples indican que es un solo Char
}

El cofre del tesoro

fun main() {
    var monedas = 50
    monedas *= 3
    monedas -= 80
    println("Tras la aventura, te quedan $monedas monedas") 
    // Imprimirá 70
}

Detecta el error

fun main() {
    val edadUsuario: Int
    edadUsuario = 25 // ¡Había que inicializarla ANTES de leerla!
    println("La edad del usuario es $edadUsuario")
}

Cuidado con las divisiones

fun main() {
    // Para forzar que un número entero se trate como decimal, ponemos .0
    val dividendo: Double = 10.0 
    val divisor: Double = 3.0
    
    println("El resultado exacto es ${dividendo / divisor}")
}

Primeros pasos con Kotlin: Tu primer «Hola mundo», variables y plantillas de texto

Introducción

Si estás empezando en el mundo del desarrollo (ya sea para Android o backend), Kotlin es uno de los lenguajes más modernos, limpios y seguros que puedes aprender hoy en día.

En esta unidad, vamos a dar los primeros pasos. Entenderemos cómo funciona la estructura básica de un programa, cómo almacenar información usando variables y cómo imprimir mensajes por pantalla de forma elegante. Al final, tendrás ejercicios prácticos para asentar lo aprendido.

El clásico «Hola mundo»

Por tradición, el primer programa que todo desarrollador escribe al aprender un lenguaje nuevo es el que imprime las palabras «¡Hola, mundo!» en la pantalla.

En Kotlin, se hace así de fácil:

fun main() {
    println("¡Hola, mundo!")
    // ¡Hola, mundo!
}

Analizando el código línea a línea:

  • fun: Es la palabra reservada (abreviatura de function) que usamos en Kotlin para declarar una función. Una función no es más que un bloque de código que realiza una tarea específica.
  • main(): No es una función cualquiera. Es el punto de entrada (entry point) de tu programa. Siempre que ejecutes una aplicación en Kotlin, el ordenador buscará esta función y empezará a ejecutar las instrucciones que haya dentro de ella.
  • { } (Las llaves): Todo lo que esté dentro de las llaves es el «cuerpo» de la función, es decir, las instrucciones que se van a ejecutar.
  • println(): Es una función nativa de Kotlin que coge lo que le pongas entre paréntesis (los «argumentos») y lo imprime por la salida estándar (la consola de tu pantalla). Al terminar de imprimir, añade un salto de línea (por eso termina en ln, de line). Si usas print() (sin el ln), el texto se imprimirá, pero el siguiente mensaje aparecerá pegado justo a la derecha, en la misma línea.

El dato: ¿Te has dado cuenta de algo? ¡No hay punto y coma (;) al final de la línea! En lenguajes más antiguos como Java o C++, olvidar el punto y coma era un dolor de cabeza. Kotlin es un lenguaje moderno y no los necesita.

Variables: Guardando información en memoria

Cualquier programa informático necesita almacenar datos (nombres de usuario, puntuaciones de un juego, precios…). Para eso utilizamos las variables.

En Kotlin, tenemos dos formas principales de crear variables, y esto es muy importante:

  1. Variables de solo lectura (inmutables) con val (de value).
  2. Variables mutables con var (de variable).

Para asignarle un valor a una variable, simplemente usamos el operador de asignación =. Fíjate en este ejemplo:

fun main() { 
    val palomitas = 5   // Tenemos 5 cajas de palomitas
    val perritos = 7    // Tenemos 7 perritos calientes
    var clientes = 10   // Hay 10 clientes en la cola
    
    // De repente, un par de clientes se cansan de esperar y se van de la cola
    clientes = 8
    println(clientes) 
    // Resultado en pantalla: 8
}

val vs var: ¿Cuál debería usar?

Como clientes se declaró con var, pudimos reasignarle un nuevo valor (8) más adelante en el programa. Sin embargo, si intentáramos hacer palomitas = 6, el compilador de Kotlin nos daría un error, porque palomitas es un val y no puede cambiar una vez se le ha dado un valor inicial.

La regla de oro en Kotlin: Utiliza siempre val por defecto. Usa var única y exclusivamente cuando sepas seguro que ese valor va a tener que cambiar en el futuro (como un contador, o la vida de un personaje en un juego). Esto hace que tu código sea mucho más seguro, predecible y libre de errores accidentales (los temidos bugs).

Plantillas de cadenas (String Templates): Texto inteligente

En programación, es súper común querer imprimir un texto que contenga el valor de nuestras variables. En otros lenguajes tendrías que «sumar» o concatenar trozos de texto con el símbolo +, lo cual queda feo y es fácil equivocarse.

Kotlin lo soluciona de forma magistral con las String Templates (plantillas de cadenas).

Una cadena de texto (String) se escribe siempre entre comillas dobles " ". Para inyectar el valor de una variable dentro de ese texto, solo tienes que poner el símbolo del dólar $ seguido del nombre de la variable.

Y si lo que quieres es hacer una operación matemática u otro código más complejo dentro del texto, lo metes entre llaves ${ }. Mira por ejemplo el siguiente código:

fun main() { 
    val clientes = 10
    
    // Imprimiendo una variable directamente
    println("Actualmente hay $clientes clientes esperando.")
    // Salida: Actualmente hay 10 clientes esperando.
    
    // Haciendo una operación matemática dentro del texto
    println("Si llega uno más, habrá ${clientes + 1} clientes en total.")
    // Salida: Si llega uno más, habrá 11 clientes en total.
}

La magia oculta: La inferencia de tipos

Si prestas atención, en ningún momento le hemos dicho a Kotlin que clientes es un número. Kotlin es muy listo: al ver que le asignamos un 10, él automáticamente infiere (deduce) que el tipo de dato es un número entero (Int).

Ejercicios

La programación solo se aprende tecleando. Aquí tienes varios ejercicios de menos a más dificultad. Intenta resolverlos por tu cuenta en un editor o en el Kotlin Playground antes de mirar las soluciones más abajo.

Presentando a Mary

Completa el siguiente código para que el programa imprima por consola el mensaje exacto: "Mary tiene 20 años". Debes usar obligatoriamente las variables dadas y las plantillas de cadenas ($).

fun main() {
    val nombre = "Mary"
    val edad = 20
    // Escribe tu código debajo de esta línea:
    
}

Batería del móvil

Crea un pequeño programa que simule la batería de tu móvil.

  1. Declara una variable llamada bateria que empiece al 100%. (Piensa si debe ser val o var).
  2. Imprime el texto: "La batería inicial es del 100%".
  3. Simula que juegas a un videojuego y la batería baja a 75. Actualiza el valor de la variable.
  4. Vuelve a imprimir el texto: "Tras jugar, la batería es del 75%".

Calculadora de la compra inteligente

Declara dos variables inmutables: precioZapatillas con un valor de 50, y cantidadCajas con un valor de 2.

Utilizando un único println() y las llaves de expresión ${}, imprime el siguiente mensaje calculando el total sobre la marcha:

"Has comprado 2 pares de zapatillas. El precio total es de 100 euros."

¿print o println?

Imagina que quieres que la consola muestre exactamente esto en dos líneas:

Hola Mundo
¡Amo Kotlin!

Escribe el código usando tres instrucciones de impresión para conseguir esa salida exacta. Haz que la palabra «Hola» y «Mundo» se impriman en instrucciones separadas pero se queden en la misma línea.

Soluciones a los ejercicios

¡No hagas trampas! Solo mira esto si ya has intentado resolverlos.

Presentando a Mary

fun main() {
    val nombre = "Mary"
    val edad = 20
    println("$nombre tiene $edad años")
}

Batería del móvil

fun main() {
    // Usamos 'var' porque la batería va a cambiar a lo largo del tiempo
    var bateria = 100
    println("La batería inicial es del $bateria%")
    
    bateria = 75
    println("Tras jugar, la batería es del $bateria%")
}

Calculadora de la compra inteligente

fun main() {
    val precioZapatillas = 50
    val cantidadCajas = 2
    
    // Usamos ${} para multiplicar variables dentro del mismo texto
    println("Has comprado $cantidadCajas pares de zapatillas. El precio total es de ${precioZapatillas * cantidadCajas} euros.")
}

¿print o println?

fun main() {
    print("Hola ")       // Usa print, por lo que la siguiente palabra se pega a esta. (Fíjate en el espacio al final)
    println("Mundo")     // Se pega al 'Hola ', y luego hace un salto de línea por el 'ln'
    println("¡Amo Kotlin!") // Se imprime en la nueva línea
}

Programación con Kotlin nivel básico

Primeros pasos

Tipos de datos básicos

Colecciones (listas, conjuntos y mapas)

Control de flujo

Funciones y lambdas

Clases y objetos

Seguridad contra nulos

Programando el juego Space Invaders en Java paso a paso

Introducción

¿Alguna vez te has preguntado cómo funcionan las entrañas de un videojuego clásico? Hoy no vamos a usar librerías gráficas pesadas como Unity o Unreal. Vamos a bajar al metal, al código puro. Vamos a construir un juego funcional para Space Invaders utilizando únicamente la consola de Java.

Aprenderemos sobre Herencia, Polimorfismo, Ciclos de Juego (Game Loops) y Detección de Colisiones en una rejilla.

En este enlace puedes encontrar una presentación explicando el código, y a continuación dispones de una infografía y un vídeo explicativo.

Control de entrada con Enum

En lugar de esparcir caracteres mágicos ('a', 'd', 'f') por todo el código, vamos a profesionalizar la entrada del usuario. Usaremos un Enum.

¿Y por qué un Enum? Un enumerado nos permite definir un tipo de dato que tiene un conjunto fijo de constantes. Esto hace que el código sea seguro (no puedes pasar una tecla que no exista) y legible. Además, encapsulamos la lógica: cada tecla sabe cuál es su descripción y su carácter asociado. Fíjate por ejemplo en el método detectar. Recorre todos los valores posibles y compara. Si el usuario escribe «I» o «i», el juego entenderá Input.IZQUIERDA independientemente de lo que pase después.

import java.util.Scanner;

/**
 * Enumeración que gestiona el control de entrada del usuario.
 * Define las teclas válidas y asocia cada una con una acción del juego.
 */
enum Input {
    IZQUIERDA("i", "Izquierda"), 
    DERECHA("d", "Derecha"), 
    FUEGO("f", "FUEGO"), 
    SALIR("x", "Salir"), 
    NADA("", ""); 

    private String tecla;
    private String descripcion;

    /**
     * Constructor del enum Input.
     * @param tecla Carácter que activa la acción.
     * @param descripcion Nombre legible de la acción.
     */
    Input(String tecla, String descripcion) { 
        this.tecla = tecla; 
        this.descripcion = descripcion;
    }

    /**
     * Imprime en la consola la lista de controles disponibles para el jugador.
     */
    public static void imprimirControles() {
        for (Input i : Input.values()) {
            if (!i.tecla.isEmpty()) System.out.print("[" + i.tecla + "]" + i.descripcion + "  ");
        }
        System.out.print("\nAcción > ");
    }

    /**
     * Detecta qué comando corresponde al texto introducido por el usuario.
     * @param texto Entrada del teclado.
     * @return La instancia de Input correspondiente o NADA si no coincide.
     */
    public static Input detectar(String texto) {
        for (Input i : Input.values()) {
            if (i.tecla.equalsIgnoreCase(texto)) return i;
        }
        return NADA;
    }
}

Herencia y polimorfismo

Aplicamos el principio DRY (Don’t Repeat Yourself). Una Nave, un Alien y una Bala son cosas muy distintas, pero comparten cierta información: todos tienen una coordenada X, una coordenada Y y un icono para dibujarse.

La clase Entidad

Esta clase define lo que es un objeto en nuestro juego. Además, incluye una lógica de protección: el método mover base se asegura de que ningún objeto se salga del mapa usando Math.max y Math.min.

/**
 * Clase base para todos los objetos del juego (Nave, Alien, Bala).
 * Contiene la posición y el icono representativo.
 */
class Entidad {
    protected int x, y;
    protected char icono;

    public Entidad(int x, int y, char icono) {
        this.x = x; 
        this.y = y; 
        this.icono = icono;
    }

    /**
     * Actualiza la posición de la entidad y aplica restricciones de los límites del mapa.
     * @param accion La acción de entrada del usuario.
     */
    public void mover(Input accion) {
        // Restringir X entre 0 y ANCHO - 1
        x = Math.max(0, Math.min(x, JuegoInvasores.ANCHO - 1));
        // Restringir Y entre -1 y ALTO
        y = Math.max(-1, Math.min(y, JuegoInvasores.ALTO));
    }
}

Las especializaciones: Nave, Alien y Bala

Aquí es donde brilla la Orientación a Objetos. Cada clase sobrescribe (@Override) el método mover para comportarse de forma distinta, pero el motor del juego las tratará a todas igual (como Entidad).

  1. Nave: Solo reacciona si el jugador pulsa Izquierda o Derecha.
  2. Alien: Ignora al jugador y siempre baja (y++).
  3. Bala: Ignora al jugador y siempre sube (y--).
class Nave extends Entidad {
    public Nave(int x, int y) { super(x, y, 'A'); }
    
    @Override
    public void mover(Input accion) {
        if (accion == Input.IZQUIERDA) x--;
        if (accion == Input.DERECHA)   x++;
        super.mover(accion); // Importante: Llamamos al padre para que verifique los bordes
    }
}

class Alien extends Entidad {
    public Alien(int x, int y) { super(x, y, 'V'); }
    
    @Override
    public void mover(Input accion) {
        y++; // El alien cae por gravedad
        super.mover(accion);
    }
}

class Bala extends Entidad {
    public Bala(int x, int y) { super(x, y, '|'); }
    
    @Override
    public void mover(Input accion) {
        y--; // La bala desafía la gravedad
        super.mover(accion);
    }
}

Gestión de memoria y bucle principal

En Java, especialmente en juegos simples, un Array es la estructura más eficiente. Definimos un array Entidad[] objetos que contendrá todo lo que existe en el universo del juego.

El Bucle de Juego (Game Loop) es infinito (while(jugando)) y sigue estos pasos:

  1. Render: Dibujar el mundo.
  2. Input: Leer al usuario.
  3. Update: Mover cosas y disparar.
  4. Collision: Verificar reglas del juego.

Inicialización y disparo

Observa cómo manejamos el disparo. No creamos listas dinámicas (ArrayList) para simplificar. Buscamos un hueco null en el array y metemos la bala ahí. Es una técnica de «Object Pooling» primitiva pero eficaz.

public class JuegoInvasores {
    static final int ANCHO = 10, ALTO = 12;
    static final int MAX_ALIENS = 2, MAX_ENTIDADES = 30;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Entidad[] objetos = new Entidad[MAX_ENTIDADES];

        // Inicializamos la Nave y los Aliens
        objetos[0] = new Nave(ANCHO / 2, ALTO - 1);
        for (int i = 1; i <= MAX_ALIENS; i++) {
            objetos[i] = new Alien((int)(Math.random() * ANCHO), 0);
        }

        boolean jugando = true;
        while (jugando) {
            dibujarJuego(objetos);
            Input.imprimirControles(); 
            Input accion = Input.detectar(scanner.nextLine());

            if (accion == Input.SALIR) {
                jugando = false;
            } else {
                // LOGICA DE DISPARO
                if (accion == Input.FUEGO) {
                    boolean balaDisparada = false; 
                    // Buscamos un hueco vacío en el array para crear la bala
                    for (int i = 0; i < objetos.length && !balaDisparada; i++) {
                        if (objetos[i] == null && objetos[0] != null) {
                            objetos[i] = new Bala(objetos[0].x, objetos[0].y);
                            balaDisparada = true; 
                        }
                    }
                }

                // ACTUALIZACION DE POSICIONES (POLIMORFISMO)
                for (Entidad e : objetos) {
                    if (e != null) e.mover(accion);
                }
                
                // ... (Ver siguiente sección)

Detección de Colisiones

Esta es la parte más compleja matemáticamente. En un juego por turnos en consola, una bala se mueve de la casilla 5 a la 4, y un alien de la 3 a la 4. ¿Pero qué pasa si se cruzan? Hemos implementado una verificación predictiva. Comprobamos si la bala está en la misma casilla que el alien (posibleBala.y == alien.y) O si acaba de cruzarlo (posibleBala.y == alien.y - 1).

    // ... dentro del bucle main
    int estadoJuego = procesarColisiones(objetos);
    if (estadoJuego == -1) {
        dibujarJuego(objetos);
        System.out.println("\n¡GAME OVER!");
        jugando = false;
    } else if (estadoJuego == 0) {
        dibujarJuego(objetos);
        System.out.println("\n¡VICTORIA!");
        jugando = false;
    }

Funciones de lógica del juego

Aquí se decide quién vive y quién muere. Usamos instanceof para saber qué tipo de entidad estamos comprobando.

    static int procesarColisiones(Entidad[] objetos) {
        int aliensVivos = 0;
        boolean invasion = false;

        for (int i = 0; i < objetos.length; i++) {
            Entidad ent = objetos[i];
            if (ent != null) {
                
                // Limpieza: Si algo se sale del mapa, lo borramos (ponemos a null)
                if (ent.y < 0 || ent.y >= ALTO) {
                    if (!(ent instanceof Nave)) objetos[i] = null;
                } else {
                    if (ent instanceof Alien) {
                        // Si un alien toca el fondo, perdemos
                        if (ent.y >= ALTO - 1) invasion = true;
                        
                        // Verificamos si este alien ha chocado con alguna bala
                        boolean impactado = verificarImpacto(ent, objetos);

                        if (impactado) {
                            objetos[i] = null; // Eliminamos el Alien
                            System.out.println("¡ALIEN DESTRUIDO!");
                        } else {
                            aliensVivos++;
                        }
                    }
                }
            }
        }
        if (invasion) return -1;
        return aliensVivos;
    }

    // Algoritmo de colisión cruzada
    static boolean verificarImpacto(Entidad alien, Entidad[] todosLosObjetos) {
        boolean impacto = false;
        for (int j = 0; j < todosLosObjetos.length; j++) {
            if (!impacto) {
                 Entidad posibleBala = todosLosObjetos[j];
                 if (posibleBala instanceof Bala 
                     && posibleBala.x == alien.x 
                     && (posibleBala.y == alien.y || posibleBala.y == alien.y - 1)) {
                    
                     todosLosObjetos[j] = null; // Eliminamos la Bala
                     impacto = true;
                 }
            }
        }
        return impacto;
    }

Dibujando la matriz

Finalmente, necesitamos ver lo que ocurre. La técnica usada aquí es crear un «Lienzo en blanco» (matriz de puntos), pintar las entidades encima y luego imprimirlo todo de golpe. Esto evita parpadeos y asegura que si dos objetos están en la misma casilla, uno se dibuje sobre el otro (el último en procesarse gana).

    static void dibujarJuego(Entidad[] lista) {
        // 1. Crear lienzo vacío
        char[][] matriz = new char[ALTO][ANCHO];
        for(int y=0; y<ALTO; y++) {
            for(int x=0; x<ANCHO; x++) {
                matriz[y][x] = '.';
            }
        }

        // 2. Pintar entidades
        for(Entidad e : lista) {
            if (e != null && e.y >= 0 && e.y < ALTO) {
                matriz[e.y][e.x] = e.icono;
            }
        }

        // 3. Imprimir en consola
        System.out.print("\n\n");
        for(int y=0; y<ALTO; y++) {
            for(int x=0; x<ANCHO; x++) {
                System.out.print(matriz[y][x] + " ");
            }
            System.out.println();
        }
    }
}

Todo el código

import java.util.Scanner;

/**
 * Enumeración que gestiona el control de entrada del usuario.
 * Define las teclas válidas y asocia cada una con una acción del juego.
 */
enum Input {
    IZQUIERDA("i", "Izquierda"), 
    DERECHA("d", "Derecha"), 
    FUEGO("f", "FUEGO"), 
    SALIR("x", "Salir"), 
    NADA("", ""); 

    private String tecla;
    private String descripcion;

    /**
     * Constructor del enum Input.
     * @param tecla Carácter que activa la acción.
     * @param descripcion Nombre legible de la acción.
     */
    Input(String tecla, String descripcion) { 
        this.tecla = tecla; 
        this.descripcion = descripcion;
    }

    /**
     * Imprime en la consola la lista de controles disponibles para el jugador.
     */
    public static void imprimirControles() {
        for (Input i : Input.values()) {
            if (!i.tecla.isEmpty()) System.out.print("[" + i.tecla + "]" + i.descripcion + "  ");
        }
        System.out.print("\nAcción > ");
    }

    /**
     * Detecta qué comando corresponde al texto introducido por el usuario.
     * @param texto Entrada del teclado.
     * @return La instancia de Input correspondiente o NADA si no coincide.
     */
    public static Input detectar(String texto) {
        for (Input i : Input.values()) {
            if (i.tecla.equalsIgnoreCase(texto)) return i;
        }
        return NADA;
    }
}

// ========================================================
// 2. CLASES (HERENCIA Y COMPORTAMIENTO BASE)
// ========================================================

/**
 * Clase base para todos los objetos del juego (Nave, Alien, Bala).
 * Contiene la posición y el icono representativo.
 */
class Entidad {
    protected int x, y;
    protected char icono;

    /**
     * Constructor de una Entidad.
     * @param x Posición horizontal inicial.
     * @param y Posición vertical inicial.
     * @param icono Carácter que representa a la entidad en el mapa.
     */
    public Entidad(int x, int y, char icono) {
        this.x = x; 
        this.y = y; 
        this.icono = icono;
    }

    /**
     * Actualiza la posición de la entidad y aplica restricciones de los límites del mapa.
     * @param accion La acción de entrada del usuario (opcional según el tipo de entidad).
     */
    public void mover(Input accion) {
        // Restringir X entre 0 y ANCHO - 1
        x = Math.max(0, Math.min(x, JuegoInvasores.ANCHO - 1));

        // Restringir Y entre -1 (para que las balas salgan) y ALTO
        y = Math.max(-1, Math.min(y, JuegoInvasores.ALTO));
    }
}

/**
 * Representa la nave controlada por el jugador.
 */
class Nave extends Entidad {
    /**
     * Crea una nave en una posición específica con el icono 'A'.
     * @param x Posición horizontal.
     * @param y Posición vertical.
     */
    public Nave(int x, int y) { 
        super(x, y, 'A'); 
    }
    
    /**
     * Mueve la nave horizontalmente según el input.
     * @param accion Dirección del movimiento (IZQUIERDA o DERECHA).
     */
    @Override
    public void mover(Input accion) {
        if (accion == Input.IZQUIERDA) x--;
        if (accion == Input.DERECHA)   x++;
        super.mover(accion); // Ejecuta el control de límites de la clase Entidad
    }
}

/**
 * Representa a los enemigos que descienden por la pantalla.
 */
class Alien extends Entidad {
    /**
     * Crea un alien con el icono 'V'.
     * @param x Posición horizontal.
     * @param y Posición vertical.
     */
    public Alien(int x, int y) { 
        super(x, y, 'V'); 
    }
    
    /**
     * Mueve al alien una posición hacia abajo en cada turno.
     * @param accion No se utiliza para el alien, ya que su movimiento es automático.
     */
    @Override
    public void mover(Input accion) {
        y++; // El alien siempre baja
        super.mover(accion);
    }
}

/**
 * Representa el proyectil disparado por la nave.
 */
class Bala extends Entidad {
    /**
     * Crea una bala con el icono '|'.
     * @param x Posición horizontal.
     * @param y Posición vertical.
     */
    public Bala(int x, int y) { 
        super(x, y, '|'); 
    }
    
    /**
     * Mueve la bala una posición hacia arriba en cada turno.
     * @param accion No se utiliza para la bala.
     */
    @Override
    public void mover(Input accion) {
        y--; // La bala siempre sube
        super.mover(accion);
    }
}

// ========================================================
// 3. MOTOR DEL JUEGO
// ========================================================

/**
 * Clase principal que contiene el bucle del juego y la lógica de renderizado.
 */
public class JuegoInvasores {
    static final int ANCHO = 12, ALTO = 12;
    static final int MAX_ALIENS = 3, MAX_ENTIDADES = 30; // Limite total de entidades

    /**
     * Punto de entrada principal del programa.
     * @param args Argumentos de línea de comandos.
     */
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Entidad[] objetos = new Entidad[MAX_ENTIDADES];

        // --- INICIO ---
        objetos[0] = new Nave(ANCHO / 2, ALTO - 1);
        for (int i = 1; i <= MAX_ALIENS; i++) {
            objetos[i] = new Alien((int)(Math.random() * ANCHO), 0);
        }

        boolean jugando = true;
        while (jugando) {
            dibujarJuego(objetos);
            Input.imprimirControles(); 
            Input accion = Input.detectar(scanner.nextLine());

            if (accion == Input.SALIR) {
                jugando = false;
            } else {
                // --- DISPARAR (Crea una bala en la posición de la nave) ---
                if (accion == Input.FUEGO) {
                    boolean balaDisparada = false; 
                    for (int i = 0; i < objetos.length && !balaDisparada; i++) {
                        if (objetos[i] == null) {
                            if (objetos[0] != null) {
                                objetos[i] = new Bala(objetos[0].x, objetos[0].y);
                                balaDisparada = true; // Evita seguir buscando huecos
                            }
                        }
                    }
                }

                // --- ACTUALIZAR POSICIONES ---
                for (Entidad e : objetos) {
                    if (e != null) e.mover(accion);
                }

                // --- GESTIÓN DE COLISIONES Y ESTADO ---
                int estadoJuego = procesarColisiones(objetos);

                if (estadoJuego == -1) {
                    dibujarJuego(objetos);
                    System.out.println("\n¡GAME OVER!");
                    jugando = false;
                } else if (estadoJuego == 0) {
                    dibujarJuego(objetos);
                    System.out.println("\n¡VICTORIA!");
                    jugando = false;
                }
            }
        }
        scanner.close();
    }

    /**
     * Procesa la limpieza de proyectiles fuera del mapa, detecta invasiones y gestiona colisiones.
     * @param objetos Array que contiene todas las entidades activas del juego.
     * @return El número de aliens vivos, o -1 si el jugador ha perdido (invasión).
     */
    static int procesarColisiones(Entidad[] objetos) {
        int aliensVivos = 0;
        boolean invasion = false;

        for (int i = 0; i < objetos.length; i++) {
            Entidad ent = objetos[i];

            if (ent != null) {
                // 1. Limpieza de proyectiles o aliens fuera de rango
                if (ent.y < 0 || ent.y >= ALTO) {
                    if (!(ent instanceof Nave)) objetos[i] = null;
                } else {
                    // 2. Lógica específica para Aliens
                    if (ent instanceof Alien) {
                        
                        // Comprobar si ha llegado al final (invasión)
                        if (ent.y >= ALTO - 1) { 
                             invasion = true;
                        }
                        
                        // 3. Colisión Bala-Alien
                        // Pasamos el alien actual (ent) y la lista completa para buscar balas
                        boolean impactado = verificarImpacto(ent, objetos);

                        if (impactado) {
                            objetos[i] = null; // Borra Alien
                            System.out.println("¡ALIEN DESTRUIDO!");
                        } else {
                            // Solo contamos el alien si NO ha sido destruido
                            aliensVivos++;
                        }
                    }
                }
            }
        }

        if (invasion) return -1; // Código de derrota
        return aliensVivos;      // Retorna cuántos quedan
    }

    /**
     * Verifica si una bala ha impactado en un alien específico.
     * @param alien El alien que se va a comprobar.
     * @param todosLosObjetos Array completo para buscar balas activas.
     * @return true si el alien fue impactado por una bala, false en caso contrario.
     */
    static boolean verificarImpacto(Entidad alien, Entidad[] todosLosObjetos) {
        boolean impacto = false;
        
        for (int j = 0; j < todosLosObjetos.length; j++) {
            // Comprobamos solo si no hemos impactado ya en este ciclo (sin break)
            if (!impacto) {
                 Entidad posibleBala = todosLosObjetos[j];
                 
                 // Verificamos si es Bala y si las coordenadas coinciden
                 // Nota: (alien.y - 1) arregla el problema de colisiones cruzadas
                 if (posibleBala instanceof Bala 
                     && posibleBala.x == alien.x 
                     && (posibleBala.y == alien.y || posibleBala.y == alien.y - 1)) {
                    
                     todosLosObjetos[j] = null; // Borra Bala
                     impacto = true;   // Marca que hubo impacto
                 }
            }
        }
        return impacto;
    }

    /**
     * Dibuja el estado actual del juego en la consola mediante una matriz de caracteres.
     * @param lista Array de entidades a dibujar sobre el mapa.
     */
    static void dibujarJuego(Entidad[] lista) {
        char[][] matriz = new char[ALTO][ANCHO];
        for(int y=0; y<ALTO; y++) {
            for(int x=0; x<ANCHO; x++) {
                matriz[y][x] = '.';
            }
        }

        for(Entidad e : lista) {
            if (e != null && e.y >= 0 && e.y < ALTO) {
                matriz[e.y][e.x] = e.icono;
            }
        }

        System.out.print("\n\n");
        for(int y=0; y<ALTO; y++) {
            for(int x=0; x<ANCHO; x++) {
                System.out.print(matriz[y][x] + " ");
            }
            System.out.println();
        }
    }
}

Conclusión

Hemos construido un juego completo en unas pocas clases. Lo importante de este ejercicio no es el juego en sí, sino cómo hemos estructurado los datos:

  1. Entidades autónomas: La clase Nave no necesita saber cómo funciona el Alien.
  2. Control centralizado: El Main orquesta todo, pero delega el movimiento a cada objeto.
  3. Tipado fuerte: El uso de Enum previene errores tontos de teclado.

Programando el juego Hundir la flota en Java paso a paso

Introducción

En esta unidad, vamos a desglosar una implementación avanzada del juego clásico de estrategia naval. A diferencia de otros ejemplos que pueden ser más simples, este código introduce los siguientes conceptos a nivel más avanzado:

  1. Motor de renderizado ANSI: Manipulación de la consola del sistema operativo para mostrar colores.
  2. Modelo de datos robusto: Uso de enumerados (enum) para encapsular comportamiento y configuración.
  3. Algoritmos de colocación espacial: Lógica matemática para gestionar colisiones y perímetros de seguridad en una matriz.
  4. Niebla de guerra: Separación entre el estado interno del programa y lo que se muestra al usuario.

En este enlace puedes encontrar una presentación explicando el código, y a continuación dispones de una infografía y un vídeo explicativo.

Estructuras de datos: El uso de enum frente a enteros

Implementación en el enum TipoBarco

El objetivo principal de este enum es actuar como bloque de definición de datos para la configuración de la flota, persiguiendo los siguientes objetivos:

  • Centralización de reglas: Imagina que quieres cambiar las reglas del juego para que el submarino sea más grande (4 casillas en vez de 3). Si usaras variables sueltas dispersas por el código, tendrías que buscar y cambiar ese número en varios sitios. Con este enum, cambias el número en una sola línea (SUBMARINO(4)) y todo el juego (condiciones de victoria, lógica de colocación, etc.) se actualiza automáticamente.
  • Eliminación de «números mágicos»: En programación, encontrar un 5 suelto en el código es confuso (¿es la munición? ¿es el tamaño del tablero?). Al usar TipoBarco.PORTAAVIONES.getLongitud(), el código se explica por sí mismo.
  • Agrupación lógica: Vincula inseparablemente el nombre de la entidad (portaaviones) con su propiedad (tamaño 5). No puedes tener un portaaviones sin tamaño, ni un tamaño sin barco.

Cuando el programa arranca, Java crea automáticamente una única instancia para cada uno de estos nombres. Por ejemplo, al escribir PORTAAVIONES(5), Java está haciendo internamente algo similar a: new TipoBarco(5). Le está pasando el valor 5 al constructor. De esta forma, cada barco tiene su propia variable interna llamada longitud, con las siguientes características:

  • Es private: Nadie desde fuera puede modificarla directamente. Para acceder a su valor, utilizaremos un método público para que el resto del programa pueda preguntar: «¿Cuánto mide este barco?»
  • Es final: Una vez asignada, no puede cambiar. Un destructor siempre medirá 2 durante toda la ejecución del programa. Esto garantiza la integridad de las reglas.
    /**
     * Define los tipos de barcos y sus longitudes.
     * Centraliza la configuración: si cambiamos un número aquí, el juego se adapta.
     */
    enum TipoBarco {
        PORTAAVIONES(5),
        ACORAZADO(4),
        CRUCERO(3),
        SUBMARINO(3),
        DESTRUCTOR(2);

        private final int longitud;

        TipoBarco(int longitud) {
            this.longitud = longitud;
        }

        public int getLongitud() {
            return longitud;
        }
    }

Estructura de colores RGB en consola

Esta es una de las partes más interesantes del código. Java, por defecto, imprime texto plano en la consola. Sin embargo, los terminales modernos (CMD, PowerShell, Bash) soportan Secuencias de Escape ANSI.

Cuando imprimimos un carácter especial llamado ESCAPE (código ASCII 27, representado en Java como \u001B), le estamos diciendo a la consola: «Atención, lo que viene a continuación no es texto para leer, es una orden de configuración».

El código implementa el estándar RGB TrueColor con la estructura «\u001B[38;2;R;G;Bm«:

  • \u001B[: Inicio de la secuencia de control (CSI).
  • 38: Indica que vamos a cambiar el color del texto (foreground).
  • 2: Indica que usaremos el modo RGB (Red, Green, Blue).
  • R;G;B: Son tres números del 0 al 255 que definen la mezcla de color.
  • m: Indica el fin de la instrucción.

Implementación en el enum EstadoCasilla

En lugar de escribir estos códigos crípticos cada vez que queremos imprimir algo, los encapsulamos en el constructor del enum:

  1. Constructor: Recibe los valores enteros R, G y B.
  2. String.format: Construye la secuencia ANSI dinámica.
  3. toString: Sobrescribe el método estándar para devolver: COLOR + SÍMBOLO + RESET. El código de RESET (\u001B[0m) es vital; si no lo pusiéramos, toda la consola se quedaría pintada de ese color indefinidamente.
    /**
     * Motor de colores RGB.
     * Transforma códigos numéricos en secuencias de escape ANSI.
     */
    enum EstadoCasilla {
        // Definición semántica con sus valores visuales (Símbolo, R, G, B)
        AGUA     ( "~",      0, 180, 255),   // Agua (azul claro)
        BARCO    ( "B",    160,  32, 240),   // Púrpura (visible al acabar)
        TOCADO   ( "X",    255,   0,   0),   // Rojo puro (impacto)
        FALLO    ( "o",    180, 180, 180);   // Gris (agua con impacto)

        private final String simbolo;
        private final String codigoColor;
        // Secuencia para restablecer el color por defecto de la terminal
        private static final String RESET = "\u001B[0m";

        /**
         * Constructor que inyecta la lógica de color.
         * La secuencia mágica es: \u001B[38;2;R;G;Bm
         */
        EstadoCasilla(String simbolo, int r, int g, int b) {
            this.simbolo = simbolo;
            this.codigoColor = String.format("\u001B[38;2;%d;%d;%dm", r, g, b);
        }

        @Override
        public String toString() {
            // Devuelve: [Instrucción Color] + [Carácter] + [Instrucción Reset]
            return codigoColor + simbolo + RESET;
        }
    }

Gestión de memoria y estado global

Para gestionar el tablero, utilizamos una matriz bidimensional de objetos EstadoCasilla.

  • static EstadoCasilla[][] oceano: Es la representación en memoria del mar. Cada celda [fila][columna] contiene una referencia a una de las constantes del Enum (AGUA, BARCO, etc.).
  • Cálculo dinámico de victoria: La constante IMPACTOS_NECESARIOS no se fija a mano (ej: 17). Se calcula llamando a una función que suma las longitudes de todos los barcos definidos. Esto hace el código extremadamente robusto: si mañana añadimos un nuevo barco al enum, la condición de victoria se recalcula sola sin tocar el resto del código.
    // CONSTANTES Y ESTADO GLOBAL

    static final int DIMENSION = 10; // Tamaño del tablero 10x10
    static final int NO_ENCONTRADO = -1; // Centinela para búsquedas fallidas

    // Configuración de dificultad
    static final int MUNICION_MAXIMA = 50;
    // Cálculo dinámico de la condición de victoria
    static final int IMPACTOS_NECESARIOS = calcularPuntosTotales();

    // La Matriz Principal (El "Tablero")
    static EstadoCasilla[][] oceano = new EstadoCasilla[DIMENSION][DIMENSION];
    
    // Herramientas de entrada y aleatoriedad
    static Scanner teclado = new Scanner(System.in);
    static Random radar = new Random();

El ciclo de vida del juego

El método main se encarga de la ejecución temporal del programa. Podemos distinguir tres fases claras:

  1. Fase de inicialización:
    • Limpia el tablero (inicializarOceano).
    • Coloca los barcos sin que el jugador sepa dónde están (colocarFlotaCompleta).
  2. Fase de ejecución (bucle while):
    • Renderizado: Llama a imprimirOceano(false). El parámetro false activa la «niebla de guerra», ocultando los barcos.
    • Input: Solicita coordenadas y valida que no se repitan.
    • Lógica: Actualiza el estado de la matriz (de AGUA/BARCO a FALLO/TOCADO).
  3. Fase de clausura:
    • Al terminar (victoria o derrota), llama a imprimirOceano(true). El parámetro true desactiva la niebla y revela la ubicación de todos los barcos.
    public static void main(String[] args) {
        System.out.println("--- HUNDIR LA FLOTA ---");
        System.out.println("Tablero: " + DIMENSION + "x" + DIMENSION);
        System.out.println("Objetivo: " + IMPACTOS_NECESARIOS + " impactos.");
        System.out.println("Munición: " + MUNICION_MAXIMA + " misiles.");

        // 1. Fase de Preparación
        inicializarOceano();
        colocarFlotaCompleta();

        // Variables de control de flujo
        int aciertos = 0;
        int misilesRestantes = MUNICION_MAXIMA;
        boolean juegoTerminado = false;

        // 2. Bucle Principal (Game Loop)
        while (!juegoTerminado) {
            // Renderizamos con Niebla de Guerra (false)
            imprimirOceano(false); 

            System.out.println("------------------------------------------------");
            System.out.println("Misiles: " + misilesRestantes + " | Aciertos: " + aciertos + "/" + IMPACTOS_NECESARIOS);

            // Turno del jugador
            boolean impacto = realizarDisparo();
            misilesRestantes--;

            // Feedback visual inmediato
            if (impacto) {
                aciertos++;
                System.out.println(">>> ¡IMPACTO CONFIRMADO! <<<");
            } else {
                System.out.println(">>> Agua. Sin rastro del enemigo. <<<");
            }

            // 3. Comprobación de condiciones de fin
            if (aciertos == IMPACTOS_NECESARIOS) {
                juegoTerminado = true;
                imprimirOceano(true); // Revelamos el mapa (Cheat mode activado legalmente)
                System.out.println("\n¡VICTORIA! Has desmantelado la flota enemiga.");

            } else if (misilesRestantes == 0) {
                juegoTerminado = true;
                imprimirOceano(true);
                System.out.println("\n¡MUNICIÓN AGOTADA! Retirada táctica.");
            }
        }
    }

La meta del juego: calcularPuntosTotales()

Esta función es muy inteligente porque no usa un número «fijo» (como decir que se gana con 10 puntos). En su lugar, pregunta a los barcos cuánto miden.

  • TipoBarco.values(): Esto obtiene una lista de todos los tipos de barcos que has definido (Portaaviones, Submarino, etc.).
  • El bucle for: Recorre cada barco, mira su longitud (getLongitud()) y la suma a la variable suma.
  • ¿Por qué es útil?: Si mañana decides añadir un barco más o cambiar el tamaño de uno, no tienes que tocar el código de victoria; el programa calculará automáticamente que ahora hacen falta, por ejemplo, 17 puntos en lugar de 15.
    /**
     * Calcula dinámicamente cuántos puntos hacen falta para ganar.
     */
    static int calcularPuntosTotales() {
        int suma = 0;
        for (TipoBarco barco : TipoBarco.values()) {
            suma += barco.getLongitud();
        }
        return suma;
    }

El turno del jugador: realizarDisparo()

Esta función debe validar los datos introducidos por el usuario y actualizar el mapa. Se divide en dos bloques:

El bucle de validación (while)

El objetivo de este bloque es que el jugador no pierda el turno por un error en la introducción de datos o en la elección de la casilla donde quiere efectuar el disparo.

  1. Entrada de datos: Pide fila y columna.
  2. Comprobación de repetición: Mira en la matriz oceano[fila][col].
    • Si el valor es TOCADO o FALLO, significa que ya hubo un proyectil ahí.
    • Importante: Mientras el disparo no sea «nuevo», disparoValido sigue siendo false y el bucle se repite indefinidamente. El jugador no saldrá de aquí hasta que dé una coordenada válida donde no se haya disparado todavía.

Resolución del impacto (if-else)

Una vez que tenemos una coordenada válida, el programa decide qué hay debajo:

  • Si hay un BARCO:
    • Cambia el estado a TOCADO (para que el dibujo del mapa cambie).
    • return true: Esto le dice al resto del programa: «¡Oye, suma un punto al marcador!».
  • Si no hay barco (es decir, hay agua):
    • Cambia el estado a FALLO.
    • return false: El contador de impactos no cambia.

Conceptos clave de Java en este código

  • Enums (EstadoCasilla y TipoBarco): El código no usa números sueltos (como 0 o 1), sino nombres claros. Es mucho más fácil leer EstadoCasilla.TOCADO que leer un 2 y tener que recordar qué significaba.
  • Booleans como control: El uso de disparoValido actúa como un «cerrojo» para asegurar que la gestión del impacto se realice con datos correctos.
    /**
     * Procesa el turno de disparo.
     * Devuelve true si acertamos a un barco, false si damos en agua.
     */
    static boolean realizarDisparo() {
        boolean disparoValido = false;
        int fila = NO_ENCONTRADO;
        int col = NO_ENCONTRADO;

        // Bucle de validación de entrada lógica
        while (!disparoValido) {
            fila = pedirCoordenada("Fila (0-" + (DIMENSION - 1) + "): ");
            col = pedirCoordenada("Columna (0-" + (DIMENSION - 1) + "): ");

            // Evitar disparar dos veces al mismo sitio
            if (oceano[fila][col] == EstadoCasilla.TOCADO || oceano[fila][col] == EstadoCasilla.FALLO) {
                System.out.println("Ya has disparado en esa zona. Elige otra.");
            } else {
                disparoValido = true;
            }
        }

        // Resolución del impacto en la matriz
        if (oceano[fila][col] == EstadoCasilla.BARCO) {
            oceano[fila][col] = EstadoCasilla.TOCADO;
            return true;
        } else {
            oceano[fila][col] = EstadoCasilla.FALLO;
            return false;
        }
    }

Distribución de barcos: geometría y «regla del aire»

Esta es la sección más compleja algorítmicamente. El objetivo es colocar barcos aleatoriamente pero cumpliendo dos reglas estrictas:

  1. El barco no debe salirse del tablero.
  2. Regla del aire: El barco no puede tocar a otro, ni siquiera en diagonal. Debe haber al menos una casilla de agua de separación.

El algoritmo de «bounding box» (caja delimitadora)

En la función esPosicionValida, calculamos un rectángulo de seguridad. Por ejemplo, si el barco va de la fila F a la fila F+3 (tamaño 4), revisamos desde F-1 hasta F+4.

Para evitar errores de «índice fuera de rango» (por ejemplo, si intentáramos revisar la fila -1), utilizamos las funciones matemáticas Math.max(0, ...) y Math.min(DIMENSION-1, ...). Esto «recorta» el área de búsqueda a los límites reales del tablero.

    // ALGORITMOS DE COLOCACIÓN

    static void colocarFlotaCompleta() {
        for (TipoBarco barco : TipoBarco.values()) {
            colocarBarcoAleatorio(barco);
        }
    }

    /**
     * Algoritmo de Fuerza Bruta (Trial & Error).
     * Intenta coordenadas al azar hasta encontrar una válida.
     */
    static void colocarBarcoAleatorio(TipoBarco barco) {
        boolean colocado = false;

        while (!colocado) {
            int fila = radar.nextInt(DIMENSION);
            int col = radar.nextInt(DIMENSION);
            boolean horizontal = radar.nextBoolean();

            if (esPosicionValida(fila, col, barco.getLongitud(), horizontal)) {
                pintarBarcoEnMatriz(fila, col, barco.getLongitud(), horizontal);
                colocado = true;
            }
        }
    }

    /**
     * Valida si un barco cabe y respeta el perímetro de seguridad.
     */
    static boolean esPosicionValida(int f, int c, int longitud, boolean horizontal) {
        // 1. Determinar dimensiones del barco
        int anchoBarco = horizontal ? longitud : 1;
        int altoBarco  = horizontal ? 1 : longitud;

        // 2. Verificar límites del tablero
        if (f + altoBarco > DIMENSION || c + anchoBarco > DIMENSION) {
            return false;
        }

        // 3. Definir el "Marco de Seguridad" (Bounding Box)
        // Usamos Math.max y min para no salirnos de los índices 0-9
        int filaInicio = Math.max(0, f - 1);
        int colInicio  = Math.max(0, c - 1);

        int filaFin = Math.min(DIMENSION - 1, f + altoBarco);
        int colFin  = Math.min(DIMENSION - 1, c + anchoBarco);

        // 4. Escaneo de Área
        for (int i = filaInicio; i <= filaFin; i++) {
            for (int j = colInicio; j <= colFin; j++) {
                // Si encontramos CUALQUIER COSA que no sea agua pura, la posición es inválida
                if (oceano[i][j] != EstadoCasilla.AGUA) {
                    return false; 
                }
            }
        }
        return true; // Zona despejada
    }

    static void pintarBarcoEnMatriz(int f, int c, int longitud, boolean horizontal) {
        for (int i = 0; i < longitud; i++) {
            if (horizontal) {
                oceano[f][c + i] = EstadoCasilla.BARCO;
            } else {
                oceano[f + i][c] = EstadoCasilla.BARCO;
            }
        }
    }

Renderizado y utilidades

Finalmente, necesitamos dibujar el tablero. La función imprimirOceano aplica el concepto de niebla de guerra.

  • Recorre la matriz celda por celda.
  • Si la celda contiene un BARCO y estamos en modo juego (revelarTodo == false), el programa miente al usuario e imprime el símbolo de AGUA.
  • Esto demuestra cómo separar los datos (lo que hay en memoria) de la vista (lo que ve el usuario).

También incluimos pedirCoordenada con hasNextInt() para evitar que el programa falle si el usuario introduce letras.

    // UTILIDADES Y VISTA

    static void inicializarOceano() {
        for (int f = 0; f < DIMENSION; f++) {
            for (int c = 0; c < DIMENSION; c++) {
                oceano[f][c] = EstadoCasilla.AGUA;
            }
        }
    }

    /**
     * Dibuja el tablero aplicando la lógica de ocultación.
     * @param revelarTodo Si es true, muestra los barcos (Game Over).
     */
    static void imprimirOceano(boolean revelarTodo) {
        System.out.println();

        // Eje de coordenadas X (Columnas)
        System.out.print("   ");
        for (int c = 0; c < DIMENSION; c++) {
            System.out.print(c + " ");
        }
        System.out.println();

        for (int f = 0; f < DIMENSION; f++) {
            System.out.print(f + "| "); // Eje de coordenadas Y (Filas)
            for (int c = 0; c < DIMENSION; c++) {
                EstadoCasilla actual = oceano[f][c];

                // LÓGICA DE NIEBLA DE GUERRA
                if (actual == EstadoCasilla.BARCO && !revelarTodo) {
                    // Si hay barco pero el juego sigue, ocultamos con agua
                    System.out.print(EstadoCasilla.AGUA + " ");
                } else {
                    // En cualquier otro caso, mostramos la realidad
                    System.out.print(actual + " ");
                }
            }
            System.out.println("|");
        }
    }

    /**
     * Lectura segura de enteros desde teclado.
     */
    static int pedirCoordenada(String mensaje) {
        int valor = NO_ENCONTRADO;
        boolean valido = false;

        while (!valido) {
            System.out.print(mensaje);
            if (teclado.hasNextInt()) {
                valor = teclado.nextInt();
                if (valor >= 0 && valor < DIMENSION) {
                    valido = true;
                } else {
                    System.out.println("Error: El número debe estar entre 0 y " + (DIMENSION - 1));
                }
            } else {
                teclado.next(); // Limpiar buffer de entrada errónea
                System.out.println("Error: Debes introducir un número entero.");
            }
        }
        return valor;
    }
}

Todo el código

import java.util.Random;
import java.util.Scanner;

/**
 * Juego de "Hundir la Flota" (Versión un jugador).
 * El objetivo es encontrar todos los barcos enemigos ocultos en el tablero
 * antes de que se agoten los intentos (misiles) disponibles.
 *
 * Este ejemplo refuerza el uso de matrices para ocultar información (niebla de guerra)
 * y la gestión de estados mediante Enums.
 */
public class Main {

    // --- ENUMS: CONFIGURACIÓN Y ESTÉTICA ---

    /**
     * Define los tipos de barcos y sus longitudes.
     */
    enum TipoBarco {
        PORTAAVIONES(5),
        ACORAZADO(4),
        CRUCERO(3),
        SUBMARINO(3),
        DESTRUCTOR(2);

        private final int longitud;

        TipoBarco(int longitud) {
            this.longitud = longitud;
        }

        public int getLongitud() {
            return longitud;
        }
    }

    /**
     * Enum que define los colores usando formato RGB (Red, Green, Blue).
     * Esto permite usar cualquier color de los 16 millones disponibles.
     */
    enum EstadoCasilla {
        // Símbolo y color en formato RGB
        AGUA     ( "~",      0, 180, 255),   // Azul cielo (agua)
        BARCO    ( "B",    160,  32, 240),   // Púrpura (barco sin impacto)
        TOCADO   ( "X",    255,   0,   0),   // Rojo puro (impacto)
        FALLO    ( "o",    180, 180, 180);   // Gris claro (fallo)

        private final String simbolo;
        private final String codigoColor;
        private static final String RESET = "\u001B[0m";

        /**
         * Constructor que acepta valores RGB (0-255).
         * Convierte los números a la secuencia ANSI TrueColor automáticamente.
         */
        EstadoCasilla(String simbolo, int r, int g, int b) {
            this.simbolo = simbolo;
            // La secuencia mágica para RGB es: \u001B[38;2;R;G;Bm
            this.codigoColor = String.format("\u001B[38;2;%d;%d;%dm", r, g, b);
        }

        @Override
        public String toString() {
            return codigoColor + simbolo + RESET;
        }
    }

    // --- CONSTANTES Y ESTADO GLOBAL ---

    static final int DIMENSION = 10;
    static final int NO_ENCONTRADO = -1;

    // Configuración de dificultad
    static final int MUNICION_MAXIMA = 50;
    static final int IMPACTOS_NECESARIOS = calcularPuntosTotales();

    // Matriz del tablero y herramientas
    static EstadoCasilla[][] oceano = new EstadoCasilla[DIMENSION][DIMENSION];
    static Scanner teclado = new Scanner(System.in);
    static Random radar = new Random();

    /**
     * Hilo principal de ejecución.
     * Gestiona el bucle de juego, el control de turnos y las condiciones de victoria/derrota.
     * @param args Argumentos de consola (no utilizados).
     */
    public static void main(String[] args) {
        System.out.println("--- HUNDIR LA FLOTA ---");
        System.out.println("Tablero: " + DIMENSION + "x" + DIMENSION);
        System.out.println("Regla especial: Los barcos no pueden tocarse entre sí.");
        System.out.println("Objetivo: " + IMPACTOS_NECESARIOS + " impactos.");
        System.out.println("Munición: " + MUNICION_MAXIMA + " misiles.");

        // 1. Preparación
        inicializarOceano();
        colocarFlotaCompleta();

        // 2. Variables de estado
        int aciertos = 0;
        int misilesRestantes = MUNICION_MAXIMA;
        boolean juegoTerminado = false;

        // 3. Bucle Principal
        while (!juegoTerminado) {
            imprimirOceano(false); // false = Modo Niebla de Guerra

            System.out.println("------------------------------------------------");
            System.out.println("Misiles: " + misilesRestantes + " | Aciertos: " + aciertos + "/" + IMPACTOS_NECESARIOS);

            // Turno de juego
            boolean impacto = realizarDisparo();
            misilesRestantes--;

            // Feedback inmediato
            if (impacto) {
                aciertos++;
                System.out.println(">>> ¡IMPACTO CONFIRMADO! <<<");
            } else {
                System.out.println(">>> Agua. Sin rastro del enemigo. <<<");
            }

            // Comprobación de fin de partida
            if (aciertos == IMPACTOS_NECESARIOS) {
                juegoTerminado = true;
                imprimirOceano(true); // Revelamos el mapa
                System.out.println("\n¡VICTORIA! Has desmantelado la flota enemiga.");

            } else if (misilesRestantes == 0) {
                juegoTerminado = true;
                imprimirOceano(true);
                System.out.println("\n¡MUNICIÓN AGOTADA! Retirada táctica.");
            }
        }
    }

    // --- LÓGICA DE JUEGO ---

    /**
     * Calcula dinámicamente cuántos aciertos hacen falta para ganar.
     * @return Total de casillas ocupadas por barcos.
     */
    static int calcularPuntosTotales() {
        int suma = 0;
        for (TipoBarco barco : TipoBarco.values()) {
            suma += barco.getLongitud();
        }
        return suma;
    }

    /**
     * Gestiona la interacción con el usuario para realizar un disparo.
     * Verifica que la coordenada sea válida y no se haya disparado antes allí.
     * @return true si el disparo acierta en un barco, false si falla.
     */
    static boolean realizarDisparo() {
        boolean disparoValido = false;
        int fila = NO_ENCONTRADO;
        int col = NO_ENCONTRADO;

        while (!disparoValido) {
            fila = pedirCoordenada("Fila (0-" + (DIMENSION - 1) + "): ");
            col = pedirCoordenada("Columna (0-" + (DIMENSION - 1) + "): ");

            if (oceano[fila][col] == EstadoCasilla.TOCADO || oceano[fila][col] == EstadoCasilla.FALLO) {
                System.out.println("Ya has disparado en esa zona. Elige otra.");
            } else {
                disparoValido = true;
            }
        }

        if (oceano[fila][col] == EstadoCasilla.BARCO) {
            oceano[fila][col] = EstadoCasilla.TOCADO;
            return true;
        } else {
            oceano[fila][col] = EstadoCasilla.FALLO;
            return false;
        }
    }

    // --- ALGORITMOS DE COLOCACIÓN (IA) ---

    /**
     * Recorre el catálogo de barcos y delega la colocación de cada uno.
     */
    static void colocarFlotaCompleta() {
        for (TipoBarco barco : TipoBarco.values()) {
            colocarBarcoAleatorio(barco);
        }
    }

    /**
     * Intenta colocar un barco en una posición aleatoria.
     * Si la posición elegida no es válida (choca, se toca con otro o se sale), repite el intento.
     * @param barco El tipo de barco a colocar.
     */
    static void colocarBarcoAleatorio(TipoBarco barco) {
        boolean colocado = false;

        while (!colocado) {
            int fila = radar.nextInt(DIMENSION);
            int col = radar.nextInt(DIMENSION);
            boolean horizontal = radar.nextBoolean();

            if (esPosicionValida(fila, col, barco.getLongitud(), horizontal)) {
                pintarBarcoEnMatriz(fila, col, barco.getLongitud(), horizontal);
                colocado = true;
            }
        }
    }

    /**
     * Verifica si el barco cabe y cumple la "Regla del Aire".
     * La regla del aire implica que no solo las casillas del barco deben estar libres,
     * sino también todas las casillas adyacentes (incluyendo diagonales).
     * @param f Fila inicial.
     * @param c Columna inicial.
     * @param longitud Tamaño del barco.
     * @param horizontal Orientación (true = horizontal, false = vertical).
     * @return true si el barco y su perímetro están libres.
     */
    static boolean esPosicionValida(int f, int c, int longitud, boolean horizontal) {
        // 1. Calculamos las dimensiones que ocupará el barco
        int anchoBarco = horizontal ? longitud : 1;
        int altoBarco  = horizontal ? 1 : longitud;

        // 2. Validar límites del tablero (Si se sale, devolvemos false)
        if (f + altoBarco > DIMENSION || c + anchoBarco > DIMENSION) {
            return false;
        }

        // 3. Definir el "Marco de Seguridad" (Barco + 1 casilla alrededor)
        // Usamos Math.max/min para no salirnos de los bordes (0 y 9)
        int filaInicio = Math.max(0, f - 1);
        int colInicio  = Math.max(0, c - 1);

        int filaFin = Math.min(DIMENSION - 1, f + altoBarco);
        int colFin  = Math.min(DIMENSION - 1, c + anchoBarco);

        // 4. Escanear esa área buscando obstáculos
        for (int i = filaInicio; i <= filaFin; i++) {
            for (int j = colInicio; j <= colFin; j++) {
                if (oceano[i][j] != EstadoCasilla.AGUA) {
                    return false; // Colisión detectada (barco o vecino)
                }
            }
        }

        return true; // Todo limpio
    }

    /**
     * Escribe el barco en la matriz una vez validada la posición.
     */
    static void pintarBarcoEnMatriz(int f, int c, int longitud, boolean horizontal) {
        for (int i = 0; i < longitud; i++) {
            if (horizontal) {
                oceano[f][c + i] = EstadoCasilla.BARCO;
            } else {
                oceano[f + i][c] = EstadoCasilla.BARCO;
            }
        }
    }

    // --- UTILIDADES Y VISTA ---

    /**
     * Limpia el tablero llenándolo de agua.
     */
    static void inicializarOceano() {
        for (int f = 0; f < DIMENSION; f++) {
            for (int c = 0; c < DIMENSION; c++) {
                oceano[f][c] = EstadoCasilla.AGUA;
            }
        }
    }

    /**
     * Dibuja el tablero en consola.
     * Utiliza la lógica de "Niebla de Guerra".
     * @param revelarTodo true para mostrar la ubicación de los barcos (Game Over).
     */
    static void imprimirOceano(boolean revelarTodo) {
        System.out.println();

        // Cabecera de columnas
        System.out.print("   ");
        for (int c = 0; c < DIMENSION; c++) {
            System.out.print(c + " ");
        }
        System.out.println();

        for (int f = 0; f < DIMENSION; f++) {
            System.out.print(f + "| "); // Índice de fila
            for (int c = 0; c < DIMENSION; c++) {
                EstadoCasilla actual = oceano[f][c];

                // Si es un barco y estamos jugando, lo mostramos como agua
                if (actual == EstadoCasilla.BARCO && !revelarTodo) {
                    System.out.print(EstadoCasilla.AGUA + " ");
                } else {
                    System.out.print(actual + " ");
                }
            }
            System.out.println("|");
        }
    }

    /**
     * Pide un número entero al usuario de forma segura.
     * @param mensaje Texto a mostrar.
     * @return Un entero validado dentro del rango del tablero.
     */
    static int pedirCoordenada(String mensaje) {
        int valor = NO_ENCONTRADO;
        boolean valido = false;

        while (!valido) {
            System.out.print(mensaje);
            if (teclado.hasNextInt()) {
                valor = teclado.nextInt();
                if (valor >= 0 && valor < DIMENSION) {
                    valido = true;
                } else {
                    System.out.println("Error: El número debe estar entre 0 y " + (DIMENSION - 1));
                }
            } else {
                teclado.next(); // Limpiar el buffer
                System.out.println("Error: Debes introducir un número entero.");
            }
        }
        return valor;
    }
}

Programando el juego Conecta 4 en Java paso a paso

Introducción

El desarrollo de videojuegos de consola es una de las herramientas pedagógicas más recomendadas para comprender la programación estructurada. En esta unidad, no nos limitaremos a «copiar y pegar» código; realizaremos una disección técnica completa de una implementación del clásico Conecta 4.

Analizaremos el uso de enumerados (enums) para la seguridad de tipos, la gestión de la memoria en arrays bidimensionales, la validación robusta de entradas de usuario y, finalmente, cómo implementar una Inteligencia Artificial basada en algoritmos de simulación y backtracking.

En este enlace puedes encontrar una presentación explicando el código, y a continuación dispones de una infografía y un vídeo explicativo.

Estructuras de Datos: El uso de Enum frente a enteros

En programas más antiguos o muy simples, es común ver el uso de números enteros para representar estados (ej: 0 para vacío, 1 para jugador). Sin embargo, esto es una mala práctica conocida como «números mágicos», ya que carecen de significado semántico y pueden llevar a errores si asignamos un número no válido (ej: tablero[0][0] = 99).

Para solucionar esto, utilizamos tipos enumerados (enum).

Enumerado Dificultad

Este enumerado define los niveles de inteligencia de la máquina. Observad que no es una lista simple; cada elemento (FACIL, MEDIO, DIFICIL) invoca a un constructor que almacena una descripción de texto.

Al sobrescribir el método toString(), permitimos que, al imprimir la dificultad por pantalla, se muestre automáticamente el texto amigable («Fácil (Aleatorio)») en lugar del nombre técnico de la constante.

    enum Dificultad {
        FACIL("Fácil (Aleatorio)"),
        MEDIO("Medio (Defensivo)"),
        DIFICIL("Difícil (Inteligente)");

        private final String descripcion;

        Dificultad(String descripcion) {
            this.descripcion = descripcion;
        }

        @Override
        public String toString() {
            return descripcion;
        }
    }

Enumerado Ficha

Define los tres estados posibles de una celda en el tablero.

Debemos destacar que con este código llevamos a cabo separación de responsabilidades. Este enumerado vincula la lógica del juego (JUGADOR, MAQUINA) con su representación visual (X, O). Si mañana quisiéramos cambiar la X por un color o un emoji, solo tendríamos que modificar la cadena de texto en este enumerado, sin tocar el resto del código.

    enum Ficha {
        VACIO("-"),
        JUGADOR("X"),
        MAQUINA("O");

        private final String simbolo;

        Ficha(String simbolo) {
            this.simbolo = simbolo;
        }

        @Override
        public String toString() {
            return simbolo;
        }
    }

Configuración del entorno y constantes

Antes de iniciar la lógica, debemos preparar el «terreno de juego». En Java, dentro del paradigma estructurado en una clase principal, utilizamos variables y constantes static.

  • Constantes (final): Definimos FILAS y COLUMNAS para evitar escribir 6 y 7 repetidamente. Esto facilita la escalabilidad: si queremos cambiar el tamaño del tablero a 10×10, solo modificamos estas dos líneas.
  • La constante NO_ENCONTRADO: Asignamos el valor -1 a una constante con nombre descriptivo. En programación, esto mejora la legibilidad. Es mucho más claro leer if (resultado == NO_ENCONTRADO) que if (resultado == -1).
  • El tablero: Declaramos una matriz de tipo Ficha. Inicialmente, Java llenará esto con null, por lo que deberemos inicializarlo más adelante.
    // Dimensiones del tablero
    static final int FILAS = 6;
    static final int COLUMNAS = 7;

    // Constante para indicar que una búsqueda no ha tenido éxito.
    static final int NO_ENCONTRADO = -1;

    // Estado global del juego
    static Ficha[][] tablero = new Ficha[FILAS][COLUMNAS];
    static Scanner teclado = new Scanner(System.in);
    static Random azar = new Random();

El flujo principal (main) y validación de entrada

El método main actúa como director de orquesta. Su función es inicializar el tablero, gestionar la selección de dificultad y ejecutar el bucle principal del juego (Game Loop).

Generación dinámica de menús

Fijaos en el bucle for que imprime el menú. No escribimos las opciones a mano. Usamos Dificultad.values() para iterar sobre los elementos del enumerado. Esto significa que el menú se autogenera basándose en la definición del enum:

        // Generamos el menú dinámicamente
        for (Dificultad d : Dificultad.values()) {
            System.out.println((d.ordinal() + 1) + ". " + d);
        }

Validación de entrada de datos (Scanner)

Uno de los puntos críticos es leer un número del teclado. Si el usuario introduce una letra, el método nextInt() lanzaría una excepción InputMismatchException y el programa terminaría abruptamente.

Para evitarlo, implementamos un patrón de comprobación:

  1. teclado.hasNextInt(): Preguntamos al buffer de entrada si lo siguiente es un entero.
  2. Si es : Leemos el dato y comprobamos que esté en el rango correcto (1-3).
  3. Si es No: Es vital ejecutar teclado.next(). Esta instrucción «consume» y elimina la entrada errónea (la letra) del buffer. Si no hiciéramos esto, el bucle while leería la misma letra infinitamente, bloqueando el programa en el bucle.

Código del método principal

    public static void main(String[] args) {
        inicializarTablero();

        System.out.println("--- CUATRO EN RAYA ---");
        System.out.println("Elige dificultad:");

        // Generamos el menú dinámicamente
        for (Dificultad d : Dificultad.values()) {
            System.out.println((d.ordinal() + 1) + ". " + d);
        }

        // Selección y validación de la dificultad
        Dificultad dificultadSeleccionada = null;

        while (dificultadSeleccionada == null) {
            System.out.print("Opción: ");
            if (teclado.hasNextInt()) {
                int opcion = teclado.nextInt();
                // Verificamos rango válido (1 a N)
                if (opcion > 0 && opcion <= Dificultad.values().length) {
                    dificultadSeleccionada = Dificultad.values()[opcion - 1];
                } else {
                    System.out.println("Número no válido. Elige entre 1 y " + Dificultad.values().length);
                }
            } else {
                teclado.next(); // IMPORTANTE: Consumir entrada incorrecta
                System.out.println("Por favor, introduce un número entero.");
            }
        }

        System.out.println("Has elegido: " + dificultadSeleccionada);

        // Bucle principal del juego
        boolean turnoJugador = true;
        boolean juegoTerminado = false;

        while (!juegoTerminado) {
            imprimirTablero();

            if (turnoJugador) {
                hacerMovimientoJugador();
            } else {
                hacerMovimientoMaquina(dificultadSeleccionada);
            }

            // Verificación de estado tras el movimiento
            Ficha fichaActual = turnoJugador ? Ficha.JUGADOR : Ficha.MAQUINA;

            if (comprobarVictoria(fichaActual)) {
                imprimirTablero();
                System.out.println(turnoJugador ? "¡ENHORABUENA! Has ganado." : "FIN. Ha ganado el ordenador.");
                juegoTerminado = true;
            } else if (tableroLleno()) {
                imprimirTablero();
                System.out.println("¡EMPATE! No quedan casillas libres.");
                juegoTerminado = true;
            }

            // Alternar turno
            turnoJugador = !turnoJugador;
        }
    }

Lógica de interacción del jugador

Esta función encapsula la interacción humana. Se asegura de no permitir avanzar al juego hasta que el jugador haya introducido una columna que cumpla dos condiciones:

  1. Ser un número entero dentro de los límites (0-6).
  2. Ser una columna con espacio disponible (no llena hasta el tope).

Nuevamente, utilizamos el patrón hasNextInt() para prevenir errores de tipo de dato.

    static void hacerMovimientoJugador() {
        boolean movimientoValido = false;
        int c;

        while (!movimientoValido) {
            System.out.print("Tu turno (Columna 0-" + (COLUMNAS - 1) + "): ");

            if (teclado.hasNextInt()) {
                c = teclado.nextInt();
                if (columnaValida(c)) {
                    colocarFicha(c, Ficha.JUGADOR);
                    movimientoValido = true;
                } else {
                    System.out.println("Movimiento no válido (columna llena o fuera de rango).");
                }
            } else {
                teclado.next();
                System.out.println("Por favor, introduce un número entero.");
            }
        }
    }

Algoritmos de Decisión

Aquí reside la lógica más avanzada del programa. No pretendemos jugar simplemente al azar (a menos que esté en modo fácil), si no que debemos intentar tomar decisiones basadas en prioridades.

Sistema de prioridades en cascada

La función hacerMovimientoMaquina evalúa la situación en orden de importancia:

  1. Ataque (modo difícil): ¿Puedo ganar en este turno? Si la respuesta es sí, lo hace inmediatamente.
  2. Defensa (modo medio/difícil): ¿Puede ganar el rival en su siguiente turno? Si la respuesta es sí, la máquina debe bloquear esa columna obligatoriamente.
  3. Movimiento aleatorio: Si no hay victoria inminente ni amenaza de derrota, juega en cualquier columna válida al azar.

Algoritmo de simulación (backtracking simplificado)

La función buscarMovimientoGanador implementa una técnica muy habitual en estos casos: la simulación. El ordenador «imagina» qué pasaría si colocara una ficha en cada columna:

  1. Hacer: Coloca una ficha temporalmente (tablero[f][c] = ficha).
  2. Verificar: Llama a comprobarVictoria para ver si esa jugada resulta en éxito.
  3. Deshacer (backtracking): Es fundamental retirar la ficha (tablero[f][c] = Ficha.VACIO) antes de continuar el bucle. Si no hiciéramos esto, el tablero se llenaría de fichas de prueba y la partida se corrompería.

Código de movimientos

    static void hacerMovimientoMaquina(Dificultad dificultad) {
        System.out.println("Turno del ordenador...");

        int columnaElegida = NO_ENCONTRADO;

        // 1. PRIORIDAD DE ATAQUE
        if (dificultad == Dificultad.DIFICIL) {
            columnaElegida = buscarMovimientoGanador(Ficha.MAQUINA);
        }

        // 2. PRIORIDAD DE DEFENSA
        if (columnaElegida == NO_ENCONTRADO && (dificultad == Dificultad.MEDIO || dificultad == Dificultad.DIFICIL)) {
            columnaElegida = buscarMovimientoGanador(Ficha.JUGADOR);
        }

        // 3. MOVIMIENTO POR DEFECTO (ALEATORIO)
        if (columnaElegida == NO_ENCONTRADO) {
            do {
                columnaElegida = azar.nextInt(COLUMNAS);
            } while (!columnaValida(columnaElegida));
        }

        colocarFicha(columnaElegida, Ficha.MAQUINA);
    }

    static int buscarMovimientoGanador(Ficha ficha) {
        int columnaGanadora = NO_ENCONTRADO;

        for (int c = 0; c < COLUMNAS && columnaGanadora == NO_ENCONTRADO; c++) {
            if (columnaValida(c)) {
                int f = obtenerFilaLibre(c);

                // Simular jugada
                tablero[f][c] = ficha;

                // Comprobar resultado
                if (comprobarVictoria(ficha)) {
                    columnaGanadora = c;
                }

                // Deshacer jugada (Backtracking)
                tablero[f][c] = Ficha.VACIO;
            }
        }
        return columnaGanadora;
    }

Física del tablero: gravedad y renderizado

Estas funciones auxiliares manejan la lógica interna de la matriz.

  • inicializarTablero: Recorre la matriz estableciendo Ficha.VACIO. Es necesario porque al crear el array, Java lo llena con null.
  • imprimirTablero: Recorre la matriz e imprime el método .toString() de cada ficha (definido en el enum).
  • columnaValida: Verifica si la columna está dentro de los límites y si la fila superior (0) está vacía.
  • obtenerFilaLibre: Simulación de gravedad. A diferencia de una lectura normal, recorremos la columna de abajo hacia arriba (desde FILAS - 1 hasta 0). La primera casilla vacía encontrada es donde la ficha se «posará».
    static void inicializarTablero() {
        for (int f = 0; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS; c++) {
                tablero[f][c] = Ficha.VACIO;
            }
        }
    }

    static void imprimirTablero() {
        System.out.println();
        System.out.print(" ");
        for(int c = 0; c < COLUMNAS; c++) System.out.print(c + " ");
        System.out.println();
        System.out.println("---------------");
        for (int f = 0; f < FILAS; f++) {
            System.out.print("|");
            for (int c = 0; c < COLUMNAS; c++) {
                System.out.print(tablero[f][c] + "|");
            }
            System.out.println();
        }
        System.out.println("---------------");
    }

    static boolean columnaValida(int c) {
        return c >= 0 && c < COLUMNAS && tablero[0][c] == Ficha.VACIO;
    }

    static int obtenerFilaLibre(int c) {
        int filaEncontrada = NO_ENCONTRADO;
        // Bucle inverso para simular gravedad
        for (int f = FILAS - 1; f >= 0 && filaEncontrada == NO_ENCONTRADO; f--) {
            if (tablero[f][c] == Ficha.VACIO) {
                filaEncontrada = f;
            }
        }
        return filaEncontrada;
    }

    static void colocarFicha(int c, Ficha ficha) {
        int f = obtenerFilaLibre(c);
        if (f != NO_ENCONTRADO) {
            tablero[f][c] = ficha;
        }
    }

    static boolean tableroLleno() {
        for (int c = 0; c < COLUMNAS; c++) {
            if (tablero[0][c] == Ficha.VACIO) {
                return false;
            }
        }
        return true;
    }

Verificación de victoria

Esta es la función algorítmicamente más densa. Para detectar una victoria, debemos buscar 4 fichas consecutivas idénticas en cuatro direcciones: Horizontal, Vertical, Diagonal ascendente y Diagonal descendente.

En este paso es habitual cometer el error de no comprobar bien los límites del Array. Si intentamos buscar 4 fichas a la derecha estando en la última columna, obtendremos un error ArrayIndexOutOfBoundsException. Por ello, ajustamos los límites de los bucles for:

  • Horizontal: El bucle de columnas solo llega hasta COLUMNAS - 3.
  • Vertical: El bucle de filas solo llega hasta FILAS - 3.
  • Diagonales: Combinan ambas restricciones.
    static boolean comprobarVictoria(Ficha ficha) {
        // Horizontal (-)
        for (int f = 0; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f][c+1] == ficha &&
                    tablero[f][c+2] == ficha &&
                    tablero[f][c+3] == ficha) return true;
            }
        }

        // Vertical (|)
        for (int f = 0; f < FILAS - 3; f++) {
            for (int c = 0; c < COLUMNAS; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f+1][c] == ficha &&
                    tablero[f+2][c] == ficha &&
                    tablero[f+3][c] == ficha) return true;
            }
        }

        // Diagonal Ascendente (/)
        // Empieza en fila 3 porque necesita espacio hacia arriba
        for (int f = 3; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f-1][c+1] == ficha &&
                    tablero[f-2][c+2] == ficha &&
                    tablero[f-3][c+3] == ficha) return true;
            }
        }

        // Diagonal Descendente (\)
        for (int f = 0; f < FILAS - 3; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f+1][c+1] == ficha &&
                    tablero[f+2][c+2] == ficha &&
                    tablero[f+3][c+3] == ficha) return true;
            }
        }

        return false;
    }

Todo el código

import java.util.Random;
import java.util.Scanner;

/**
 * Juego de Cuatro en Raya para consola.
 * Este programa implementa una versión del clásico juego utilizando programación estructurada.
 * Se hace uso de Enums para gestionar estados y dificultades.
 * Se evita el uso de "break" para mantener un flujo de control limpio y predecible.
 */
public class Main {

    /**
     * Define los niveles de dificultad del juego.
     * Cada nivel contiene su propia descripción textual para facilitar la generación de menús.
     */
    enum Dificultad {
        FACIL("Fácil (Aleatorio)"),
        MEDIO("Medio (Defensivo)"),
        DIFICIL("Difícil (Inteligente)");

        private final String descripcion;

        Dificultad(String descripcion) {
            this.descripcion = descripcion;
        }

        @Override
        public String toString() {
            return descripcion;
        }
    }

    /**
     * Representa los posibles estados de una casilla en el tablero.
     * Encapsula el símbolo visual asociado a cada estado para separar lógica de presentación.
     */
    enum Ficha {
        VACIO("-"),
        JUGADOR("X"),
        MAQUINA("O");

        private final String simbolo;

        Ficha(String simbolo) {
            this.simbolo = simbolo;
        }

        @Override
        public String toString() {
            return simbolo;
        }
    }

    // --- CONFIGURACIÓN DEL JUEGO ---

    // Dimensiones del tablero
    static final int FILAS = 6;
    static final int COLUMNAS = 7;

    // Constante para indicar que una búsqueda no ha tenido éxito.
    // Usamos esto en lugar de -1 directamente para mejorar la legibilidad.
    static final int NO_ENCONTRADO = -1;

    // Estado global del juego
    static Ficha[][] tablero = new Ficha[FILAS][COLUMNAS];
    static Scanner teclado = new Scanner(System.in);
    static Random azar = new Random();

    /**
     * Punto de entrada principal del programa.
     * Orquesta el flujo de la aplicación: configuración inicial, bucle de juego y finalización.
     * @param args Argumentos de consola (no utilizados).
     */
    public static void main(String[] args) {
        inicializarTablero();

        System.out.println("--- CUATRO EN RAYA ---");
        System.out.println("Elige dificultad:");

        // Generamos el menú dinámicamente basándonos en los valores del Enum.
        // Esto permite añadir nuevas dificultades en el futuro sin modificar este código.
        for (Dificultad d : Dificultad.values()) {
            System.out.println((d.ordinal() + 1) + ". " + d);
        }

        // Selección y validación de la dificultad
        Dificultad dificultadSeleccionada = null;

        while (dificultadSeleccionada == null) {
            System.out.print("Opción: ");
            if (teclado.hasNextInt()) {
                int opcion = teclado.nextInt();
                // Verificamos rango válido (1 a N)
                if (opcion > 0 && opcion <= Dificultad.values().length) {
                    dificultadSeleccionada = Dificultad.values()[opcion - 1];
                } else {
                    System.out.println("Número no válido. Elige entre 1 y " + Dificultad.values().length);
                }
            } else {
                teclado.next(); // Consumir entrada incorrecta para evitar bucle infinito
                System.out.println("Por favor, introduce un número entero.");
            }
        }

        System.out.println("Has elegido: " + dificultadSeleccionada);

        // Bucle principal del juego
        boolean turnoJugador = true;
        boolean juegoTerminado = false;

        while (!juegoTerminado) {
            imprimirTablero();

            if (turnoJugador) {
                hacerMovimientoJugador();
            } else {
                hacerMovimientoMaquina(dificultadSeleccionada);
            }

            // Determinamos qué ficha se acaba de colocar para verificar victoria
            Ficha fichaActual = turnoJugador ? Ficha.JUGADOR : Ficha.MAQUINA;

            if (comprobarVictoria(fichaActual)) {
                imprimirTablero();
                System.out.println(turnoJugador ? "¡ENHORABUENA! Has ganado." : "FIN. Ha ganado el ordenador.");
                juegoTerminado = true;
            } else if (tableroLleno()) {
                imprimirTablero();
                System.out.println("¡EMPATE! No quedan casillas libres.");
                juegoTerminado = true;
            }

            // Alternar turno
            turnoJugador = !turnoJugador;
        }
    }

    // --- LÓGICA DE TURNOS ---

    /**
     * Gestiona el turno del jugador humano.
     * Solicita una columna repetidamente hasta que el usuario introduce una válida.
     */
    static void hacerMovimientoJugador() {
        boolean movimientoValido = false;
        int c;

        // Bucle controlado por bandera (flag) para evitar el uso de 'break'
        while (!movimientoValido) {
            // Usamos las constantes para que el texto coincida siempre con el tamaño real del tablero
            System.out.print("Tu turno (Columna 0-" + (COLUMNAS - 1) + "): ");

            if (teclado.hasNextInt()) {
                c = teclado.nextInt();
                if (columnaValida(c)) {
                    colocarFicha(c, Ficha.JUGADOR);
                    movimientoValido = true;
                } else {
                    System.out.println("Movimiento no válido (columna llena o fuera de rango).");
                }
            } else {
                teclado.next();
                System.out.println("Por favor, introduce un número entero.");
            }
        }
    }

    /**
     * Ejecuta la lógica de la Inteligencia Artificial.
     * Decide el movimiento basándose en un sistema de prioridades en cascada.
     * @param dificultad Nivel de inteligencia seleccionado.
     */
    static void hacerMovimientoMaquina(Dificultad dificultad) {
        System.out.println("Turno del ordenador...");

        int columnaElegida = NO_ENCONTRADO;

        // PRIORIDAD DE ATAQUE: Si puedo ganar, lo hago. (Solo en modo Difícil)
        if (dificultad == Dificultad.DIFICIL) {
            columnaElegida = buscarMovimientoGanador(Ficha.MAQUINA);
        }

        // PRIORIDAD DE DEFENSA: Si no tengo ataque, bloqueo al rival si va a ganar.
        // (Disponible en Medio y Difícil)
        if (columnaElegida == NO_ENCONTRADO && (dificultad == Dificultad.MEDIO || dificultad == Dificultad.DIFICIL)) {
            columnaElegida = buscarMovimientoGanador(Ficha.JUGADOR);
        }

        // MOVIMIENTO POR DEFECTO: Si no hay estrategia, elijo al azar.
        if (columnaElegida == NO_ENCONTRADO) {
            do {
                columnaElegida = azar.nextInt(COLUMNAS);
            } while (!columnaValida(columnaElegida));
        }

        colocarFicha(columnaElegida, Ficha.MAQUINA);
    }

    /**
     * Simula colocar una ficha en cada columna disponible para ver si produce una victoria.
     * Utiliza la técnica de "Backtracking" (colocar, comprobar, retirar).
     * @param ficha La ficha para la cual buscamos la victoria (propia o del rival).
     * @return El índice de la columna ganadora o NO_ENCONTRADO si no existe.
     */
    static int buscarMovimientoGanador(Ficha ficha) {
        int columnaGanadora = NO_ENCONTRADO;

        // Recorremos las columnas. La condición 'columnaGanadora == NO_ENCONTRADO'
        // detiene el bucle eficientemente en cuanto encontramos una solución.
        for (int c = 0; c < COLUMNAS && columnaGanadora == NO_ENCONTRADO; c++) {
            if (columnaValida(c)) {
                int f = obtenerFilaLibre(c);

                // Simular jugada
                tablero[f][c] = ficha;

                // Comprobar resultado
                if (comprobarVictoria(ficha)) {
                    columnaGanadora = c;
                }

                // Deshacer jugada (dejar el tablero como estaba)
                tablero[f][c] = Ficha.VACIO;
            }
        }
        return columnaGanadora;
    }

    // --- FUNCIONES DEL TABLERO ---

    /**
     * Inicializa el tablero llenando todas las celdas con símbolo VACIO.
     */
    static void inicializarTablero() {
        for (int f = 0; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS; c++) {
                tablero[f][c] = Ficha.VACIO;
            }
        }
    }

    /**
     * Dibuja el estado actual del tablero en la consola.
     * Utiliza el método toString() de cada Ficha para la representación visual.
     */
    static void imprimirTablero() {
        System.out.println();

        // Cabecera con números de columna
        System.out.print(" ");
        for(int c = 0; c < COLUMNAS; c++) {
            System.out.print(c + " ");
        }
        System.out.println();

        System.out.println("---------------");
        for (int f = 0; f < FILAS; f++) {
            System.out.print("|");
            for (int c = 0; c < COLUMNAS; c++) {
                System.out.print(tablero[f][c] + "|");
            }
            System.out.println();
        }
        System.out.println("---------------");
    }

    /**
     * Verifica si es legal colocar una ficha en la columna indicada.
     * @param c Índice de la columna.
     * @return true si la columna está dentro del rango y tiene espacio libre arriba.
     */
    static boolean columnaValida(int c) {
        return c >= 0 && c < COLUMNAS && tablero[0][c] == Ficha.VACIO;
    }

    /**
     * Busca la primera posición libre en una columna, empezando desde abajo.
     * Simula el efecto de la gravedad.
     * @param c Columna donde buscar.
     * @return El índice de la fila libre o NO_ENCONTRADO si la columna está llena.
     */
    static int obtenerFilaLibre(int c) {
        int filaEncontrada = NO_ENCONTRADO;

        // Recorremos de abajo hacia arriba. Paramos al encontrar el primer hueco.
        for (int f = FILAS - 1; f >= 0 && filaEncontrada == NO_ENCONTRADO; f--) {
            if (tablero[f][c] == Ficha.VACIO) {
                filaEncontrada = f;
            }
        }
        return filaEncontrada;
    }

    /**
     * Coloca una ficha de manera definitiva en el tablero.
     * Se asume que la validación de la columna se ha hecho previamente.
     * @param c Columna seleccionada.
     * @param ficha Tipo de ficha a colocar.
     */
    static void colocarFicha(int c, Ficha ficha) {
        int f = obtenerFilaLibre(c);

        if (f != NO_ENCONTRADO) {
            tablero[f][c] = ficha;
        }
    }

    /**
     * Comprueba si el tablero está lleno (condición de empate).
     * Basta con verificar si quedan huecos en la fila superior de alguna columna.
     * @return true si el tablero está completo y no caben más fichas.
     */
    static boolean tableroLleno() {
        // Si encontramos al menos una casilla vacía arriba, el tablero NO está lleno.
        for (int c = 0; c < COLUMNAS; c++) {
            // El 'return' hace que salgamos de la función inmediatamente.
            if (tablero[0][c] == Ficha.VACIO) {
                return false;
            }
        }
        // Si el bucle termina sin haber ejecutado el return anterior,
        // significa que no había huecos: el tablero está lleno.
        return true;
    }

    /**
     * Algoritmo de comprobación de victoria.
     * Verifica secuencias de 4 fichas iguales en horizontal, vertical y diagonales.
     * @param ficha La ficha que acaba de jugar (JUGADOR o MAQUINA).
     * @return true si hay 4 en raya.
     */
    static boolean comprobarVictoria(Ficha ficha) {
        // Horizontal (-)
        for (int f = 0; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f][c+1] == ficha &&
                    tablero[f][c+2] == ficha &&
                    tablero[f][c+3] == ficha) return true;
            }
        }

        // Vertical (|)
        for (int f = 0; f < FILAS - 3; f++) {
            for (int c = 0; c < COLUMNAS; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f+1][c] == ficha &&
                    tablero[f+2][c] == ficha &&
                    tablero[f+3][c] == ficha) return true;
            }
        }

        // Diagonal Ascendente (/)
        for (int f = 3; f < FILAS; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f-1][c+1] == ficha &&
                    tablero[f-2][c+2] == ficha &&
                    tablero[f-3][c+3] == ficha) return true;
            }
        }

        // Diagonal Descendente (\)
        for (int f = 0; f < FILAS - 3; f++) {
            for (int c = 0; c < COLUMNAS - 3; c++) {
                if (tablero[f][c] == ficha &&
                    tablero[f+1][c+1] == ficha &&
                    tablero[f+2][c+2] == ficha &&
                    tablero[f+3][c+3] == ficha) return true;
            }
        }

        return false;
    }
}

Laberinto, un juego arcade con música, nave, obstáculos, colisiones, enemigos, niveles, imagen de fondo y game over, hecho con Godot

Las imágenes y los ficheros de audio

Para nuestro juego necesitaremos varias imágenes y ficheros de audio. Para poder incluirlas en nuestro proyecto, primero debemos descargarlas en nuestro ordenador, y a continuación las arrastraremos a la zona de «Sistema de Archivos» de Godot. Las tienes disponibles en este archivo ZIP.

Pasos a seguir

Esqueleto del juego (todas las funciones vacías)

extends Node2D

# ==============================================================================
# CONFIGURACIÓN Y CONSTANTES
# ==============================================================================

# --- TAMAÑOS Y AJUSTES ---
const TAM_CELDA = 100    # Píxeles por bloque
const MARGEN    = 5      # Píxeles de separación interna

# --- VELOCIDADES ---
const VEL_JUGADOR = 350
const VEL_ENEMIGO = 150

# --- CARGA DE IMÁGENES ---
const TEX_FONDO     = preload("res://assets/fondo.png")
const TEX_JUGADOR   = preload("res://assets/jugador.png")
const TEX_ENEMIGO   = preload("res://assets/enemigo.png")
const TEX_PARED     = preload("res://assets/bloque.png")
const TEX_META      = preload("res://assets/meta.png")
const TEX_CERRAR    = preload("res://assets/boton_cerrar.png")
const TEX_REINICIAR = preload("res://assets/boton_reiniciar.png")
const TEX_GAMEOVER  = preload("res://assets/game_over.png")
const TEX_SIGUIENTE = preload("res://assets/siguiente_nivel.png")

# --- CARGA DE AUDIO ---
const AUDIO_FONDO   = preload("res://assets/musica_fondo.mp3")
const AUDIO_MUERTE  = preload("res://assets/sfx_muerte.mp3")

# ==============================================================================
# VARIABLES GLOBALES
# ==============================================================================

var pantalla: Vector2
var tam_tablero: Vector2
var celda_dim: Vector2

var paredes: Array[Rect2] = []
var enemigos: Array[Dictionary] = []
var jugador: Rect2
var meta: Rect2

var nivel_actual: int = 1
var game_over: bool = false
var siguiente_nivel: bool = false

var btn_cerrar: Rect2
var btn_reiniciar: Rect2

var music_player: AudioStreamPlayer
var sfx_player: AudioStreamPlayer

# ==============================================================================
# 1. HERRAMIENTAS BÁSICAS
# ==============================================================================

func _crear_rect(x_grid, y_grid, margen_interno):
	# Ayuda a convertir coordenadas de la cuadrícula (ej: 2,3) a píxeles reales en pantalla.
	pass

func _es_zona_segura(pos: Vector2):
	# Devuelve true si la coordenada es inicio, fin o esquinas clave.
	pass

func _colisiona_con_paredes(rect: Rect2):
	# Comprueba si un rectángulo intercepta alguna pared del array.
	pass

func _reproducir_sonido(stream):
	# Función auxiliar para reproducir un efecto de sonido puntual.
	pass

func _es_posicion_valida(rect: Rect2):
	# Valida si un objeto está dentro de la pantalla y libre de paredes.
	pass

# ==============================================================================
# 2. GENERACIÓN DE DATOS (Lógica de Nivel)
# ==============================================================================

func _generar_paredes(cantidad):
	# Coloca obstáculos aleatorios asegurándose de no bloquear el inicio o fin.
	pass

func _generar_enemigos(cantidad):
	# Genera enemigos solo en la mitad inferior para dar tiempo al jugador.
	pass

func _iniciar_nivel():
	# Resetea las variables y genera un nuevo mapa limpio.
	# Calcula la dificultad basada en el nivel actual.
	pass

# ==============================================================================
# 3. CONFIGURACIÓN Y VISUALIZACIÓN
# ==============================================================================

func _ready():
	# Inicialización básica: Configura el tamaño de pantalla, la cuadrícula y el audio.
	# Se ejecuta una única vez al arrancar la escena.
	pass

func _draw():
	# Dibuja texturas, enemigos, jugador e interfaz en pantalla.
	# IMPORTANTE: El orden importa. Lo primero que se dibuja queda al fondo.
	pass

# ==============================================================================
# 4. MOVIMIENTO Y FÍSICAS
# ==============================================================================

func _mover_jugador(delta):
	# Calcula el movimiento del jugador y evita que atraviese paredes.
	pass

func _mover_enemigos(delta):
	# Mueve enemigos verticalmente y los hace rebotar al chocar.
	pass

func _comprobar_colisiones():
	# Verifica condiciones de victoria o derrota por colisión.
	pass

# ==============================================================================
# 5. BUCLE PRINCIPAL E INPUT
# ==============================================================================

func _process(delta):
	# Bucle principal: Gestiona movimiento y colisiones en cada frame.
	pass

func _input(event):
	# Gestiona clics del ratón para botones y reinicio de juego.
	pass

crear_rect()

func _crear_rect(x_grid, y_grid, margen_interno) -> Rect2:
	# Ayuda a convertir coordenadas de la cuadrícula (ej: 2,3) a píxeles reales en pantalla.
	
	# Retorna un Rect2. La posición X es (columna * ancho_celda) + margen.
	# El tamaño es (ancho_celda - doble margen) para que quede centrado.
	return Rect2(
		x_grid * celda_dim.x + margen_interno,
		y_grid * celda_dim.y + margen_interno,
		celda_dim.x - margen_interno * 2,
		celda_dim.y - margen_interno * 2
	)

es_zona_segura()

func _es_zona_segura(pos: Vector2) -> bool:
	# Devuelve true si la coordenada es inicio, fin o esquinas clave.
	
	if pos == Vector2(0, 0): return true # Esquina Jugador
	if pos == Vector2(tam_tablero.x - 1, tam_tablero.y - 1): return true # Esquina Meta
	# Protegemos las otras dos esquinas para evitar bloqueos imposibles en mapas pequeños
	if pos == Vector2(tam_tablero.x - 1, 0): return true
	if pos == Vector2(0, tam_tablero.y - 1): return true
	return false

colisiona_con_paredes()

func _colisiona_con_paredes(rect: Rect2) -> bool:
	# Comprueba si un rectángulo intercepta alguna pared del array.
	
	for pared in paredes:
		# Usamos .grow(-1) para reducir un píxel el hitbox de chequeo.
		# Esto evita que detecte colisión si solo se están rozando lado con lado.
		if rect.grow(-1).intersects(pared): return true
	return false

reproducir_sonido()

func _reproducir_sonido(stream):
	# Función auxiliar para reproducir un efecto de sonido puntual.
	sfx_player.stream = stream
	sfx_player.play()

es_posicion_valida()

func _es_posicion_valida(rect: Rect2) -> bool:
	# Valida si un objeto está dentro de la pantalla y libre de paredes.
	
	# 'encloses' devuelve true si el rect está TOTALMENTE dentro de la pantalla
	if not get_viewport_rect().encloses(rect): return false
	
	# Si choca con pared, devuelve false (posición no válida)
	return not _colisiona_con_paredes(rect)

generar_paredes()

func _generar_paredes(cantidad):
	# Coloca obstáculos aleatorios asegurándose de no bloquear el inicio o fin.
	
	var intentos = 0
	# Usamos un while con límite de intentos para evitar bucles infinitos si no hay sitio
	while paredes.size() < cantidad and intentos < 1000:
		intentos += 1
		
		# Elegimos una columna y fila al azar
		var pos = Vector2(randi() % int(tam_tablero.x), randi() % int(tam_tablero.y))
		
		# Si la posición elegida es vital (inicio/fin), la descartamos y probamos otra
		if _es_zona_segura(pos): continue
		
		# Creamos el rectángulo provisional
		var nueva_pared = _crear_rect(pos.x, pos.y, 0)
		
		# Verificamos que no caiga encima de otra pared ya existente
		if not _colisiona_con_paredes(nueva_pared):
			paredes.append(nueva_pared)

generar_enemigos()

func _generar_enemigos(cantidad):
	# Genera enemigos solo en la mitad inferior para dar tiempo al jugador.
	
	var intentos = 0
	var fila_minima = int(tam_tablero.y / 2) # Calculamos la mitad del tablero
	
	while enemigos.size() < cantidad and intentos < 1000:
		intentos += 1
		
		# Coordenada X aleatoria (cualquier columna)
		var cx = randi() % int(tam_tablero.x)
		# Coordenada Y aleatoria (solo desde la mitad hacia abajo)
		var cy = randi_range(fila_minima, int(tam_tablero.y) - 1)
		
		if _es_zona_segura(Vector2(cx, cy)): continue
		
		var rect_temp = _crear_rect(cx, cy, 0)
		
		# Evitamos que un enemigo nazca dentro de una pared
		if _colisiona_con_paredes(rect_temp): continue
		
		# Añadimos el enemigo como un diccionario con sus propiedades
		enemigos.append({
			"rect": rect_temp.grow(-15), # Hacemos la hitbox más pequeña para perdonar roces
			"dir": 1 if randf() > 0.5 else -1, # 50% probabilidad de ir arriba o abajo
			"velocidad": VEL_ENEMIGO
		})

iniciar_nivel()

func _iniciar_nivel():
	# Resetea las variables y genera un nuevo mapa limpio.
	# Calcula la dificultad basada en el nivel actual.
	
	# Limpiamos los arrays para borrar el nivel anterior
	paredes.clear()
	enemigos.clear()
	
	# Reiniciamos estados lógicos
	game_over = false
	siguiente_nivel = false
	
	# Creamos al jugador en la posición (0,0) (Arriba-Izquierda)
	jugador = _crear_rect(0, 0, MARGEN)
	
	# Creamos la meta en la última celda disponible (Abajo-Derecha)
	meta = _crear_rect(tam_tablero.x - 1, tam_tablero.y - 1, 0)
	
	# Fórmula de dificultad: Más nivel = más paredes y enemigos
	var cant_paredes = 5 + (nivel_actual * 3)
	var cant_enemigos = 2 + (nivel_actual * 1)
	
	# Ejecutamos los bucles de generación
	_generar_paredes(cant_paredes)
	_generar_enemigos(cant_enemigos)

ready()

func _ready():
	# Inicialización básica: Configura el tamaño de pantalla, la cuadrícula y el audio.
	# Se ejecuta una única vez al arrancar la escena.
	
	# Inicializa la semilla aleatoria para que cada partida sea distinta
	randomize()
	
	# Obtenemos el tamaño visible de la ventana del juego
	pantalla = get_viewport_rect().size
	
	# Calculamos cuántas columnas y filas caben dividiendo el ancho/alto por el tamaño de celda
	var cols = round(pantalla.x / TAM_CELDA)
	var filas = round(pantalla.y / TAM_CELDA)
	tam_tablero = Vector2(cols, filas)
	
	# Recalculamos el tamaño exacto de la celda para que se ajuste perfectamente a la pantalla
	celda_dim = Vector2(pantalla.x / cols, pantalla.y / filas)
	
	# Definimos la geometría de los botones UI
	# Botón cerrar: Esquina superior derecha
	btn_cerrar = Rect2(pantalla.x - celda_dim.x, 0, celda_dim.x, celda_dim.y)
	# Botón reiniciar: Esquina inferior izquierda
	btn_reiniciar = Rect2(0, (tam_tablero.y - 1) * celda_dim.y, celda_dim.x, celda_dim.y)
	
	# Instanciamos y configuramos el nodo de música
	music_player = AudioStreamPlayer.new()
	music_player.stream = AUDIO_FONDO
	music_player.volume_db = -10 # Bajamos un poco el volumen
	add_child(music_player) # Lo añadimos al árbol de nodos
	music_player.play()
	
	# Instanciamos el nodo para efectos de sonido (SFX)
	sfx_player = AudioStreamPlayer.new()
	add_child(sfx_player)
	
	# Llamamos a la función que prepara el primer nivel
	_iniciar_nivel()

draw()

func _draw():
	# Dibuja texturas, enemigos, jugador e interfaz en pantalla.
	# IMPORTANTE: El orden importa. Lo primero que se dibuja queda al fondo.
	
	# 1. Fondo (Capa más baja)
	draw_texture_rect(TEX_FONDO, Rect2(Vector2.ZERO, pantalla), false)
	
	# 2. Objetivos
	draw_texture_rect(TEX_META, meta, false)
	
	# 3. Obstáculos
	for p in paredes:
		draw_texture_rect(TEX_PARED, p, false, Color(1,1,1,0.5)) # Transparencia 0.5
	
	# 4. Entidades (enemigos y jugador)
	for e in enemigos:
		draw_texture_rect(TEX_ENEMIGO, e.rect, false)
	
	draw_texture_rect(TEX_JUGADOR, jugador, false)
	
	# 5. Interfaz de Usuario (Capa superior)
	draw_texture_rect(TEX_CERRAR, btn_cerrar, false)
	draw_texture_rect(TEX_REINICIAR, btn_reiniciar, false)
	
	# Texto informativo
	draw_string(ThemeDB.fallback_font, Vector2(0, 50), "NIVEL " + str(nivel_actual), 
		HORIZONTAL_ALIGNMENT_CENTER, pantalla.x, 40)
	
	# 6. Pantallas Superpuestas (Overlays)
	if game_over:
		# Dibuja game over con un tinte ligeramente transparente (0.8)
		draw_texture_rect(TEX_GAMEOVER, Rect2(Vector2.ZERO, pantalla), false, Color(1,1,1,0.8))
	elif siguiente_nivel:
		draw_texture_rect(TEX_SIGUIENTE, Rect2(Vector2.ZERO, pantalla), false, Color(1,1,1,0.8))

mover_jugador()

func _mover_jugador(delta):
	# Calcula el movimiento del jugador y evita que atraviese paredes.
	
	# Obtiene un vector (-1, 0, 1) según las teclas pulsadas
	var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	
	# Si no se pulsa nada, salimos para ahorrar cálculos
	if dir == Vector2.ZERO: return

	# Calculamos cuánto desplazarse en este frame (Dirección * Velocidad * Tiempo)
	var velocidad = dir * VEL_JUGADOR * delta
	
	# --- MOVIMIENTO EJE X ---
	# Creamos una copia temporal del jugador movida en X
	var test_x = jugador
	test_x.position.x += velocidad.x
	
	# Si la copia no choca, movemos al jugador real
	if _es_posicion_valida(test_x): jugador.position.x = test_x.position.x
	
	# --- MOVIMIENTO EJE Y ---
	# Hacemos lo mismo para el eje Y por separado. 
	# (Hacerlo separado permite deslizarse por las paredes en lugar de atascarse)
	var test_y = jugador
	test_y.position.y += velocidad.y
	
	if _es_posicion_valida(test_y): jugador.position.y = test_y.position.y

mover_enemigos()

func _mover_enemigos(delta):
	# Mueve enemigos verticalmente y los hace rebotar al chocar.
	
	for e in enemigos:
		# Calculamos el paso vertical
		var paso = e.dir * e.velocidad * delta
		e.rect.position.y += paso
		
		# Verificamos si se salió de la pantalla (Arriba < 0, Abajo > pantalla)
		var choca_borde = e.rect.position.y < 0 or e.rect.end.y > pantalla.y
		
		# Si toca borde o pared...
		if choca_borde or _colisiona_con_paredes(e.rect):
			e.rect.position.y -= paso # Revertimos el movimiento para que no se quede pegado
			e.dir *= -1               # Invertimos la dirección (1 a -1, o viceversa)

comprobar_colisiones()

func _comprobar_colisiones():
	# Verifica condiciones de victoria o derrota por colisión.
	
	# 1. VICTORIA: ¿El jugador toca la meta?
	if jugador.intersects(meta):
		siguiente_nivel = true
		_reproducir_sonido(AUDIO_FONDO)
	
	# 2. DERROTA: ¿El jugador toca algún enemigo?
	for e in enemigos:
		# Reducimos un poco el hitbox del enemigo (grow -4) para ser benévolos con el jugador
		if jugador.intersects(e.rect.grow(-4)):
			game_over = true
			music_player.stop()              # Silenciamos música
			_reproducir_sonido(AUDIO_MUERTE) # Sonido de choque
			return

process()

func _process(delta):
	# Bucle principal: Gestiona movimiento y colisiones en cada frame.
	
	# Si el juego acabó, salimos de la función inmediatamente (no procesamos nada más)
	if game_over or siguiente_nivel: return
	
	# Actualizamos la lógica
	_mover_jugador(delta)
	_mover_enemigos(delta)
	_comprobar_colisiones()
	
	# Solicitamos redibujar la pantalla (llama a _draw automáticamente)
	queue_redraw()

input()

func _input(event):
	# Gestiona clics del ratón para botones y reinicio de juego.
	
	# Solo nos interesan los eventos que sean Clics de ratón y que estén Presionados
	if not (event is InputEventMouseButton and event.pressed): return

	# Chequeo de botones de UI
	if btn_cerrar.has_point(event.position):
		get_tree().quit() # Cierra el juego
		return

	if btn_reiniciar.has_point(event.position):
		_iniciar_nivel() # Reinicia el nivel actual
		return

	# Lógica para continuar tras ganar o perder
	if game_over:
		music_player.play()
		_iniciar_nivel() # Reintenta el mismo nivel
	elif siguiente_nivel:
		nivel_actual += 1 # Sube la dificultad
		_iniciar_nivel()

El resultado

Desde Godot podemos exportar este mismo proyecto para poder jugar en cualquier navegador. Puedes ver el resultado y jugar directamente mediante el siguiente enlace:

HTML. Unidad 7. Formato de texto avanzado.

Presentación

Infografías

Diapositivas y vídeos

Podrás familiarizarte con los contenidos clave de esta unidad a través de esta presentación y esta otra; estas diapositivas; y este vídeo y finalmente, este otro .

Introducción

HTML dispone de numerosos elementos para dar formato al texto que no abordamos en las unidades anteriores. Aunque las etiquetas que se describen en esta unidad son menos conocidas o de uso más específico, conocerlas resulta de gran utilidad para enriquecer nuestros documentos. Aprenderás a realizar el marcado semántico correcto de abreviaturas, citas y referencias, fragmentos de código informático, ecuaciones matemáticas y datos de contacto.

Abreviaturas

El elemento de abreviatura de HTML (<abbr>) representa una abreviatura o un acrónimo. A través del atributo opcional title, podemos proporcionar el significado desarrollado del término o una descripción legible para el usuario.

Por lo general, los navegadores presentan este texto mediante una etiqueta emergente (tooltip) que aparece automáticamente cuando se pasa el cursor del ratón sobre el elemento. Es importante tener en cuenta que, si se incluye el atributo title, este debe contener única y exclusivamente la descripción completa, sin añadir texto adicional.

A continuación, veamos algunos ejemplos prácticos en los que resulta adecuado el uso de abreviaturas o acrónimos:

Usamos HTML para estructurar nuestros documentos web.
Puedes utilizar CSS para dar estilo a tu HTML.
La NASA realiza un trabajo fascinante, sin duda.
El chiste de Ashok me hizo LOL muchísimo.

Veamos ahora qué código HTML debemos escribir para conseguir ese resultado:

<p>Usamos <abbr title="Lenguaje de Marcado de Hipertexto">HTML</abbr> para estructurar nuestros documentos web.</p>

<p>Puedes utilizar <abbr title="Hojas de Estilo en Cascada">CSS</abbr> para dar estilo a tu <abbr title="Lenguaje de Marcado de Hipertexto">HTML</abbr>.</p>

<p>La <abbr title="Administración Nacional de Aeronáutica y el Espacio">NASA</abbr> realiza un trabajo fascinante, sin duda.</p>

<p>El chiste de Ashok me hizo <abbr title="Reír a carcajadas">LOL</abbr> muchísimo.</p>

El objetivo principal de este elemento es aportar valor semántico y facilitar información adicional. Aunque todos los navegadores lo muestran como un elemento en línea (inline) de forma predeterminada, su estilo visual por defecto no es consistente y varía según el navegador (si bien esto siempre puede unificarse añadiendo reglas CSS).

Las diferencias de estilo más comunes son las siguientes:

  • Sin estilo visual: Algunos navegadores, como Internet Explorer, no le aplican ninguna apariencia distintiva, mostrándolo exactamente igual que si fuera un elemento <span>.
  • Subrayado: Navegadores como Opera o Firefox suelen añadir un subrayado de puntos al texto de la abreviatura para diferenciarlo.
  • Versalitas: Unos pocos navegadores van un paso más allá y, además del subrayado punteado, transforman el texto a versalitas (small caps).

Ejercicio propuesto: Ejemplo de uso de abreviaturas

Crea una página web utilizando el código del ejemplo anterior, y añade varios párrafos nuevos que contengan algunas abreviaturas y verifica el resultado en tu navegador. Recuerda incluir el resto de etiquetas básicas de HTML necesarias para la estructura del documento y no olvides validar tu código.

Ejercicio propuesto: Abreviaturas para chatear

El mundo del correo electrónico, los mensajes de texto y la mensajería instantánea han dado lugar a toda una serie de acrónimos y abreviaturas que permiten a los usuarios redactar sus mensajes con mayor rapidez. En este ejercicio deberás crear un par de listas. La primera incluirá la relación de abreviaturas que se enumeran a continuación (puedes añadir otras que conozcas). La segunda lista deberá contener al menos veinte frases que ilustren cómo se utilizan dichas abreviaturas en una conversación de chat.

Además, utiliza encabezados <h1> y <h2> para añadir títulos a las listas y proporcionar así un texto descriptivo previo.

Por ejemplo, la primera lista podría mostrar las abreviaturas de la siguiente manera:

  • bn – Bien
  • bss – Besos
  • finde – Fin de semana
  • hl – Hola
  • kdms – Quedamos
  • npi – Ni puñetera idea
  • ns / nc – No sabe / No contesta
  • pf / xfa – Por favor
  • pq / xq – Por qué
  • q tal – Qué tal
  • rt – Retuit (compartido / de acuerdo)
  • salu2 – Saludos
  • tb – También
  • tqm – Te quiero mucho
  • tpc – Tampoco
  • vdd – Verdad
  • xo – Pero
  • +o- – Más o menos

La segunda lista deberá incluir al menos veinte frases en las que se empleen dichas abreviaturas. Por ejemplo:

  • Espero que te encuentres bn, hace tiempo que no hablamos.
  • Me tengo que ir corriendo, hablamos luego. ¡bss!
  • ¿Tenéis algún plan interesante para este finde?
  • hl, ¿te ha llegado ya el paquete que te envié?
  • Si os parece bien, kdms a las 20:00 en la plaza mayor.
  • No me preguntes cómo arreglarlo porque no tengo npi.
  • Le he preguntado si vendrá a la cena de empresa, pero de momento ns / nc.

Puedes utilizar el siguiente código como ejemplo para saber qué etiquetas debes utilizar para obtener ambas listas:

<h1>Algunas abreviaturas que uso para chatear</h1>

<h2>Las abreviaturas</h2>

<ul>
    <li>bn - Bien</li>
    <li>bss - Besos</li>
    <li>finde - Fin de semana</li>
    <li>hl - Hola</li>
    <li>kdms - Quedamos</li>
    <li>npi - Ni puñetera idea</li>
    <li>ns / nc - No sabe / No contesta</li>
    <li>...</li>
</ul>

<h2>Algunos ejemplos</h2>

<ul>
    <li>Espero que te encuentres <abbr title="Bien">bn</abbr>, hace tiempo que no hablamos.</li>
    <li>Me tengo que ir corriendo, hablamos luego. ¡<abbr title="Besos">bss</abbr>!</li>
    <li>¿Tenéis algún plan interesante para este <abbr title="Fin de semana">finde</abbr>?</li>
    <li><abbr title="Hola">hl</abbr>, ¿te ha llegado ya el paquete que te envié?</li>
    <li>Si os parece bien, <abbr title="Quedamos">kdms</abbr> a las 20:00 en la plaza mayor.</li>
    <li>No me preguntes cómo arreglarlo porque no tengo <abbr title="Ni puñetera idea">npi</abbr>.</li>
    <li>Le he preguntado si vendrá a la cena de empresa, pero de momento <abbr title="No sabe / No contesta">ns / nc</abbr>.</li>
    <li>...</li>
</ul>

Citas

El elemento <q>

El elemento HTML <q> indica que el texto que encierra es una cita corta en línea (inline). La gran mayoría de los navegadores modernos interpretan este elemento envolviendo automáticamente el texto entre comillas.

Asimismo, puedes utilizar el atributo cite para especificar una URL que señale el documento de origen o la fuente de la información citada, aunque es importante tener en cuenta que, por defecto, el navegador no muestra esta URL al usuario de forma visible.

A continuación mostramos un par de ejemplos analizando primero el resultado visual (en la mayoría de los casos la única diferencia es que las comillas aparecen automáticamente para acotar la cita):

Cuando Dave le pide a HAL que abra las compuertas de la cápsula, HAL responde: "Lo siento, Dave. Me temo que no puedo hacerlo".

Según el sitio web de Mozilla, "Firefox 1.0 se lanzó en 2004 y se convirtió en un gran éxito".


Y ahora el código fuente correspondiente:

<p>
    Cuando Dave le pide a HAL que abra las compuertas de la cápsula, HAL responde: <q cite="https://www.imdb.com/es-es/title/tt0062622/">"Lo siento, Dave. Me temo que no puedo hacerlo"</q>.
</p>

<p>
    Según el sitio web de Mozilla,
    <q cite="https://www.mozilla.org/en-US/about/history/details/">"Firefox 1.0 se lanzó en 2004 y se convirtió en un gran éxito"</q>.
</p>

Importante: El elemento <q> está diseñado para citas breves que no requieren saltos de párrafo. Para citas extensas, utiliza el elemento <blockquote>.

Ejercicio propuesto: Citas en línea

Crea una página web utilizando el código del ejemplo anterior, y luego añade varios párrafos que contenga citas de tu elección y verifica el resultado en el navegador. Recuerda incluir el resto de etiquetas básicas de HTML para estructurar el documento y no olvides validar tu código.

Puedes usar por ejemplo citas de tu película favorita, tal como aparecen en «https://www.fotogramas.es/noticias-cine/g32183447/star-wars-mejores-frases-de-la-saga/«.

El elemento <blockquote>

El elemento HTML <blockquote> (o elemento de cita en bloque) indica que el texto que encierra constituye una cita extensa. Por lo general, los navegadores representan este elemento visualmente aplicando una sangría o indentación al texto.

Es posible especificar la URL de la fuente de la cita mediante el atributo cite, pero para indicar la referencia de la fuente en formato de texto visible se emplea el elemento completo <cite>. Por ejemplo:

Vuestro trabajo va a llenar gran parte de vuestra vida, y la única forma de estar realmente satisfecho es hacer lo que creéis que es un gran trabajo. Y la única forma de hacer un gran trabajo es amar lo que hacéis. Si aún no lo habéis encontrado, seguid buscando. No os conforméis. Como con todo lo que tiene que ver con el corazón, lo sabréis cuando lo encontréis. Steve Jobs
<blockquote cite="https://www.brainyquote.com/quotes/steve_jobs_416859">
    Vuestro trabajo va a llenar gran parte de vuestra vida, y la única forma de estar realmente satisfecho es hacer lo que creéis que es un gran trabajo. Y la única forma de hacer un gran trabajo es amar lo que hacéis. Si aún no lo habéis encontrado, seguid buscando. No os conforméis. Como con todo lo que tiene que ver con el corazón, lo sabréis cuando lo encontréis.
    <a href="https://www.brainyquote.com/quotes/steve_jobs_416859"><cite>Steve Jobs</cite></a>
</blockquote>

Ejercicio propuesto: Citas célebres

Crea una página web que recopile al menos veinte de las citas más famosas de la historia. Puedes encontrar muchas de ellas en sitios como «https://www.proverbia.net«, «https://www.mundifrases.com/«, o también puedes utilizar cualquier otra fuente que prefieras.

Debes utilizar el elemento <blockquote> para envolver la cita completa, y el elemento <cite> dentro de cada cita para indicar el nombre del autor, tal como se muestra en el ejemplo anterior y en los siguientes:
La mayor gloria de vivir no radica en no caer nunca, sino en levantarnos cada vez que caemos. Nelson Mandela

La manera de empezar es dejar de hablar y comenzar a actuar. Walt Disney

Vuestro tiempo es limitado, así que no lo malgastéis viviendo la vida de otro. No os dejéis atrapar por el dogma, que es vivir según los resultados del pensamiento de otros. No dejéis que el ruido de las opiniones de los demás ahogue vuestra propia voz interior. Y lo más importante, tened el coraje de seguir a vuestro corazón y vuestra intuición. Steve Jobs

Si la vida fuera predecible, dejaría de ser vida y no tendría sabor. Eleanor Roosevelt

Si miras lo que tienes en la vida, siempre tendrás más. Si miras lo que no tienes en la vida, nunca tendrás suficiente. Oprah Winfrey

Combinando los elementos <q>, <blockquote> y <cite>

El elemento de cita HTML (<cite>) se utiliza para referenciar el título de una obra creativa citada (como un libro, un artículo, un ensayo, una película, etc.). Según las convenciones de metadatos más apropiadas para el contexto, esta referencia puede presentarse de forma abreviada. Veamos un ejemplo práctico en el que resulta útil combinar estos tres elementos para crear una mejor estructura semántica:

Hola y bienvenidos a mi página de motivación. Como dice el sitio Citas de Confucio :

No importa lo lento que vayas mientras no te detengas.

También me encanta el concepto del pensamiento positivo y la necesidad de «mantener tus pensamientos positivos» (como se menciona en Citas Positivas ).


Y ahora echemos un vistazo al código fuente que debemos escribir para obtener ese resultado:

<p>
    Hola y bienvenidos a mi página de motivación. Como dice el sitio 
    <a href="https://proverbia.net/autor/frases-de-confucio">
        <cite>Citas de Confucio</cite>
    </a>:
</p>

<blockquote cite="https://www.proverbia.net/citasautor/confucio">
    No importa lo lento que vayas mientras no te detengas.
</blockquote>

<p>
    También me encanta el concepto del pensamiento positivo y la necesidad de 
    <q cite="https://www.mundifrases.com/tema/pensamiento-positivo/">
        mantener tus pensamientos positivos
    </q> 
    (como se menciona en 
    <a href="https://psicologiaymente.com/reflexiones/frases-positivas">
        <cite>Citas Positivas</cite>
    </a>).
</p>

Ejercicio propuesto: Ejemplo de citas completas

Crea una página web utilizando primero el código del ejemplo anterior y verifica el resultado en tu navegador. A continuación añade otro bloque similar combinando las tres etiquetas mencionadas, y vuelve a comprobar el resultado en el navegador.

No olvides incluir el resto de las etiquetas básicas de HTML (incluyendo títulos <h1> y <h2>) y valida tu código para asegurar que es correcto.

Representación de código informático

HTML pone a nuestra disposición una serie de elementos específicos para el marcado de código informático:

  • <code>: Se utiliza para marcar fragmentos genéricos de código informático.
  • <pre>: Se usa para preservar los espacios en blanco (generalmente empleado para bloques de código). Si utilizas sangría o espacios en blanco adicionales dentro del texto, los navegadores los ignorarán por defecto y no se verán reflejados en la página renderizada. No obstante, como ya explicamos en una unidad anterior, si envuelves el texto entre etiquetas <pre></pre>, los espacios en blanco se mostrarán idénticos a como aparecen en tu editor de texto.
  • <var>: Sirve para marcar específicamente nombres de variables.
  • <kbd>: Se utiliza para marcar la entrada de teclado (u otros tipos de entrada de datos) introducida en el ordenador.
  • <samp>: Se emplea para marcar la salida (output) de un programa informático.

El elemento <code>

El elemento HTML <code> muestra su contenido con un estilo visual diseñado para indicar que el texto es un fragmento breve de código informático. Por defecto, los navegadores muestran el texto de este elemento utilizando la fuente monoespaciada predeterminada del sistema.

Veamos un par de ejemplos:

El método push() añade uno o más elementos al final de un array y devuelve la nueva longitud del mismo.

La función selectAll() resalta todo el texto en el campo de entrada para que el usuario pueda, por ejemplo, copiar o eliminar el texto.


Y este sería el código HTML del ejemplo anterior:

<p>
    El método <code>push()</code> añade uno o más elementos al final de un array y devuelve la nueva longitud del mismo.
</p>

<p>
    La función <code>selectAll()</code> resalta todo el texto en el campo de entrada para que el usuario pueda, por ejemplo, copiar o eliminar el texto.
</p>

Ejercicio propuesto: Código en línea

Crea una página web con el código del ejemplo anterior, luego añade algunos ejemplos más, y verifica los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código.

Los elementos <pre> + <code>

El elemento <code> por sí solo representa únicamente una instrucción o una única línea de código. Para representar múltiples líneas de código (un bloque), debemos envolver el elemento <code> dentro de un elemento <pre>. De esta forma, respetaremos los espacios y saltos de línea. Por ejemplo:

if (a > b) {
    console.log('¡Hola!'); // Ejemplo de código en el lenguaje JavaScript
}

Código HTML del ejemplo:

<pre><code>
if (a > b) {
    console.log('¡Hola!'); // Ejemplo de código en el lenguaje JavaScript
}
</code></pre>

Ejercicio propuesto: Bloque de código

Crea una página web con el código del ejemplo anterior, añade algunos bloque adicionales con cualquier fragmento de código de cualquier lenguaje y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código

El elemento <var>

El elemento de variable HTML (<var>) representa el nombre de una variable dentro de una expresión matemática o en un contexto de programación. Por lo general, se presenta visualmente utilizando una versión en cursiva de la tipografía actual, aunque este comportamiento depende de cada navegador.

Por ejemplo:

Una ecuación simple: x = y + 2

El volumen de una caja es l × w × h, donde l representa la longitud, w la anchura y h la altura de la caja.

Las variables minSpeed y maxSpeed controlan la velocidad mínima y máxima del aparato en revoluciones por minuto (RPM).


Código HTML del ejemplo:

<p>
    Una ecuación simple: <var>x</var> = <var>y</var> + 2
</p>

<p>
    El volumen de una caja es <var>l</var> × <var>w</var> × <var>h</var>, donde <var>l</var> 
    representa la longitud, <var>w</var> la anchura y <var>h</var> la 
    altura de la caja.
</p>

<p>
    Las variables <var>minSpeed</var> y <var>maxSpeed</var> controlan la velocidad mínima y máxima 
    del aparato en revoluciones por minuto (RPM).
</p>

Ejercicio propuesto: Ecuaciones y variables

Crea una página web con el código del ejemplo anterior, y añade un par de párrafos adicionales con cualquier ecuación que desees y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código

El elemento <kbd>

El elemento HTML de entrada de teclado (<kbd>) representa un fragmento de texto en línea que denota una entrada del usuario, ya sea a través de un teclado convencional, comandos de voz o cualquier otro dispositivo de entrada de texto.

Por convención, el navegador renderiza el contenido de un elemento <kbd> utilizando su fuente monoespaciada predeterminada para que destaque visualmente. Veamos algunos ejemplos:

Por favor, pulsa Ctrl + Mayús + R para recargar una página de MDN.

Usa el comando help micomando para ver la documentación del comando «micomando».

Puedes crear un nuevo documento utilizando el atajo de teclado Ctrl + N.


<p>
    Por favor, pulsa <kbd>Ctrl</kbd> + <kbd>Mayús</kbd> + <kbd>R</kbd> para recargar una página de MDN.
</p>

<p>
    Usa el comando <kbd>help micomando</kbd> para ver la documentación del comando "micomando".
</p>

<p>
    Puedes crear un nuevo documento utilizando el atajo de teclado <kbd>Ctrl</kbd> + <kbd>N</kbd>.
</p>

Ejercicio propuesto: Atajos de teclado

Crea una página web con el código del ejemplo anterior, añade un par de párrafos con cualquier atajo de teclado que conozcas y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código.

El elemento <samp>

El elemento de muestra HTML (<samp>) se utiliza para encerrar texto en línea que representa la salida (output) de muestra o el resultado devuelto por un programa informático. Su contenido se muestra habitualmente utilizando la fuente monoespaciada predeterminada del navegador (como Courier o Lucida Console) para diferenciarlo del texto normal.

Veamos un par de ejemplos:

Estaba intentando arrancar mi ordenador, pero me salió este mensaje tan irónico:

Teclado no encontrado
Pulse F1 para continuar

Cuando el proceso finalice, la utilidad mostrará el texto Escaneo completado. Se han encontrado N resultados. Entonces podrás proceder al siguiente paso.


Código HTML del ejemplo:

<p>Estaba intentando arrancar mi ordenador, pero me salió este mensaje tan irónico:</p>

<p>
    <samp>Teclado no encontrado <br>Pulse F1 para continuar</samp>
</p>

<p>...</p>

<p>
    Cuando el proceso finalice, la utilidad mostrará el texto <samp>Escaneo completado. Se han encontrado <em>N</em> resultados.</samp> Entonces podrás proceder al siguiente paso.
</p>

Ejercicio propuesto: Salida de muestra

Crea una página web con el código del ejemplo anterior, junto con un par de párrafos más, y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código para asegurar que es correcto

Un ejemplo completo

Veamos ahora un ejemplo completo que combina todos estos elementos (<code>, <pre>, <var>, <kbd>, <samp>) para ver cómo interactúan entre sí.

A continuación mostramos el resultado visual:

var para = document.querySelector('p');

para.onclick = function() {
  alert('¡Ay, deja de pincharme!');
}

No deberías utilizar elementos de presentación como <font> y <center>.

En el ejemplo de JavaScript anterior, para representa un elemento de párrafo.

Selecciona todo el texto con Ctrl/Cmd + A.

$ ping mozilla.org
PING mozilla.org (63.245.215.20): 56 data bytes
64 bytes from 63.245.215.20: icmp_seq=0 ttl=40 time=158.233 ms

Y aquí el código fuente correspondiente:

<pre><code>var para = document.querySelector('p');

para.onclick = function() {
  alert('¡Ay, deja de pincharme!');
}</code></pre>

<p>No deberías utilizar elementos de presentación como <code>&lt;font&gt;</code> y <code>&lt;center&gt;</code>.</p>

<p>En el ejemplo de JavaScript anterior, <var>para</var> representa un elemento de párrafo.</p>

<p>Selecciona todo el texto con <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>A</kbd>.</p>

<pre>$ <kbd>ping mozilla.org</kbd>
<samp>PING mozilla.org (63.245.215.20): 56 data bytes
64 bytes from 63.245.215.20: icmp_seq=0 ttl=40 time=158.233 ms</samp></pre>

Ejercicio propuesto: Código, atajos y salida

Crea una página web con el código del ejemplo anterior, junto con algún bloque <pre>...</pre> adicional, y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código .

Ejercicio propuesto: Comandos de Linux

Crea una página web que muestre una tabla con algunos de los comandos de Linux más importantes y su descripción de uso, siguiendo aproximadamente el modelo de la tabla que se muestra a continuación.

Debes cumplir los siguientes requisitos semánticos:
1. Utiliza la etiqueta <code> para los comandos en la columna de la izquierda.
2. Utiliza la etiqueta <kbd> para los ejemplos de entrada del usuario (user’s input) dentro de las descripciones.
3. Recuerda que la tabla debe tener encabezados (<th>) y un título o leyenda (<caption>).
Comandos de Linux
Comandos Descripción
passwd Cambia tu contraseña de usuario:
1. Escribe tu antigua contraseña
2. Introduce la nueva contraseña
3. Confirma la nueva contraseña
~ Directorio personal del usuario (Home)
(atajo para: /home/usuario)
ls Lista las carpetas y archivos del directorio actual
mkdir Crea un nuevo directorio dentro del actual:
mkdir nuevodir
cd Cambia de directorio:
cd test (ir a un directorio llamado ‘test’)
cd .. (ir al directorio padre/superior)
cd ~ (ir al directorio personal)
rm Elimina el archivo o directorio especificado:
rm nombrearchivo (elimina un solo archivo)
rm *.txt (elimina TODOS los archivos .txt del directorio actual)
rm -r nombredir (elimina el directorio y sus archivos)

¡Por favor, ten cuidado al usar la opción -f!
rmdir Elimina el directorio VACÍO especificado
rmdir nombredir
pwd Imprime la ruta absoluta actual
man Muestra la página de manual del comando especificado:
man ls (muestra la ayuda de ls)
vi x.sh VI es un editor de texto. Si x.sh no existe, vi crea un nuevo archivo llamado x.sh y lo abre;
de lo contrario, simplemente abre el archivo existente.
less archivotexto less es un paginador de texto. Abre (solo lectura) el archivo archivotexto. Puedes usar las flechas arriba y abajo para desplazarte por el texto; comparte muchos comandos con VI.
chmod Cambia los permisos POSIX de un archivo o directorio. Permite proteger archivos contra accesos no deseados:
r : permiso de lectura
w : permiso de escritura
x : permiso de ejecución

chmod +x archivo.sh (permite la ejecución)
chmod -w archivo.sh (deniega la escritura)
chown Cambia el propietario de un archivo o directorio:
chown usuario archivo.sh
top Muestra los procesos en ejecución actualmente
cat Imprime el contenido de un archivo en pantalla
grep Filtra el archivo de texto especificado y muestra las líneas que contienen el patrón:
grep patron archivo.sh
También puedes usar tuberías (pipes) con la salida de otro comando:
cat archivo.sh | grep home
cat archivo.sh | grep "home page"

Marcado de datos de contacto

HTML proporciona un elemento específico para marcar semánticamente la información de contacto: <address>. Este elemento es muy versátil y puede utilizarse en diversos contextos; por ejemplo, para facilitar los datos de contacto de una empresa en el encabezado o pie de página de un sitio web, o para indicar quién es el autor de un artículo o una publicación concreta.

Su funcionamiento es sencillo: basta con envolver los detalles de contacto dentro de esta etiqueta. Veamos un ejemplo básico:

<address>
    <p>Fernando Ruiz, IES San Vicente, España</p>
</address>

No obstante, el contenido del elemento <address> puede albergar un marcado mucho más complejo. De hecho, la información suministrada puede adoptar la forma que mejor se ajuste al contexto, incluyendo cualquier dato necesario: dirección física, URL, correo electrónico, número de teléfono, redes sociales, coordenadas geográficas, etc. .

Regla importante: Debes tener en cuenta que siempre hay que incluir, como mínimo, el nombre de la persona, grupo u organización a la que hacen referencia dichos datos de contacto.

Veamos un ejemplo más completo y estructurado:

<address>
    <p>
        Juan Pérez<br>
        Calle Mayor, 10<br>
        Madrid, 28013<br>
        España
    </p>

    <ul>
        <li>Tel: 91 123 45 67</li>
        <li>Email: [email protected]</li>
    </ul>
</address>

También es correcto utilizar este elemento para referenciar información de contacto que se encuentra en otra página o enlace, como en el siguiente caso:

<address>
    Puedes contactar con el autor en <a href="http://www.midominio.com/contacto">www.midominio.com</a>.<br>

    Si encuentras algún error, por favor <a href="mailto:[email protected]">contacta con el webmaster</a>.<br>

    También puedes visitarnos en:<br>
    Fundación Mozilla<br>
    331 E Evelyn Ave<br>
    Mountain View, CA 94041<br>
    EE. UU.
</address>

Ejercicio propuesto: Información de contacto

Crea una página web utilizando el código de los ejemplos anteriores y verifica el resultado en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código para asegurar que la estructura es correcta .

El elemento <figure>

El elemento HTML <figure> (Figura con leyenda opcional) representa contenido independiente (self-contained content), el cual puede ir acompañado de un título o leyenda opcional especificado mediante el elemento <figcaption>.

La figura, su leyenda y su contenido se referencian semánticamente como una única unidad.

Figuras con imágenes

Este es el uso más común: asociar imágenes con sus pies de foto. Por ejemplo:

Paisaje nórdico.
Paisaje nórdico

Perro pidiendo algo de comida
Perro pidiendo comida

Código HTML del ejemplo:

<figure>
    <img src="https://picsum.photos/id/235/300/200"
         alt="Paisaje nórdico.">
    <figcaption>Paisaje nórdico</figcaption>
</figure>

<hr>

<figure>
    <img src="https://picsum.photos/id/237/300/200"
         alt="Perro pidiendo algo de comida">
    <figcaption>Perro pidiendo comida</figcaption>
</figure>

Ejercicio propuesto: Imágenes con leyendas

Crea una página web utilizando el código del ejemplo anterior, añade un par de figuras (<figure>) nuevas con imágenes y elige una leyenda adecuada (<figcaption>) para cada una. Finalmente, comprueba los resultados en tu navegador. No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código.

Puedes utilizar cualquier imagen que te guste; por ejemplo, las del servicio «https://picsum.photos/images«, tal como hicimos en ejercicios de la unidad anterior.

Figuras con poemas

El elemento <figure> no sirve solo para imágenes; también es excelente para enmarcar poemas o fragmentos literarios, como por ejemplo:

Caminante, son tus huellas
el camino y nada más;
Caminante, no hay camino,
se hace camino al andar.
Al andar se hace el camino,
y al volver la vista atrás
se ve la senda que nunca
se ha de volver a pisar.

Proverbios y cantares, de Antonio Machado

Código HTML del ejemplo:

<figure>
    <p>
        Caminante, son tus huellas<br>
        el camino y nada más;<br>
        Caminante, no hay camino,<br>
        se hace camino al andar.<br>
        Al andar se hace el camino,<br>
        y al volver la vista atrás<br>
        se ve la senda que nunca<br>
        se ha de volver a pisar.
    </p>
    <figcaption>
        <cite>Proverbios y cantares</cite>, de Antonio Machado
    </figcaption>
</figure>

Ejercicio propuesto: Poemas

Crea una página web con el código del ejemplo anterior (el de Machado), añade una nueva figura con otro poema que te guste (quizás de Lorca, Bécquer o Neruda) y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código .

Figuras con código

El elemento <figure> es ideal para mostrar fragmentos de código que tienen un título descriptivo o que funcionan como ejemplos independientes.

Veamos un ejemplo práctico:

Obtener detalles del navegador usando navigator:
function EjemploNavigator() {
  var txt;
  txt = "Nombre en clave: " + navigator.appCodeName + "; ";
  txt+= "Nombre del navegador: " + navigator.appName + "; ";
  txt+= "Versión del navegador: " + navigator.appVersion + "; ";
  txt+= "Cookies habilitadas: " + navigator.cookieEnabled + "; ";
  txt+= "Plataforma: " + navigator.platform + "; ";
  txt+= "Cabecera User-agent: " + navigator.userAgent + "; ";
  
  console.log("EjemploNavigator", txt);
}

Código HTML del ejemplo:

<figure>
    <figcaption>Obtener detalles del navegador usando <code>navigator</code>:</figcaption>
    
    <pre><code>
      function EjemploNavigator() {
        var txt;
        txt = "Nombre en clave: " + navigator.appCodeName + "; ";
        txt+= "Nombre del navegador: " + navigator.appName + "; ";
        txt+= "Versión del navegador: " + navigator.appVersion + "; ";
        txt+= "Cookies habilitadas: " + navigator.cookieEnabled + "; ";
        txt+= "Plataforma: " + navigator.platform + "; ";
        txt+= "Cabecera User-agent: " + navigator.userAgent + "; ";
  
        console.log("EjemploNavigator", txt);
      }
    </code></pre>

</figure>

Ejercicio propuesto: Bloque de código en figura

Crea una página web con el código del ejemplo anterior, junto con otro más, y comprueba los resultados en tu navegador.

No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código .

Figuras con citas

Como vimos anteriormente, podemos usar <blockquote> por sí solo, pero si envolvemos la cita dentro de un elemento <figure>, podemos usar <figcaption> para indicar el autor o la fuente con mayor implicación semánticamente. Por ejemplo:

Edsger Dijkstra:

Si depurar es el proceso de eliminar errores de software, entonces programar debe ser el proceso de introducirlos.

Código HTML del ejemplo:

<figure>
    <figcaption><cite>Edsger Dijkstra:</cite></figcaption>
    <blockquote>
        Si depurar es el proceso de eliminar errores de software, 
        entonces programar debe ser el proceso de introducirlos.
    </blockquote>
</figure>

Ejercicio propuesto: Citas famosas (versión con figuras)

Crea una página web con el código del ejemplo anterior y añade algunas citas famosas más utilizando este mismo formato (<figure> + <figcaption> + <blockquote>) para mostrar al menos diez citas.

Finalmente, comprueba los resultados en tu navegador. No olvides incluir el resto de las etiquetas básicas de HTML y validar tu código.

Puedes reutilizar las citas que empleaste en ejercicios anteriores o buscar nuevas en los sitios que recomendamos previamente «https://proverbia.net/«, «https://www.mundifrases.com/«, o alguna en inglés como «https://www.brainyquote.com/«, o cualquier otra página web que conozcas.

Test

Comprueba tus conocimientos con este test sobre tablas y otros conceptos relacionados con esta unidad.

Aprende el abecedario en tres idiomas

Español

Resumen de audio

Resumen de vídeo

Canción del abecedario

A de abeja vuela sin parar,
Be de barco que va por el mar,
Ce de casa donde voy a jugar,
De de dado me gusta lanzar.

E de elefante grandote es,
Efe de flor perfumada a la vez,
Ge de gato saltando otra vez,
Hache de héroe valiente ya ves.

(Estribillo)
A be ce, vamos a aprender,
cantando juntos lo vas a saber.
Del a a la zeta lo vas a lograr,
el abecedario vas a dominar.

I de isla en el medio del mar,
Jota de jirafa que quiere bailar,
Ka de koala que quiere abrazar,
Ele de luna que se ve al brillar.

Eme de manzana roja y genial,
Ene de nube que vuela en el cielo,
Eñe de ñandú caminando ligero,
O de oso en un bosque de hielo.

(Estribillo)
A be ce, vamos a aprender,
cantando juntos lo vas a saber.
De la a a la zeta lo vas a lograr,
el abecedario vas a dominar.

Pe de pez nadando feliz,
Cu de queso que guardo yo aquí,
Erre de rana que hace “croac” así,
Ese de sol que ilumina mi país.

Te de tren que hace “chucu chucu” ya,
U de unicornio con magia y paz,
Uve de vaca que hace “muuu” de verdad,
Uve doble wifi pa’ conectarnos más.

Equis de xilófono que suena genial,
I griega de yogur que me voy a tomar,
Zeta de zorro que saluda al final.

(Estribillo final)
A be ce, ya lo sabes bien,
siempre cantando lo harás muy muy bien.
A be ce, vamos a aplaudir,
con el abecedario puedes sonreír.

Inglés

Resumen de audio

Resumen de vídeo

Catalán

Resumen de audio

Resumen de vídeo

Serpiente v2, un juego con música, efectos de sonido, colisiones, comida, imagen de fondo y game over, hecho con Godot

Assets

Pasos a seguir

Esqueleto del juego

extends Node2D

# ----------------------------------------
# SERPIENTE (SNAKE) v2 GODOT 4.x
# ----------------------------------------
#
# - Incorpora música de fondo, efectos de sonido (comer, morir).
# - Fondo de pantalla (imagen).
# - Imágenes para los segmentos de la serpiente (cabeza, cuerpo, cola) y la comida.
# - Pantalla de "Game Over" con imagen.
# - Botón de cerrar el juego.
# - Estructura de funciones similar al ejemplo de Asteroides.
#
# INPUT MAP necesario (Proyecto → Configuración del Proyecto → Mapa de Entrada):
#   "ui_left" (← / A), "ui_right" (→ / D), "ui_up" (↑ / W), "ui_down" (↓ / S)
#   "ui_accept" (Enter/Espacio) para reiniciar.
#
# Imágenes y sonidos en la carpeta 'assets':
#   cabeza_serpiente.png, cuerpo_serpiente.png, cola_serpiente.png, comida_manzana.png
#   fondo_jungla.png, game_over_snake.png, boton_cerrar.png
#   musica_fondo_snake.mp3, sfx_comer.mp3, sfx_muerte_snake.mp3

# -------------------------
# CONSTANTES DE CONFIGURACIÓN
# -------------------------
const TAM_CUADRICULA = 75    # Tamaño de cada "cuadrado" de la cuadrícula en píxeles
const TAM_CABEZA = 90        # Tamaño de la cabeza en píxeles
const TAM_COMIDA = 90        # Tamaño de la comida en píxeles
const VELOCIDAD_JUEGO = 0.2  # Segundos entre cada movimiento (más bajo = más rápido)
const TAM_BOTON_CERRAR = 50
const TAM_TEXTO = 40
const PAUSA_GAME_OVER = 1

# Colores (para el texto de la UI)
const COLOR_TEXTO = Color(1, 1, 1, 0.8)

# Imágenes (pon los archivos en la carpeta 'assets')
const TEX_FONDO: Texture2D = preload("res://assets/fondo.png")
const TEX_CABEZA_SERPIENTE: Texture2D = preload("res://assets/cabeza.png")
const TEX_CUERPO_SERPIENTE: Texture2D = preload("res://assets/cuerpo.png")
const TEX_COLA_SERPIENTE: Texture2D = preload("res://assets/cola.png")
const TEX_COMIDA: Texture2D = preload("res://assets/comida.png")
const TEX_GAME_OVER: Texture2D = preload("res://assets/game_over.png")
const TEX_BOTON_CERRAR: Texture2D = preload("res://assets/boton_cerrar.png")

# Audios (pon los archivos en la carpeta 'assets')
const MUSICA_FONDO: AudioStream = preload("res://assets/musica_fondo.mp3")
const SFX_COMER: AudioStream = preload("res://assets/sfx_comer.mp3")
const SFX_MUERTE: AudioStream = preload("res://assets/sfx_muerte.mp3")

# -------------------------
# ESTADO DEL JUEGO
# -------------------------
var pantalla: Vector2
var cuadricula_ancho: int
var cuadricula_alto: int

# La serpiente es un array de posiciones en la *cuadrícula* (no píxeles)
# La cabeza es el *último* elemento (serpiente.back())
var serpiente: Array[Vector2] = []
var comida: Vector2 # Posición de la comida en la cuadrícula
var direccion: Vector2 = Vector2.RIGHT
var proxima_direccion: Vector2 = Vector2.RIGHT

var muerto: bool = false
var crecer_serpiente: bool = false
var puntuacion: int = 0

# Control del "tick" del juego usando _process
var tiempo_acumulado: float = 0.0

# UI
var etiqueta_puntuacion: Label
var boton_cerrar: Rect2

# Audio players
var musica_fondo_player: AudioStreamPlayer
var sfx_comer_player: AudioStreamPlayer
var sfx_muerte_player: AudioStreamPlayer

# -------------------------
# CICLO DE VIDA PRINCIPAL
# -------------------------
func _ready():
	# Ejecutar al iniciar la escena
	randomize()
	_inicializar_pantalla()
	_crear_ui()
	_crear_boton_cerrar()
	_inicializar_audio()
	_reproducir_audio(musica_fondo_player) # Reproducir música de fondo en bucle
	_iniciar_juego()

func _process(delta: float):
	# Ejecutar la lógica del juego en cada frame
	
	# Si estamos muertos o en pausa, no hacemos nada más que esperar el reinicio
	if muerto: return

	# Acumulamos el tiempo para simular un "tick" de juego
	tiempo_acumulado += delta
	
	# Si no ha pasado suficiente tiempo, salimos
	if tiempo_acumulado < VELOCIDAD_JUEGO: return
	
	# ¡Tick! Es hora de moverse. Reiniciamos el acumulador
	tiempo_acumulado = 0.0
	
	# La lógica principal se ejecuta aquí, a la velocidad de VELOCIDAD_JUEGO
	_actualizar_movimiento()
	
	# Pedir a Godot que vuelva a dibujar la pantalla
	queue_redraw()

func _draw():
	# Dibujar en pantalla los elementos del juego
	# El orden importa (lo que se dibuja último, queda encima)
	_dibujar_fondo()
	_dibujar_serpiente()
	_dibujar_comida()
	_mostrar_boton_cerrar()
	if muerto: _mostrar_game_over()


func _input(event: InputEvent):
	# Comprobar la entrada de teclado, ratón o táctil
	_comprobar_controles(event)

# -------------------------
# INICIALIZACIÓN BÁSICA
# -------------------------
func _inicializar_pantalla():
	# Guardar el tamaño actual de la pantalla (ancho y alto)
	pass

# -------------------------
# AUDIO
# -------------------------
func _crear_audio_player(stream: AudioStream, bus: String, volumen = 0.0):
	# Instanciar y configurar un AudioStreamPlayer con stream, bus y volumen inicial, y devolverlo
	pass

func _inicializar_audio():
	# Construir y registrar players de música y SFX con buses/volúmenes apropiados
	pass

func _reproducir_audio(audio_player: AudioStreamPlayer):
	# Reproducir el AudioStreamPlayer
	pass

func _detener_audio(audio_player: AudioStreamPlayer):
	# Parar el AudioStreamPlayer
	pass

# -------------------------
# UI (INTERFAZ DE USUARIO)
# -------------------------
func _crear_ui():
	# Crear un Label para la puntuación
	pass

func _actualizar_etiqueta_puntuacion():
	# Muestra la puntuación actual en pantalla
	pass

# -------------------------
# BOTÓN CERRAR
# -------------------------
func _crear_boton_cerrar():
	# Definir el área clicable del botón de cierre en la esquina superior derecha
	pass

func _mostrar_boton_cerrar():
	# Dibujar la textura del botón de cierre dentro de su rectángulo clicable
	pass

# -------------------------
# LÓGICA DEL JUEGO (MOVIMIENTO)
# -------------------------
func _iniciar_juego():
	# (Re)iniciar todas las variables del juego
	pass

func _actualizar_movimiento():
	# Esta función se llama en cada "tick" (definido por VELOCIDAD_JUEGO)
	pass

# -------------------------
# SERPIENTE
# -------------------------
func _mover_serpiente(nueva_cabeza: Vector2):
	# Añadir la nueva cabeza al final del array
	pass

func _convertir_direccion_a_angulo(dir: Vector2):
	# Convertir dirección (Vector2) a ángulo en radianes (0° = derecha)
	pass

func _dibujar_textura_rotada_centrada(tex: Texture2D, centro: Vector2, angulo: float, tamano: Vector2):
	# Dibujar textura rotada alrededor de un punto central
	pass

func _dibujar_serpiente():
	pass

# -------------------------
# COMIDA
# -------------------------
func _mover_comida():
	# Mover la comida a una posición aleatoria que no esté sobre la serpiente
	pass

func _dibujar_comida():
	# Dibujar la comida en su posición
	pass

func _comprobar_comida(cabeza: Vector2):
	# Comprobar si la cabeza está en la misma casilla que la comida
	pass

# -------------------------
# CONTROLES
# -------------------------
func _comprobar_controles(event: InputEvent):
	# Comprobación del botón de cerrar
	pass

# -------------------------
# COLISIONES Y GAME OVER
# -------------------------
func _comprobar_colisiones(cabeza: Vector2):
	# Comprueba si la cabeza ha chocado con algo. Devuelve 'true' si hay colisión.
	pass

func _pausa_game_over():
	# Activar una pausa breve no bloqueante tras el Game Over esperando a un temporizador asincrónico
	pass

func _game_over():
	# Termina el juego
	pass

func _reiniciar_juego():
	# Reiniciar la escena actual para devolver todo a su estado inicial
	pass

# -------------------------
# FONDO Y CAPAS
# -------------------------
func _dibujar_fondo():
	# Dibujar la imagen de fondo que cubre toda la pantalla
	pass

func _mostrar_game_over():
	# Dibujar la textura de Game Over. Podemos modularla para un efecto.
	pass

inicializar_pantalla()

Esta función se encarga de configurar las dimensiones del juego. Primero, obtiene el tamaño actual de la ventana o viewport (el área visible del juego) y lo almacena en la variable pantalla. Luego, basándose en el tamaño de la pantalla en píxeles y la constante TAM_CUADRICULA (que define el tamaño de un «cuadrado» de nuestro tablero), calcula cuántos cuadrados caben a lo ancho (cuadricula_ancho) y a lo alto (cuadricula_alto). Esto nos permite trabajar con coordenadas de cuadrícula (ej. 0,0 o 5,10) en lugar de píxeles (ej. 0,0 o 640,1280).

func _inicializar_pantalla():
	# Guardar el tamaño actual de la pantalla (ancho y alto)
	pantalla = get_viewport_rect().size
	# Calcular cuántos "cuadrados" de la cuadrícula caben en la pantalla
	cuadricula_ancho = int(pantalla.x / TAM_CUADRICULA)
	cuadricula_alto = int(pantalla.y / TAM_CUADRICULA)

crear_audio_player()

Esta es una función «ayudante» (o helper function) diseñada para crear y configurar reproductores de sonido de forma limpia y reutilizable. Recibe el archivo de audio (stream), el canal por donde debe sonar (bus, ej: «Music» o «SFX») y un volumen opcional. Crea un nuevo nodo AudioStreamPlayer, le asigna estas propiedades, lo añade como hijo a la escena actual (usando add_child, lo cual es crucial para que funcione) y finalmente devuelve el nodo ya configurado.

func _crear_audio_player(stream: AudioStream, bus: String, volumen = 0.0):
	# Instanciar y configurar un AudioStreamPlayer con stream, bus y volumen inicial, y devolverlo
	var player = AudioStreamPlayer.new()
	player.stream = stream
	player.bus = bus
	player.volume_db = volumen
	add_child(player)
	return player

inicializar_audio()

Esta función utiliza la función ayudante _crear_audio_player (que acabamos de ver) para preparar todos los sonidos que el juego necesitará. Crea tres reproductores de audio y los asigna a las variables globales del script: musica_fondo_player (asignado al bus «Music»), sfx_comer_player (asignado al bus «SFX») y sfx_muerte_player (también en «SFX»).

func _inicializar_audio():
	# Construir y registrar players de música y SFX con buses/volúmenes apropiados
	musica_fondo_player  = _crear_audio_player(MUSICA_FONDO, "Music", -5.0)
	sfx_comer_player     = _crear_audio_player(SFX_COMER, "SFX", 0.0)
	sfx_muerte_player    = _crear_audio_player(SFX_MUERTE, "SFX")
	
	# La música de fondo se reproduce en bloque
	musica_fondo_player.finished.connect(func(): musica_fondo_player.play())

reproducir_audio()

Esta función se usa para reproducir un sonido. Recibe el audio_player que debe sonar. Primero comprueba que el player existe (if audio_player) y que tiene un archivo de sonido cargado (and audio_player.stream). Si es así, inicia la reproducción con play(). Adicionalmente, tiene un parámetro loop (bucle) que si se establece en true, conectará una señal (finished) para que, cuando el sonido termine, vuelva a empezar automáticamente.

func _reproducir_audio(audio_player: AudioStreamPlayer):
	# Reproducir el AudioStreamPlayer
	if audio_player and audio_player.stream: audio_player.play()

detener_audio()

Función simple para parar un sonido. Comprueba si el audio_player existe y si se está reproduciendo actualmente (.playing). Si ambas condiciones son ciertas, detiene la reproducción inmediatamente usando stop().

func _detener_audio(audio_player: AudioStreamPlayer):
	# Parar el AudioStreamPlayer
	if audio_player and audio_player.playing: audio_player.stop()

crear_ui()

Prepara la interfaz de usuario básica. En este caso, crea un nuevo nodo de tipo Label (etiqueta de texto) y lo asigna a la variable etiqueta_puntuacion. Establece su posición en la esquina superior izquierda (10, 10). Luego, configura programáticamente su apariencia (tamaño de fuente TAM_TEXTO y color COLOR_TEXTO) usando un objeto LabelSettings. Finalmente, añade la etiqueta a la escena con add_child().

func _crear_ui():
	# Crear un Label para la puntuación
	etiqueta_puntuacion = Label.new()
	etiqueta_puntuacion.position = Vector2(10, 10)
	
	# Configurar la fuente (programáticamente)
	var fuente_puntuacion = LabelSettings.new()
	fuente_puntuacion.font_size = TAM_TEXTO
	fuente_puntuacion.font_color = COLOR_TEXTO
	etiqueta_puntuacion.label_settings = fuente_puntuacion
	add_child(etiqueta_puntuacion)

actualizar_etiqueta_puntuacion()

Esta función se llama cada vez que la puntuación cambia (al comer una fruta o al iniciar el juego). Simplemente actualiza la propiedad text de la etiqueta_puntuacion (creada en la función anterior) para mostrar el texto «Puntuación: » seguido del valor actual de la variable puntuacion.

func _actualizar_etiqueta_puntuacion():
	# Muestra la puntuación actual en pantalla
	etiqueta_puntuacion.text = "Puntuación: %s" % puntuacion

crear_boton_cerrar()

Esta función no crea un nodo de botón, sino que define el área donde el botón de cerrar será «clicable». Crea un Rect2 (un rectángulo) usando las coordenadas de la pantalla (pantalla.x) y el tamaño del botón (TAM_BOTON_CERRAR) para posicionarlo en la esquina superior derecha, con un pequeño margen de 10 píxeles. Esta área se almacenará en la variable boton_cerrar y se usará después en _input para detectar clics.

func _crear_boton_cerrar():
	# Definir el área clicable del botón de cierre en la esquina superior derecha
	boton_cerrar = Rect2(pantalla.x - TAM_BOTON_CERRAR - 10, 10, TAM_BOTON_CERRAR, TAM_BOTON_CERRAR)

mostrar_boton_cerrar()

Esta función se llama dentro de _draw(). Se encarga de dibujar la textura del botón (TEX_BOTON_CERRAR) en la pantalla, usando exactamente el rectángulo (boton_cerrar) que definimos en la función anterior. También le aplica un tinte (un color gris semitransparente) para que no sea tan brillante.

func _mostrar_boton_cerrar():
	# Dibujar la textura del botón de cierre dentro de su rectángulo clicable
	draw_texture_rect(TEX_BOTON_CERRAR, boton_cerrar, false, Color(0.8, 0.8, 0.8, 1.0))

iniciar_juego()

Esta es la función clave para empezar (o reiniciar) una partida. Restablece todas las variables de estado a sus valores iniciales: vacía la serpiente (serpiente.clear()), pone la puntuación a 0, actualiza la etiqueta, quita el estado muerto, resetea la direccion a Vector2.RIGHT y reinicia el tiempo_acumulado. Después, crea la serpiente inicial añadiendo tres segmentos (cola, cuerpo y cabeza) al array serpiente en posiciones de cuadrícula fijas. Finalmente, llama a _mover_comida() para colocar la primera manzana.

func _iniciar_juego():
	# (Re)iniciar todas las variables del juego
	serpiente.clear()
	puntuacion = 0
	_actualizar_etiqueta_puntuacion()
	muerto = false
	crecer_serpiente = false
	direccion = Vector2.RIGHT
	proxima_direccion = Vector2.RIGHT
	tiempo_acumulado = 0.0

	# Crear serpiente inicial (en posiciones de cuadrícula)
	serpiente.push_back(Vector2(3, 5)) # Cola
	serpiente.push_back(Vector2(4, 5)) # Cuerpo
	serpiente.push_back(Vector2(5, 5)) # Cabeza

	_mover_comida()

actualizar_movimiento()

Este es el corazón de la lógica del juego, se ejecuta en cada «tick» (controlado por VELOCIDAD_JUEGO en _process). Primero, actualiza la direccion actual con la proxima_direccion (que guardó el input). Segundo, calcula cuál será la nueva_cabeza sumando la dirección a la posición de la cabeza actual. Tercero, comprueba si esa nueva_cabeza choca con algo (_comprobar_colisiones); si choca, llama a _game_over() y se detiene. Cuarto, comprueba si come (_comprobar_comida). Quinto y último, llama a _mover_serpiente() para efectuar el movimiento.

func _actualizar_movimiento():
	# Esta función se llama en cada "tick" (definido por VELOCIDAD_JUEGO)
	
	# 1. Actualizar dirección
	direccion = proxima_direccion
	
	# 2. Calcular nueva posición de la cabeza
	var cabeza_actual = serpiente.back() # .back() es el último elemento
	var nueva_cabeza = cabeza_actual + direccion
	
	# 3. Comprobar colisiones
	if _comprobar_colisiones(nueva_cabeza):
		_game_over()
		return # Detener movimiento si morimos
	
	# 4. Comprobar si comemos
	_comprobar_comida(nueva_cabeza)
	
	# 5. Mover serpiente
	_mover_serpiente(nueva_cabeza)

mover_serpiente()

Esta función actualiza el array serpiente para simular el movimiento. Siempre añade la nueva_cabeza al final del array (push_back). Luego, comprueba la variable crecer_serpiente. Si es false (no hemos comido), elimina el primer segmento del array (pop_front), que es la cola. Esto hace que la serpiente mantenga su tamaño pero avance. Si crecer_serpiente es true (hemos comido), no elimina la cola, haciendo que la serpiente crezca un segmento, y resetea el flag a false.

func _mover_serpiente(nueva_cabeza: Vector2):
	# Añadir la nueva cabeza al final del array
	serpiente.push_back(nueva_cabeza)

	# Si no hemos comido (no crecemos), borramos el primer segmento (la cola)
	if not crecer_serpiente:
		serpiente.pop_front() # .pop_front() elimina el primer elemento
	else:
		# Si crecimos, reseteamos el flag y no borramos la cola
		crecer_serpiente = false

convertir_direccion_a_angulo()

Esta función recibe una dirección representada como un vector 2D (como Vector2.UP, Vector2.RIGHT, etc.) y devuelve el ángulo correspondiente en radianes, asumiendo que la orientación por defecto apunta hacia la derecha (orientación estándar en muchos motores 2D). Usa una estructura match para evaluar la dirección de entrada: si es UP, devuelve -90 grados convertidos a radianes (lo que apunta hacia arriba); si es DOWN, devuelve 90 grados (hacia abajo); si es LEFT, devuelve 180 grados (hacia la izquierda); y para cualquier otro caso —especialmente Vector2.RIGHT—, devuelve 0.0, ya que esa es la orientación base. Esta función es útil para rotar texturas (como la cabeza de la serpiente) de forma que siempre miren en la dirección en la que se están moviendo.

func _convertir_direccion_a_angulo(dir: Vector2):
	# Convertir dirección (Vector2) a ángulo en radianes (0° = derecha)
	match dir:
		Vector2.UP:    return deg_to_rad(-90)
		Vector2.DOWN:  return deg_to_rad(90)
		Vector2.LEFT:  return deg_to_rad(180)
		_:             return 0.0  # Vector2.RIGHT

dibujar_textura_rotada_centrada()

Esta función se encarga de dibujar una textura (tex) en la pantalla rotada alrededor de su propio centro, en una posición específica (centro). Para lograrlo, primero aplica una transformación de dibujo con draw_set_transform(centro, angulo, Vector2(1, 1)), que desplaza el origen del sistema de coordenadas al punto centro y lo rota según el ángulo dado (en radianes). Luego, dibuja la textura usando draw_texture_rect, pero con un rectángulo cuyo origen está en -tamano / 2.0, lo que centra la textura respecto al nuevo origen (el punto centro). Finalmente, restablece la transformación de dibujo a la identidad con draw_set_transform(Vector2.ZERO, 0.0, Vector2(1, 1)) para que los siguientes dibujos no se vean afectados por esta rotación ni traslación. Es una forma común y eficaz de dibujar sprites rotados centrados en Godot usando el sistema de dibujo personalizado (_draw).

func _dibujar_textura_rotada_centrada(tex: Texture2D, centro: Vector2, angulo: float, tamano: Vector2):
	# Dibujar textura rotada alrededor de un punto central
	draw_set_transform(centro, angulo, Vector2(1, 1))
	draw_texture_rect(tex, Rect2(-tamano / 2.0, tamano), false)
	draw_set_transform(Vector2.ZERO, 0.0, Vector2(1, 1))

dibujar_serpiente()

Esta función se encarga de renderizar visualmente todos los segmentos de la serpiente en la pantalla, diferenciando entre cabeza, cuerpo y cola, y orientando cada parte según la dirección en la que apunta. Primero verifica que la serpiente no esté vacía; si lo está, no hace nada. Luego, recorre cada segmento (almacenado como coordenadas en una cuadrícula) y convierte su posición a píxeles. Calcula el centro de la celda para dibujar la textura centrada. Dependiendo de la posición del segmento en el array (i), asigna la textura correspondiente: la cabeza (último elemento) se orienta según la dirección actual de movimiento (direccion); la cola (primer elemento) se orienta según la dirección desde la cola hacia el siguiente segmento (serpiente[1] - serpiente[0]); y los segmentos intermedios del cuerpo se orientan según la dirección entre el segmento actual y el siguiente (serpiente[i + 1] - serpiente[i]). En todos los casos, usa _dibujar_textura_rotada_centrada() para dibujar la textura rotada correctamente alrededor de su centro, logrando una serpiente visualmente coherente y alineada con su trayectoria.

func _dibujar_serpiente():
	# Dibujar todos los segmentos de la serpiente (cabeza, cuerpo, cola) en la pantalla
	if serpiente.is_empty(): return

	for i in range(serpiente.size()):
		var posicion_celda = serpiente[i]
		var posicion_pixel = posicion_celda * TAM_CUADRICULA
		var centro = posicion_pixel + Vector2(TAM_CUADRICULA, TAM_CUADRICULA) / 2.0
		var textura: Texture2D
		var angulo_rotacion = 0.0

		if i == serpiente.size() - 1:  # Cabeza
			textura = TEX_CABEZA_SERPIENTE
			angulo_rotacion = _convertir_direccion_a_angulo(direccion)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CABEZA, TAM_CABEZA))
		elif i == 0:  # Cola
			textura = TEX_COLA_SERPIENTE
			var dir_cola = serpiente[1] - serpiente[0]
			angulo_rotacion = _convertir_direccion_a_angulo(dir_cola)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CUADRICULA, TAM_CUADRICULA))
		else:  # Cuerpo
			textura = TEX_CUERPO_SERPIENTE
			var dir_cuerpo = serpiente[i + 1] - serpiente[i]
			angulo_rotacion = _convertir_direccion_a_angulo(dir_cuerpo)
			_dibujar_textura_rotada_centrada(textura, centro, angulo_rotacion, Vector2(TAM_CUADRICULA, TAM_CUADRICULA))

mover_comida()

Esta función coloca la comida en una posición aleatoria dentro de la cuadrícula del juego, asegurándose de que no aparezca encima de ningún segmento de la serpiente. Para ello, entra en un bucle infinito (while true) que genera repetidamente coordenadas aleatorias (nueva_pos) usando randi_range, dentro de los límites horizontales (cuadricula_ancho) y verticales (cuadricula_alto). Cada vez que genera una posición, verifica si esa coordenada no está ya ocupada por la serpiente (con nueva_pos not in serpiente). En cuanto encuentra una ubicación libre, sale del bucle con break y asigna esa posición a la variable global comida. Este enfoque garantiza que la comida siempre aparezca en un lugar accesible.

func _mover_comida():
	# Mover la comida a una posición aleatoria que no esté sobre la serpiente
	var nueva_pos: Vector2
	
	# Generar posiciones aleatorias hasta encontrar una que no esté ocupada por la serpiente
	while true:
		nueva_pos = Vector2(
			randi_range(0, cuadricula_ancho - 1),
			randi_range(0, cuadricula_alto - 1)
		)
		if nueva_pos not in serpiente: break  # Salir del bucle cuando la posición sea válida
	
	# Asignar la posición válida a la comida
	comida = nueva_pos

dibujar_comida()

Esta función se encarga de renderizar la comida en la pantalla de forma centrada dentro de su celda de la cuadrícula. Primero calcula el centro exacto de la celda donde se encuentra la comida: multiplica las coordenadas de la cuadrícula (comida) por el tamaño de cada celda (TAM_CUADRICULA) para obtener la esquina superior izquierda, y luego suma la mitad del tamaño de la celda (TAM_CUADRICULA / 2) en ambas direcciones (usando Vector2.ONE) para llegar al centro. Luego, llama a la función auxiliar _dibujar_textura_rotada_centrada() pasando la textura de la comida (TEX_COMIDA), ese punto central, un ángulo de rotación de 0.0 (ya que la comida no necesita girar) y su tamaño personalizado (TAM_COMIDA). Esto asegura que la comida se dibuje perfectamente centrada en su celda, independientemente del tamaño de la textura o de la cuadrícula, manteniendo una apariencia limpia y alineada con el resto del juego.

func _dibujar_comida():
	# Dibujar la comida en su posición, centrada y con su tamaño personalizado
	
	# 1. Calcular el centro de la celda de la cuadrícula
	var centro_celda = (comida * TAM_CUADRICULA) + (Vector2.ONE * TAM_CUADRICULA / 2.0)
	
	# 2. Llamar a nuestra función auxiliar para dibujar la comida
	_dibujar_textura_rotada_centrada(
		TEX_COMIDA,
		centro_celda,
		0.0, # Ángulo (0.0 para la comida)
		Vector2(TAM_COMIDA, TAM_COMIDA) # Tamaño
	)

comprobar_comida()

Esta función, llamada en _actualizar_movimiento, comprueba si la cabeza de la serpiente ha aterrizado en la misma casilla que la comida. Si cabeza == comida, activa el flag crecer_serpiente a true (para que _mover_serpiente la haga crecer), incrementa la puntuacion, actualiza la etiqueta de texto, llama a _mover_comida() para buscar una nueva posición, y reproduce el sonido de comer (sfx_comer_player).

func _comprobar_comida(cabeza: Vector2):
	# Comprobar si la cabeza está en la misma casilla que la comida
	if cabeza == comida:
		crecer_serpiente = true # Marcar para crecer en el próximo movimiento
		puntuacion += 1
		_actualizar_etiqueta_puntuacion()
		_mover_comida() # Mover la comida a un nuevo sitio
		_reproducir_audio(sfx_comer_player) # Reproducir SFX de comer

comprobar_controles()

Esta función gestiona toda la entrada del jugador de forma ordenada y segura. Primero, verifica si el usuario ha hecho clic (con ratón o pantalla táctil) dentro del área del botón de cerrar; si es así, finaliza la aplicación de forma segura usando get_tree().quit.call_deferred(). Luego, si el juego ya terminó (muerto == true), únicamente permite reiniciar al pulsar la acción "ui_accept" (como Enter o Espacio), ignorando cualquier otro input. Finalmente, cuando el juego está en marcha, traduce las entradas de dirección (ui_up, ui_down, etc.) a vectores (Vector2.UP, DOWN, etc.), pero evita giros completos sobre sí misma (por ejemplo, de abajo a arriba) comparando la dirección propuesta con la opuesta a la actual (-direccion). Solo si la nueva dirección es válida (distinta de la opuesta), se asigna a proxima_direccion, lo que garantiza un movimiento fluido y evita que la serpiente se suicide al retroceder directamente sobre su propio cuerpo.

func _comprobar_controles(event: InputEvent):
	# Comprobación del botón de cerrar
	if (event is InputEventScreenTouch or event is InputEventMouseButton) and event.pressed:
		if boton_cerrar.has_point(event.position):
			if get_tree(): get_tree().quit.call_deferred() # Cerrar de forma segura
			return

	# Si el juego ha terminado, solo escuchamos 'Enter' para reiniciar
	if muerto:
		# 'ui_accept' es 'Enter' o 'Espacio'
		if event.is_action_pressed("ui_accept"): _reiniciar_juego()
		return

	# Si el juego está activo, leemos las flechas de dirección
	var direccion_opuesta = -direccion
	var nueva_dir = null

	if   event.is_action_pressed("ui_up"):    nueva_dir = Vector2.UP
	elif event.is_action_pressed("ui_down"):  nueva_dir = Vector2.DOWN
	elif event.is_action_pressed("ui_left"):  nueva_dir = Vector2.LEFT
	elif event.is_action_pressed("ui_right"): nueva_dir = Vector2.RIGHT
	
	# Evitar que la serpiente se dé la vuelta sobre sí misma	
	if nueva_dir and nueva_dir != direccion_opuesta: proxima_direccion = nueva_dir

comprobar_colisiones()

Esta función recibe la posición de la cabeza y devuelve true si hay colisión, o false si es seguro. Comprueba dos cosas: 1) Si la cabeza está fuera de los límites de la cuadrícula (ej. cabeza.x < 0). 2) Recorre el array serpiente (excepto el último segmento, que es la cabeza actual) y comprueba si la cabeza es igual a la posición de alguno de esos segmentos. Si cualquiera de estas comprobaciones es positiva, devuelve true.

func _comprobar_colisiones(cabeza: Vector2):
	# Comprueba si la cabeza ha chocado con algo. Devuelve 'true' si hay colisión.

	# Colisión con bordes
	if cabeza.x < 0 or cabeza.x >= cuadricula_ancho or \
	   cabeza.y < 0 or cabeza.y >= cuadricula_alto:
		return true

	# Colisión con el cuerpo (excluyendo la cabeza)
	return cabeza in serpiente.slice(0, -1)

pausa_game_over()

Esta es una función asíncrona (usa await) que sirve para crear una pequeña pausa después de morir. Al ser llamada, crea un temporizador (create_timer) con la duración PAUSA_GAME_OVER (1 segundo) y «espera» (await) a que ese temporizador emita la señal timeout. Esto evita que el jugador pueda reiniciar el juego instantáneamente al morir, dándole un segundo para ver qué pasó.

func _pausa_game_over():
	# Activar una pausa breve no bloqueante tras el Game Over esperando a un temporizador asincrónico
	await get_tree().create_timer(PAUSA_GAME_OVER).timeout

game_over()

Esta función se llama cuando _comprobar_colisiones detecta un choque. Establece el estado muerto a true, lo que detiene el bucle principal en _process. Detiene la música de fondo y reproduce el sonido de muerte. Llama a queue_redraw() para forzar un último dibujado (que mostrará la pantalla de Game Over) y finalmente llama a _pausa_game_over() para iniciar la pausa antes de poder reiniciar.

func _game_over():
	# Termina el juego
	muerto = true
	_detener_audio(musica_fondo_player)
	_reproducir_audio(sfx_muerte_player)
	queue_redraw() # Forzar un último dibujado para mostrar la pantalla de Game Over
	_pausa_game_over() # Añadir una breve pausa ant

reiniciar_juego()

La forma más sencilla y robusta de reiniciar el juego. Simplemente le pide al árbol de escenas (get_tree()) que vuelva a cargar la escena actual (reload_current_scene()). Esto restablece todas las variables y nodos a su estado inicial, como si el juego se acabara de abrir.

func _reiniciar_juego():
	# Reiniciar la escena actual para devolver todo a su estado inicial
	get_tree().reload_current_scene()

dibujar_fondo()

Función de dibujo llamada en _draw(). Dibuja la textura TEX_FONDO ocupando toda la pantalla (desde Vector2.ZERO hasta pantalla). Le aplica un tinte gris semitransparente para oscurecerla ligeramente y que la serpiente y la comida destaquen más.

func _dibujar_fondo():
	# Dibujar la imagen de fondo que cubre toda la pantalla
	draw_texture_rect(TEX_FONDO, Rect2(Vector2.ZERO, pantalla), false, Color(0.5, 0.5, 0.5))

mostrar_game_over()

Esta función de dibujo solo se activa (gracias a la comprobación if muerto: en _draw()) cuando el juego ha terminado. Dibuja la textura TEX_GAME_VER ocupando toda la pantalla, también con un tinte semitransparente.

func _mostrar_game_over():
	# Dibujar la textura de Game Over. Podemos modularla para un efecto.
	draw_texture_rect(TEX_GAME_OVER, Rect2(Vector2.ZERO, pantalla), false, Color(0.75, 0.75, 0.75, 0.75))

El resultado