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.
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.
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.
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.
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
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
.
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("|")
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
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
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)
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.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
Ver comentarios
MIL GRACIAS SOS UN SOLASO I LOVE YOU
Excelente juego Luis! muchas gracias
Muy buen trabajo. Gracias,