Programación de juego conecta 4 en Python

Conecta 4 en Python

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.

Requisitos del juego

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:

  • El jugador que juega primero se escoge al azar.
  • Cada jugador tira una sola ficha por turno.
  • El tablero se compone de columnas, no de filas. Eso quiere decir que las fichas solo pueden
    introducirse en la parte de arriba de una columna, y caer hasta llegar al la parte más baja o encima de otra ficha, no puede quedar flotando por ahí.
  • El juego se gana solo si uno de los jugadores logra forman una fila de cuatro fichas del mismo
    color.
  • Si el tablero se llena de fichas y no hay ganador, el juego quedó empatado

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.

Explicación del algoritmo general

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.

Ajustes del juego

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.

Tablero de juego

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.

Tablero de juego de Conecta 4 en Python con fichas de colores distintos
Tablero de juego de Conecta 4 en Python con fichas de colores distintos

No olvides que debes instalar colorama con pip, así: pip install colorama.

Colocando ficha

Colocar ficha en tablero de Conecta 4
Colocar ficha en tablero de Conecta 4

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.

Contando fichas conectadas

Conteo de fichas conectadas para saber si jugador gana - Programando en Python
Conteo de fichas conectadas para saber si jugador gana – Programando en Python

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.

Inteligencia artificial de CPU

Conecta 4 programado en Python - CPU eligiendo mejor columna
Conecta 4 programado en Python – CPU eligiendo mejor columna

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í")

Jugador contra jugador

Jugando conecta 4 en modo jugador contra jugador
Jugando conecta 4 en modo jugador contra jugador

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

Poniendo todo junto

Programación de juego conecta 4 en Python
Programación de juego conecta 4 en Python

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.

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.

1 comentario en “Conecta 4 en Python”

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *