Proyecto avanzado en PSeInt: crear un juego de Batalla Naval con matrices y SubProcesos


En este proyecto vamos a crear un juego de Batalla Naval en PSeInt para un jugador contra la computadora. Es un proyecto ideal para practicar matrices, números aleatorios, Funciones, SubProcesos, validaciones y manejo de estados.

La idea es construir un juego completo, pero manteniendo el código suficientemente claro como para poder entender qué hace cada parte.

¿Cómo se juega a Batalla Naval?

Cada jugador tiene un tablero oculto donde coloca sus barcos. Durante la partida, ambos se turnan para disparar indicando una fila y una columna.

  • Si el disparo cae sobre agua, se marca como agua.
  • Si el disparo alcanza una parte de un barco, se marca como impacto.
  • Cuando todas las posiciones de un barco fueron alcanzadas, el barco queda hundido.
  • Gana quien hunda todos los barcos del rival.

La versión que vamos a programar

Para que la partida sea ágil vamos a utilizar un tablero de 8 x 8.

Cada jugador tendrá:

  • 1 barco de 3 casilleros;
  • 2 barcos de 2 casilleros;
  • 3 barcos de 1 casillero.

En total, cada jugador tendrá 6 barcos y 10 casilleros ocupados.

Los barcos se colocarán automáticamente en posiciones aleatorias y podrán estar en forma horizontal o vertical.

¿Cómo representaremos el tablero?

Usaremos una matriz de 8 filas por 8 columnas. Cada celda tendrá un número interno:

0  = agua sin disparar
1 a 6 = barco identificado por su número
10 = agua ya disparada
11 a 16 = parte impactada de los barcos 1 a 6

Este sistema tiene una ventaja importante: cuando una parte de un barco es alcanzada todavía podemos saber a qué barco pertenecía. Eso nos permite detectar cuándo quedó completamente hundido.

Mezcla y posiciones aleatorias

Para colocar los barcos usaremos Aleatorio(). El programa elegirá:

  • una fila inicial;
  • una columna inicial;
  • una orientación horizontal o vertical.

Antes de colocar el barco comprobaremos que entre completamente dentro del tablero y que no se superponga con otro.

Código completo

Funcion puede <- PuedeColocar(tablero, fila, columna, tamano, direccion)
    Definir i Como Entero;

    puede <- Verdadero;

    Si direccion = 1 Entonces
        Si columna + tamano - 1 > 8 Entonces
            puede <- Falso;
        SiNo
            Para i <- 0 Hasta tamano - 1 Hacer
                Si tablero[fila,columna + i] <> 0 Entonces
                    puede <- Falso;
                FinSi
            FinPara
        FinSi
    SiNo
        Si fila + tamano - 1 > 8 Entonces
            puede <- Falso;
        SiNo
            Para i <- 0 Hasta tamano - 1 Hacer
                Si tablero[fila + i,columna] <> 0 Entonces
                    puede <- Falso;
                FinSi
            FinPara
        FinSi
    FinSi
FinFuncion


Funcion hundido <- EstaHundido(tablero, idBarco)
    Definir fila, columna Como Entero;

    hundido <- Verdadero;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            Si tablero[fila,columna] = idBarco Entonces
                hundido <- Falso;
            FinSi
        FinPara
    FinPara
FinFuncion


Funcion quedan <- QuedanBarcos(tablero)
    Definir fila, columna Como Entero;

    quedan <- Falso;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            Si tablero[fila,columna] >= 1 Y tablero[fila,columna] <= 6 Entonces
                quedan <- Verdadero;
            FinSi
        FinPara
    FinPara
FinFuncion


SubProceso InicializarTablero(tablero Por Referencia)
    Definir fila, columna Como Entero;

    Para fila <- 1 Hasta 8 Hacer
        Para columna <- 1 Hasta 8 Hacer
            tablero[fila,columna] <- 0;
        FinPara
    FinPara
FinSubProceso


SubProceso ColocarUnBarco(tablero Por Referencia, idBarco, tamano)
    Definir fila, columna, direccion, i Como Entero;
    Definir colocado Como Logico;

    colocado <- Falso;

    Mientras NO colocado Hacer
        fila <- Aleatorio(1,8);
        columna <- Aleatorio(1,8);
        direccion <- Aleatorio(1,2);

        Si PuedeColocar(tablero, fila, columna, tamano, direccion) Entonces
            Si direccion = 1 Entonces
                Para i <- 0 Hasta tamano - 1 Hacer
                    tablero[fila,columna + i] <- idBarco;
                FinPara
            SiNo
                Para i <- 0 Hasta tamano - 1 Hacer
                    tablero[fila + i,columna] <- idBarco;
                FinPara
            FinSi

            colocado <- Verdadero;
        FinSi
    FinMientras
