Programando el juego Hundir la flota en Java paso a paso

Introducción

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.

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;
    }
}