Creando un juego de serpiente en C

CCBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este proyecto, aprenderás a crear un sencillo juego de la serpiente en C utilizando la biblioteca ncurses. Este juego clásico consiste en controlar una serpiente para que coma comida, crezca más larga y evite chocar contra las paredes y consigo misma. La funcionalidad del juego se divide en varios componentes clave: inicialización, bucle del juego, movimiento de la serpiente, detección de colisiones, etc. Al final de este proyecto, tendrás un juego básico de la serpiente que se puede ejecutar en una terminal.

👀 Vista previa

Juego de la serpiente

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo implementar el bucle del juego para actualizar la posición de la serpiente y manejar la entrada del usuario.
  • Cómo crear funciones para inicializar el juego, dibujar la ventana del juego y mostrar mensajes de fin de juego.
  • Cómo implementar la detección de colisiones para comprobar si hay colisiones con las paredes, el propio cuerpo de la serpiente y la comida.
  • Cómo desarrollar características como aumentar la longitud de la serpiente cuando come comida.

🏆 Logros

Después de completar este proyecto, podrás:

  • Utilizar la biblioteca ncurses en C para crear un juego basado en terminal.
  • Implementar la lógica del juego, incluyendo la actualización del estado del juego y el manejo de la entrada del usuario.
  • Crear y manipular estructuras de datos para representar objetos del juego, como la serpiente y la comida.
  • Implementar la detección de colisiones para establecer las reglas del juego y determinar cuándo debe terminar el juego.

Conocimientos básicos

En la era de la amplia utilización de máquinas teletype, las máquinas teletype actuaban como terminales de salida conectadas a computadoras centrales a través de cables. Los usuarios tenían que enviar una serie de comandos de control específicos al programa de terminal para controlar la salida en la pantalla del terminal. Por ejemplo, cambiar la posición del cursor en la pantalla, borrar el contenido de un área determinada de la pantalla, hacer scroll de la pantalla, cambiar los modos de visualización, subrayar texto, cambiar la apariencia, el color, la brillo, etc. de los caracteres. Estos controles se implementan a través de una cadena llamada secuencia de escape. Las secuencias de escape se llaman así porque estos bytes contínuos empiezan con un carácter 0x1B, que es el carácter de escape (el carácter introducido al presionar la tecla ESC). Incluso ahora, podemos simular el efecto de salida de las terminales teletype de esa época al introducir secuencias de escape en programas de emulación de terminal. Si quieres mostrar un texto en la terminal (o programa de emulación de terminal) con fondo coloreado, puedes introducir la siguiente secuencia de escape en tu terminal de comandos:

echo "^[[0;31;40mIn Color"

Aquí, ^ y [ son los llamados caracteres de escape. (Nota: en este caso, ^[ es un carácter. No se introduce escribiendo secuencialmente los caracteres ^ y [. Para imprimir este carácter, primero debes presionar Ctrl+V y luego la tecla ESC.) Después de ejecutar el comando anterior, deberías ver que el fondo de In Color se ha cambiado a rojo. A partir de entonces, todo el texto mostrado se saldrá con este efecto. Si quieres terminar este efecto y volver al formato original, puedes utilizar el siguiente comando:

echo "^[[0;37;40m"

¿Ahora sabes el propósito de estos caracteres (secuencias de escape)? (Intenta cambiar los parámetros entre los dos puntos y ver qué resultados obtienes.) Quizás sea diferente de lo que imaginas. Puede ser porque el entorno de la terminal es diferente, lo que depende de los diferentes terminales o sistemas operativos. (No puedes hacer que una terminal monocroma muestre caracteres coloreados, ¿verdad?) Para evitar estos problemas de compatibilidad y lograr una salida consistente en diferentes terminales, los diseñadores de UNIX inventaron un mecanismo llamado termcap. termcap es en realidad un archivo que se libera junto con las secuencias de escape. Este archivo lista todas las secuencias de escape que el terminal actual puede ejecutar correctamente, lo que garantiza que el resultado de la ejecución de las secuencias de escape introducidas coincida con las especificaciones de este archivo. Sin embargo, en los años siguientes a la invención de este mecanismo, otro mecanismo llamado terminfo fue reemplazando gradualmente a termcap. Desde entonces, los usuarios ya no necesitan consultar las complejas especificaciones de secuencias de escape en termcap cuando programan, y solo necesitan acceder a la base de datos terminfo para controlar la salida de la pantalla.

Asumiendo que todas las aplicaciones acceden a la base de datos terminfo para controlar la salida (por ejemplo, enviar caracteres de control, etc.) en la situación de utilizar terminfo, pronto estas llamadas de código harán que todo el programa sea difícil de controlar y administrar. El surgimiento de estos problemas dio lugar al nacimiento de CURSES. El nombre CURSES proviene de un juego de palabras llamado cursor optimization.

La biblioteca CURSES ofrece a los usuarios una API (interfaz de programación de aplicaciones) flexible y eficiente al encapsular los códigos de control subyacentes (secuencias de escape) de la terminal, lo que permite a los usuarios controlar el cursor, crear ventanas, cambiar los colores de primer plano y de fondo y manejar operaciones con el mouse. Esto permite a los usuarios evitar esos molestos mecanismos de bajo nivel cuando escriben aplicaciones en terminales de caracteres.

NCURSES es una clonación de CURSES a partir de System V Release 4.0 (SVr4). Es una biblioteca libremente configurable que es completamente compatible con versiones anteriores de CURSES. En resumen, es una biblioteca que permite a las aplicaciones controlar directamente la visualización de la pantalla de la terminal. Cuando se menciona la biblioteca CURSES más adelante, también se hace referencia a la biblioteca NCURSES.

NCURSES no solo encapsula las funciones de terminal subyacentes, sino que también proporciona un marco de trabajo bastante estable para generar interfaces hermosas. Incluye funciones para crear ventanas. Y sus bibliotecas hermanas, Menu, Panel y Form, son extensiones de la biblioteca base CURSES. Estas bibliotecas generalmente se distribuyen junto con CURSES. Podemos construir una aplicación que contenga múltiples ventanas, menús, paneles y formularios. Las ventanas se pueden administrar independientemente, como hacer scroll o ocultarlas. Los menús permiten a los usuarios crear opciones de comando para facilitar la ejecución de comandos. Los formularios permiten a los usuarios crear ventanas para la entrada y visualización sencilla de datos. Los paneles son extensiones de las funciones de gestión de ventanas de NCURSES y pueden superponer o apilar ventanas.

✨ Revisar Solución y Practicar

Definir constantes

Primero, abre la terminal y ejecuta el siguiente comando para instalar la biblioteca ncurses:

sudo apt-get update
sudo apt-get install libncurses5-dev

Navega hasta el directorio ~/project y crea el archivo del proyecto snake.c:

cd ~/project
touch snake.c

A continuación, necesitamos escribir el código en C. El primer paso es incluir los archivos de encabezado:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>

Antes de escribir la función main(), completemos algunas tareas básicas. Dado que estamos utilizando una interfaz de caracteres de terminal, los caracteres ASCII son esenciales. Por lo tanto, necesitamos definir algunas constantes:

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // Alimento
#define SHAPE_SNAKE      '#'  // Cuerpo de la serpiente
#define GAMEWIN_YLEN     15
#define GAMEWIN_XLEN     60
#define LOGWIN_YLEN      7
#define LOGWIN_XLEN      (GAMEWIN_XLEN)
#define LOGBUF_NUM       (LOGWIN_YLEN-2)
#define LOGBUF_LEN       (GAMEWIN_XLEN-2)
#define MAXLEVEL         12

#define GetSnakeTail(s)  ((s)->head->front)

WINDOW *logwin; // Declarar una ventana de registro
#define INITRUNLOG()     logwin = newlogw() // Crear una ventana de registro llamando a la función personalizada newlogw()
#define RUNLOG(str)      runlog(logwin, str) // Ejecutar la ventana de registro para mostrar los mensajes de juego
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // Nivel del jugador, variable global

También hemos agregado algunas definiciones de struct y enum:

enum TDirection {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
};

struct TFood {
    int y;
    int x;
};

struct TSnakeNode {
    int y;
    int x;
    struct TSnakeNode *front;
};

struct TSnake {
    int    length;
    struct TSnakeNode *head;
    enum   TDirection  dir;
};

Ahora declararemos las funciones que vamos a crear:

int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);
✨ Revisar Solución y Practicar

Función principal

int main()
{
    initscr();  /* Inicializar, entrar al modo ncurses */
    raw();      /* Deshabilitar el buffer de línea, ver los resultados inmediatamente */
    noecho();   /* No mostrar caracteres de control en la terminal, como Ctrl+C */
    keypad(stdscr, TRUE);   /* Permitir al usuario utilizar el teclado en la terminal */
    curs_set(0);    /* Establecer la visibilidad del cursor, 0 es invisible, 1 es visible, 2 es completamente visible */
    refresh();      /* Escribir el contenido de la pantalla virtual en la pantalla y actualizar */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Presiona 'q' o 'Q' para salir.");
    RUNLOG("  Presiona 'w/s/a/d' o 'W/S/A/D' para mover la serpiente.");
    RUNLOG("Información:");

    WINDOW *gwin = newgamew(); /* Crear la ventana del juego, implementado por una función personalizada llamada newgamew */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

    while (refreshgamew(gwin, psnake) >= 0)
        ;

    /* getch() es diferente de getchar() */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* Limpiar la ventana del juego y liberar la memoria e información de la estructura de datos de la ventana */
    DESTROYRUNLOG(); /* Limpiar la ventana de visualización de información */
    endwin();        /* Salir del modo ncurses */

    return 0;
}

En keypad(stdscr, TRUE), stdscr se refiere a una ventana virtual donde todas nuestras operaciones se escriben primero, y luego el contenido de stdscr se muestra en la pantalla utilizando la función refresh.

Cuando usamos printw, los datos se imprimen en realidad en una ventana virtual llamada stdscr, que no se envía directamente a la pantalla. El propósito de la función printw() es escribir continuamente algunos marcadores de visualización y estructuras de datos relacionadas en la pantalla virtual y escribir estos datos en el búfer de stdscr. Entonces, para mostrar los datos en este búfer, debemos usar la función refresh() para decirle al sistema curses que envíe el contenido del búfer a la pantalla. Este mecanismo permite al programador escribir datos continuamente en la pantalla virtual y que parezca que se hace todo de una vez cuando se llama a refresh(). Debido a que la función refresh() solo verifica las partes de la ventana y los datos que han cambiado, este diseño flexible proporciona un mecanismo de retroalimentación eficiente.

✨ Revisar Solución y Practicar

Mecanismo de ventanas

El mecanismo de ventanas es un concepto central de CURSES. Como has visto en los ejemplos anteriores, todas las funciones operan por defecto en una "ventana" (stdscr). Incluso si estás diseñando la interfaz de usuario gráfica (GUI) más simple, todavía necesitarás utilizar ventanas. Una de las principales razones para utilizar ventanas es que puedes dividir la pantalla en diferentes partes y operar dentro de ellas simultáneamente. Esto puede mejorar la eficiencia. Otra razón es que siempre debes esforzarte por un diseño mejor y más manejable en tu programa. Si estás diseñando una interfaz de usuario grande y compleja, pre-diseñar estas partes mejorará tu eficiencia.

WINDOW* newlogw()
{
    /* Los parámetros son: altura, anchura, posición de inicio (y,x) de la ventana */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* Los parámetros son: puntero a la ventana conocida, 0 y 0 son las posiciones de inicio predeterminadas de fila y columna de los caracteres */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // Actualizar la ventana especificada

    return win;
}

En la función WINDOW* newlogw(), se puede ver que la creación de una ventana comienza con newwin(). Aunque hemos creado una ventana, todavía no es visible, lo que es similar a un elemento <div> en HTML. Si no le agregas estilos a un elemento <div>, no verás nada en la página web. Entonces necesitamos utilizar la función box para agregar bordes a la ventana conocida.

mvwprintw imprime el contenido especificado en las coordenadas (y,x) especificadas dentro de la ventana especificada.

✨ Revisar Solución y Practicar

Mostrar información del juego

RUNLOG es una macro que llama a la función personalizada runlog.

void runlog(WINDOW *win, char *str)
{
    static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
    static int  index = 0;

    strcpy(logbuf[index], str);

    int i = 0;

    /* #define LOGBUF_NUM  (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
    for (; i < LOGBUF_NUM; ++i) {

        /* Función personalizada, cleanline */
        cleanline(win, i+1, 1);

        /* Imprimir cadena en cada línea */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

    index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}

runlog toma una ventana conocida y la información a mostrar como parámetros. La str se almacenará en una matriz bidimensional (logbuf) utilizando la función strcpy. Luego, se llama a la función personalizada cleanline para limpiar la línea que se imprimirá a continuación. La función mvwprintw se utiliza para imprimir la información.

/* Limpiar las coordenadas (x,y) de la ventana win */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* Establecer las posiciones 0-56 del array en caracteres vacíos */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* Mover el cursor a la posición (y,x) en la ventana win y imprimir la cadena EMPTYLINE */
    mvwprintw(win, y, x, EMPTYLINE);

    /* Mostrar el contenido en la ventana especificada */
    wrefresh(win);
}
✨ Revisar Solución y Practicar

Inicializar la cola de la serpiente

Ahora, definirás la estructura de datos de la serpiente y crearás funciones para inicializarla y dibujarla en la ventana del juego.

struct TSnake* initsnake()
{
    struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // Inicializar la longitud de la serpiente a 4

    newsnakenode (
        &newsnakenode (
            &newsnakenode (
                &newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
            )->front, 4, 52
        )->front, 4, 51
    )->front = psnake->head;

    return psnake;
}

struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
    *ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
    (*ppsnode)->y = y;
    (*ppsnode)->x = x;
    (*ppsnode)->front = NULL;

    return *ppsnode;
}

El propósito de estas dos funciones es inicializar la estructura de datos de la serpiente en un juego de serpiente, incluyendo establecer la dirección y la longitud inicial de la serpiente, así como crear una serpiente con un nodo inicial de cuerpo. La función newsnakenode se utiliza para crear nodos individuales, y luego estos nodos se conectan juntos a través de llamadas anidadas de funciones para formar la serpiente inicial.

✨ Revisar Solución y Practicar

Mostrar la serpiente en la ventana del juego

void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
    static int taily = 0;
    static int tailx = 0;
    if (taily!= 0 && tailx!= 0) {
        mvwaddch(win, taily, tailx, ' ');
    }

    /* #define GetSnakeTail(s)  ((s)->head->front) */
    struct TSnakeNode *psnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i) {
        mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
        psnode = psnode->front;
    }

    taily = GetSnakeTail(psnake)->y;
    tailx = GetSnakeTail(psnake)->x;

    wrefresh(win);
}

Para mostrar las coordenadas (y,x) de cada nodo, podemos utilizar la definición de macro GetSnakeTail para recuperar las coordenadas. El paso restante es utilizar un bucle para mostrarlas utilizando mvwaddch. mvwaddch se utiliza para mover el cursor a la posición especificada (taily,tailx) en la ventana especificada (win) y luego emitir un carácter.