FinSubProceso


SubProceso ColocarFlota(tablero Por Referencia)
    ColocarUnBarco(tablero, 1, 3);
    ColocarUnBarco(tablero, 2, 2);
    ColocarUnBarco(tablero, 3, 2);
    ColocarUnBarco(tablero, 4, 1);
    ColocarUnBarco(tablero, 5, 1);
    ColocarUnBarco(tablero, 6, 1);
FinSubProceso


SubProceso MostrarTablero(tablero, ocultarBarcos)
    Definir fila, columna, valor Como Entero;

    Escribir "    1 2 3 4 5 6 7 8";

    Para fila <- 1 Hasta 8 Hacer
        Escribir Sin Saltar fila, " | ";

        Para columna <- 1 Hasta 8 Hacer
            valor <- tablero[fila,columna];

            Si valor = 0 Entonces
                Escribir Sin Saltar "~ ";
            SiNo
                Si valor >= 1 Y valor <= 6 Entonces
                    Si ocultarBarcos Entonces
                        Escribir Sin Saltar "~ ";
                    SiNo
                        Escribir Sin Saltar "B ";
                    FinSi
                SiNo
                    Si valor = 10 Entonces
                        Escribir Sin Saltar "O ";
                    SiNo
                        Escribir Sin Saltar "X ";
                    FinSi
                FinSi
            FinSi
        FinPara

        Escribir "";
    FinPara
FinSubProceso


SubProceso ProcesarDisparo(tablero Por Referencia, fila, columna, impacto Por Referencia, barcoHundido Por Referencia)
    Definir idBarco Como Entero;

    impacto <- Falso;
    barcoHundido <- Falso;

    Si tablero[fila,columna] = 0 Entonces
        tablero[fila,columna] <- 10;
    SiNo
        idBarco <- tablero[fila,columna];

        Si idBarco >= 1 Y idBarco <= 6 Entonces
            impacto <- Verdadero;
            tablero[fila,columna] <- idBarco + 10;

            Si EstaHundido(tablero, idBarco) Entonces
                barcoHundido <- Verdadero;
            FinSi
        FinSi
    FinSi
FinSubProceso


SubProceso TurnoJugador(tableroCPU Por Referencia)
    Definir fila, columna Como Entero;
    Definir valido, impacto, barcoHundido Como Logico;

    valido <- Falso;

    Mientras NO valido Hacer
        Escribir "Fila (1 a 8):";
        Leer fila;

        Escribir "Columna (1 a 8):";
        Leer columna;

        Si fila >= 1 Y fila <= 8 Y columna >= 1 Y columna <= 8 Entonces
            Si tableroCPU[fila,columna] < 10 Entonces
                valido <- Verdadero;
            SiNo
                Escribir "Ya disparaste en esa posición.";
            FinSi
        SiNo
            Escribir "Coordenadas inválidas.";
        FinSi
    FinMientras

    ProcesarDisparo(tableroCPU, fila, columna, impacto, barcoHundido);

    Si impacto Entonces
        Escribir "¡IMPACTO!";

        Si barcoHundido Entonces
            Escribir "¡HUNDISTE UN BARCO!";
        FinSi
    SiNo
        Escribir "Agua.";
    FinSi
FinSubProceso


SubProceso TurnoCPU(tableroJugador Por Referencia)
    Definir fila, columna Como Entero;
    Definir valido, impacto, barcoHundido Como Logico;

    valido <- Falso;

    Mientras NO valido Hacer
        fila <- Aleatorio(1,8);
        columna <- Aleatorio(1,8);

        Si tableroJugador[fila,columna] < 10 Entonces
            valido <- Verdadero;
        FinSi
    FinMientras

    Escribir "La computadora dispara a fila ", fila, ", columna ", columna;

    ProcesarDisparo(tableroJugador, fila, columna, impacto, barcoHundido);

    Si impacto Entonces
        Escribir "La computadora impactó uno de tus barcos.";

        Si barcoHundido Entonces
            Escribir "La computadora hundió uno de tus barcos.";
        FinSi
    SiNo
        Escribir "La computadora disparó al agua.";
    FinSi
FinSubProceso


Proceso Batalla_Naval
    Definir tableroJugador, tableroCPU Como Entero;
    Definir jugando Como Logico;

    Dimension tableroJugador[8,8];
    Dimension tableroCPU[8,8];

    InicializarTablero(tableroJugador);
    InicializarTablero(tableroCPU);

    ColocarFlota(tableroJugador);
    ColocarFlota(tableroCPU);

    jugando <- Verdadero;

    Mientras jugando Hacer
        Limpiar Pantalla;

        Escribir "====================================";
        Escribir "           BATALLA NAVAL";
        Escribir "====================================";

        Escribir "";
        Escribir "TU TABLERO";
        MostrarTablero(tableroJugador, Falso);

        Escribir "";
        Escribir "TABLERO ENEMIGO";
        MostrarTablero(tableroCPU, Verdadero);

        Escribir "";
        TurnoJugador(tableroCPU);

        Si NO QuedanBarcos(tableroCPU) Entonces
            Limpiar Pantalla;
            Escribir "====================================";
            Escribir "           ¡VICTORIA!";
            Escribir "====================================";
            Escribir "Hundiste toda la flota enemiga.";
            jugando <- Falso;
        SiNo
            Esperar Tecla;

            Escribir "";
            TurnoCPU(tableroJugador);

            Si NO QuedanBarcos(tableroJugador) Entonces
                Limpiar Pantalla;
                Escribir "====================================";
                Escribir "             DERROTA";
                Escribir "====================================";
                Escribir "La computadora hundió toda tu flota.";
                jugando <- Falso;
            SiNo
                Esperar Tecla;
            FinSi
        FinSi
    FinMientras
FinProceso

¿Cómo funciona la colocación de los barcos?

ColocarUnBarco() genera una posición y una orientación aleatorias. Después consulta a PuedeColocar().

La función comprueba dos cosas:

  • que el barco no se salga del tablero;
  • que todas las posiciones estén libres.

Si alguna condición falla, se generan nuevas coordenadas hasta encontrar una posición válida.

¿Cómo evitamos disparos repetidos?

Una posición que todavía no recibió disparos contiene agua (0) o el identificador de un barco (1 a 6).

Después de un disparo pasa a tener un valor igual o superior a 10. Por eso podemos verificar fácilmente si esa coordenada ya fue utilizada:

Si tablero[fila,columna] < 10 Entonces
    // La posición todavía no fue disparada
FinSi

¿Cómo sabemos si un barco fue hundido?

Cada barco posee un identificador entre 1 y 6. Cuando una parte es impactada, sumamos 10 a ese identificador.

Por ejemplo, si una celda tenía el valor 3, después del impacto pasa a valer 13.

EstaHundido() recorre toda la matriz buscando posiciones que todavía tengan el identificador original del barco. Si no encuentra ninguna, significa que todas sus partes fueron alcanzadas.

¿Cómo mostramos el tablero enemigo sin revelar sus barcos?

MostrarTablero() recibe el parámetro ocultarBarcos.

Cuando mostramos nuestro propio tablero, las posiciones con barcos aparecen con B. En el tablero de la computadora esas mismas posiciones se muestran como agua hasta que reciben un disparo.

Los símbolos utilizados son:

~ = posición desconocida o agua
B = uno de nuestros barcos
O = disparo al agua
X = impacto

La computadora

En esta versión la CPU dispara a una fila y columna aleatorias. Antes de hacerlo verifica que no haya disparado anteriormente a esa misma posición.

Esto hace que el rival sea funcional, aunque todavía no sea demasiado inteligente.

Ideas para mejorar la inteligencia de la CPU

Una mejora interesante sería hacer que, después de conseguir un impacto, la computadora pruebe primero las posiciones vecinas:

  • arriba;
  • abajo;
  • izquierda;
  • derecha.

De esta forma la CPU comenzaría a comportarse de manera parecida a un jugador real.

Otros desafíos para ampliar el juego

  • permitir al jugador colocar manualmente sus barcos;
  • usar un tablero de 10 x 10;
  • agregar barcos más grandes;
  • mostrar cuántos barcos quedan vivos;
  • llevar un contador de disparos;
  • calcular precisión: impactos / disparos;
  • hacer que quien acierta vuelva a disparar;
  • crear distintos niveles de dificultad;
  • crear una modalidad para dos jugadores humanos;
  • hacer una CPU que persiga un barco después del primer impacto.

Desafío

Modificá la computadora para que, cuando consiga un impacto, intente disparar alrededor de esa posición antes de volver a elegir coordenadas completamente al azar.

Después agregá un contador que indique cuántos disparos necesitó cada jugador para terminar la partida.

Repasos recomendados

Este proyecto utiliza matrices y SubProcesos de forma intensiva. Si necesitás repasar esos temas:

20 ejercicios resueltos de PSeInt con matrices →

Funciones y SubProcesos en PSeInt →

¿Te sirvió este proyecto? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, juegos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Seguí aprendiendo PSeInt

Encontrá todos los ejercicios, guías y proyectos en nuestra página principal de PSeInt:

Aprender PSeInt: guía completa y ejercicios resueltos →


Descubre más desde Club Programador

Suscríbete y recibe las últimas entradas en tu correo electrónico.

Deja un comentario

Descubre más desde Club Programador

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo