python

Batalla naval en Python – Programación de juego

Hoy vamos a ver un juego programado en Python sin usar librerías para el funcionamiento del mismo.

Se trata del juego Battleship, juego de los barquitos, hundir la flota, hundiendo barquitos  o también llamado Batalla Naval programado totalmente en Python. Los requisitos del juego los veremos a continuación.

En este post te mostraré cómo programarlo, cómo jugarlo y dónde descargar el código fuente.

Requisitos de batalla naval en Python

  • Contar con una matriz de M columnas y N filas por cada jugador. Al inicio del juego toda la matriz tendrá únicamente el mar.
  • Colocar barcos al azar. Algunos de ellos ocupan dos celdas, y otros solo una. Los barcos que ocupan dos celdas pueden ser horizontales o verticales, pero ningún barco puede quedar encima de otro.
  • Los barcos están ocultos al inicio, y no importa qué tan cerca estén unos de los otros.
  • Cuando el juego inicia se debe indicar la cantidad de barcos que hay ocultos en el tablero, y se le debe dar al usuario la posibilidad de disparar indicando la fila y columna. La fila se indica con una letra.
  • Durante el desarrollo del juego se deben indicar los disparos restantes del jugador, así como los tiros acertados y tiros fallados. Estos tiros deben indicarse con un carácter.
  • También debe reproducirse un sonido distinto por cada disparo.
  • El juego se desarrolla por turnos y termina cuando un jugador gana o el otro pierde. Si un jugador acierta un disparo, puede seguir disparando.
  • Un jugador gana cuando derriba todos los barcos enemigos, y pierde cuando se le terminan sus disparos.
  • Cuando el juego termina se deben mostrar los barcos.
  • Al inicio se debe mostrar un menú con opciones de: Jugar, Acerca de y Salir. Siempre que se elija una de las primeras dos opciones se debe regresar a este menú.

Nota: he usado PyGame únicamente para reproducir los sonidos; ya que todo el juego es código nativo y se desarrolla en la terminal. Así que necesitas instalar la librería de pygame con pip install pygame.

Nota 2: este juego está basado altamente en mi juego de Batalla naval con Arduino. Obviamente el juego del post actual es mucho más fácil que el otro.

Algoritmo general

Vamos a tener varias funciones que reciben y devuelven una matriz, y esa matriz representa el tablero de un jugador. Con este paradigma podemos tener una batalla naval de infinitos jugadores.

Al inicio vamos a llenar la matriz con una constante de mar, que será un espacio vacío. Y luego en las funciones vamos a modificar esa matriz ya sea para colocar los barcos, ver si todos los barcos están hundidos, etcétera.

Todo está dividido en funciones que tienen un propósito y nombre que te ayudarán a entender el código.

Iniciando constantes

Primero veamos las constantes que van a definir a los barcos, sonidos, filas, columnas, etcétera. Básicamente toda la configuración del juego:

import random
import os
"""
Deshabilitar mensaje inicial de PyGame. Al final solo lo usamos para los sonidos
"""
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "sí"
import pygame

pygame.init()
pygame.mixer.init()

sonido_acertado = pygame.mixer.Sound("acertado.wav")
sonido_fallado = pygame.mixer.Sound("fallado.wav")

FILAS = 5
COLUMNAS = 5
MAR = " "
SUBMARINO = "S"  # Ocupa una celda
DESTRUCTOR = "D"  # Ocupa dos celdas
DESTRUCTOR_VERTICAL = "A"  # Ocupa dos celdas
DISPARO_FALLADO = "-"
DISPARO_ACERTADO = "*"
DISPAROS_INICIALES = 10
CANTIDAD_BARCOS_INICIALES = 8
JUGADOR_1 = "J1"
JUGADOR_2 = "J2"

Desde aquí puedes ver que estoy definiendo los tres tipos de “naves”: el submarino, el destructor horizontal y el destructor vertical.

También estoy iniciando los sonidos de PyGame que son para el disparo acertado y el disparo fallado.

Matriz inicial con mar

Al inicio vamos a tener un tablero que solo tiene mar, así que lo iniciamos así:

def obtener_matriz_inicial():
    matriz = []
    for y in range(FILAS):
        # Agregamos un arreglo a la matriz, que sería una fila básicamente
        matriz.append([])
        for x in range(COLUMNAS):
            # Y luego agregamos una celda a esa fila. Por defecto lleva "Mar"
            matriz[y].append(MAR)
    return matriz

Colocando barcos

Se nos pide que coloquemos las naves de ambos tableros en posiciones aleatorias. Entonces necesitamos varias funciones que nos permitan calcular coordenadas aleatorias, comprobar si hay mar en determinada coordenada, etcétera.

Aquí las tenemos:

# Indica si una coordenada de la matriz está vacía
def es_mar(x, y, matriz):
    return matriz[y][x] == MAR


def coordenada_en_rango(x, y):
    return x >= 0 and x <= COLUMNAS-1 and y >= 0 and y <= FILAS-1


def colocar_e_imprimir_barcos(matriz, cantidad_barcos, jugador):
    # Dividimos y redondeamos a entero hacia abajo (ya que no podemos colocar una parte no entera de un barco)
    barcos_una_celda = cantidad_barcos//2
    barcos_dos_celdas_verticales = cantidad_barcos//4
    barcos_dos_celdas_horizontales = cantidad_barcos//4
    if jugador == JUGADOR_1:
        print("Imprimiendo barcos del jugador 1 ")
    else:
        print("Imprimiendo barcos del jugador 2 ")
    print(f"Barcos de una celda: {barcos_una_celda}\nBarcos verticales de dos celdas: {barcos_dos_celdas_verticales}\nBarcos horizontales de dos celdas: {barcos_dos_celdas_horizontales}\nTotal: {barcos_una_celda+barcos_dos_celdas_verticales+barcos_dos_celdas_horizontales}")
    # Primero colocamos los de dos celdas para que se acomoden bien
    matriz = colocar_barcos_de_dos_celdas_horizontal(
        barcos_dos_celdas_horizontales, DESTRUCTOR, matriz)
    matriz = colocar_barcos_de_dos_celdas_vertical(
        barcos_dos_celdas_verticales, DESTRUCTOR_VERTICAL, matriz)
    matriz = colocar_barcos_de_una_celda(barcos_una_celda, SUBMARINO, matriz)
    return matriz


def obtener_x_aleatoria():
    return random.randint(0, COLUMNAS-1)


def obtener_y_aleatoria():
    return random.randint(0, FILAS-1)


def colocar_barcos_de_una_celda(cantidad, tipo_barco, matriz):
    barcos_colocados = 0
    while True:
        x = obtener_x_aleatoria()
        y = obtener_y_aleatoria()
        if es_mar(x, y, matriz):
            matriz[y][x] = tipo_barco
            barcos_colocados += 1
        if barcos_colocados >= cantidad:
            break
    return matriz


def colocar_barcos_de_dos_celdas_horizontal(cantidad, tipo_barco, matriz):
    barcos_colocados = 0
    while True:
        x = obtener_x_aleatoria()
        y = obtener_y_aleatoria()
        x2 = x+1
        if coordenada_en_rango(x, y) and coordenada_en_rango(x2, y) and es_mar(x, y, matriz) and es_mar(x2, y, matriz):
            matriz[y][x] = tipo_barco
            matriz[y][x2] = tipo_barco
            barcos_colocados += 1
        if barcos_colocados >= cantidad:
            break
    return matriz


def colocar_barcos_de_dos_celdas_vertical(cantidad, tipo_barco, matriz):
    barcos_colocados = 0
    while True:
        x = obtener_x_aleatoria()
        y = obtener_y_aleatoria()
        y2 = y+1
        if coordenada_en_rango(x, y) and coordenada_en_rango(x, y2) and es_mar(x, y, matriz) and es_mar(x, y2, matriz):
            matriz[y][x] = tipo_barco
            matriz[y2][x] = tipo_barco
            barcos_colocados += 1
        if barcos_colocados >= cantidad:
            break
    return matriz


Básicamente estas funciones calculan coordenadas aleatorias, verifican que haya mar en esas coordenadas y colocan el barco en cuestión.

