Jugando conecta 4 en C sharp (c#)

Conecta 4 en C# con pequeña Inteligencia Artificial

En este post de programación en C# también conocido como C sharp te enseñaré el juego de Conecta 4. He programado este juego para que se pueda jugar en la consola, pero obviamente puedes adaptarlo a una interfaz gráfica.

Jugando conecta 4 en C sharp (c#)
Jugando conecta 4 en C sharp (c#)

El código escrito en C# simula completamente el juego, y permite jugar en modo humano contra humano, humano contra CPU (con una pequeña IA) y también en modo CPU contra CPU.

A lo largo del post te enseñaré los detalles de este juego, mismo que se basa en mi otro programa ya escrito en lenguaje ANSI C.

Funcionamiento general

Todo el juego de Conecta 4 en c# se desarrolla en una matriz, también conocida como arreglo bidimensional o array de dos dimensiones. Ahí es en donde se dejan caer las piezas.

En cada turno, ya sea del CPU o del Humano, se debe elegir una columna en donde se va a colocar la pieza. Se dice que un jugador gana cuando se pueden conectar 4 piezas en cualquier dirección.

Por cierto, este juego está programado de tal manera que el tablero de juego es libre de tener cualquier cantidad de filas y columnas, además de que se puede cambiar el número de piezas que se deben conectar para ganar el juego.

De este modo podríamos jugar a Conecta 5, Conecta 6, etcétera.

Por cierto, para hacer que el CPU piense y elija una columna ganadora se ha usado este algoritmo.

Funciones ayudantes

Vamos a necesitar ciertas funciones. En primer caso una que nos permita clonar una matriz, pues dentro del juego vamos a simular colocar piezas sobre todo cuando sea el turno del CPU.

La otra función que necesitamos es una que nos dé números aleatorios, ya que necesitamos saber cuál jugador inicia el juego, entre otras cosas.

static int aleatorio_en_rango(int minimo, int maximo)
{
    System.Random rnd = new System.Random();
    return rnd.Next(minimo, maximo + 1);
}

static string[,] clonarMatriz(string[,] tableroOriginal)
{
    return tableroOriginal.Clone() as string[,];
}

Trabajando con el tablero

Necesitamos varias operaciones que hacer con el tablero. Por ejemplo, debemos limpiarlo, imprimirlo (mostrando las piezas), validar que las columnas sean correctas, etcétera. Para empezar, el tablero es el siguiente:

string[,] tablero = new string[FILAS, COLUMNAS];

Este arreglo bidimensional es en donde se lleva a cabo todo el juego. Hay varias funciones que usan, pero la principal es la siguiente:

static int colocarPieza(string jugador, int columna, string[,] tablero)
{
    if (columna < 0 || columna >= COLUMNAS)
    {
        return ERROR_FILA_INVALIDA;
    }
    int fila = obtenerFilaDesocupada(columna, tablero);
    if (fila == FILA_NO_ENCONTRADA)
    {
        return ERROR_COLUMNA_LLENA;
    }
    tablero[fila, columna] = jugador;
    return ERROR_NINGUNO;
}

Esta función coloca la pieza del jugador en la columna especificada dentro del tablero. También verifica si la columna está llena y si la misma es válida.

Contando conexiones

Dentro del juego necesitamos saber si determinado jugador ha ganado, es decir, que ha conectado más de 4 piezas en cualquier dirección. Las funciones que hacen el conteo son las siguientes:

static int contarArriba(int x, int y, string jugador, string[,] tablero)
{
    int yInicio = (y - CONECTA >= 0) ? y - CONECTA + 1 : 0;
    int contador = 0;
    for (; yInicio <= y; yInicio++)
    {
        if (tablero[yInicio, x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
    }
    return contador;
}

static int contarDerecha(int x, int y, string jugador, string[,] tablero)
{
    int xFin = (x + CONECTA < COLUMNAS) ? x + CONECTA - 1 : COLUMNAS - 1;
    int contador = 0;
    for (; x <= xFin; x++)
    {
        if (tablero[y, x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
    }
    return contador;
}

static int contarArribaDerecha(int x, int y, string jugador, string[,] tablero)
{
    int xFin = (x + CONECTA < COLUMNAS) ? x + CONECTA - 1 : COLUMNAS - 1;
    int yInicio = (y - CONECTA >= 0) ? y - CONECTA + 1 : 0;
    int contador = 0;
    while (x <= xFin && yInicio <= y)
    {
        if (tablero[y, x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
        x++;
        y--;
    }
    return contador;
}

static int contarAbajoDerecha(int x, int y, string jugador, string[,] tablero)
{
    int xFin = (x + CONECTA < COLUMNAS) ? x + CONECTA - 1 : COLUMNAS - 1;
    int yFin = (y + CONECTA < FILAS) ? y + CONECTA - 1 : FILAS - 1;
    int contador = 0;
    while (x <= xFin && y <= yFin)
    {
        if (tablero[y, x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
        x++;
        y++;
    }
    return contador;
}

Simplemente verifican si se hace una línea recta y devuelven el contador.

Ganador y empate

Veamos ahora las funciones que dicen si el usuario es ganador o si se ha dado un empate.

Para determinar si es empate en este juego de conecta 4 en C# simplemente contamos las piezas que formen una línea y si son 4 (o las especificadas para ganar) indicamos el jugador ganador.

En el caso del empate, verificamos si ya no quedan espacios vacíos en el tablero.

static bool esEmpate(string[,] tablero)
{
    int i;
    for (i = 0; i < COLUMNAS; ++i)
    {
        int resultado = obtenerFilaDesocupada(i, tablero);
        if (resultado != FILA_NO_ENCONTRADA)
        {
            return false;
        }
    }
    return true;
}

Para verificar el ganador:

static int ganador(string jugador, string[,] tablero)
{
    /*
     * Solo necesitamos
     * Arriba
     * Derecha
     * Arriba derecha
     * Abajo derecha
     *
     * */
    int y;
    for (y = 0; y < FILAS; y++)
    {
        int x;
        for (x = 0; x < COLUMNAS; x++)
        {
            int conteoArriba = contarArriba(x, y, jugador, tablero);
            if (conteoArriba >= CONECTA)
            {
                return CONECTA_ARRIBA;
            }
            if (contarDerecha(x, y, jugador, tablero) >= CONECTA)
            {
                return CONECTA_DERECHA;
            }
            if (contarArribaDerecha(x, y, jugador, tablero) >= CONECTA)
            {
                return CONECTA_ARRIBA_DERECHA;
            }
            if (contarAbajoDerecha(x, y, jugador, tablero) >= CONECTA)
            {
                return CONECTA_ABAJO_DERECHA;
            }
        }
    }
    return NO_CONECTA;
}

Inteligencia artificial de Conecta 4 en C sharp

Pasemos a la inteligencia artificial del CPU para que el mismo pueda jugar contra el ser humano. Simplemente se hacen simulaciones del tablero y se elige la mejor columna, siguiendo el algoritmo que mencioné anteriormente.

static int obtenerColumnaGanadora(string jugador, string[,] tableroOriginal)
{
    string[,] tablero = new string[FILAS, COLUMNAS];
    int i;
    for (i = 0; i < COLUMNAS; i++)
    {
        tablero = clonarMatriz(tableroOriginal);
        int resultado = colocarPieza(jugador, i, tablero);
        if (resultado == ERROR_NINGUNO)
        {
            int gana = ganador(jugador, tablero);
            if (gana != NO_CONECTA)
            {
                return i;
            }
        }
    }
    return COLUMNA_GANADORA_NO_ENCONTRADA;
}

static int obtenerPrimeraFilaLlena(int columna, string[,] tablero)
{
    int i;
    for (i = 0; i < FILAS; ++i)
    {
        if (tablero[i, columna] != ESPACIO_VACIO)
        {
            return i;
        }
    }
    return FILA_NO_ENCONTRADA;
}

static (int, int) obtenerColumnaEnLaQueSeObtieneMayorPuntaje(string jugador, string[,] tableroOriginal)
{

    int conteoMayor = 0, indiceColumnaConConteoMayor = -1;
    string[,] tablero = new string[FILAS, COLUMNAS];
    int i;
    for (i = 0; i < COLUMNAS; ++i)
    {
        tablero = clonarMatriz(tableroOriginal);
        int estado = colocarPieza(jugador, i, tablero);
        if (estado == ERROR_NINGUNO)
        {
            int filaDePiezaRecienColocada = obtenerPrimeraFilaLlena(i, tablero);
            if (filaDePiezaRecienColocada != FILA_NO_ENCONTRADA)
            {
                int c = contarArriba(i, filaDePiezaRecienColocada, jugador, tablero);
                if (c > conteoMayor)
                {
                    conteoMayor = c;
                    indiceColumnaConConteoMayor = i;
                }
                c = contarArribaDerecha(i, filaDePiezaRecienColocada, jugador, tablero);
                if (c > conteoMayor)
                {
                    conteoMayor = c;
                    indiceColumnaConConteoMayor = i;
                }
                c = contarDerecha(i, filaDePiezaRecienColocada, jugador, tablero);
                if (c > conteoMayor)
                {
                    conteoMayor = c;
                    indiceColumnaConConteoMayor = i;
                }
                c = contarAbajoDerecha(i, filaDePiezaRecienColocada, jugador, tablero);
                if (c > conteoMayor)
                {
                    conteoMayor = c;
                    indiceColumnaConConteoMayor = i;
                }
            }
        }
    }
    return (conteoMayor, indiceColumnaConConteoMayor);
}



static int obtenerColumnaAleatoria(string jugador, string[,] tableroOriginal)
{
    while (true)
    {
        string[,] tablero = new string[FILAS, COLUMNAS];
        tablero = clonarMatriz(tableroOriginal);
        int columna = aleatorio_en_rango(0, COLUMNAS - 1);
        int resultado = colocarPieza(jugador, columna, tablero);
        if (resultado == ERROR_NINGUNO)
        {
            return columna;
        }
    }
}

static int obtenerColumnaCentral(string jugador, string[,] tableroOriginal)
{
    string[,] tablero = new string[FILAS, COLUMNAS];
    tablero = clonarMatriz(tableroOriginal);
    int mitad = (COLUMNAS - 1) / 2;
    int resultado = colocarPieza(jugador, mitad, tablero);
    if (resultado == ERROR_NINGUNO)
    {
        return mitad;
    }
    return COLUMNA_GANADORA_NO_ENCONTRADA;
}

static int elegirColumnaCpu(string jugador, string[,] tablero)
{
    // Voy a comprobar si puedo ganar...
    int posibleColumnaGanadora = obtenerColumnaGanadora(jugador, tablero);
    if (posibleColumnaGanadora != COLUMNA_GANADORA_NO_ENCONTRADA)
    {
        System.Console.Write("*elijo ganar*\n");
        return posibleColumnaGanadora;
    }
    // Si no, voy a comprobar si mi oponente gana con el siguiente movimiento, para evitarlo
    string oponente = obtenerOponente(jugador);
    int posibleColumnaGanadoraDeOponente = obtenerColumnaGanadora(oponente, tablero);
    if (posibleColumnaGanadoraDeOponente != COLUMNA_GANADORA_NO_ENCONTRADA)
    {
        System.Console.Write("*elijo evitar que mi oponente gane*\n");
        return posibleColumnaGanadoraDeOponente;
    }
    // En caso de que nadie pueda ganar en el siguiente movimiento, buscaré en dónde se obtiene el mayor
    // puntaje al colocar la pieza
    var (conteoCpu, columnaCpu) = obtenerColumnaEnLaQueSeObtieneMayorPuntaje(jugador, tablero);
    var (conteoOponente, columnaOponente) = obtenerColumnaEnLaQueSeObtieneMayorPuntaje(oponente, tablero);
    if (conteoOponente > conteoCpu)
    {
        System.Console.Write("*elijo quitarle el puntaje a mi oponente*\n");
        return columnaOponente;
    }
    else if (conteoCpu > 1)
    {
        System.Console.Write("*elijo colocarla en donde obtengo un mayor puntaje*\n");
        return columnaCpu;
    }
    // Si no, regresar la central por si está desocupada

    int columnaCentral = obtenerColumnaCentral(jugador, tablero);
    if (columnaCentral != COLUMNA_GANADORA_NO_ENCONTRADA)
    {
        System.Console.Write("*elijo ponerla en el centro*\n");
        return columnaCentral;
    }
    // Finalmente, devolver la primera disponible de manera aleatoria
    int columna = obtenerColumnaAleatoria(jugador, tablero);
    if (columna != FILA_NO_ENCONTRADA)
    {
        System.Console.Write("*elijo la primera vacía aleatoria*\n");
        return columna;
    }
    System.Console.Write("Esto no debería suceder\n");
    return 0;
}

Puedes leer los comentarios de la función para entender cómo se hace la elección de la columna. En este caso la CPU elige la mejor columna, eligiendo entre ganar o evitar que el oponente gane.

Jugando Conecta 4

Finalmente veamos cómo se desarrolla el juego. Este programa soporta 3 modos, mismos que son:

  1. Humano vs humano
  2. Humano vs CPU
  3. CPU vs CPU
static void jugar(int modo)
{
    string[,] tablero = new string[FILAS, COLUMNAS];
    limpiarTablero(tablero);
    string jugadorActual = elegirJugadorAlAzar();
    System.Console.WriteLine("Comienza el jugador " + jugadorActual);
    while (true)
    {
        int columna = 0;
        System.Console.WriteLine("\nTurno del jugador " + jugadorActual);
        dibujarTablero(tablero);
        if (modo == MODO_HUMANO_CONTRA_CPU)
        {
            if (jugadorActual == JUGADOR_CPU_2)
            {
                System.Console.Write("CPU 2 pensando...");
                columna = elegirColumnaCpu(jugadorActual, tablero);
            }
            else
            {
                columna = solicitarColumnaAJugador();
            }
        }
        else if (modo == MODO_CPU_CONTRA_CPU)
        {

            System.Console.Write($"CPU {(jugadorActual == JUGADOR_CPU_1 ? "1" : "2")} pensando...");
            columna = elegirColumnaCpu(jugadorActual, tablero);
        }
        else if (modo == MODO_HUMANO_CONTRA_HUMANO)
        {
            columna = solicitarColumnaAJugador();
        }
        int estado = colocarPieza(jugadorActual, columna, tablero);
        if (estado == ERROR_COLUMNA_LLENA)
        {
            System.Console.Write("Error: columna llena");
        }
        else if (estado == ERROR_FILA_INVALIDA)
        {
            System.Console.Write("Fila no correcta");
        }
        else if (estado == ERROR_NINGUNO)
        {
            int g = ganador(jugadorActual, tablero);
            if (g != NO_CONECTA)
            {
                dibujarTablero(tablero);
                System.Console.WriteLine("Gana el jugador " + jugadorActual);
                break;
            }
            else if (esEmpate(tablero))
            {
                dibujarTablero(tablero);
                System.Console.Write("Empate");
                break;
            }
        }
        jugadorActual = obtenerOponente(jugadorActual);
    }
}

Básicamente es un if muy simple que coloca la columna en el tablero. Lo que cambia es la solicitud de la columna dependiendo del modo, ya que el programa le dice al CPU que piense en una columna o se la pregunta al usuario.

Como puedes ver es un ciclo infinito que se rompe cuando hay un empate o hay un ganador.

Probando el juego

Puedes compilar el juego con cualquier herramienta. Yo uso el compilador del proyecto Mono, pero puedes usar Visual Studio y todas esas cosas que no me agradan de Microsoft (imagina tener que descargar un IDE solo para ejecutar un simple archivo .cs jajaja)

En fin, una vez que tengas el .exe puedes pasarle los valores de un archivo de texto para probar el juego. He incluido varios dentro del repositorio, hay uno para causar un empate, otro para ganar en diagonal, etcétera.

Por ejemplo, si yo quiero causar un empate hago esto:

Conecta4.exe < empate.txt

Obviamente hay que cambiar el nombre del ejecutable.

Poniendo todo junto

No puedo poner y colocar todo el código aquí, pues me llevaría bastante tiempo. Te dejaré el código completo junto con todas las constantes (para que puedas cambiar el tamaño de tablero de juego de Conecta 4 en C# o las piezas a conectar) en GitHub.

Jugando conecta 4 en C sharp (c#)
Jugando conecta 4 en C sharp (c#)

Como te dije anteriormente, se puede hacer en interfaz gráfica. De hecho yo lo hice en JavaScript con GUI usando el mismo algoritmo que en ANSI C, así que lo puedes portar a cualquier lenguaje.

Si quieres puedes leer más sobre C# en este otro.

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.

Dejar un comentario

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