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:
- Motor de renderizado ANSI: Manipulación de la consola del sistema operativo para mostrar colores.
- Modelo de datos robusto: Uso de enumerados (
enum) para encapsular comportamiento y configuración. - Algoritmos de colocación espacial: Lógica matemática para gestionar colisiones y perímetros de seguridad en una matriz.
- 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
5suelto en el código es confuso (¿es la munición? ¿es el tamaño del tablero?). Al usarTipoBarco.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:
- Constructor: Recibe los valores enteros R, G y B.
String.format: Construye la secuencia ANSI dinámica.toString: Sobrescribe el método estándar para devolver:COLOR + SÍMBOLO + RESET. El código deRESET(\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_NECESARIOSno 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 alenum, 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:
- Fase de inicialización:
- Limpia el tablero (
inicializarOceano). - Coloca los barcos sin que el jugador sepa dónde están (
colocarFlotaCompleta).
- Limpia el tablero (
- Fase de ejecución (bucle
while):- Renderizado: Llama a
imprimirOceano(false). El parámetrofalseactiva 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/BARCOaFALLO/TOCADO).
- Renderizado: Llama a
- Fase de clausura:
- Al terminar (victoria o derrota), llama a
imprimirOceano(true). El parámetrotruedesactiva la niebla y revela la ubicación de todos los barcos.
- Al terminar (victoria o derrota), llama a
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 variablesuma. - ¿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.
- Entrada de datos: Pide fila y columna.
- Comprobación de repetición: Mira en la matriz
oceano[fila][col].- Si el valor es
TOCADOoFALLO, significa que ya hubo un proyectil ahí. - Importante: Mientras el disparo no sea «nuevo»,
disparoValidosigue siendofalsey 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.
- Si el valor es
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!».
- Cambia el estado a
- Si no hay barco (es decir, hay agua):
- Cambia el estado a
FALLO. return false: El contador de impactos no cambia.
- Cambia el estado a
Conceptos clave de Java en este código
- Enums (
EstadoCasillayTipoBarco): El código no usa números sueltos (como 0 o 1), sino nombres claros. Es mucho más fácil leerEstadoCasilla.TOCADOque leer un2y tener que recordar qué significaba. - Booleans como control: El uso de
disparoValidoactú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:
- El barco no debe salirse del tablero.
- 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
BARCOy estamos en modo juego (revelarTodo == false), el programa miente al usuario e imprime el símbolo deAGUA. - 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;
}
}




