/* * Created by snapshot112 on 15/10/2025 */ #define _POSIX_C_SOURCE 199309 #include "snake.h" #include #include #include #include #include #include "../../engine/grid_game_engine.h" #define CELL_EMPTY ' ' #define CELL_FOOD '$' #define CELL_WALL '#' #define DIRECTION_COUNT 4 #define OFFSET_X 6 #define OFFSET_Y 3 typedef enum { SNAKE_MOVE = 0, SNAKE_EAT = 1, SNAKE_DIE = 2 } snake_action; typedef enum { DIRECTION_UP = 0, DIRECTION_RIGHT = 1, DIRECTION_DOWN = 2, DIRECTION_LEFT = 3 } direction; // Snake globals static direction PREVIOUS_DIRECTION; static direction CURRENT_DIRECTION; static coordinate SNAKE_HEAD; static coordinate SNAKE_TAIL; // Map globals static coordinate RENDER_AT; static coordinate MESSAGE_LOCATION = {0,0}; static int MAP_HEIGHT = 20; static int MAP_WIDTH = 20; static grid *GRID; static pthread_mutex_t SNAKE_MUTEX; /* * Create a snake body part. * * Input: * dir: the direction the body part should point to. * * Output: * a character representing that body part. */ static char get_body_part(const direction dir) { return (char)(dir + '0'); } /* * Gets the direction of a body part. * * Input: * body_part: A part of the snake's body. * * Output: * The direction the next body part is pointing to. */ static direction get_body_part_direction(const char body_part) { return (direction)(body_part - '0'); } /* * Create a grid for snake with a given height and width. * * Input: * height: The height of the map. * width: The width of the map. * * Returns: * A pointer to the grid. */ static void create_grid(void) { const int grid_size = (MAP_WIDTH + 1) * MAP_HEIGHT + 1; char *map = malloc(grid_size * sizeof(char)); if (map == NULL) { return; } for (int i = 1; i <= (MAP_WIDTH + 1) * MAP_HEIGHT; i++) { // Also subtract the null terminator const int bottom_line = i > grid_size - (MAP_WIDTH + 2); const int top_line = i < MAP_WIDTH + 1; const int line_position = modulo(i, MAP_WIDTH + 1); const int line_start = line_position == 1; const int line_end = line_position == MAP_WIDTH; const int newline = line_position == 0; if (newline) { map[i - 1] = '\n'; } else if (top_line || bottom_line || line_start || line_end) { map[i - 1] = CELL_WALL; } else { map[i - 1] = CELL_EMPTY; } } map[grid_size - 1] = '\0'; GRID = grid_create_from_string(map); free(map); } /* * Spawn a piece of food at an empty grid location. * * Side Effect: * One of the empty grid spaces gets replaced with a piece of food. */ static void generate_food(void) { coordinate empty_spots[MAP_HEIGHT * MAP_WIDTH]; int available_spots = 0; for (int x = 0; x < MAP_WIDTH; x++) { for (int y = 0; y < MAP_HEIGHT; y++) { if (grid_fetch(GRID, x, y) == CELL_EMPTY) { const coordinate new_spot = {x, y}; empty_spots[available_spots] = new_spot; available_spots++; } } } const coordinate food_location = empty_spots[modulo(rand(), available_spots)]; grid_put(GRID, food_location.x, food_location.y, CELL_FOOD); } /* * Setup the game(map) * * Output: * A pointer to the game grid. * * Side Effects: * Seed random with the current time. * (Re)set the global variables. * initialize the mutex * */ static void initialize(void) { // Seed random. srand(time(NULL)); // Create the grid. create_grid(); if (GRID == NULL) { return; } // Set globals. CURRENT_DIRECTION = DIRECTION_DOWN; PREVIOUS_DIRECTION = DIRECTION_DOWN; SNAKE_HEAD = (coordinate){.x = grid_width(GRID) / 2, .y = grid_height(GRID) / 2}; SNAKE_TAIL = SNAKE_HEAD; RENDER_AT = (coordinate){.x = OFFSET_X, .y = OFFSET_Y}; MESSAGE_LOCATION = (coordinate){.x = RENDER_AT.x, .y = RENDER_AT.y + grid_height(GRID) + 2}; // Create the first body part and spawn the first piece of food. grid_put(GRID, SNAKE_HEAD.x, SNAKE_HEAD.y, get_body_part(CURRENT_DIRECTION)); generate_food(); pthread_mutex_init(&SNAKE_MUTEX, NULL); } /* * Checks what happens when the snake moves over a given char. * * Input: * c: The char to check. * * Returns: * The snake will move forward: SNAKE_MOVE * The snake will eat an apple: SNAKE_EAT * The snake will die: SNAKE_DIE */ static snake_action collision_check(const char c) { if (c == CELL_EMPTY) { return SNAKE_MOVE; } if (c == CELL_FOOD) { return SNAKE_EAT; } return SNAKE_DIE; } /* * Move the snake to a given location * * Input: * new_location: The location the snake should move to. * * Side effects: * In case nothing is encountered: The snake moves to the new location. * In case an obstacle is encountered (wall or part of the snake): The snake dies. * In case a piece of food is encountered: The snake eats the food and grows by 1. * * Note: * Moving to a location the snake can't reach is undefined behaviour. */ static void update_snake(const coordinate new_location) { const snake_action action = collision_check(grid_fetch(GRID, new_location.x, new_location.y)); if (action == SNAKE_DIE) { grid_put_state(GRID, STATE_VERLOREN); return; } if (action == SNAKE_MOVE) { coordinate new_tail = SNAKE_TAIL; switch (get_body_part_direction(grid_fetch(GRID, SNAKE_TAIL.x, SNAKE_TAIL.y))) { case DIRECTION_UP: new_tail.y--; break; case DIRECTION_RIGHT: new_tail.x++; break; case DIRECTION_DOWN: new_tail.y++; break; case DIRECTION_LEFT: new_tail.x--; break; } grid_put(GRID, SNAKE_TAIL.x, SNAKE_TAIL.y, CELL_EMPTY); SNAKE_TAIL = new_tail; } // New head placed after tail moves. It can occupy the empty space created by the tail moving. grid_put(GRID, new_location.x, new_location.y, get_body_part(CURRENT_DIRECTION)); SNAKE_HEAD = new_location; PREVIOUS_DIRECTION = CURRENT_DIRECTION; if (action == SNAKE_EAT) { generate_food(); } } /* * Handle snake movement. * * Input: * NULL, this signature is so it can be run as a thread. * * Returns: * NULL when finished * * Side Effects: * While the game is running, it moves the snake forward every 0.25 seconds and updates the game * state accordingly. * * In case an obstacle is encountered (wall or part of the snake): It dies. * In case a piece of food is encountered: It eats the apple and grows by 1. */ static void *snake_move(void *arg) { struct timespec timer; timer.tv_sec = 0; timer.tv_nsec = 250000000L; // Snake moves every 0.25 seconds. while (grid_fetch_state(GRID) == STATE_AAN_HET_SPELEN) { nanosleep(&timer, NULL); pthread_mutex_lock(&SNAKE_MUTEX); coordinate new_location = SNAKE_HEAD; switch (CURRENT_DIRECTION) { case DIRECTION_UP: new_location.y--; break; case DIRECTION_RIGHT: new_location.x++; break; case DIRECTION_DOWN: new_location.y++; break; case DIRECTION_LEFT: new_location.x--; break; } update_snake(new_location); show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y); pthread_mutex_unlock(&SNAKE_MUTEX); } return NULL; } /* * Turn the snake in the given direction. * * Input: * direction: The direction the snake should turn to. * * Side effects: * If the snake is able to turn in the given direction: * The snake's direction gets updated with the new value. * If the snake is unable to turn in the given direction: * The snake's direction remains unchanged. */ static void turn_snake(const direction dir) { // If the snake has a length of 1, it is able to turn around on the spot. if ((direction)modulo((int)dir + 2, DIRECTION_COUNT) == PREVIOUS_DIRECTION && !same_coordinate(SNAKE_HEAD, SNAKE_TAIL)) { return; } grid_put(GRID, SNAKE_HEAD.x, SNAKE_HEAD.y, get_body_part(dir)); CURRENT_DIRECTION = dir; show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y); } /* * Handle user input. * * Input: * NULL, this signature is so it can be run as a thread. * * Returns: * NULL when finished * * Side Effects: * While the game is running, it constantly updates the direction the snake is pointing to * based on user input. * * In case ESCAPE or BACKSPACE is pressed it quits the game. */ static void *user_input(void *arg) { while (grid_fetch_state(GRID) == STATE_AAN_HET_SPELEN) { timeout(1); const int c = getch(); pthread_mutex_lock(&SNAKE_MUTEX); switch (c) { case KEY_UP: // fallthrough case 'w': case 'W': turn_snake(DIRECTION_UP); break; case KEY_DOWN: // fallthrough case 's': case 'S': turn_snake(DIRECTION_DOWN); break; case KEY_LEFT: // fallthrough case 'a': case 'A': turn_snake(DIRECTION_LEFT); break; case KEY_RIGHT: // fallthrough case 'd': case 'D': turn_snake(DIRECTION_RIGHT); break; case KEY_BACKSPACE: case KEY_ESCAPE: grid_put_state(GRID, STATE_QUIT); break; } pthread_mutex_unlock(&SNAKE_MUTEX); } return NULL; } /* * Waits for the user to press their SPACEBAR before starting the game. * * Side Effects: * If the user presses backspace or escape, the whole game quits. */ static void wait_for_start(void) { const char start_message[] = "Press SPACEBAR to start playing."; const char empty_message[] = " "; mvaddstr(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, start_message); grid_put_state(GRID, STATE_AAN_HET_SPELEN); for (int c = getch(); c != ' '; c = getch()) { switch (c) { case KEY_BACKSPACE: case KEY_ESCAPE: grid_put_state(GRID, STATE_QUIT); mvaddstr(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, empty_message); return; } } // Cleanup the start playing message. mvaddstr(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, empty_message); } /* * Cleanup the snake memory * * Side effects: * Frees all the memory used by the snake */ static void snake_cleanup(void) { pthread_mutex_destroy(&SNAKE_MUTEX); grid_cleanup(GRID); } void snake(void) { initialize(); if (GRID == NULL) { return; } // Show game. erase(); show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y); wait_for_start(); if (grid_fetch_state(GRID) == STATE_AAN_HET_SPELEN) { // Create and start necessary threads. pthread_t input_thread; pthread_t game_tick_thread; pthread_create(&input_thread, NULL, user_input, NULL); pthread_create(&game_tick_thread, NULL, snake_move, NULL); // Wait until the gamestate is no longer STATE_AAN_HET_SPELEN pthread_join(game_tick_thread, NULL); pthread_join(input_thread, NULL); } game_exit_message(GRID, MESSAGE_LOCATION); snake_cleanup(); }