Files
minigame-menu/snake.c
2025-10-17 11:15:53 +02:00

325 lines
7.5 KiB
C

//
// Created by snapshot112 on 10/15/25.
//
#include "snake.h"
#include <ncurses.h>
#include <stdlib.h>
#include <string.h>
#include <time.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;
/*
* 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 rooster *create_grid(void) {
const int grid_size = (MAP_WIDTH + 1) * MAP_HEIGHT + 1;
char *map = malloc(grid_size * sizeof(char));
if (map == NULL) {
return NULL;
}
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';
rooster *grid = grid_from_string(map);
free(map);
rooster_plaats(grid, rooster_breedte(grid) / 2, rooster_hoogte(grid) / 2, get_body_part(CURRENT_DIRECTION));
return grid;
}
/*
*
*/
static void generate_food(rooster *gp) {
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(gp, 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(gp, 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 rooster *initialize(void) {
srand(time(NULL));
rooster *grid = create_grid();
if (grid == NULL) {
return NULL;
}
// 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(grid);
if (SNAKE_HEAD.x == -1) {
return NULL;
}
MESSAGE_LOCATION.y = rooster_hoogte(grid) + OFFSET_Y + 5;
MESSAGE_LOCATION.x = OFFSET_X - 5 >= 0 ? OFFSET_X - 5 : 0;
return grid;
}
/*
* 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(rooster *gp, coordinate new_head, snake_action action) {
if (action == SNAKE_DIE) {
rooster_zet_toestand(gp, STATE_VERLOREN);
return;
}
if (action == SNAKE_MOVE) {
coordinate new_tail = SNAKE_TAIL;
switch (get_body_part_direction(rooster_kijk(gp, 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(gp, 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(gp, 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(gp);
}
}
static void snake_move(rooster *gp) {
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(gp, new_head.x, new_head.y));
update_snake(gp, new_head, action);
show_grid_on_offset(gp, OFFSET_X, OFFSET_Y);
}
static void turn_snake(rooster *gp, direction dir) {
if ((direction)modulo(dir + 2, DIRECTION_COUNT) == PREVIOUS_DIRECTION) {
return;
}
rooster_plaats(gp, SNAKE_HEAD.x, SNAKE_HEAD.y, get_body_part(dir));
CURRENT_DIRECTION = dir;
show_grid_on_offset(gp, OFFSET_X, OFFSET_Y);
}
static void play_snake(rooster *gp) {
timeout(500);
switch (getch()) {
case KEY_UP: // fallthrough
case 'w':
case 'W':
turn_snake(gp, DIRECTION_UP);
break;
case KEY_DOWN: // fallthrough
case 's':
case 'S':
turn_snake(gp, DIRECTION_DOWN);
break;
case KEY_LEFT: // fallthrough
case 'a':
case 'A':
turn_snake(gp, DIRECTION_LEFT);
break;
case KEY_RIGHT: // fallthrough
case 'd':
case 'D':
turn_snake(gp, DIRECTION_RIGHT);
break;
case KEY_BACKSPACE:
rooster_zet_toestand(gp, STATE_QUIT);
break;
case ERR:
snake_move(gp);
}
}
void snake(void) {
rooster* const gp = initialize();
if (gp == NULL) {
return;
}
erase();
//todo: ?? Win condition?
show_grid_on_offset(gp, OFFSET_X, OFFSET_Y);
mvprintw(MESSAGE_LOCATION.y, MESSAGE_LOCATION.x, "Press SPACEBAR to start playing.");
while (getch() != ' ') {}
rooster_zet_toestand(gp, STATE_AAN_HET_SPELEN);
while (rooster_vraag_toestand(gp) == STATE_AAN_HET_SPELEN) {
play_snake(gp);
}
// game_exit_screen(gp);
rooster_klaar(gp);
graceful_exit();
}