358 lines
8.7 KiB
C
358 lines
8.7 KiB
C
//
|
|
// Created by snapshot112 on 10/15/25.
|
|
//
|
|
|
|
#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;
|
|
|
|
static direction PREVIOUS_DIRECTION = DIRECTION_DOWN;
|
|
static direction CURRENT_DIRECTION = DIRECTION_DOWN;
|
|
static coordinate SNAKE_HEAD;
|
|
static coordinate SNAKE_TAIL;
|
|
static coordinate MESSAGE_LOCATION = {0,0};
|
|
static int MAP_HEIGHT = 20;
|
|
static int MAP_WIDTH = 20;
|
|
static rooster *GRID;
|
|
static pthread_mutex_t MUTEX;
|
|
|
|
/*
|
|
* Create a snake body part.
|
|
*
|
|
* Input:
|
|
* dir: the direction the body part should point to.
|
|
*
|
|
* Output:
|
|
* a character representing that bodypart.
|
|
*/
|
|
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
|
|
int bottom_line = i > grid_size - (MAP_WIDTH + 2);
|
|
int top_line = i < MAP_WIDTH + 1;
|
|
|
|
int line_position = modulo(i, MAP_WIDTH + 1);
|
|
|
|
int line_start = line_position == 1;
|
|
int line_end = line_position == MAP_WIDTH;
|
|
|
|
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_from_string(map);
|
|
free(map);
|
|
|
|
rooster_plaats(GRID, rooster_breedte(GRID) / 2, rooster_hoogte(GRID) / 2, get_body_part(CURRENT_DIRECTION));
|
|
}
|
|
|
|
/*
|
|
*
|
|
*/
|
|
static void generate_food() {
|
|
coordinate empty_spots[MAP_HEIGHT * MAP_WIDTH];
|
|
|
|
// Usable as index when initialized like this.
|
|
int available_spots = 0;
|
|
|
|
for (int x = 0; x < MAP_WIDTH; x++) {
|
|
for (int y = 0; y < MAP_HEIGHT; y++) {
|
|
if (rooster_kijk(GRID, x, y) == CELL_EMPTY) {
|
|
coordinate new_spot = {x, y};
|
|
empty_spots[available_spots] = new_spot;
|
|
available_spots++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Available spots will now be a counter.
|
|
|
|
const coordinate food_location = empty_spots[modulo(rand(), available_spots)];
|
|
|
|
rooster_plaats(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.
|
|
*/
|
|
static void initialize(void) {
|
|
srand(time(NULL));
|
|
|
|
create_grid();
|
|
|
|
if (GRID == NULL) {
|
|
return;
|
|
}
|
|
|
|
CURRENT_DIRECTION = DIRECTION_DOWN;
|
|
PREVIOUS_DIRECTION = DIRECTION_DOWN;
|
|
|
|
// Set snake head and snake tail.
|
|
rooster_zoek(GRID, get_body_part(CURRENT_DIRECTION), &SNAKE_HEAD.x, &SNAKE_HEAD.y);
|
|
SNAKE_TAIL.x = SNAKE_HEAD.x;
|
|
SNAKE_TAIL.y = SNAKE_HEAD.y;
|
|
|
|
generate_food();
|
|
|
|
if (SNAKE_HEAD.x == -1) {
|
|
free(GRID);
|
|
GRID = NULL;
|
|
return;
|
|
}
|
|
|
|
MESSAGE_LOCATION.y = rooster_hoogte(GRID) + OFFSET_Y + 5;
|
|
MESSAGE_LOCATION.x = OFFSET_X - 5 >= 0 ? OFFSET_X - 5 : 0;
|
|
}
|
|
|
|
/*
|
|
* Checks what happens when the snake moves over a given char.
|
|
*
|
|
* Input:
|
|
* c: The char to check.
|
|
*
|
|
* Returns:
|
|
* The snake will move forward: 0
|
|
* The snake will eat an apple: 1
|
|
* The snake will die: 2
|
|
*/
|
|
static snake_action collision_check(char c) {
|
|
if (c == CELL_EMPTY) {
|
|
return SNAKE_MOVE;
|
|
}
|
|
|
|
if (c == CELL_FOOD) {
|
|
return SNAKE_EAT;
|
|
}
|
|
|
|
return SNAKE_DIE;
|
|
}
|
|
|
|
static void update_snake(coordinate new_head, snake_action action) {
|
|
if (action == SNAKE_DIE) {
|
|
rooster_zet_toestand(GRID, STATE_VERLOREN);
|
|
return;
|
|
}
|
|
|
|
if (action == SNAKE_MOVE) {
|
|
coordinate new_tail = SNAKE_TAIL;
|
|
|
|
switch (get_body_part_direction(rooster_kijk(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;
|
|
}
|
|
|
|
rooster_plaats(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.
|
|
rooster_plaats(GRID, new_head.x, new_head.y, get_body_part(CURRENT_DIRECTION));
|
|
SNAKE_HEAD = new_head;
|
|
PREVIOUS_DIRECTION = CURRENT_DIRECTION;
|
|
|
|
if (action == SNAKE_EAT) {
|
|
generate_food();
|
|
}
|
|
}
|
|
|
|
static void *snake_move(void *arg) {
|
|
struct timespec timer;
|
|
timer.tv_sec = 0;
|
|
timer.tv_nsec = 250000000L; // Snake moves every 0.25 seconds.
|
|
|
|
while (rooster_vraag_toestand(GRID) == STATE_AAN_HET_SPELEN) {
|
|
nanosleep(&timer, NULL);
|
|
pthread_mutex_lock(&MUTEX);
|
|
coordinate new_head = SNAKE_HEAD;
|
|
switch (CURRENT_DIRECTION) {
|
|
case DIRECTION_UP:
|
|
new_head.y--;
|
|
break;
|
|
case DIRECTION_RIGHT:
|
|
new_head.x++;
|
|
break;
|
|
case DIRECTION_DOWN:
|
|
new_head.y++;
|
|
break;
|
|
case DIRECTION_LEFT:
|
|
new_head.x--;
|
|
break;
|
|
}
|
|
|
|
snake_action action = collision_check(rooster_kijk(GRID, new_head.x, new_head.y));
|
|
|
|
update_snake(new_head, action);
|
|
show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y);
|
|
pthread_mutex_unlock(&MUTEX);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void turn_snake(direction dir) {
|
|
if ((direction)modulo(dir + 2, DIRECTION_COUNT) == PREVIOUS_DIRECTION
|
|
&& !same_coordinate(SNAKE_HEAD, SNAKE_TAIL)) {
|
|
return;
|
|
}
|
|
rooster_plaats(GRID, SNAKE_HEAD.x, SNAKE_HEAD.y, get_body_part(dir));
|
|
CURRENT_DIRECTION = dir;
|
|
show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y);
|
|
}
|
|
|
|
static void *play_snake(void *arg) {
|
|
while (rooster_vraag_toestand(GRID) == STATE_AAN_HET_SPELEN) {
|
|
timeout(1);
|
|
int c = getch();
|
|
pthread_mutex_lock(&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:
|
|
rooster_zet_toestand(GRID, STATE_QUIT);
|
|
break;
|
|
}
|
|
pthread_mutex_unlock(&MUTEX);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
//todo: ?? Win condition?
|
|
void snake(void) {
|
|
initialize();
|
|
if (GRID == NULL) {
|
|
return;
|
|
}
|
|
|
|
// Created necessary threads.
|
|
pthread_mutex_init(&MUTEX, NULL);
|
|
pthread_t user_input;
|
|
pthread_t game_tick;
|
|
|
|
// Show game.
|
|
erase();
|
|
show_grid_on_offset(GRID, OFFSET_X, OFFSET_Y);
|
|
mvaddstr(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, "Press SPACEBAR to start playing.");
|
|
|
|
rooster_zet_toestand(GRID, STATE_AAN_HET_SPELEN);
|
|
|
|
while (getch() != ' ') {}
|
|
|
|
// Cleanup the start playing message.
|
|
mvaddstr(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, " ");
|
|
|
|
pthread_create(&user_input, NULL, play_snake, NULL);
|
|
pthread_create(&game_tick, NULL, snake_move, NULL);
|
|
|
|
// Wait until the gamestate is no longer STATE_AAN_HET_SPELEN
|
|
pthread_join(game_tick, NULL);
|
|
pthread_join(user_input, NULL);
|
|
pthread_mutex_destroy(&MUTEX);
|
|
|
|
rooster_klaar(GRID);
|
|
graceful_exit(MESSAGE_LOCATION);
|
|
}
|