Files
minigame-menu/snake.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 "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();
}