En este post te mostraré el código fuente del juego conecta 4 (Connect 4) programado con Python, además de explicarte cómo es que ha sido programado y cómo funciona.
Te cuento que éste fue el programa que inspiró a programar el mismo juego en C, C sharp y JavaScript (mismos que encuentras en mi blog); ya que alguien me pidió programarlo en Python pero como no pude publicarlo antes mejor decidí hacerlo en C y luego en sus otras versiones.
El tiempo ha pasado y ahora ya puedo publicar este proyecto de Conecta 4 en Python con todo su código fuente.
Este proyecto consiste en implementar un juego llamado Connect4, cuyas reglas de juego estaremos explicando más adelante.
Se implementará el proyecto en texto. Su proyecto se divide en tres partes básicas: persona persona, persona-máquina y máquina-máquina.
Entonces, la complejidad del proyecto consiste en el desarrollo de un algoritmo que juegue por sí solo. A continuación todas las especificaciones necesarias, deben leerlas antes de comenzar a pensar en el proyecto.
Connect4 es un juego bastante sencillo, sin embargo, para poder implementarlo en un programa deben saber jugarlo bien. A continuación le explicaremos de qué se trata:
El juego está formado por un tablero, y fichas de dos colores diferentes. Es un juego para dos jugadores y cada jugador tiene asignado un color de fichas.
El objetivo de Connect4, como su nombre lo dice, consiste en conectar 4 fichas del mismo color en una línea, en cualquier dirección, horizontal, vertical, y diagonalmente (hacia ambos lados).
El jugador que forme la fila de cuatro fichas primero ganará. Las reglas del juego no son muy complicadas:
Veamos entonces cómo programar este juego con Python.
Nota: no implementé la parte del CPU contra sí mismo, pero solo basta con invocar a la función del CPU dos veces por turnos, como lo hice en las versiones del otro lenguaje.
Tenemos una matriz que va a ser el tablero de juego. También tenemos constantes para identificar al jugador 1 y al jugador dos.
En el desarrollo del juego simplemente validamos las coordenadas y obtenemos la última fila vacía de determinada columna, contando de abajo hacia arriba para hacer como que la ficha “cae” en el tablero.
Una de las cosas más tediosas es saber si hay un ganador, pues hay que contar si hay una fila de fichas en todas las direcciones, aunque en las versiones del juego más recientes que he programado ya he optimizado esa parte.
Y ya para el caso de que el algoritmo piense por sí mismo ya lo expliqué en otro artículo que te recomiendo leer.
Veamos las constantes y ajustes del juego. Lo pongo aquí porque vamos a usarlas a lo largo de todo el código fuente de Conecta 4:
MINIMO_FILAS = 5
MAXIMO_FILAS = 10
MINIMO_COLUMNAS = 6
MAXIMO_COLUMNAS = 10
ESPACIO_VACIO = " "
COLOR_1 = "x"
COLOR_2 = "o"
JUGADOR_1 = 1
# La CPU también es el jugador 2
JUGADOR_2 = 2
CONECTA = 4
ESTA_JUGANDO_CPU = False
Ahí puedes ver que tenemos el mínimo y máximo de filas. También tenemos un carácter para identificar a los jugadores, y la cantidad de fichas que hay que conectar para ganar.
Esta constante de CONECTA
es interesante, pues así puedes cambiar las fichas que se conectan para ganar. Por lo que teóricamente puedes jugar a Conecta 2, Conecta 3, Conecta 1000, etcétera.
Al inicio de todo vamos a crear el tablero de juego. Esto es básicamente inicializar la matriz con un espacio vacío.
Es importante que este espacio vacío esté bien definido en una constante, pues nos va a servir para comparar si podemos poner una ficha ahí.
def crear_tablero(filas, columnas):
tablero = []
for fila in range(filas):
tablero.append([])
for columna in range(columnas):
tablero[fila].append(ESPACIO_VACIO)
return tablero
Por otro lado, veamos la función que imprime el tablero.
def imprimir_tablero(tablero):
# Imprime números de columnas
print("|", end="")
for f in range(1, len(tablero[0]) + 1):
print(f, end="|")
print("")
# Datos
for fila in tablero:
print("|", end="")
for valor in fila:
color_terminal = Fore.GREEN
if valor == COLOR_2:
color_terminal = Fore.RED
print(color_terminal + valor, end="")
print(Style.RESET_ALL, end="")
print("|", end="")
print("")
# Pie
print("+", end="")
for f in range(1, len(tablero[0]) + 1):
print("-", end="+")
print("")
En este caso los colores de las fichas deben ser distintos, así que he usado lo que ya expuse en otro post para imprimir texto con color en Python.
No olvides que debes instalar colorama con pip, así: pip install colorama
.
Ahora veamos el proceso de colocar una ficha, o mejor dicho, dejar caer una ficha. Necesitamos solicitar una columna válida (solicitando un entero válido) y una vez que la tengamos debemos obtener la última fila válida de la misma:
def obtener_fila_valida_en_columna(columna, tablero):
indice = len(tablero) - 1
while indice >= 0:
if tablero[indice][columna] == ESPACIO_VACIO:
return indice
indice -= 1
return -1
def solicitar_columna(tablero):
"""
Solicita la columna y devuelve la columna ingresada -1 para ser usada fácilmente como índice
"""
while True:
columna = solicitar_entero_valido("Ingresa la columna para colocar la pieza: ")
if columna <= 0 or columna > len(tablero[0]):
print("Columna no válida")
elif tablero[0][columna - 1] != ESPACIO_VACIO:
print("Esa columna ya está llena")
else:
return columna - 1
def colocar_pieza(columna, jugador, tablero):
"""
Coloca una pieza en el tablero. La columna debe
comenzar en 0
"""
color = COLOR_1
if jugador == JUGADOR_2:
color = COLOR_2
fila = obtener_fila_valida_en_columna(columna, tablero)
if fila == -1:
return False
tablero[fila][columna] = color
return True
Aquí podemos ver 3 funciones que sirven para colocar la pieza, mismas que guardan también el color del jugador así como el jugador.
La colocación real está en la línea 35, que es cuando ya pasaron todas las validaciones.
Ahora viene la parte tediosa en donde verificamos si hay suficientes fichas conectadas para saber si un jugador gana.
Son varias funciones que se repiten y hacen casi lo mismo. Ya te dije que en versiones de otros lenguajes mejoré eso, pero por el momento tenemos lo siguiente que cuenta las fichas de este juego de Conecta 4 en Python:
def obtener_conteo_derecha(fila, columna, color, tablero):
fin_columnas = len(tablero[0])
contador = 0
for i in range(columna, fin_columnas):
if contador >= CONECTA:
return contador
if tablero[fila][i] == color:
contador += 1
else:
contador = 0
return contador
def obtener_conteo_izquierda(fila, columna, color, tablero):
contador = 0
# -1 porque no es inclusivo
for i in range(columna, -1, -1):
if contador >= CONECTA:
return contador
if tablero[fila][i] == color:
contador += 1
else:
contador = 0
return contador
def obtener_conteo_abajo(fila, columna, color, tablero):
fin_filas = len(tablero)
contador = 0
for i in range(fila, fin_filas):
if contador >= CONECTA:
return contador
if tablero[i][columna] == color:
contador += 1
else:
contador = 0
return contador
def obtener_conteo_arriba(fila, columna, color, tablero):
contador = 0
for i in range(fila, -1, -1):
if contador >= CONECTA:
return contador
if contador >= CONECTA:
return contador
if tablero[i][columna] == color:
contador += 1
else:
contador = 0
return contador
def obtener_conteo_arriba_derecha(fila, columna, color, tablero):
contador = 0
numero_fila = fila
numero_columna = columna
while numero_fila >= 0 and numero_columna < len(tablero[0]):
if contador >= CONECTA:
return contador
if tablero[numero_fila][numero_columna] == color:
contador += 1
else:
contador = 0
numero_fila -= 1
numero_columna += 1
return contador
def obtener_conteo_arriba_izquierda(fila, columna, color, tablero):
contador = 0
numero_fila = fila
numero_columna = columna
while numero_fila >= 0 and numero_columna >= 0:
if contador >= CONECTA:
return contador
if tablero[numero_fila][numero_columna] == color:
contador += 1
else:
contador = 0
numero_fila -= 1
numero_columna -= 1
return contador
def obtener_conteo_abajo_izquierda(fila, columna, color, tablero):
contador = 0
numero_fila = fila
numero_columna = columna
while numero_fila < len(tablero) and numero_columna >= 0:
if contador >= CONECTA:
return contador
if tablero[numero_fila][numero_columna] == color:
contador += 1
else:
contador = 0
numero_fila += 1
numero_columna -= 1
return contador
def obtener_conteo_abajo_derecha(fila, columna, color, tablero):
contador = 0
numero_fila = fila
numero_columna = columna
while numero_fila < len(tablero) and numero_columna < len(tablero[0]):
if contador >= CONECTA:
return contador
if tablero[numero_fila][numero_columna] == color:
contador += 1
else:
contador = 0
numero_fila += 1
numero_columna += 1
return contador
def obtener_direcciones():
return [
'izquierda',
'arriba',
'abajo',
'derecha',
'arriba_derecha',
'abajo_derecha',
'arriba_izquierda',
'abajo_izquierda',
]
def obtener_conteo(fila, columna, color, tablero):
direcciones = obtener_direcciones()
for direccion in direcciones:
funcion = globals()['obtener_conteo_' + direccion]
conteo = funcion(fila, columna, color, tablero)
if conteo >= CONECTA:
return conteo
return 0
Todas las funciones regresan un número que indica cuántas fichas están conectadas, dependiendo del color que contamos.
Algo interesante es que en lugar de hacer un if
muy grande, estoy invocando a una función por su nombre como cadena en la línea 135.
Ya para las otras funciones de conteo en todas las direcciones lo que hacemos es iniciar x
e y
para después ir aumentando dependiendo de la dirección en donde vayamos.
Si encontramos fichas seguidas vamos aumentando el contador, y si no, lo reiniciamos.
Ahora veamos la “inteligencia artificial” de la CPU que elige la columna. Queda así:
def elegir_columna_ideal(jugador, tableroOriginal):
"""
Reglas:
1- Si hay un movimiento para ganar, tomarlo
2- Si el oponente tiene un movimiento para ganar, evitarlo
3- Si nada de lo de arriba se cumple, buscar columna en donde se obtendría el mayor puntaje
4- Si lo de arriba no se cumple, buscar columna en donde el adversario obtendría el mayor puntaje
5- Preferir tomar cosas centrales antes de bordes
"""
tablero = deepcopy(tableroOriginal)
# Puedo ganar?
columna_ganadora = obtener_columna_ganadora(jugador, tablero)
if columna_ganadora != -1:
return columna_ganadora
# Si no, mi oponente puede ganar? en caso de que sí, debo evitarlo
columna_perdedora = obtener_columna_ganadora(obtener_jugador_contrario(jugador), tablero)
if columna_perdedora != -1:
return columna_perdedora
umbral_puntaje = 1
# Si no, buscaré un lugar en donde al colocar mi pieza me dé más posibilidades de conectar 4
puntaje_ganador, columna_mia = obtener_columna_con_mayor_puntaje(jugador, tablero)
# Pero también necesito el de mi adversario
puntaje_ganador_adversario, columna_adversario = obtener_columna_con_mayor_puntaje(
obtener_jugador_contrario(jugador), tablero)
if puntaje_ganador > umbral_puntaje and puntaje_ganador_adversario > umbral_puntaje:
# Aquí se puede elegir entre ataque o defensa. Se prefiere la defensa
if puntaje_ganador_adversario > puntaje_ganador:
return columna_adversario
else:
return columna_mia
# Si lo demás falla, elegir una columna central
central = obtener_columna_central(jugador, tablero)
if central != -1:
return central
# Y de últimas, elegir la primer columna que no esté vacía
columna_disponible = obtener_primera_columna_vacia(jugador, tablero)
if columna_disponible != -1:
return columna_disponible
# Si no, no sé qué más hacer. Esto no debería pasar
print("Error. No se debería llegar hasta aquí")
Recuerda que tenemos dos modos, el de jugador contra jugador y el modo jugador contra CPU. El de jugador contra jugador se ve así:
def jugador_vs_jugador(tablero):
jugador_actual = elegir_jugador_al_azar()
while True:
imprimir_tablero(tablero)
imprimir_tiradas_faltantes(tablero)
columna = imprimir_y_solicitar_turno(jugador_actual, tablero)
pieza_colocada = colocar_pieza(columna, jugador_actual, tablero)
if not pieza_colocada:
print("No se puede colocar en esa columna")
ha_ganado = comprobar_ganador(jugador_actual, tablero)
if ha_ganado:
imprimir_tablero(tablero)
felicitar_jugador(jugador_actual)
break
elif es_empate(tablero):
imprimir_tablero(tablero)
indicar_empate()
break
else:
if jugador_actual == JUGADOR_1:
jugador_actual = JUGADOR_2
else:
jugador_actual = JUGADOR_1
Y la del jugador contra CPU así:
def jugador_vs_computadora(tablero):
global ESTA_JUGANDO_CPU
ESTA_JUGANDO_CPU = True
jugador_actual = elegir_jugador_al_azar()
while True:
imprimir_tablero(tablero)
imprimir_tiradas_faltantes(tablero)
if jugador_actual == JUGADOR_1:
columna = imprimir_y_solicitar_turno(jugador_actual, tablero)
else:
print("CPU pensando...")
columna = obtener_columna_segun_cpu(jugador_actual, tablero)
pieza_colocada = colocar_pieza(columna, jugador_actual, tablero)
if not pieza_colocada:
print("No se puede colocar en esa columna")
ha_ganado = comprobar_ganador(jugador_actual, tablero)
if ha_ganado:
imprimir_tablero(tablero)
felicitar_jugador(jugador_actual)
break
elif es_empate(tablero):
imprimir_tablero(tablero)
indicar_empate()
break
else:
if jugador_actual == JUGADOR_1:
jugador_actual = JUGADOR_2
else:
jugador_actual = JUGADOR_1
ESTA_JUGANDO_CPU = False
Ya te mostré las funciones más importantes, pero no son todas. El código completo te lo dejo en GitHub haciendo clic aquí; es un archivo único pero ahí voy a poner todas las actualizaciones en caso de que las haga.
Para terminar te dejo con enlaces a Conecta 4 en C, C# y JavaScript. También te dejo más programas en Python.
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…
Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…
Esta web usa cookies.
Ver comentarios
exelente tu trabajo men aplique un poco de ia para variar uf exelente