450 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			450 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Created by snapshot112 on 15/10/2025
 | |
|  */
 | |
| 
 | |
| #define _POSIX_C_SOURCE 199309
 | |
| 
 | |
| #include "snake.h"
 | |
| 
 | |
| #include <ncurses.h>
 | |
| #include <stdlib.h>
 | |
| #include <time.h>
 | |
| #include <pthread.h>
 | |
| #include <unistd.h>
 | |
| 
 | |
| #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();
 | |
| }
 |