Para mostrarlo en realidad en la pantalla, necesitamos utilizar wrefresh(win).

✨ Revisar Solución y Practicar

Escribir el núcleo del juego

Mire la punto y coma después de while (refreshgamew(gwin, psnake) >= 0);. Esto establece un bucle para manejar el movimiento de la serpiente y el cambio de longitud, así como la detección de límites y otros problemas.

El bucle while llama a la función refreshgamew:

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* Cuando se inicia el juego o cuando se come la comida, ffood=False, y se ejecuta drawfoodw para volver a dibujar la comida */
    if (!ffood) {
        drawfoodw(win, &pfood, psnake);
        ffood = True;
    }
    int key = -1;

    fd_set set;
    FD_ZERO(&set);
    FD_SET(0, &set);

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;

    // A continuación, usaremos la API del núcleo select, que escuchará una serie de entradas de teclado y ratón. Usamos `key=getch()` y `switch` para determinar el tipo de entrada. El segundo `switch` se utiliza para mostrar el nivel y la velocidad de movimiento actual de la serpiente.
    if (select(1, &set, NULL, NULL, &timeout) < 0)
        return -1;

    if (FD_ISSET(0, &set)) {
        while ((key = getch()) == -1);

        switch (key) {
        case 'w':
        case 'W':
            (psnake->dir == DIR_DOWN)? : (psnake->dir = DIR_UP);
            break;

        case's':
        case 'S':
            (psnake->dir == DIR_UP)? : (psnake->dir = DIR_DOWN);
            break;

        case 'a':
        case 'A':
            (psnake->dir == DIR_RIGHT)? : (psnake->dir = DIR_LEFT);
            break;

        case 'd':
        case 'D':
            (psnake->dir == DIR_LEFT)? : (psnake->dir = DIR_RIGHT);
            break;

        case 'q':
        case 'Q':
            RUNLOG("Quit Game!");
            gameover(win, "Quit Game!");
            return -1;

        default:
            break;
        }
    }
    movesnake(psnake);
    drawsnakew(win, psnake);
    switch (checksnake(&pfood, psnake)) {
    case 0:
        break;

    // Se ha comido la comida, establece ffood en 0 para volver a dibujar la comida.
    case 1:
        ffood = False;
        if (++g_level > MAXLEVEL) {
            RUNLOG("Win!!!");
            gameover(win, "Win!!!");
            return -1;
        }
        mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
        mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
        wrefresh(win);
        RUNLOG("Level UP!");
        snakegrowup(&pfood, psnake);
        break;

    default:
        RUNLOG("Game over!");
        gameover(win, "Game over!");
        return -1;
    }

    return 1;
}
✨ Revisar Solución y Practicar