Como siempre existe la posibilidad de que ya haya un barco ahí, hacemos un ciclo while que solo se detendrá hasta que termine de colocar los barcos.

Por cierto, recuerda que debe haber suficiente espacio en el tablero, ya que si no, se hará un ciclo infinito al tratar de colocar los barcos.

Nota: la colocación está en la función colocar_e_imprimir_barcos.

Imprimiendo tablero

Imprimir tablero de batalla naval con Python

Una parte importante de este juego de Battleship en Python es la de imprimir el tablero.

Obviamente no le vamos a mostrar los barcos al jugador al menos que el juego haya terminado, así que si detectamos un barco no imprimimos su verdadero carácter.

Recuerda que las filas se identifican por letras, y las columnas por números, así que hice una función que aumenta una letra en Python.

def imprimir_matriz(matriz, deberia_mostrar_barcos, jugador):
    print(f"Este es el mar del jugador {jugador}: ")
    letra = "A"
    for y in range(FILAS):
        imprimir_separador_horizontal()
        print(f"| {letra} ", end="")
        for x in range(COLUMNAS):
            celda = matriz[y][x]
            valor_real = celda
            if not deberia_mostrar_barcos and valor_real != MAR and valor_real != DISPARO_FALLADO and valor_real != DISPARO_ACERTADO:
                valor_real = " "
            print(f"| {valor_real} ", end="")
        letra = incrementar_letra(letra)
        print("|",)  # Salto de línea
    imprimir_separador_horizontal()
    imprimir_fila_de_numeros()
    imprimir_separador_horizontal()




def incrementar_letra(letra):
    return chr(ord(letra)+1)


def imprimir_separador_horizontal():
    # Imprimir un renglón dependiendo de las columnas
    for _ in range(COLUMNAS+1):
        print("+---", end="")
    print("+")


def imprimir_fila_de_numeros():
    print("|   ", end="")
    for x in range(COLUMNAS):
        print(f"| {x+1} ", end="")
    print("|")

Solicitar coordenadas y disparar

Solicitar coordenadas para disparar – Juego battleship con Python

Hasta ahora ya estamos imprimiendo nuestro tablero de batalla naval pero no estamos dando la posibilidad de disparar.

Recuerda que esto es por turnos, así que en cada turno debemos recoger las coordenadas del jugador totalmente validadas:

def solicitar_coordenadas(jugador):
    print(f"Solicitando coordenadas de disparo al jugador {jugador}")
    # Ciclo infinito. Se rompe cuando ingresan una fila correcta
    y = None
    x = None
    while True:
        letra_fila = input(
            "Ingresa la letra de la fila tal y como aparece en el tablero: ")
        # Necesitamos una letra de 1 carácter. Si no es de 1 carácter usamos continue para repetir este ciclo
        if len(letra_fila) != 1:
            print("Debes ingresar únicamente una letra")
            continue
        # Convertir la letra a un índice para acceder a la matriz
        # La A equivale al ASCII 65, la B al 66, etcétera. Para convertir la letra a índice
        # convertimos la letra a su ASCII y le restamos 65 (el 65 es el ASCII de la A, por lo que A es 0)
        y = ord(letra_fila) - 65
        # Verificar si es válida. En caso de que sí, rompemos el ciclo
        if coordenada_en_rango(0, y):
            break
        else:
            print("Fila inválida")
    # Hacemos lo mismo pero para la columna
    while True:
        try:
            x = int(input("Ingresa el número de columna: "))
            if coordenada_en_rango(x-1, 0):
                x = x-1  # Queremos el índice, así que restamos un 1 siempre
                break
            else:
                print("Columna inválida")
        except:
            print("Ingresa un número válido")

    return x, y

Como puedes ver, la función va a devolver las coordenadas como números hasta que las mismas sean válidas. El usuario no puede ingresar coordenadas incorrectas o alguna letra incorrecta.

Ahora veamos la función que dispara. Esta función regresa un booleano indicando si el disparo fue exitoso:

