Tres en línea (tic tac toe) programado en C - Desarrollo del juego

Tres en línea en C – Programación de juego

En este post de programación en C te mostraré el código fuente para el juego conocido como tres en línea, tres en raya, tic tac toe, gatitos, etcétera.

Tres en línea (tic tac toe) programado en C - Desarrollo del juego
Tres en línea (tic tac toe) programado en C – Desarrollo del juego

He programado el juego en C estándar así que se puede compilar en varios sistemas operativos, y también se puede compilar con un compilador de C++.

El juego de tres en línea que he desarrollado en C soporta el modo jugador contra jugador, jugador contra CPU (con una pequeña inteligencia artificial) y CPU contra CPU. A lo largo del post te explicaré el código y cómo es que funciona cada parte del programa.

Por cierto, en este caso el juego se desarrolla en la consola pero se le puede agregar una interfaz usando alguna librería como Allegro.

Funcionamiento básico del juego

Todo el juego se desarrolla en una matriz de tipo char en donde estará un espacio vacío, la X o la O. En cada turno, se eligen unas coordenadas (ya sea que se le soliciten al usuario o el programa las elija) y se coloca la pieza.

Empate en tres en línea con C
Empate en tres en línea con C

Inmediatamente después de colocar la pieza se evalúa si hay un ganador. Se dice que hay un ganador cuando se pueden conectar 3 piezas en una línea recta sin importar la dirección de ésta, así que es necesario hacer un conteo en todas las direcciones para saber si alguien gana en este juego de tic tac toe en C.

Si no hay un ganador, se comprueba si hay un empate. Para saber si hay empate se recorre todo el tablero verificando que no haya ningún espacio vacío.

Finalmente si no hay ganador ni un empate, se le pasa el turno al oponente del jugador actual. Así hasta que alguna condición que termina el juego se cumple.

Lo que expliqué arriba se traduce en el siguiente código de C:

// Loop principal del juego
void iniciarJuego(int modo)
{
    if (modo != JUGADOR_JUGADOR && modo != JUGADOR_CPU && modo != CPU_CPU)
    {
        printf("Modo de juego no permitido");
        return;
    }

    // Para que salgan cosas aleatorias
    srand(getpid());
    // Iniciar tablero de juego
    char tablero[FILAS][COLUMNAS];
    // Y limpiarlo
    limpiarTablero(tablero);
    // Elegir jugador que inicia al azar
    char jugadorActual = jugadorAleatorio();
    printf("El jugador que inicia es: %c\n", jugadorActual);
    int x, y;
    // Y allá vamos
    while (1)
    {
        imprimirTablero(tablero);
        if (modo == JUGADOR_JUGADOR || (modo == JUGADOR_CPU && jugadorActual == JUGADOR_X))
        {
            printf("Jugador %c. Ingresa coordenadas (x,y) para colocar la pieza separadas por una coma. Por ejemplo: 5,5\n", jugadorActual);
            scanf("%d,%d", &x, &y);
            // Al usuario se le solicitan números comenzando a contar en 1, pero en los arreglos comenzamos desde el 0
            // así que necesitamos restar uno en ambas variables
            x--;
            y--;
        }
        else if (modo == CPU_CPU || (modo == JUGADOR_CPU && jugadorActual == JUGADOR_CPU_O))
        {
            // Si es modo CPU contra CPU o es el turno del CPU, dejamos que las coordenadas las elija
            // el programa
            elegirCoordenadasCpu(jugadorActual, tablero, &y, &x);
        }
        // Sin importar cuál modo haya sido, colocamos la pieza según las coordenadas elegidas
        colocarPieza(y, x, jugadorActual, tablero);
        // Puede que después de colocar la pieza el jugador gane o haya un empate, así que comprobamos
        if (comprobarSiGana(jugadorActual, tablero))
        {
            imprimirTablero(tablero);
            printf("El jugador %c gana\n", jugadorActual);
            return;
        }
        else if (empate(tablero))
        {
            imprimirTablero(tablero);
            printf("Empate");
            return;
        }
        // Si no, es turno del otro jugador
        jugadorActual = oponenteDe(jugadorActual);
    }
}

El código es muy simple y los comentarios proporcionan detalles extra de lo que se está haciendo (por si los nombres de las funciones no son obvios).

Modos de juego de tres en línea

Como mencioné al inicio, este juego tiene tres modos. En el primer caso se puede jugar un humano contra un humano. En cada turno se solicitan las coordenadas (números separados por coma) en donde se coloca la pieza que puede ser O o X.

Para el modo de CPU contra humano, el CPU tiene que pensar en su mejor movimiento; justo ahí es en donde se hace una pequeña simulación del lugar donde le conviene colocar la pieza.

Finalmente en el modo CPU contra CPU se enfrenta a la computadora contra sí misma.

Eligiendo la mejor posición según inteligencia artificial

Victoria del CPU en Tic tac toe programado en C
Victoria del CPU en Tic tac toe programado en C

Para que el CPU elija en dónde colocar las piezas debe hacer algunas simulaciones de lo que pasa si coloca la pieza en todas las opciones posibles. El orden que sigue es:

  1. Ganar si se puede
  2. Hacer perder al oponente si está a punto de ganar
  3. Tomar el mejor movimiento del oponente (en donde obtiene el mayor puntaje)
  4. Tomar mi mejor movimiento (en donde obtengo mayor puntaje)
  5. Elegir la de la esquina superior izquierda (0,0)
  6. Coordenadas aleatorias

En el peor de los casos elegirá alguna posición aleatoria, pero también es capaz de atacar y defenderse.

Por cierto, esto está altamente inspirado en el juego de Conecta 4. El código para este caso es el siguiente:

// Hace que el CPU elija unas coordenadas para ganar
void elegirCoordenadasCpu(char jugador, char tablero[FILAS][COLUMNAS], int *yDestino, int *xDestino)
{
    hablar("Estoy pensando...", jugador);
    /*
    El orden en el que el CPU infiere las coordenadas que toma es:
    1. Ganar si se puede
    2. Hacer perder al oponente si está a punto de ganar
    3. Tomar el mejor movimiento del oponente (en donde obtiene el mayor puntaje)
    4. Tomar mi mejor movimiento (en donde obtengo mayor puntaje)
    5. Elegir la de la esquina superior izquierda (0,0)
    6. Coordenadas aleatorias
    */
    int y, x, conteoJugador, conteoOponente;
    char oponente = oponenteDe(jugador);
    // 1
    coordenadasParaGanar(jugador, tablero, &y, &x);
    if (y != -1 && x != -1)
    {
        hablar("Ganar", jugador);
        *yDestino = y;
        *xDestino = x;
        return;
    }
    // 2
    coordenadasParaGanar(oponente, tablero, &y, &x);
    if (y != -1 && x != -1)
    {
        hablar("Tomar victoria de oponente", jugador);
        *yDestino = y;
        *xDestino = x;
        return;
    }
    // 3
    coordenadasParaMayorPuntaje(jugador, tablero, &y, &x, &conteoJugador);
    coordenadasParaMayorPuntaje(oponente, tablero, &y, &x, &conteoOponente);
    if (conteoOponente > conteoJugador)
    {
        hablar("Tomar puntaje mayor del oponente", jugador);
        *yDestino = y;
        *xDestino = x;
        return;
    }
    else
    {
        hablar("Tomar mi mayor puntaje", jugador);
        *yDestino = y;
        *xDestino = x;
        return;
    }
    // 4
    if (coordenadasVacias(0, 0, tablero))
    {
        hablar("Tomar columna superior izquierda", jugador);
        *yDestino = 0;
        *xDestino = 0;
        return;
    }
    // 5
    hablar("Coordenadas aleatorias", jugador);
    obtenerCoordenadasAleatorias(jugador, tablero, yDestino, xDestino);
}

Es importante que mencione que se reciben dos apuntadores a dos enteros (variables por referencia) porque en C no podemos devolver dos valores en una función, así que mejor se reciben los apuntadores.

Dentro del código también vemos algunas funciones ayudantes, por ejemplo la función que devuelve el oponente de un jugador.

Saber si gana o es empate

Para saber si un jugador gana se verifica si se forma una línea recta y si el conteo es de 3 (por eso es que se llama tres en línea). Las funciones que cuentan y verifican son:

/*
Funciones de conteo. Simplemente cuentan cuántas piezas del mismo jugador están
alineadas
*/

int contarHaciaArriba(int x, int y, char jugador, char tablero[FILAS][COLUMNAS])
{
    int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
    int contador = 0;
    for (; yInicio <= y; yInicio++)
    {
        if (tablero[yInicio][x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
    }
    return contador;
}

int contarHaciaDerecha(int x, int y, char jugador, char tablero[FILAS][COLUMNAS])
{
    int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
    int contador = 0;
    for (; x <= xFin; x++)
    {
        if (tablero[y][x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
    }
    return contador;
}

int contarHaciaArribaDerecha(int x, int y, char jugador, char tablero[FILAS][COLUMNAS])
{
    int xFin = (x + CONTEO_PARA_GANAR < COLUMNAS) ? x + CONTEO_PARA_GANAR - 1 : COLUMNAS - 1;
    int yInicio = (y - CONTEO_PARA_GANAR >= 0) ? y - CONTEO_PARA_GANAR + 1 : 0;
    int contador = 0;
    while (x <= xFin && yInicio <= y)
    {
        if (tablero[y][x] == jugador)
        {
            contador++;
        }
        else
        {
            contador = 0;
        }
        x++;
        y--;
    }
    return contador;
}

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

// Indica si el jugador gana
int comprobarSiGana(char jugador, char tablero[FILAS][COLUMNAS])
{
    int y;
    for (y = 0; y < FILAS; y++)
    {
        int x;
        for (x = 0; x < COLUMNAS; x++)
        {
            if (
                contarHaciaArriba(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
                contarHaciaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
                contarHaciaArribaDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR ||
                contarHaciaAbajoDerecha(x, y, jugador, tablero) >= CONTEO_PARA_GANAR)
            {
                return 1;
            }
        }
    }
    // Terminamos de recorrer y no conectó
    return 0;
}

Es un algoritmo simple pero tedioso, pues se tiene que recorrer el tablero desde una posición hacia todas las direcciones posibles. En caso de que se conecte hacia arriba, la derecha, arriba derecha o abajo derecha, se devuelve un 1 indicando que el jugador gana.

Si el jugador no gana, se devuelve un 0. Y así es como se define si un jugador obtiene la victoria en este juego de gatitos en C. Ya para el caso de la verificación del empate el código queda así:

// Debería llamarse después de verificar si alguien gana
// Indica si hay un empate
int empate(char tableroOriginal[FILAS][COLUMNAS])
{
    int y;
    for (y = 0; y < FILAS; y++)
    {
        int x;
        for (x = 0; x < COLUMNAS; x++)
        {
            // Si hay al menos un espacio vacío se dice que no hay empate
            if (tableroOriginal[y][x] == ESPACIO_VACIO)
            {
                return 0;
            }
        }
    }
    return 1;
}

Poniendo todo junto

Hasta este punto te he explicado el funcionamiento general del juego. Obviamente te dejo el código fuente completo en mi GitHub, para que puedas analizarlo, mejorarlo, ejecutarlo, etcétera.

El código completo es un poco más extenso pero igualmente se explica por sí mismo. Y así es como se puede tener el juego de Tic Tac Toe completamente en C.

Recuerda que este código se puede compilar con gcc, clang y g++. También lo puedes importar en Dev C++, CodeBlocks, CLion, etcétera.

Te dejo con más tutoriales de C en mi blog.

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 *