El movimiento de la Serpiente Voraz

El movimiento de la Serpiente Voraz se implementa utilizando la función movesnake. Analizaremos el segmento case DIR_UP, ya que el resto es similar:

/* La estructura TSnake es una lista enlazada invertida con la cabeza y la cola conectadas.
 * Ejemplo: [a]<-[b]<-[c]<-[d]    a es la cabeza
 *          |              ^     Cuando la serpiente se mueve, solo la cabeza apunta a d,
 *          `--------------'     y las coordenadas (y,x) de d se modifican a la posición a la que se mueve la cabeza. */
void movesnake(struct TSnake *psnake)
{
    int hy = psnake->head->y;
    int hx = psnake->head->x;

    psnake->head = GetSnakeTail(psnake);

    switch (psnake->dir) {
    case DIR_UP:
        psnake->head->y = hy - 1;
        psnake->head->x = hx;
        break;

    case DIR_DOWN:
        psnake->head->y = hy + 1;
        psnake->head->x = hx;
        break;

    case DIR_LEFT:
        psnake->head->y = hy;
        psnake->head->x = hx - 1;
        break;

    case DIR_RIGHT:
        psnake->head->y = hy;
        psnake->head->x = hx + 1;
        break;

    default:
        break;
    }
}

Dado que el origen de coordenadas se encuentra en la esquina superior izquierda, con los valores de (y,x) aumentando hacia abajo y hacia la derecha, el sistema de coordenadas se invierte 180 grados a lo largo del eje x. Por lo tanto, para hacer que la serpiente se mueva hacia arriba, se resta 1. Así, cuando el jugador presiona la tecla w/W, la serpiente se moverá hacia arriba.