def disparar(x, y, matriz) -> bool:
    if es_mar(x, y, matriz):
        matriz[y][x] = DISPARO_FALLADO
        return False
    # Si ya había disparado antes, se le cuenta como falla igualmente
    elif matriz[y][x] == DISPARO_FALLADO or matriz[y][x] == DISPARO_ACERTADO:
        return False
    else:
        matriz[y][x] = DISPARO_ACERTADO
        return True

Saber si jugador gana Battleship

Jugador gana la partida de batalla naval – Mostrar ubicación de barcos

Veamos la siguiente función que indica si un tablero ya tiene todos los barcos hundidos. Esto significa que toda la matriz está llena de disparos acertados o de mar:

def todos_los_barcos_hundidos(matriz):
    for y in range(FILAS):
        for x in range(COLUMNAS):
            celda = matriz[y][x]
            # Si no es mar o un disparo, significa que todavía hay un barco por ahí
            if celda != MAR and celda != DISPARO_ACERTADO and celda != DISPARO_FALLADO:
                return False
    # Acabamos de recorrer toda la matriz y no regresamos en la línea anterior. Entonces todos los barcos han sido hundidos
    return True

Jugar

Finalmente llegamos a la parte de este juego programado con Python. Simplemente hacemos un ciclo que se romperá cuando el jugador acierte o pierda, y eso lo comprobamos en cada iteración.

Espero que el código se explique por sí mismo:

def jugar():
    disparos_restantes_j1 = DISPAROS_INICIALES
    disparos_restantes_j2 = DISPAROS_INICIALES
    cantidad_barcos = 5
    matriz_j1, matriz_j2 = obtener_matriz_inicial(), obtener_matriz_inicial()
    matriz_j1 = colocar_e_imprimir_barcos(
        matriz_j1, cantidad_barcos, JUGADOR_1)
    matriz_j2 = colocar_e_imprimir_barcos(
        matriz_j2, cantidad_barcos, JUGADOR_2)
    turno_actual = JUGADOR_1
    print("===============")
    while True:
        print(f"Turno de {turno_actual}")
        disparos_restantes = disparos_restantes_j2
        if turno_actual == JUGADOR_1:
            disparos_restantes = disparos_restantes_j1
        imprimir_disparos_restantes(disparos_restantes, turno_actual)
        matriz_oponente = matriz_j1
        if turno_actual == JUGADOR_1:
            matriz_oponente = matriz_j2
        imprimir_matriz(matriz_oponente, False,
                        oponente_de_jugador(turno_actual))
        x, y = solicitar_coordenadas(turno_actual)
        acertado = disparar(x, y, matriz_oponente)
        if turno_actual == JUGADOR_1:
            disparos_restantes_j1 -= 1
        else:
            disparos_restantes_j2 -= 1

        imprimir_matriz(matriz_oponente, False,
                        oponente_de_jugador(turno_actual))
        if acertado:
            print("Disparo acertado")
            reproducir_sonido_acertado()
            if todos_los_barcos_hundidos(matriz_oponente):
                indicar_victoria(turno_actual)
                imprimir_matrices_con_barcos(matriz_j1, matriz_j2)
                break
        else:
            print("Disparo fallado")
            reproducir_sonido_error()
            if disparos_restantes-1 <= 0:
                indicar_fracaso(turno_actual)
                imprimir_matrices_con_barcos(matriz_j1, matriz_j2)
                break
            turno_actual = oponente_de_jugador(turno_actual)

Básicamente solicitamos el disparo dependiendo del jugador, disparamos, reproducimos el sonido, comprobamos si pierde o gana para ver si terminamos el juego, y si no se termina entonces le damos el turno al otro jugador para que se siga cumpliendo el ciclo.

Por cierto, no te mostré las funciones para reproducir los sonidos. Quedan así:

def reproducir_sonido_acertado():
    pygame.mixer.Sound.play(sonido_acertado)


def reproducir_sonido_error():
    pygame.mixer.Sound.play(sonido_fallado)


Poniendo todo junto

Jugando batalla naval – Juego programado con Python

El código completo para descargar te lo dejo en GitHub. Es un simple archivo que más tarde puedes separar por módulos, pero lo dejo así por si lo actualizo más adelante.

Para terminar te dejo con más tutoriales, programas y juegos programados con Python.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

3 días hace

Esta web usa cookies.