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:

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

Inteligencia artificial

¿Cómo ponerte al día?

Canales de YouTube recomendados

  • Jon Hernández. Divulgación semanal sobre IA. Publica vídeos nuevos cada lunes por la noche. Proporciona actualizaciones constantes sobre tecnologías emergentes, combinando noticias y explicaciones accesibles.
  • Carlos Santana (Dot CSV). Explicaciones profundas sobre IA y aprendizaje automático, presentadas de manera clara y amena. Uno de los divulgadores en español más reconocidos de IA. Mezcla precisión técnica con narrativa accesible, ideal para quienes buscan profundidad sin perder claridad.
  • Xavier Mitjana. Tutoriales que conectan IA con creatividad y producción, mostrando aplicaciones prácticas e inspiradoras. Orientado a quienes desean integrar IA en procesos creativos o narrativos, como cine, diseño o automatización visual.
  • Alejavi Rivera. IA explicada de forma práctica, directa y con casos reales de uso. Combina teoría con aplicación concreta, ideal para emprendedores o quienes buscan resultados inmediatos y efectivos con IA.
  • Javi Galué. Herramientas, estrategias, hacks y formas de monetizar usando IA. Muy orientado a la productividad, la creatividad aplicada y la generación de ingresos reales mediante IA.
  • Elena Santos (ChicaGeek). Consejos tecnológicos prácticos (apps, trucos, gadgets y tutoriales) pensados para facilitar la vida digital. Muy accesible y útil para el día a día; ideal para público general que busca mejorar su uso de tecnología sin tecnicismos.

Comparación de los canales recomendados

CanalEnfoque principalA destacar
Jon HernándezActualizaciones semanales de IAFrecuencia constante; enfoque claro y accesible
Carlos Santana (Dot CSV)IA explicada en profundidadEquilibrio entre rigor técnico y divulgación clara
Xavier MitjanaIA aplicada a creatividad y negociosEnfoque profesional, creativo y muy visual
Alejavi RiveraCasos prácticos de IAAplicabilidad directa; visión de marketing digital
Javi GaluéHacks, monetización y productividad con IAOrientación a resultados reales y uso eficiente
Elena Santos (ChicaGeek)Trucos tecnológicos cotidianosConsejos muy prácticos para la vida digital diaria

Vídeo con las últimas novedades

Blogs recomendados

  • Microsiervos.com. Temática muy variada (tecnología, ciencia, Internet), pero también humor, astronomía, aviación, conspiraciones, puzzles, reseñas de libros o películas y experiencias personales.
  • Xataka.com. Noticias, análisis, reseñas extensas de dispositivos (móviles, gadgets, informática), exploración espacial, energías renovables, etc., cubriendo toda la actualidad tecnológica.

Comparación de los blogs recomendados

CaracterísticaMicrosiervos.comXataka.com
OrígenesBlog personal de un grupo de amigosBlog profesional
TemáticaMuy variada: tecnología, ciencia, humor, curiosidadesTecnología aplicada, noticias, análisis, gadgets
EstiloInformal, personal, humorísticoInformativo, técnico, bien estructurado
FormatoPosts sueltos, blog estilo “diario”Publicación periódica con secciones y verticales
EquipoPequeño grupo de autores (Alvy, Wicho y colaboraciones)Equipo amplio y diversificado de periodistas y editores

¿Qué IA debo utilizar?

ChatGPT

Se puede acceder online (https://chatgpt.com) y con la app:

Muy versátil, entiende bien el lenguaje natural, ideal para redacción, resúmenes, creación de materiales didácticos y resolver dudas de alumnos. Ventaja: calidad y fluidez. Inconveniente: no siempre gratuito en sus versiones más nuevas.

Z.ai

Se puede acceder online (https://chat.z.ai).

Destaca por ser ligero, rápido y económico, con buen rendimiento en chino, inglés y español. Ventaja: eficiencia y bajo coste, y generación excelente de aplicaciones informáticas y presentaciones con diapositivas. Inconveniente: menos precisión que modelos grandes en explicaciones complejas.

Qwen

Se puede acceder online (https://chat.qwen.ai) y con la app:

Excelentes resultados en razonamiento matemático y técnico, con versiones grandes y pequeñas. Ideal para programación y ciencias. Ventaja: muy bueno en lógica y coste relativamente bajo. Inconveniente: menos natural en la redacción creativa.

Deepseek

Se puede acceder online (https://www.deepseek.com/en) y con la app:

Especializado en eficiencia y bajo consumo de recursos, pensado para análisis de datos y asistentes técnicos. Ventaja: rapidez y buena relación coste-rendimiento. Inconveniente: menos pulido en lenguaje pedagógico o creativo.

Mistral

Se puede acceder online (https://chat.mistral.ai/chat) y con la app:

Modelos europeos, abiertos y de calidad, muy buenos para tareas multilingües y razonamiento. Ventaja: muy buenos resultados con idiomas europeos. Inconveniente: no dispone de generación de presentaciones de manera automática.

Google AI Studio

Se puede acceder online (https://aistudio.google.com/welcome) y con la app:

Integración directa con Google, ideal si ya usas Google Workspace (Docs, Drive, Classroom). Ventaja: se integra fácilmente en el ecosistema escolar. Inconveniente: depende de conexión a Google y la privacidad puede ser un tema sensible en colegios, donde se suele trabajar con herramientas Microsoft.

NotebookLM

Se puede acceder online desde https://notebooklm.google.com/.

Pensado para estudiar y resumir documentos. Muy útil para alumnos y maestros que quieren convertir apuntes o libros en resúmenes y cuestionarios. Ventaja: especializado en aprendizaje. Inconveniente: no es tan flexible como otros modelos generalistas.

Grok

Accesible desde https://grok.com/.

Integrado en la red social X, con estilo más informal. Ventaja: útil para comunicación desenfadada y creatividad. Inconveniente: menos serio y todavía limitado en educación formal.

Claude

Accesible desde https://claude.ai/new. Más orientado a programadores.

Destaca en ética, seguridad, comprensión de textos largos, y programación de aplicaciones informáticas. Ideal para trabajar con documentos extensos y entornos donde se busca una IA más enfocada al mundo empresarial. Ventaja: maneja textos muy largos y cuida el tono. Inconveniente: menos creativo que ChatGPT.

Hugging Face

Accesible desde https://huggingface.co/spaces.

No es un modelo en sí, sino una plataforma que reúne cientos de modelos de IA abiertos. Ventaja: enorme flexibilidad y recursos gratuitos para experimentar. Inconveniente: requiere más conocimientos técnicos para elegir y usar el modelo correcto (orientado a usuarios expertos).

Comparación de los diversos modelos

IAFortalezasDebilidadesUso ideal en educación
ChatGPTMuy fluido y versátil en lenguaje; excelente para redacción, resúmenes y explicaciones creativasVersiones avanzadas pueden tener costo; menos personalizable a nivel técnicoRedacción de actividades, generación de ejemplos, guías didácticas
Z.ai (GLM‑4.5)Modelo unificado con razonamiento, codificación y agentes; código abierto y coste competitivo (Business Insider, z.ai)Comunidad menor en españolBúsquedas en Internet, razonamiento avanzado, creación de presentaciones y aplicaciones informáticas
Qwen (Alibaba)Gran variedad multimodal (texto, audio, imagen/video); razonamiento avanzado; multilingüe (Wikipedia, Wikipedia, Prismetric)Ecosistema menos conocido fuera de ChinaRecursos multimedia, explicación con voz e imagen, programación de aplicaciones informáticas
DeepSeekAltamente eficiente y abierto; razonamiento potente a muy bajo coste (The Wall Street Journal, Business Insider, Wikipedia)Censura en temas sensibles según versión; consideraciones de privacidad (The Times, Wikipedia)Para recursos locales o de investigación avanzada, cuando se tiene soporte técnico y precaución con contenido censurable
MistralOpen source, flexible, desplegable localmente, razonamiento multilingüe eficiente (kalm.works, Medium, Appvizer)Comunidad menor en educaciónPersonalización local, modelado lingüístico adaptado al contexto local
Google AI Studio / GeminiIntegración con Workspace (Docs, Drive, Classroom); resumen de archivos, notas automáticas (Android Central)Depende del ecosistema Google; preocupaciones de privacidad institucionalIntegración en flujos escolares existentes, generación de notas y resúmenes automáticos
NotebookLM (Google)Asistente de investigación: resúmenes, guías de estudio, podcasts automáticos en español (Cinco Días, Wikipedia, blog.google, Revolgy)Menor flexibilidad para otros usos creativos fuera de investigaciónPreparación de guías, podcasts educativos, interacción con documentos complejos
Grok (xAI)Integración en X y búsquedas en tiempo real, razonamiento avanzado (Grok 4) (Tom’s Guide, Wikipedia, Business Insider)Historial de respuestas controvertidas; riesgo de sesgos; requiere supervisión crítica (The Wall Street Journal)Uso en comunicación dinámica, debates o actividades creativas (con precaución)
Claude (Anthropic)Usado ampliamente a nivel profesional y creación de aplicaciones informáticas (swiftask.ai, anthropic.com)Menor creatividad espontánea; acceso limitado; ecosistema cerrado (reddit.com)Crear cuestionarios, diseño curricular, aplicaciones informáticas, uso empresarial
Hugging FacePlataforma con acceso a muchos modelos abiertos; ideal para explorar y experimentarNecesita conocimientos técnicos para elegir y probar modelos adecuadosExploración de nuevas IA y prototipos para usos específicos educativos

Ejemplos de uso utilizando Qwen, Grok y Claude

¿Qué IA es la mejor?

Ranking mundial

En la página web LMArena podemos utilizar los modelos de inteligencia artificial más populares. Es una plataforma abierta y colaborativa que permite realizar comparaciones directas mediante “batallas” anónimas: el usuario introduce un prompt, recibe dos respuestas generadas por modelos distintos y elige cuál le parece mejor; luego se revelan las identidades de los modelos y el resultado se suma a un ranking público. Creada por investigadores de la Universidad de California en Berkeley, se ha convertido en un referente porque combina transparencia (sus datos se publican para investigación), participación comunitaria y alcance global, evaluando desde modelos de código abierto hasta prototipos pre-lanzamiento de gigantes como OpenAI, Google o Anthropic.

Por ejemplo, en los siguientes enlaces se puede consultar el ranking mundial de los modelos de IA más populares para generación de texto en español, para resolución de problemas matemáticos, para escritura creativa, y de generación y edición de imágenes, según las valoraciones de los usuarios:

Además, se pueden probar todos los modelos de forma gratuita a través de este enlace: https://lmarena.ai/?mode=direct

Casos prácticos

Traducciones

Traductor de Google desde su app:

Generación y edición de imágenes e infografías completas

Ejemplos generados con ChatGPT utilizando imágenes reales (en diciembre del 2024, y agosto del 2025 respectivamente):

Ejemplo generados con «Nano Banana» desde Google AI Studio con los siguientes prompts:

  • Crea un dibujo de un niño corriendo porque llega tarde al colegio y sus padres están detrás, con estilo manga.
  • Cambia la torre del reloj por la Torre Eiffel.

Modelo recomendado «Nano Banana» accesible desde Google AI Studio (es mejor modelo de generación de imágenes que el proporcionado por ChatGPT 5):

Generación de presentaciones con diapositivas

Presentación de ejemplo sobre la Cabalgata de los Reyes Magos de Alcoy generada desde Z.ai:

Presentación de ejemplo para aprender nombres de animales en inglés, generada automáticamente desde kimi.com:

Modelos recomendados:

  • «GLM» desde Z.ai:

Generación de podcasts (o resúmenes)

Ejemplo generado automáticamente a partir de la información disponible en la Wikipedia (https://es.wikipedia.org/wiki/Cabalgata_de_Reyes_Magos_de_Alcoy):

Accesible desde https://notebooklm.google.com/:

Generación de vídeos explicativos

Ejemplo generado automáticamente a partir de la información disponible en la Wikipedia (https://es.wikipedia.org/wiki/Cabalgata_de_Reyes_Magos_de_Alcoy):

Accesible desde https://notebooklm.google.com/:

Libros ilustrados

Modelo recomendado «Gemini» desde Gemini Storybook:

Generación de series completas

Servicio proporcionado por Showrunner:

Generación de música

Las vocales en canción:

El baile del abecedario:

Modelo recomendado «Suno 4.5» de Suno.com:

Mundos virtuales

Modelo «Mirage 2» de DynamicsLab. Demostraciones disponibles aquí: https://demo.dynamicslab.ai/chaos.

Publicar libros en Amazon

Servicio accesible desde https://kdp.amazon.com/:

Manual básico de Scikit-learn

Introducción a Scikit-learn

Scikit-learn es una de las librerías más populares utilizadas en Python para el aprendizaje automático (machine learning). Fue desarrollada como parte del ecosistema científico de Python y se ha convertido en una herramienta fundamental para investigadores, analistas de datos y desarrolladores que buscan aplicar algoritmos de aprendizaje automático a problemas del mundo real.

¿Qué es Scikit-learn?

Scikit-learn es una librería de código abierto construida sobre otras librerías fundamentales de Python como NumPy, SciPy y matplotlib. Ofrece una amplia gama de herramientas para abordar la resolución de problemas mediante aprendizaje automático y análisis de datos. Incluye algoritmos de clasificación, regresión, clustering (agrupamiento), reducción de dimensionalidad, selección de modelos, y preprocesamiento de datos, tales como:

  1. Clasificación: Asignar etiquetas a datos de entrada.
    • Detección de spam: Clasificar correos electrónicos como spam o no spam.
    • Reconocimiento de imágenes: Identificar objetos en imágenes (por ejemplo, reconocimiento facial).
    • Diagnóstico médico: Clasificar si un paciente tiene o no una enfermedad basada en datos clínicos.
  2. Regresión: Predecir valores continuos.
    • Predicción de precios: Estimar el precio de viviendas en base a características como el tamaño y la ubicación.
    • Análisis de ventas: Predecir las ventas futuras basadas en datos históricos.
  3. Clustering (agrupamiento): Agrupar datos en categorías no etiquetadas.
    • Segmentación de clientes: Agrupar clientes en segmentos basados en su comportamiento de compra.
    • Compresión de datos: Reducción de la dimensionalidad de los datos para visualizar conjuntos complejos.
  4. Reducción de dimensionalidad: Reducir el número de características en un conjunto de datos para facilitar la visualización o mejorar la eficiencia de otros algoritmos.
    • Análisis de Componentes Principales (PCA): Simplificar datos de alta dimensión para exploración visual.
  5. Selección de modelos y optimización: Elegir el mejor modelo y ajustar los hiperparámetros para mejorar el rendimiento.

¿Por qué Scikit-learn es tan popular?

Scikit-learn es muy popular por varias razones que lo convierten en una herramienta esencial para cualquier persona involucrada en el análisis de datos o el desarrollo de modelos predictivos:

  1. Accesibilidad y simplicidad: Una de las principales ventajas de scikit-learn es su simplicidad, incluso para aquellos que son nuevos en el campo del aprendizaje automático. Gracias a su API consistente, es fácil aprender a usar un algoritmo y luego aplicar los conocimientos a otros sin necesidad de aprender nuevas sintaxis complejas.
  2. Versatilidad y flexibilidad: Puede utilizarse en la resolución de una amplia gama de problemas, desde algunos muy simples hasta otros extremadamente complejos. Esto hace que scikit-learn sea adecuado para aplicaciones en diversos campos, como la medicina, finanzas, marketing, y tecnología, entre otros.
  3. Eficiencia y rapidez: Scikit-learn está altamente optimizado para funcionar con grandes volúmenes de datos y realizar cálculos complejos rápidamente, lo que es crucial en entornos empresariales donde el tiempo es esencial.
  4. Facilita la experimentación y prototipado rápido: Scikit-learn permite a los usuarios probar rápidamente diferentes algoritmos y técnicas, ver sus resultados y ajustar modelos sin necesidad de un conocimiento profundo de la matemática detrás de cada algoritmo. Esto acelera el proceso de descubrimiento y la innovación.
  5. Compatibilidad con entornos de producción: Los modelos desarrollados con scikit-learn se pueden integrar fácilmente en aplicaciones en producción, lo que permite trasladar las ideas y prototipos desde el laboratorio de datos hasta el uso real en aplicaciones y servicios.
  6. Aprendizaje reforzado por la comunidad: La comunidad de scikit-learn no solo proporciona soporte continuo, sino que también contribuye con mejoras regulares, manteniendo la librería actualizada con las últimas investigaciones y avances en el campo del aprendizaje automático.
  7. Integración con herramientas de visualización: Aunque no es una librería de visualización, scikit-learn se integra bien con matplotlib y otras herramientas para crear gráficos que ayudan a interpretar los resultados de los modelos, lo cual es clave para la toma de decisiones basada en datos.

¿Qué nos aporta Scikit-learn?

  1. Amplia gama de algoritmos y funcionalidades: Incluye desde algoritmos básicos como la regresión lineal hasta técnicas avanzadas como bosques aleatorios y métodos de boosting, lo que permite abordar prácticamente cualquier problema con una sola librería.
  2. Preprocesamiento de datos: Herramientas para la limpieza, normalización, escalado y transformación de datos, permitiendo preparar los datos para el modelado de manera sencilla.
  3. Algoritmos específicos de aprendizaje automático: Scikit-learn incluye una gran variedad de algoritmos para tareas de clasificación, regresión y clustering, como Regresión Lineal, Máquinas de Soporte Vectorial (SVM), K-Means, Bosques Aleatorios, y muchos más.
  4. Interoperabilidad con el ecosistema de python: Scikit-learn se integra perfectamente con otras librerías como pandas (para manipulación de datos), NumPy (para cálculos numéricos), y matplotlib (para visualización de datos). Esto facilita la creación de flujos de trabajo completos y simplifica el desarrollo y la implementación de modelos.
  5. Evaluación y mejora continua: Ofrece herramientas robustas para evaluar la precisión y la validez de los modelos mediante métodos como la validación cruzada, matrices de confusión, y métricas de clasificación como precisión, recall y F1-score.
  6. Optimización de modelos: Herramientas como Grid Search y Random Search permiten ajustar los hiperparámetros de los modelos de manera efectiva, mejorando la precisión y el rendimiento del modelo sin necesidad de realizar ajustes manuales tediosos.
  7. Interfaz consistente: Todos los algoritmos comparten una interfaz coherente y simple, lo que facilita cambiar entre diferentes modelos con muy poco esfuerzo.
  8. Documentación actualizada: Scikit-learn cuenta con una documentación extensa, clara y llena de ejemplos prácticos. Además, tiene una gran comunidad de usuarios y desarrolladores, lo que facilita encontrar ayuda, tutoriales y soluciones a problemas comunes.

Instalación de Scikit-learn

Podemos instalar scikit-learn fácilmente utilizando el gestor de paquetes pip, que viene instalado con Python. Basta con abrir el terminal (o símbolo del sistema en Windows) y escribir el siguiente comando para instalar scikit-learn junto con sus dependencias:

pip install scikit-learn

Importar y comenzar a usar Scikit-learn

Scikit-learn se usa a través de sus módulos específicos, que se importan según las necesidades específicas de cada proyecto. Algunas de las funcionalidades más comunes son las siguientes:

  • Preprocesamiento de datos: Para escalar, normalizar y transformar datos.
  • Modelos de aprendizaje supervisado: Para tareas de regresión y clasificación.
  • Modelos de aprendizaje no supervisado: Para clustering y reducción de dimensionalidad.

Ejemplo de importación

# Importar módulos de preprocesamiento
from sklearn.preprocessing import StandardScaler, LabelEncoder

# Importar modelos de aprendizaje supervisado
from sklearn.linear_model import LinearRegression
from sklearn.svm import SVC

# Importar modelos de aprendizaje no supervisado
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

# Importar funciones de evaluación
from sklearn.metrics import accuracy_score, confusion_matrix

Jupyter Notebook y Google Colab

Para una experiencia más interactiva y visual, se pueden usar entornos como Jupyter Notebook o Google Colab:

  • Google Colab: Es un entorno basado en web que no requiere instalación y ofrece GPU gratuita para acelerar el entrenamiento de modelos. Puedes acceder a él a través de Google Colab.
  • Jupyter Notebook: Puedes instalarlo con pip usando pip install jupyterlab y luego ejecutarlo con el comando jupyter notebook. Es ideal para experimentar con código y visualizar resultados en tiempo real.

Fundamentos de Scikit-learn

Scikit-learn facilita el trabajo con modelos predictivos y de aprendizaje automático proporcionando una serie de herramientas para:

  1. Cargar y preparar datos: scikit-learn no tiene herramientas propias para la carga de datos desde archivos como CSV, Excel o bases de datos, pero se complementa perfectamente con otras librerías como pandas y numpy que permiten manipular datos fácilmente. Por lo tanto, uno de los primeros pasos en cualquier proyecto de machine learning es cargar los datos con pandas.
  2. Preprocesamiento de datos: Este es un paso esencial. Los datos crudos generalmente no están listos para usarse en un modelo. Pueden tener valores faltantes, variables categóricas que necesitan ser convertidas en números, o los datos pueden necesitar ser escalados para mejorar la precisión del modelo. Scikit-learn tiene una variedad de transformadores para realizar estos ajustes.
  3. Entrenamiento de modelos: Una vez los datos están listos, puedes entrenar un modelo. Esto implica usar un algoritmo de aprendizaje automático para ajustar el modelo a tus datos. Algunos ejemplos de algoritmos son la regresión lineal para predicción de valores numéricos y las máquinas de soporte vectorial (SVM) para clasificación.
  4. Evaluación de modelos: Después de entrenar un modelo, es fundamental medir su rendimiento. Scikit-learn proporciona diversas métricas para evaluar la precisión, como la matriz de confusión, precisión, recall, entre otras.
  5. Predicciones: Finalmente, puedes usar tu modelo entrenado para hacer predicciones sobre nuevos datos que no se usaron durante el entrenamiento. Esto es útil en aplicaciones como predicciones de ventas, clasificación de correos como spam o no spam, etc.

Preprocesamiento de datos

El preprocesamiento de datos consiste en preparar tus datos para que un modelo de aprendizaje automático los pueda usar correctamente. Esto incluye tareas como escalar los datos (ajustar la escala de las variables para que tengan la misma importancia), transformar datos categóricos en numéricos, o vectorizar texto.

Ejemplo básico: Escalar datos numéricos

Imagina que tienes un conjunto de datos con varias características numéricas, pero estas están en diferentes rangos. Por ejemplo, una columna puede tener valores entre 0 y 1000, mientras que otra tiene valores entre 0 y 1. Esto puede confundir a muchos modelos de aprendizaje automático porque algunas características parecen tener más peso simplemente porque sus números son más grandes. Para evitar esto, se utilizan técnicas de escalado.

En el ejemplo que se muestra a continuación, usamos StandardScaler de scikit-learn para estandarizar los datos de modo que tengan una media de 0 y una desviación estándar de 1:

from sklearn.preprocessing import StandardScaler
import numpy as np

# Datos de ejemplo: una matriz con tres filas y tres columnas
X = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# Crear el escalador
scaler = StandardScaler()

# Ajustar (fit) el escalador y transformar (transform) los datos
X_scaled = scaler.fit_transform(X)

print(X_scaled)

Explicación del código:

  1. StandardScaler() crea un objeto escalador que se usará para ajustar y transformar los datos.
  2. fit_transform(X) ajusta el escalador a los datos y luego transforma esos datos, es decir, calcula la media y desviación estándar de cada columna y luego aplica la transformación para escalar los valores.

El escalado de datos es particularmente útil en algoritmos como regresión lineal, SVM y redes neuronales, donde los valores numéricos deben estar en un rango similar para un mejor rendimiento.

Algoritmos de aprendizaje supervisado

Los algoritmos de aprendizaje supervisado son aquellos que aprenden a partir de datos etiquetados, es decir, datos en los que sabemos cuál es la salida correcta (etiqueta). Uno de los algoritmos más simples es la regresión lineal, que predice un valor numérico basado en las características de entrada.

Ejemplo: Regresión lineal

La regresión lineal trata de ajustar una línea recta (o un plano en dimensiones superiores) que describa la relación entre las variables independientes (X) y la variable dependiente (Y).

from sklearn.linear_model import LinearRegression
import numpy as np

# Datos de ejemplo: X son las variables independientes (entrada) y y es la variable dependiente (salida)
X = np.array([[1], [2], [3], [4]])  # Cada número es una característica, en este caso un solo valor por observación
y = np.array([3, 6, 9, 12])  # Resultados que queremos aprender a predecir

# Crear el modelo de regresión lineal
model = LinearRegression()

# Entrenar el modelo con los datos
model.fit(X, y)

# Predecir un nuevo valor con el modelo entrenado
prediction = model.predict([[5]])
print(prediction)  # Salida esperada: [15]

Explicación del código:

  1. LinearRegression() crea un modelo de regresión lineal.
  2. model.fit(X, y) entrena el modelo con los datos de entrada X y los resultados conocidos y.
  3. model.predict([[5]]) usa el modelo entrenado para predecir el resultado cuando X = 5.

Este ejemplo muestra un caso simple, pero la regresión lineal se usa ampliamente en aplicaciones como la predicción de ventas, la valoración de bienes inmuebles o incluso en la ciencia para encontrar relaciones entre variables.

Aplicación real: Predicción de precios de casas

Imaginemos que tenemos un conjunto de datos de casas con características como el tamaño, número de habitaciones, etc., y queremos predecir el precio de una casa nueva basada en esas características. Para mejorar la precisión, primero escalamos los datos usando StandardScaler.

from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
import pandas as pd

# Supongamos que tenemos un archivo CSV con datos sobre casas
data = pd.read_csv('casas.csv')
X = data.drop('Precio', axis=1)  # Variables independientes (tamaño, habitaciones, etc.)
y = data['Precio']  # Variable dependiente (precio)

# Escalar los datos
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Crear y entrenar el modelo
model = LinearRegression()
model.fit(X_scaled, y)

# Predecir el precio de una casa nueva con características específicas
nueva_casa = [[200, 3, 2, 1]]  # Tamaño de 200 m², 3 habitaciones, 2 baños, 1 garaje
nueva_casa_scaled = scaler.transform(nueva_casa)
precio_predicho = model.predict(nueva_casa_scaled)

print(precio_predicho)

Explicación del código:

  1. data = pd.read_csv('casas.csv'): Carga los datos desde un archivo CSV.
  2. X = data.drop('Precio', axis=1) y y = data['Precio']: Separa las características (X) del valor que queremos predecir (y).
  3. scaler.fit_transform(X): Escala los datos para que todas las características estén en un rango similar.
  4. model.fit(X_scaled, y): Entrena el modelo usando los datos escalados.
  5. model.predict(): Usa el modelo entrenado para hacer una predicción.

Aprendizaje supervisado para clasificación

La clasificación es otra tarea importante del aprendizaje supervisado, donde el objetivo es asignar etiquetas a datos de entrada. Un ejemplo clásico es clasificar correos electrónicos como spam o no spam.

Ejemplo: Clasificación de texto (spam vs no spam)

Este ejemplo muestra cómo usar TfidfVectorizer para convertir texto en datos numéricos y SVC (máquinas de soporte vectorial) para entrenar un modelo de clasificación.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.svm import SVC
from sklearn.pipeline import make_pipeline
import pandas as pd

# Datos de ejemplo con textos y etiquetas (1 para spam, 0 para no spam)
data = pd.DataFrame({
    'texto': ['Hola, ¿cómo estás?', 'Gana dinero rápido', 'Te extrañamos', 'Oferta exclusiva para ti'],
    'spam': [0, 1, 0, 1]
})

# Crear un pipeline con TfidfVectorizer (para vectorizar texto) y SVC (para clasificar)
model = make_pipeline(TfidfVectorizer(), SVC(probability=True))

# Entrenar el modelo con los datos
model.fit(data['texto'], data['spam'])

# Predecir si un nuevo mensaje es spam
nuevo_mensaje = '¡Oferta exclusiva para ganar dinero!'
probabilidad = model.predict_proba([nuevo_mensaje])[0][1] * 100
print(f'Probabilidad de spam: {probabilidad}%')

Explicación del código:

  1. TfidfVectorizer() convierte texto en una representación numérica que el modelo puede usar. Calcula una medida llamada «TF-IDF» que refleja la importancia de las palabras en el texto.
  2. SVC() es un modelo de clasificación que aprende a separar las clases (spam y no spam) a partir de los datos de entrenamiento.
  3. make_pipeline() crea un flujo de trabajo que primero vectoriza el texto y luego aplica el modelo de clasificación.

Aprendizaje no supervisado

En el aprendizaje no supervisado, los modelos tratan de encontrar patrones en datos que no tienen etiquetas conocidas. Un algoritmo común es k-Nearest Neighbors (k-NN), que puede usarse para sugerir recetas basadas en ingredientes disponibles.

Ejemplo: k-Nearest Neighbors (k-NN) para sugerir recetas

El siguiente ejemplo muestra cómo usar k-NN para sugerir recetas similares basadas en los ingredientes proporcionados.

from sklearn.neighbors import NearestNeighbors
from sklearn.preprocessing import MultiLabelBinarizer
import pandas as pd

# Datos de ejemplo: lista de recetas y sus ingredientes
data = pd.DataFrame({
    'receta': ['Pasta con tomate', 'Ensalada de pollo', 'Tacos de pescado'],
    'ingredientes': ['pasta,tomate', 'lechuga,pollo', 'pescado,tortilla']
})

# Convertir los ingredientes en una representación binaria
mlb = MultiLabelBinarizer()
X = mlb.fit_transform(data['ingredientes'].apply(lambda x: x.split(',')))

# Crear y entrenar el modelo k-NN
model = NearestNeighbors(n_neighbors=2, metric='jaccard')
model.fit(X)

# Sugerir recetas basadas en los ingredientes proporcionados
ingredientes = ['tomate', 'pasta']
ingredientes_vector = mlb.transform([ingredientes])
distancias, indices = model.kneighbors(ingredientes_vector, n_neighbors=2)

# Mostrar recetas sugeridas
for idx in indices[0]:
    print(data.iloc[idx]['receta'])

Explicación del código:

  1. MultiLabelBinarizer() convierte listas de ingredientes en vectores binarios donde cada posición indica si un ingrediente está presente.
  2. NearestNeighbors() encuentra las recetas más cercanas a las dadas basándose en la similitud de ingredientes.
  3. kneighbors() busca las recetas más cercanas (vecinas) a los ingredientes proporcionados.

Uso de modelos de clasificación en aplicaciones más específicas

Los modelos de clasificación intentan predecir una etiqueta o clase para cada observación en los datos. Este tipo de problemas es común en una gran variedad de aplicaciones en campos más específicos. A través de los pasos de preparación de datos, preprocesamiento, entrenamiento y predicción, scikit-learn nos facilita el desarrollo de modelos que pueden ser usados en aplicaciones del mundo real, tales como:

  • Diagnóstico médico: Determinar si un paciente tiene una enfermedad en base a sus síntomas y análisis médicos.
  • Finanzas: Predecir si un cliente cumple las condiciones adecuadas para aprobar la concesión de un crédito.
  • Crowdfunding: Predecir si una campaña tendrá éxito en base a sus características.

A continuación vamos a detallar un ejemplo donde intentaremos predecir el éxito de campañas de crowdfunding. Así mostraremos cómo se puede utilizar un modelo de clasificación para resolver un problema práctico más concreto y específico.

Ejemplo: Predicción de éxito en campañas de crowdfunding

Contexto del problema: Imagina que tienes datos históricos de campañas de crowdfunding. Cada campaña tiene varias características (como la categoría del proyecto, el monto recaudado, la meta financiera, la duración de la campaña, etc.). El objetivo es entrenar un modelo que pueda predecir si una campaña tendrá éxito o no.

Paso 1: Preparación de los datos

El primer paso en cualquier problema de clasificación es preparar los datos de entrenamiento:

# Importar las bibliotecas necesarias
from sklearn.ensemble import RandomForestClassifier
import pandas as pd

# Supongamos que tenemos datos de campañas de crowdfunding
# Cada fila representa una campaña con sus características y si tuvo éxito (1) o no (0)
data = pd.DataFrame({
    'Categoría': ['Tecnología', 'Arte', 'Música'],
    'Meta Financiera': [5000, 3000, 8000],
    'Dinero Recaudado': [4000, 3500, 2000],
    'Duración (días)': [30, 40, 25],
    'Número de Patrocinadores': [20, 30, 10],
    'Éxito': [1, 1, 0]  # 1 indica que la campaña tuvo éxito, 0 indica que no tuvo éxito
})

# Mostrar los datos de ejemplo
print(data)

Explicación de los datos:

  • Categoría: La categoría a la que pertenece la campaña, como Tecnología, Arte o Música.
  • Meta Financiera: La cantidad de dinero que se espera recaudar.
  • Dinero Recaudado: El dinero que efectivamente se recaudó.
  • Duración (días): Los días que duró la campaña.
  • Número de Patrocinadores: El número de personas que contribuyeron a la campaña.
  • Éxito: Etiqueta que indica si la campaña tuvo éxito (1) o no (0).

Paso 2: Preprocesamiento de los datos

Antes de entrenar un modelo, necesitamos transformar las variables categóricas (en este caso, la categoría del proyecto) en un formato que los modelos puedan entender. Para esto, usamos get_dummies() que convierte las categorías en variables numéricas llamadas variables dummy:

# Convertir la variable categórica 'Categoría' en variables dummy
data = pd.get_dummies(data, columns=['Categoría'])

# Separar las características (X) y la variable objetivo (y)
X = data.drop('Éxito', axis=1)  # Características de entrada
y = data['Éxito']  # Variable de salida que queremos predecir

# Mostrar las nuevas características después de la transformación
print(X)

Explicación del preprocesamiento:

  • get_dummies() crea nuevas columnas para cada categoría. Por ejemplo, si la categoría es «Tecnología», se crea una columna Categoría_Tecnología con valores 1 (si es de esa categoría) o 0 (si no lo es).
  • X contiene las características que usaremos para predecir y y contiene las etiquetas de éxito (1) o fracaso (0).

Paso 3: Entrenamiento del modelo

En este ejemplo, usaremos un Random Forest Classifier (clasificador de bosque aleatorio), que es un modelo potente y versátil. Este algoritmo combina muchos árboles de decisión para mejorar la precisión y reducir el riesgo de sobreajuste.

# Crear y entrenar el modelo de bosque aleatorio
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X, y)  # Entrenar el modelo con las características X y la variable objetivo y

Explicación del modelo:

  • RandomForestClassifier(n_estimators=100) crea un modelo de bosque aleatorio con 100 árboles de decisión. Cuantos más árboles, más robusto es el modelo, pero también se necesitará más tiempo para completar el entrenamiento.
  • model.fit(X, y) ajusta (entrena) el modelo a los datos, encontrando patrones que relacionan las características con la etiqueta de éxito o fracaso.

Paso 4: Predicción de un nuevo proyecto

Una vez que el modelo está entrenado, puedes usarlo para predecir la probabilidad de éxito de nuevas campañas. A continuación, mostramos cómo hacer una predicción para una nueva campaña:

# Crear un nuevo proyecto para predecir
nuevo_proyecto = pd.DataFrame({
    'Meta Financiera': [6000],
    'Dinero Recaudado': [4500],
    'Duración (días)': [35],
    'Número de Patrocinadores': [25],
    'Categoría_Arte': [0],
    'Categoría_Música': [0],
    'Categoría_Tecnología': [1]  # Esta campaña es de Tecnología
})

# Mostrar los datos del nuevo proyecto
print(nuevo_proyecto)

# Predecir la probabilidad de éxito del nuevo proyecto
probabilidad_exito = model.predict_proba(nuevo_proyecto)[0][1] * 100
print(f'Probabilidad de éxito: {probabilidad_exito:.2f}%')

Explicación de la predicción:

  • nuevo_proyecto contiene las características del nuevo proyecto. Observa que debe tener las mismas columnas que las características de entrenamiento (X).
  • model.predict_proba(nuevo_proyecto) devuelve la probabilidad de cada clase (0 o 1). [0][1] selecciona la probabilidad de que el proyecto sea exitoso (1).
  • Multiplicamos por 100 para convertir la probabilidad a un porcentaje.

Análisis de resultados y ajuste del modelo

Después de predecir, es fundamental analizar el rendimiento del modelo y ajustarlo si es necesario:

  1. Métricas de evaluación: Usar métricas como precisión, recall, f1-score, y matriz de confusión para evaluar la calidad de predicción del modelo. En problemas de negocio reales, un alto recall (identificar todas las campañas exitosas) puede ser más importante que la precisión.
  2. Ajuste de hiperparámetros: Experimentar con el número de árboles (n_estimators) o la profundidad máxima de cada árbol (max_depth) para mejorar el rendimiento.
  3. Validación cruzada: Usar técnicas como la validación cruzada para asegurar que el modelo no esté sobreajustado a los datos de entrenamiento y pueda generalizar bien a nuevos datos.

Conclusión

Scikit-learn se ha convertido en una herramienta muy popular y versátil dentro del campo del aprendizaje automático debido a su simplicidad, la amplitud de sus capacidades y su integración con el ecosistema de Python. Es la elección perfecta tanto para principiantes como para expertos que buscan aplicar machine learning de manera efectiva en sus proyectos. Con scikit-learn, es posible abordar problemas complejos de predicción, clasificación y análisis de datos con una curva de aprendizaje moderada, proporcionando resultados precisos y valiosos que facilitan la toma de decisiones en el mundo real.

Juegos en red y minijuegos

Multijugador

UNO (hasta 4 jugadores)

https://uno1.fernandoruizrico.com y https://uno2.fernandoruizrico.com

UNO (hasta 8 jugadores)

https://uno3.fernandoruizrico.com y https://uno4.fernandoruizrico.com

GRAVITY (shooter, conquista civilizaciones en el espacio)

https://gravity1.fernandoruizrico.com y https://gravity2.fernandoruizrico.com

EL GRAN QUESO (shooter, destruye la luna)

https://cheese1.fernandoruizrico.com y https://cheese2.fernandoruizrico.com

BALONCESTO (encestar, ambiente espacial)

https://basketball1.fernandoruizrico.com y https://basketball2.fernandoruizrico.com

MAZMORRA (shooter, deathmatch, sólo 1 sala)

https://dungeon1.fernandoruizrico.com y https://dungeon2.fernandoruizrico.com

MAZMORRA (shooter, deathmatch, varias salas)

https://dungeon3.fernandoruizrico.com y https://dungeon4.fernandoruizrico.com

DIEP.IO (arena 2D, tanques)

https://diep1.fernandoruizrico.com y https://diep2.fernandoruizrico.com

TANQS.IO (arena 2D, tanques)

https://tanks1.fernandoruizrico.com y https://tanks2.fernandoruizrico.com

TANKS (arena 3D, tanques)

https://tanks3.fernandoruizrico.com y https://tanks4.fernandoruizrico.com

ZORB.IO (3D, espacio, absorber otros jugadores)

https://zorb1.fernandoruizrico.com y https://zorb2.fernandoruizrico.com

AGAR.IO (2D, absorber otros jugadores)

https://agario1.fernandoruizrico.com y https://agario2.fernandoruizrico.com

HEX (estrategia)

https://hex1.fernandoruizrico.com y https://hex2.fernandoruizrico.com

BOMBERBOX (bomberman)

https://bomber1.fernandoruizrico.com y https://bomber2.fernandoruizrico.com

BOMBER (bomberman)

https://bomber3.fernandoruizrico.com y https://bomber4.fernandoruizrico.com

SNAKE (serpiente)

https://snakes3.fernandoruizrico.com y https://snakes4.fernandoruizrico.com

TRON (tron legacy)

https://tron1.fernandoruizrico.com y https://tron2.fernandoruizrico.com

PICTIO (pictionary)

https://pictio1.fernandoruizrico.com y https://pictio2.fernandoruizrico.com

WORDS (mini scrabble, adivinar palabras)

https://words1.fernandoruizrico.com y https://words2.fernandoruizrico.com

Minijuegos

LIBRERAMA (ritmo rápido)

https://minigames1.fernandoruizrico.com

MINIGAME MADNESS (ritmo rápido)

https://minigames2.fernandoruizrico.com

CRISP GAMES (juega con un solo botón)

https://minigames3.fernandoruizrico.com

Programación con JavaScript: Peticiones a un servidor

El método fetch de JavaScript nos proporciona la funcionalidad necesaria para comunicarnos con servidores web directamente desde el navegador. Nos permite realizar todo tipo de peticiones HTTP (como obtener datos, enviar formularios, actualizar o eliminar información) de manera sencilla y eficiente.

Introducción

Lo que hace especial a fetch es su capacidad para trabajar con promesas, lo que permite manejar respuestas asíncronas del servidor de una manera más limpia y organizada. En esencia, fetch te permite solicitar recursos o enviar datos a un servidor y luego, una vez que la petición es procesada, trabajar con la respuesta, ya sea extrayendo su contenido en diferentes formatos (como texto o JSON) o manejando posibles errores que ocurran durante el proceso. Esta funcionalidad es crucial para el desarrollo de aplicaciones web modernas, donde la interacción constante con APIs y servicios externos es una práctica común para mostrar datos dinámicos, realizar actualizaciones en tiempo real y mejorar la experiencia del usuario en la web.

Para usar la función fetch en JavaScript y hacer peticiones a un servidor, primero necesitas entender qué es y cómo funciona. La función fetch nos permite realizar peticiones HTTP (como GET, POST, PUT, y DELETE) a un servidor desde un navegador web. Esta función devuelve una promesa que se resuelve con el objeto de respuesta del servidor, permitiéndote luego manipular esta respuesta, ya sea extrayendo el cuerpo de la misma en el formato deseado (como texto, JSON, etc.) o manejando errores.

Realizar una petición básica con fetch

Para hacer una petición GET simple, puedes utilizar fetch con la URL del recurso que deseas obtener.

fetch('https://api.example.com/data')
  .then(response => response.json()) // Convierte la respuesta a JSON
  .then(data => console.log(data)) // Maneja los datos de la respuesta
  .catch(error => console.error('Hubo un error:', error)); // Maneja los errores

En este caso, fetch('https://api.example.com/data') realiza una petición GET a la URL proporcionada. La función .then(response => response.json()) recoge la respuesta HTTP y la convierte a JSON. Luego, otro .then recibe esos datos ya procesados y, por ejemplo, los imprime en la consola. Finalmente, .catch captura cualquier error que pueda ocurrir durante la petición o procesamiento de la respuesta.

Enviar una petición con método POST

Para enviar datos a un servidor, como un formulario o información en formato JSON, puedes utilizar el método POST.

fetch('https://api.example.com/submit', {
  method: 'POST', // Método HTTP
  headers: {
    'Content-Type': 'application/json', // Indica el tipo de contenido que se está enviando
  },
  body: JSON.stringify({
    name: 'Usuario',
    message: 'Hola, mundo'
  }) // Datos que se envían convertidos a cadena JSON
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Hubo un error:', error));

Aquí, el segundo argumento de fetch es un objeto que configura la petición, especificando el método HTTP (method: 'POST'), los encabezados (headers) para indicar el tipo de contenido que se está enviando, y el cuerpo de la petición (body), que contiene los datos enviados al servidor. Los datos deben ser una cadena JSON, por lo que se utiliza JSON.stringify para convertir un objeto JavaScript a esta cadena.

Control de errores y respuestas erróneas

Es importante manejar correctamente los errores y las respuestas que no indican éxito (como un estado 404 o 500). Puedes hacerlo verificando el estado de la respuesta.

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error('Hubo un problema con tu petición:', error));

En este ejemplo, if (!response.ok) verifica si el estado de la respuesta no indica éxito, lanzando un error si es necesario. Esto permite que el .catch posterior maneje tanto errores de red como respuestas no exitosas.

Conclusión

La función fetch es una herramienta poderosa y flexible para realizar peticiones HTTP en aplicaciones web modernas. Permite un control detallado sobre las peticiones y respuestas, facilitando el trabajo con APIs y servicios web. Practica con ejemplos y variaciones de estos para familiarizarte completamente con su funcionamiento y capacidades.

Programación con JavaScript: Cuadros de diálogo con SweetAlert

SweetAlert es una popular librería de JavaScript que permite crear cuadros de diálogo más atractivos y funcionales que los cuadros de diálogo nativos de JavaScript (alert, confirm, prompt). Ofrece una amplia gama de opciones personalizables, como iconos, botones, animaciones, y más, lo que la convierte en una excelente opción para mejorar la interacción del usuario en aplicaciones web modernas.

Cómo Comenzar con SweetAlert

Primero, necesitas incluir SweetAlert en tu proyecto. Puedes hacerlo de varias maneras, pero la forma más sencilla es mediante la inclusión directa de la librería desde un CDN (Content Delivery Network) en tu archivo HTML. Aquí mostramos cómo hacerlo:

<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

Asegúrate de incluir esta línea en la sección <head> o al final del cuerpo (<body>) de tu archivo HTML, antes de usar las funciones de SweetAlert.

Ejemplos Básicos de Uso

Mostrar un Alerta Simple

Swal.fire('Hola mundo!');

Este código mostrará un cuadro de diálogo simple con el mensaje «Hola mundo!».

Mostrar un Alerta con Título y Texto

Swal.fire({
  title: '¡Error!',
  text: 'Algo salió mal.',
  icon: 'error',
});

Este ejemplo muestra un cuadro de diálogo con un título, un mensaje y un ícono que indica un error.

Confirmación

SweetAlert también puede usarse para crear un cuadro de diálogo de confirmación similar al confirm() nativo de JavaScript, pero con una apariencia mucho más atractiva y opciones de personalización.

Swal.fire({
  title: '¿Estás seguro?',
  text: "¡No podrás deshacer esta acción!",
  icon: 'warning',
  showCancelButton: true,
  confirmButtonColor: '#3085d6',
  cancelButtonColor: '#d33',
  confirmButtonText: 'Sí, bórralo'
}).then((result) => {
  if (result.isConfirmed) {
    Swal.fire(
      '¡Borrado!',
      'Tu archivo ha sido borrado.',
      'success'
    )
  }
});

Este código muestra un cuadro de diálogo de confirmación con botones personalizados. Si el usuario confirma la acción, se muestra otro cuadro de diálogo indicando el éxito de la operación.

Prompt Personalizado

Para crear un cuadro de diálogo que solicite información al usuario, similar a prompt(), puedes usar SweetAlert de la siguiente manera:

(async () => {
  const { value: nombre } = await Swal.fire({
    title: 'Introduce tu nombre',
    input: 'text',
    inputLabel: 'Tu nombre',
    inputPlaceholder: 'Escribe tu nombre aquí'
  });

  if (nombre) {
    Swal.fire(`Tu nombre es: ${nombre}`);
  }
})();

Este fragmento abre un cuadro de diálogo que solicita al usuario su nombre y luego muestra un saludo personalizado con el nombre introducido.

Ventajas de Usar SweetAlert

  • Personalización: SweetAlert permite personalizar completamente los cuadros de diálogo, desde el texto y los botones hasta los colores e iconos, adaptándose así a la estética de tu aplicación web.
  • Facilidad de uso: A pesar de su riqueza en características, SweetAlert es muy fácil de usar, con una API clara y documentación extensa.
  • Mejora la experiencia del usuario: Los cuadros de diálogo creados con SweetAlert son visualmente atractivos y pueden mejorar significativamente la experiencia del usuario en comparación con los cuadros de diálogo nativos.

Consejos para principiantes

  • Practica: Experimenta con las diferentes opciones que ofrece SweetAlert. Prueba diferentes combinaciones de configuraciones para ver cómo afectan a la apariencia y el comportamiento de los cuadros de diálogo.
  • Lee la documentación: SweetAlert tiene una documentación extensa y ejemplos que pueden ayudarte a entender todas sus capacidades y cómo utilizarlas eficazmente.
  • Considera la accesibilidad: Asegúrate de que tus cuadros de diálogo sean accesibles, proporcionando una buena experiencia de usuario para todas las personas, incluidas aquellas que utilizan tecnologías de asistencia.

Conclusión

SweetAlert es una herramienta poderosa y flexible para mejorar las interacciones con el usuario en tus aplicaciones web. Con una amplia gama de opciones personalizables y una API fácil de usar, puedes crear cuadros de diálogo atractivos y funcionales que mejoren la experiencia del usuario y hagan que tu aplicación web se destaque.

Programación con JavaScript: Cuadros de diálogo

Los cuadros de diálogo en JavaScript se utilizan para interactuar con los usuarios mediante mensajes emergentes, solicitando información o simplemente notificando algo. JavaScript ofrece tres métodos principales para este propósito: alert(), confirm(), y prompt(). Estos métodos son bastante simples de usar y son fundamentales para la interacción básica en páginas web.

Alert

El método alert() se utiliza para mostrar un mensaje al usuario. Este método pausa la ejecución del script hasta que el usuario cierra el cuadro de diálogo. No retorna ningún valor.

Ejemplo de código:

alert("Hola, bienvenido a nuestra página web!");

Cómo funciona:

  • Cuando este código se ejecuta, aparece un cuadro de diálogo con el mensaje «Hola, bienvenido a nuestra página web!».
  • El usuario debe hacer clic en «Aceptar» para cerrar el cuadro de diálogo y continuar.

Confirm

El método confirm() se utiliza para mostrar un mensaje y dos botones, «Aceptar» y «Cancelar». Este método es útil cuando necesitas que el usuario confirme una acción. Devuelve un valor booleano: true si el usuario hace clic en «Aceptar» y false si el usuario hace clic en «Cancelar».

Ejemplo de código:

if (confirm("¿Estás seguro de que quieres eliminar este elemento?")) {
    // El usuario hizo clic en "Aceptar", pon aquí el código para eliminar el elemento
    alert("Elemento eliminado.");
} else {
    // El usuario hizo clic en "Cancelar", puedes manejar esta acción aquí
    alert("Operación cancelada.");
}

Cómo funciona:

  • Este código muestra un cuadro de diálogo con el mensaje «¿Estás seguro de que quieres eliminar este elemento?» y dos botones.
  • Si el usuario hace clic en «Aceptar», se muestra un nuevo cuadro de diálogo que dice «Elemento eliminado.».
  • Si el usuario hace clic en «Cancelar», se muestra un cuadro de diálogo que dice «Operación cancelada.».

Prompt

El método prompt() se utiliza para solicitar al usuario que introduzca algún texto. Aparece un cuadro de diálogo con un campo de texto y dos botones, «Aceptar» y «Cancelar». Devuelve el texto introducido si el usuario hace clic en «Aceptar» y null si el usuario hace clic en «Cancelar».

Ejemplo de código:

let nombre = prompt("¿Cuál es tu nombre?", "Escribe tu nombre aquí");
if (nombre) {
    alert("Hola, " + nombre + "!");
} else {
    alert("No has introducido tu nombre.");
}

Cómo funciona:

  • Este código muestra un cuadro de diálogo pidiendo al usuario su nombre, con un campo de texto que contiene el texto «Escribe tu nombre aquí» por defecto.
  • Si el usuario escribe su nombre y hace clic en «Aceptar», se muestra un saludo personalizado.
  • Si el usuario hace clic en «Cancelar», se muestra el mensaje «No has introducido tu nombre».

Buenas Prácticas

  • Usabilidad: Aunque los cuadros de diálogo son útiles, su uso excesivo puede ser molesto para los usuarios. Úsalos solo cuando sea necesario.
  • Claridad: Asegúrate de que los mensajes en los cuadros de diálogo sean claros y directos para evitar confusiones.
  • Accesibilidad: Considera la accesibilidad y la experiencia del usuario en dispositivos móviles.

Conclusión

Los cuadros de diálogo son herramientas sencillas y muy útiles para la interacción básica en JavaScript. Permiten obtener confirmaciones, mostrar alertas informativas y solicitar datos de los usuarios de manera efectiva. Experimentar con estos métodos te ayudará a entender mejor cómo y cuándo utilizarlos para mejorar la experiencia del usuario en tus aplicaciones web.