✨ Revisar Solución y Practicar

Aumentar la longitud del cuerpo de la serpiente

Para aumentar la longitud del cuerpo de la serpiente, se puede lograr implementando la función snakegrowup, lo cual es relativamente sencillo. Simplemente agrega un nuevo nodo a la cola.

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // Asigna memoria para un nuevo nodo de serpiente (struct TSnakeNode) y asigna el puntero pnode al nuevo nodo. Este nuevo nodo se convertirá en la nueva cabeza de la serpiente.
    struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));

    switch (psnake->dir) {
    case DIR_UP:
        pnode->y = psnake->head->y - 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_DOWN:
        pnode->y = psnake->head->y + 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_LEFT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x - 1;
        break;

    case DIR_RIGHT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x + 1;
        break;

    default:
        break;
    }

    // Establece el puntero front del nuevo nodo (pnode) en la cola actual de la serpiente para establecer una conexión entre la nueva cabeza y la cola.
    pnode->front = GetSnakeTail(psnake);

    // Establece el puntero front de la cabeza actual de la serpiente en el nuevo nodo para asegurar que el nuevo nodo se convierta en la nueva cabeza.
    psnake->head->front = pnode;
    psnake->head = pnode;

    // Incrementa la longitud de la serpiente para indicar que el cuerpo de la serpiente ha crecido en una unidad.
    ++psnake->length;
}

La declaración switch determina las coordenadas del nuevo nodo de cabeza según la dirección de movimiento actual de la serpiente (psnake->dir). Dependiendo de las diferentes direcciones, actualiza las coordenadas del nuevo nodo para moverse una celda hacia arriba, abajo, izquierda o derecha desde la cabeza actual de la serpiente (psnake->head).

El propósito de este código es aumentar la longitud del cuerpo de la serpiente en el juego de la Serpiente. Crea un nuevo nodo de cabeza basado en la dirección de movimiento actual de la serpiente e inserta este nuevo nodo de cabeza al principio de la serpiente, haciendo que el cuerpo de la serpiente sea más largo. Esta es la lógica central para aumentar la longitud del cuerpo de la serpiente después de que consume comida en el juego de la Serpiente.

✨ Revisar Solución y Practicar

La ubicación de la producción de comida

La función drawfoodw se utiliza para dibujar la comida en la ventana del juego (win):

void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
    do {
        pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
        pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
    } while (False == checkfood(pfood, psnake));
    checkfood(pfood, psnake);

    mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
    wrefresh(win);
}
  • La función random() se utiliza para generar valores de coordenadas aleatorios, lo que hará que la comida aparezca en una posición aleatoria dentro de la ventana del juego.
  • Utilizando un bucle do-while, se verifica si la posición de la comida generada coincide con cualquier nodo del cuerpo de la serpiente. Si hay una coincidencia, se genera una nueva posición de comida hasta que se encuentra una posición que no coincida con el cuerpo de la serpiente.
  • La función mvwaddch se utiliza para dibujar la forma de la comida en la posición especificada en la ventana del juego.

La función checkfood se utiliza para verificar que la comida no aparezca en la serpiente:

TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
    struct TSnakeNode *pnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i, pnode = pnode->front)
        if (pfood->y == pnode->y && pfood->x == pnode->x)
            return False;

    return True;
}
  • Primero, obtiene el nodo de la cola de la serpiente. Luego, utiliza un bucle para iterar a través de todos los nodos del cuerpo de la serpiente y verifica si las coordenadas de la comida coinciden con las coordenadas de cualquier nodo.
  • Si las coordenadas de la comida coinciden con las coordenadas de cualquier nodo de la serpiente, significa que la posición de la comida coincide con un nodo del cuerpo de la serpiente, y la función devuelve False.
  • Si no se encuentra ninguna coincidencia, la función devuelve True, lo que indica que la posición de la comida es válida.
✨ Revisar Solución y Practicar

Detección de límites

checksnake se utiliza para comprobar si la serpiente ha chocado con los bordes del juego. Esto incluye la detección de los bordes superior, inferior, izquierdo y derecho del juego. También se comprueba si la serpiente ha chocado con su propio cuerpo, ya que ambas situaciones harían que el juego terminara.

int checksnake(struct TFood *pfood, struct TSnake *psnake)
{

	/* Comprueba si las coordenadas de la cabeza de la serpiente han chocado con los bordes del juego superior, inferior, izquierdo o derecho */
    if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
      || psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
    {
        return -1;
    }

    struct TSnakeNode *pnode = GetSnakeTail(psnake);
    int i = 0;

	/* Comprueba que la cabeza de la serpiente no choque con ninguna parte de su cuerpo */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

  	/* Por supuesto, se permite chocar con la comida */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // No se produjo un choque
}
✨ Revisar Solución y Practicar

Fin del juego

La función gameover se utiliza para mostrar el mensaje de fin del juego en la ventana especificada (win) cuando termina el juego de la serpiente:

void gameover(WINDOW *win, char *str)
{
    mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
    mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Presiona cualquier tecla para salir...");
    wrefresh(win);
}
  • Utiliza la función mvwprintw para imprimir el texto del mensaje de fin del juego str en el centro de la ventana. Esto asegura que el mensaje de fin del juego se muestre centrado en la ventana.
  • A continuación, imprime "Presiona cualquier tecla para salir..." en la siguiente línea en el centro de la ventana, invitando al jugador a presionar cualquier tecla para salir del juego.
  • Finalmente, utiliza la función wrefresh para actualizar la ventana y asegurar que el mensaje de fin del juego y la invitación a salir se dibujen correctamente en la pantalla.

Para evitar fugas de memoria, utiliza la función destroysnake para liberar la memoria ocupada por el juego de la serpiente, incluyendo los nodos de la serpiente y la estructura de la serpiente misma. Esta es una etapa de limpieza común cuando el juego termina o se reinicia.

void destroysnake(struct TSnake *psnake)
{
    struct TSnakeNode *psnode = GetSnakeTail(psnake);
    struct TSnakeNode *ptmp   = NULL;

    int i = 0;
    for (; i < psnake->length; ++i) {
        ptmp   = psnode;
        psnode = psnode->front;
        free(ptmp);
    }

    free(psnake);
    psnake = NULL;
}

Dentro de la función, primero declara dos punteros, psnode y ptmp, para iterar sobre la lista enlazada de nodos de la serpiente.

Ingresa en un bucle que itera el número de veces igual a la longitud (número de nodos) de la serpiente. En cada iteración, realiza las siguientes operaciones:

  • Establece ptmp para que apunte al nodo actual psnode para liberar más tarde la memoria del nodo actual.
  • Mueve psnode al siguiente nodo (apuntado por front).

Después de que el bucle termine, todos los nodos de la serpiente han sido liberados.

Finalmente, la función libera la memoria para la estructura de la serpiente misma, free(psnake), y establece el puntero psnake que apunta a la estructura de la serpiente en NULL para asegurar de que la memoria liberada ya no se utilice.

✨ Revisar Solución y Practicar

Compilación y ejecución

La instrucción de compilación es un poco diferente a la habitual. Requiere agregar la opción -l a gcc para incluir la biblioteca ncurses:

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Snake Game
✨ Revisar Solución y Practicar

Resumen

Has creado con éxito un sencillo juego de la serpiente en el lenguaje de programación C utilizando la biblioteca ncurses. El juego incluye una ventana de juego, el movimiento de la serpiente, la generación de comida y la detección de colisiones. Siguiendo los pasos anteriores, puedes ejecutar y disfrutar del juego en la terminal. ¡Que te diviertas jugando!