refactored file structure

This commit is contained in:
2025-10-22 16:39:17 +02:00
parent ae65999622
commit 665b8135a3
19 changed files with 48 additions and 41 deletions

View File

@@ -0,0 +1,160 @@
/*
* Created by snapshot112 on 8/10/2025
*/
#include "grid_game_engine.h"
#include <locale.h>
#include <ncurses.h>
#include <stdlib.h>
int same_coordinate(const coordinate a, const coordinate b) {
return a.x == b.x && a.y == b.y;
}
int modulo(const int number, const int mod) {
int result = number % mod;
if (result < 0) {
result += mod; //This is not how math is supposed to work C, damnit.
}
return result;
}
void show_grid_on_offset(const grid *gp, const int starting_x, const int starting_y) {
const int height = grid_height(gp);
for (int y = 0; y < height; y++) {
char *rij = grid_fetch_row(gp, y);
mvprintw(starting_y + y, starting_x, "%s", rij);
free(rij);
}
refresh();
}
void show_grid(const grid *gp) {
show_grid_on_offset(gp, 0, 0);
}
void update_grid(grid *gp, const char c, const int x, const int y) {
if (grid_put(gp, x, y, c) == 1) {
mvaddch(y, x, c);
}
}
/*
* Shows the victory screen.
*
* Side Effect:
* Clears the console and prints the victory message.
*/
static void display_victory(const coordinate location) {
mvaddstr(location.y, location.x, "YOU WON!!!!!");
}
/*
* Shows the GAME OVER screen.
*
* Side Effect:
* Clears the console and prints the GAME OVER message.
*/
static void display_loss(const coordinate location) {
mvaddstr(location.y, location.x, "GAME OVER");
}
/*
* Shows the quit screen
*
* Side Effect:
* Clears the console and prints the quit message.
*/
static void display_quit(const coordinate location) {
mvaddstr(location.y, location.x, "You quit the game");
}
/*
* Shows the hacker man screen
*
* Side Effect:
* Clears the console and prints the hacker man message.
*/
static void display_hackerman(const coordinate location) {
mvaddstr(location.y, location.x, "The hacker man strikes again...");
}
void graceful_exit(const coordinate message_location) {
mvaddstr(message_location.y, message_location.x, "Press ENTER to exit.");
while (1) {
switch (getch()) {
case KEY_BACKSPACE:
case KEY_ESCAPE:
case KEY_ENTER:
case '\n':
return;
}
}
}
void game_exit_message(const grid *gp, coordinate location) {
const state current_state = grid_fetch_state(gp);
switch (current_state) {
case STATE_GEWONNEN:
display_victory(location);
break;
case STATE_VERLOREN:
display_loss(location);
break;
case STATE_QUIT:
display_quit(location);
break;
default:
display_hackerman(location);
}
location.y += 2;
graceful_exit(location);
refresh();
}
void enable_highlight(const game_colors color) {
attron(COLOR_PAIR(color));
}
void disable_highlight(const game_colors color) {
attroff(COLOR_PAIR(color));
}
/*
* Initialize ncurses.
*
* This enables cbreak, noecho, hides the cursor and enables the extra keys.
* This also creates the color pairs needed for game_colors and sets ESCDELAY to 0.
*/
static void init_ncurses(void) {
ESCDELAY = 0;
setlocale(LC_ALL, "");
initscr();
cbreak(); // So you can cancel the game with ctrl + c.
keypad(stdscr, TRUE); // Enable extra keys like the arrow keys.
noecho(); // Don't write the keyboard input to the console.
curs_set(0); // Hides the cursor.
start_color();
init_pair(BLACK, COLOR_BLACK, COLOR_BLACK);
init_pair(WHITE, COLOR_BLACK, COLOR_WHITE);
init_pair(BLUE, COLOR_BLACK, COLOR_BLUE);
init_pair(GREEN, COLOR_BLACK, COLOR_GREEN);
init_pair(CYAN, COLOR_BLACK, COLOR_CYAN);
init_pair(MAGENTA, COLOR_BLACK, COLOR_MAGENTA);
init_pair(YELLOW, COLOR_BLACK, COLOR_YELLOW);
init_pair(RED, COLOR_BLACK, COLOR_RED);
clear();
}
void init_engine(void) {
init_ncurses();
}
void cleanup_engine(void) {
endwin();
}

View File

@@ -0,0 +1,157 @@
/*
* Created by snapshot112 on 15/10/2025.
*
* A game engine build on top of a grid api to run and display games using ncurses.
*
* Please make sure to initialize the game engine before running any games.
*/
#ifndef MINIGAME_MENU_GRID_GAME_ENGINE_H
#define MINIGAME_MENU_GRID_GAME_ENGINE_H
#include "../grid/grid.h"
#define KEY_ESCAPE 27
typedef struct {
int x;
int y;
} coordinate;
// Start at 1 since the color 0 is reserved for no_color.
typedef enum {
BLACK = 1,
WHITE = 2,
BLUE = 3,
GREEN = 4,
CYAN = 5,
MAGENTA = 6,
YELLOW = 7,
RED = 8
} game_colors;
typedef struct {
char name[100];
grid *game_map;
} game_maps;
/*
* Checks if 2 coordinates are the same.
*
* Input:
* a: coordinate 1
* b: coordinate 2
*
* Returns:
* If they point to the same location: 1
* Otherwise: 0
*/
int same_coordinate(coordinate a, coordinate b);
/*
* A proper modulo function.
* The one provided by c: '%' doesn't work according to the mathematical definition.
*/
int modulo(int number, int mod);
/*
* Displays the given grid with the given offset using ncurses.
*
* Input:
* gp: A pointer to the grid.
*
* Side effect:
* The console is cleared and the grid is printed.
*/
void show_grid_on_offset(const grid *gp, int starting_x, int starting_y);
/*
* Displays the given grid with ncurses.
*
* Input:
* gp: A pointer to the grid.
*
* Side effect:
* The console is cleared and the grid is printed.
*/
void show_grid(const grid *gp);
/*
* Turn on color highlighting for ncurses
*
* Input:
* color: The color you want the highlight to be.
*
* Side Effects:
* Enables color highlighting in ncurses.
*
* Note:
* This should always be disabled with disable_highlights before being called again.
* Not disabling the current highlight before enabling another one is undefined behaviour.
*/
void enable_highlight(game_colors color);
/*
* Turn off color highlighting for ncurses
*
* Input:
* color: The color highlighting you want to turn off.
*
* Side Effects:
* Enables color highlighting in ncurses.
*
* Note:
* This should always be enabled with enable_highlights before being called.
* Disabling the highlight before enabling it is undefined behaviour.
*/
void disable_highlight(game_colors color);
/*
* Updates a single location in the grid.
*
* Input:
* gp: A pointer to the grid.
* c: The character to update the location with.
* x: The x-coordinate of the spot you want to update.
* y: The y-coordinate of the spot you want to update.
*
* Side effect:
* The update gets applied both on the grid and on the screen.
*/
void update_grid(grid *gp, char c, int x, int y);
/*
* Display the ending screen that matches the end state of the grid.
*
* Input:
* gp: A pointer to the grid.
* coordinate: The location to show the ending screen.
*
* Side Effects:
* The end of game screen gets displayed with a graceful exit.
*/
void game_exit_message(const grid *gp, coordinate location);
/*
* Waits for you to press ENTER before exiting the game.
*
* Input:
* coordinate: The location to show the message.
*
* Side effect: Prints "Press ENTER to exit." game to the console.
*/
void graceful_exit(coordinate message_location);
/*
* Initialize ncurses.
*
* This enables cbreak, noecho, hides the cursor and enables the extra keys.
* This also creates the color pairs needed for game_colors and sets ESCDELAY to 0.
*/
void init_engine(void);
/*
* Cleanup ncurses.
*/
void cleanup_engine(void);
#endif //MINIGAME_MENU_GRID_GAME_ENGINE_H

305
src/engine/grid/grid.c Normal file
View File

@@ -0,0 +1,305 @@
/*
* Created by snapshot112 on 2/10/2025
*/
#include "grid.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/*
* The grid type this program is build around.
*/
typedef struct grid_data {
char *locations;
int height;
int width;
state state;
} grid;
/*
* Translate x and y coordinates to a location index on the grid.
*/
static int internal_location(const grid *gp, const int x, const int y) {
return y * (gp->width + 1) + x;
}
grid *grid_create_from_string(const char* input) {
int width = 0;
int height = 0;
const size_t len = strlen(input) / sizeof(char);
if (input == NULL || len == 0) {
printf("invalid input\n");
return NULL;
}
for (int i = 0; input[i] != '\n'; i++) {
width++;
}
height = (int)len / (width + 1);
for (int i = 0; i < height; i = i + width + 1) {
if ((int)strcspn(&input[i], "\n") != width) {
printf("line %d was not %d wide\n", i, width);
return NULL;
}
}
const int grid_size = (width + 1) * height + 1;
grid *gp = malloc(sizeof(grid));
if (gp == NULL) {
return NULL;
}
gp->locations = malloc(grid_size * sizeof(char));
gp->height = height;
gp->width = width;
gp->state = STATE_BEGIN;
strcpy(gp->locations, input);
return gp;
}
/*
* Sets a grids width and height
*
* Input:
* fh: The stream to read the grid from.
* gp: A pointer to the grid whose width and height you want to set.
*
* Side effects:
* the grid gets its width and height set
*
* Output:
* 1 if the file width and height seem to match with its size
* 0 otherwise
*/
static int get_grid_sizes(FILE *fh, grid *gp) {
while (getc(fh) != '\n') {
if (feof(fh)) {
return 0;
}
gp->width++;
}
if (gp->width == 0) {
return 0;
}
fseek(fh, 0, SEEK_END);
// Get file size (- 1 for the blank newline and EOF at the end)
if (ftell(fh) % (gp->width + 1) != 0) {
// Not all lines are the same width
return 0;
}
gp->height = (int)ftell(fh) / (int)sizeof(char) / (gp->width + 1);
fseek(fh, 0, SEEK_SET);
return 1;
}
grid *grid_create_from_file(FILE *fh) {
if (fh == NULL) {
return NULL;
}
grid temp_grid = {
NULL,
0,
0,
STATE_BEGIN
};
// Sets the width and height of the grid.
if (get_grid_sizes(fh, &temp_grid) != 1) {
// Unlogical file structure.
return NULL;
}
const int grid_size = (temp_grid.width + 1) * temp_grid.height + 1;
temp_grid.locations = malloc(grid_size * sizeof(char));
if (temp_grid.locations == NULL) {
return NULL;
}
// This makes the strncat() work.
temp_grid.locations[0] = '\0';
char *line = malloc((temp_grid.width + 2) * sizeof(char));
if (line == NULL) {
return NULL;
}
for (int i = 0; i < temp_grid.height; i++) {
if (fgets(line, temp_grid.width + 2, fh) == NULL) {
free(temp_grid.locations);
free(line);
return NULL;
}
// Validate that the line length is correct
if ((int)strcspn(line, "\n") != temp_grid.width) {
free(temp_grid.locations);
free(line);
return NULL;
}
// Width is without the newline at the end.
strncat(temp_grid.locations, line, temp_grid.width + 1);
}
free(line);
grid *return_grid = malloc(sizeof(temp_grid));
if (return_grid == NULL) {
return NULL;
}
memcpy(return_grid, &temp_grid, sizeof(temp_grid));
return return_grid;
}
state grid_fetch_state(const grid *gp) {
if (gp != NULL) {
return gp->state;
}
return STATE_VERLOREN;
}
void grid_put_state(grid *gp, const state t) {
if (gp != NULL) {
switch (t) {
case STATE_BEGIN:
gp->state = STATE_BEGIN;
break;
case STATE_AAN_HET_SPELEN:
gp->state = STATE_AAN_HET_SPELEN;
break;
case STATE_GEWONNEN:
gp->state = STATE_GEWONNEN;
break;
case STATE_VERLOREN:
gp->state = STATE_VERLOREN;
break;
case STATE_QUIT:
gp->state = STATE_QUIT;
break;
}
}
}
void grid_cleanup(grid *gp) {
if (gp != NULL) {
if (gp->locations != NULL)
{
free(gp->locations);
}
free(gp);
}
}
int grid_width(const grid *gp) {
if (gp == NULL) {
return 0;
}
return gp->width;
}
int grid_height(const grid *gp) {
if (gp == NULL) {
return 0;
}
return gp->height;
}
int grid_contains(const grid *gp, const int x, const int y) {
if (gp != NULL && gp->locations != NULL) {
if (x >= 0 && y >= 0 && x < gp->width && y < gp->height)
{
return 1;
}
}
return 0;
}
char grid_fetch(const grid *gp, const int x, const int y) {
if (gp != NULL && gp->locations != NULL && grid_contains(gp, x, y) == 1) {
return gp->locations[internal_location(gp, x, y)];
}
return '\0';
}
int grid_put(grid *gp, const int x, const int y, const char c) {
if (gp != NULL && gp->locations != NULL && grid_contains(gp, x, y) == 1) {
gp->locations[internal_location(gp, x, y)] = c;
return 1;
}
return 0;
}
char *grid_fetch_row(const grid *gp, const int y) {
if (gp != NULL && gp->locations != NULL && grid_contains(gp, 0, y) == 1) {
// we're going to remove the newline so this is long enough
char *row = malloc((gp->width + 1) * sizeof(char));
memcpy(row, &gp->locations[internal_location(gp, 0, y)], gp->width * sizeof(char));
row[gp->width] = '\0';
return row;
}
return NULL;
}
grid *grid_copy(const grid *gp) {
if (gp != NULL && gp->locations != NULL) {
const size_t grid_memory = ((gp->width + 1) * gp->height + 1) * sizeof(char);
char *locations = malloc(grid_memory);
if (locations == NULL) {
return NULL;
}
grid *new_grid = malloc(sizeof(*gp));
if (new_grid == NULL) {
return NULL;
}
memcpy(locations, gp->locations, grid_memory);
memcpy(new_grid, gp, sizeof(*gp));
new_grid->locations = locations;
return new_grid;
}
return NULL;
}
void grid_find(const grid *gp, const char c, int *x, int *y) {
if (gp == NULL || gp->locations == NULL) {
*x = -1;
*y = -1;
return;
}
const char search[2] = {c};
const int char_index = (int)strcspn(gp->locations, search);
if (gp->locations[char_index] == '\0')
{
*x = -1;
*y = -1;
return;
}
*x = char_index % (gp->width + 1);
*y = char_index / (gp->width + 1);
}

174
src/engine/grid/grid.h Normal file
View File

@@ -0,0 +1,174 @@
/*
* Created by snapshot112 on 2/10/2025
*
* This module provides a grid api.
*
* A grid is a rectangular grid of objects. Every object in this grid is a char.
*/
#ifndef _GRID_H
#define _GRID_H
#include <stdio.h>
struct grid_data;
typedef struct grid_data grid;
typedef enum {
STATE_BEGIN,
STATE_AAN_HET_SPELEN,
STATE_GEWONNEN,
STATE_VERLOREN,
STATE_QUIT
} state;
/* Maak een rooster op basis van de data in de gegeven stream.
*
* fh: de stream waaruit het doolhof gelezen moet worden.
*
* Uitvoer: als alles goed gaat, een pointer naar een rooster (die op de heap is
* gealloceerd), dat overeenkomt met de gegeven beschrijving.
* De begintoestand is BEGIN.
*
* Als de beschrijving niet consistent is (bijvoorbeeld
* niet alle rijen zijn even lang, of er klopt iets anders niet), of
* als niet voldoende geheugen kan worden gereserveerd, dan wordt
* NULL teruggegeven. (In dat geval blijft geen gereserveerd geheugen
* achter.)
*/
grid *grid_create_from_file(FILE *fh);
/*
* Maak een rooster op basis van een gegeven string.
*
* Uitvoer: als alles goed gaat, een pointer naar een rooster (die op de heap is
* gealloceerd), dat overeenkomt met de gegeven beschrijving.
* De begintoestand is BEGIN.
*
* Als de beschrijving niet consistent is (bijvoorbeeld
* niet alle rijen zijn even lang, of er klopt iets anders niet), of
* als niet voldoende geheugen kan worden gereserveerd, dan wordt
* NULL teruggegeven. (In dat geval blijft geen gereserveerd geheugen
* achter.)
*/
grid *grid_create_from_string(const char* input);
/*
* Haal een rij uit het rooster op.
*
* Input:
* gp: een pointer naar het rooster
* y: de y-coordinaat van de rij die je wilt hebben.
*
* Output:
* Een pointer naar een nieuwe string met daarin alle karakters in die rij.
*/
char *grid_fetch_row(const grid *gp, int y);
/*
* Maak een kopie van een rooster
*
* Input:
* gp: Een pointer naar het rooster.
*
* Output:
* Een pointer naar het kopie.
*/
grid *grid_copy(const grid *gp);
/* Vraag de huidige toestand van het spel op.
*
* Input:
* gp: een pointer naar het rooster.
*
* Uitvoer: de toestand.
*/
state grid_fetch_state(const grid *gp);
/* Verander de huidige toestand van het spel.
*
* gp: een pointer naar het rooster.
* t: de nieuwe toestand.
*/
void grid_put_state(grid *gp, state t);
/* Geef alle resources vrij die zijn gealloceerd voor een rooster.
* De rooster pointer is na aanroep van deze functie niet meer bruikbaar.
*
* gp: een pointer naar het rooster.
*/
void grid_cleanup(grid *gp);
/* Vraag de breedte van het rooster op, dat wil zeggen, het aantal kolommen.
*
* gp: een pointer naar het rooster.
*
* Uitvoer: de breedte.
*/
int grid_width(const grid *gp);
/* Vraag de hoogte van het rooster op, dat wil zeggen, het aantal rijen.
*
* gp: een pointer naar het rooster.
*
* Uitvoer: de hoogte.
*/
int grid_height(const grid *gp);
/* Kijk of de gegeven positie binnen het rooster valt.
*
* gp: een pointer naar het rooster.
* x,y: de positie.
*
* Uitvoer: 1 als de positie binnen het rooster valt, anders 0.
*/
int grid_contains(const grid *gp, int x, int y);
/* Kijk welk object er staat op een bepaalde positie in het rooster.
*
* gp : een pointer naar het rooster
* x,y: de betreffende positie.
*
* Uitvoer: het object op die positie, of '\0' als de positie buiten het
* rooster valt.
*/
char grid_fetch(const grid *gp, int x, int y);
/* Schrijf een bepaald object op een bepaalde positie in het rooster.
*
* gp : een pointer naar het rooster
* x,y: de betreffende positie.
* c : het object.
*
* Effect: als (x,y) binnen het rooster ligt, wordt het object op
* de opgegeven positie geplaatst, anders verandert er niets.
*
* Uitvoer: 1 als het object is geplaatst, of 0 als het buiten de grenzen lag.
*/
int grid_put(grid *gp, int x, int y, char c);
/* Zoek een bepaald object in het rooster, en geef de coordinaten van het
* object terug via de gegeven pointers. Let op: als er meerdere objecten van
* het gezochte soort in het rooster voorkomen, is niet gedefinieerd van welke
* de positie wordt gevonden.
*
* gp : een pointer naar het rooster
* c : het object dat moet worden gezocht
* x,y: pointers naar de geheugenlocaties waar de gevonden positie moet worden
* geschreven.
*
* Uitvoer: via de pointers x en y. Als het object niet wordt gevonden worden
* *x en *y op -1 gezet.
*/
void grid_find(const grid *gp, char c, int *x, int *y);
#endif //_GRID_H

View File

@@ -0,0 +1,87 @@
*--------------------------------------------*
| |
| !!! HOWTO MINIGAME !!! |
| |
| Make sure that your terminal |
| has enough space to display |
| all the output of the game. |
| |
| This can be achieved by |
| going fullscreen and/or |
| zooming out. |
| |
| *________________* |
| | GENERAL | |
| *------------------------------------* |
| | | |
| | Exit current screen: | |
| | Escape, backspace | |
| | | |
| | Reset zoom: | |
| | "'ctrl' + '0'" | |
| | | |
| | Zoom out: | |
| | "'ctrl' + '-'" | |
| | | |
| | Zoom in: | |
| | "'ctrl' + '+'" | |
| | "'ctrl' + 'shift' + '='" | |
| | | |
| *------------------------------------* |
| |
| "'ctrl' + '-'" |
| |
| You can reset the zoom with: |
| "'ctrl' + '0'" |
| |
| *________________* |
| | MENU | |
| *------------------------------------* |
| | | |
| | Move down: | |
| | 's', arrow_down | |
| | | |
| | Move up: | |
| | 'w', arrow_up | |
| | | |
| | Select: | |
| | 'f', enter, space bar | |
| | | |
| *------------------------------------* |
| |
| *-----------------* |
| | MAZE RUNNER | |
| *------------------------------------* |
| | | |
| | Move up: | |
| | 'w', arrow_up | |
| | | |
| | Move down: | |
| | 's', arrow_down | |
| | | |
| | Move right: | |
| | 'd', arrow_right | |
| | | |
| | Move left: | |
| | 'a', arrow_left | |
| | | |
| *------------------------------------* |
| |
| *-----------------* |
| | SNAKE | |
| *------------------------------------* |
| | | |
| | Turn up: | |
| | 'w', arrow_up | |
| | | |
| | Turn down: | |
| | 's', arrow_down | |
| | | |
| | Turn right: | |
| | 'd', arrow_right | |
| | | |
| | Turn left: | |
| | 'a', arrow_left | |
| | | |
| *------------------------------------* |
*--------------------------------------------*

36
src/games/manual/manual.c Normal file
View File

@@ -0,0 +1,36 @@
/*
* Created by snapshot112 on 10/17/2025
*/
#include "manual.h"
#include <ncurses.h>
#include "../../engine/grid_game_engine.h"
void manual(const coordinate display_location) {
erase();
FILE *fp = fopen("assets/manual.txt", "r");
if (fp == NULL) {
return;
}
grid *grid = grid_create_from_file(fp);
if (grid == NULL) {
mvaddstr(display_location.y, display_location.x, "Error loading grid");
return;
}
fclose(fp);
show_grid_on_offset(grid, display_location.x, display_location.y);
// Wait until ESCAPE or BACKSPACE is pressed.
timeout(200);
for (int ch = getch(); ch != KEY_ESCAPE && ch != KEY_BACKSPACE; ch = getch()) {
// Update the screen in the meantime to accommodate windows resizes.
show_grid_on_offset(grid, display_location.x, display_location.y);
}
grid_cleanup(grid);
}

27
src/games/manual/manual.h Normal file
View File

@@ -0,0 +1,27 @@
/*
* Created by snapshot112 on 10/17/2025
*
* Provides a way to display the minigame manual in the assets in game.
*/
#ifndef MINIGAME_MENU_MANUAL_H
#define MINIGAME_MENU_MANUAL_H
#include "../../engine/grid_game_engine.h"
/*
* An in game manual for the minigames menu.
*
* Please make sure to include and initialize the game engine before opening the manual
*
* Input:
* display_location: The location to display the manual.
*
* Side Effects:
* Clears the console and uses it to display the manual.
*
* Controls:
* Press ESCAPE or BACKSPACE to exit the manual.
*/
void manual(coordinate display_location);
#endif //MINIGAME_MENU_MANUAL_H

View File

@@ -0,0 +1,13 @@
#######################
#*# #
# # $ ########### #
# ######## #
# # ############
# ###### # #
# # # ############ #
# #### # # #
# # ##### # XXXXXX#
# XXX# # #
# #########XXXXXXX #
# X #
#######################

View File

@@ -0,0 +1,147 @@
/*
* Created by snapshot112 on 6/10/2025
*/
#include "maze_runner.h"
#include <ncurses.h>
#include <stdlib.h>
#include "../../engine/grid_game_engine.h"
#define EMPTY ' '
#define WALL '#'
#define LIVING_PLAYER '*'
#define DEAD_PLAYER '@'
#define MAZE_EXIT '$'
#define TRAP 'X'
/*
* Reads in the maze from the assets file.
*
* Side Effects:
* Memory is allocated to store the grid in.
*/
static grid *get_maze(void) {
// Open het doolhof bestand en lees het rooster.
FILE *fh = fopen("assets/maze.txt", "r");
if (fh == NULL) {
perror("loading maze");
return NULL;
}
grid *gp = grid_create_from_file(fh);
fclose(fh);
// Bepaal of het lezen van het rooster is gelukt.
if (gp == NULL) {
fprintf(stderr, "Kan rooster niet maken.\n");
return NULL;
}
return gp;
}
/* Voert de benodigde veranderingen in het rooster door als de speler in een
* bepaalde richting probeert te bewegen.
* Input:
* gp : een pointer naar het rooster
* dx,dy: de richting waarin de speler probeert te bewegen. De vier mogelijk-
* heden voor (dx,dy) zijn (-1,0), (1,0), (0,-1), (0,1) voor resp.
* links, rechts, omhoog en omlaag.
*
* Side effect: het rooster wordt aangepast op basis van de handeling van
* de speler.
*/
static void maze_runner_move(grid *gp, const int dx, const int dy) {
coordinate player_position = {0, 0};
grid_find(gp, LIVING_PLAYER, &player_position.x, &player_position.y);
if (player_position.y == -1) {
printf("Player not found!");
grid_put_state(gp, STATE_BEGIN);
return;
}
if (grid_contains(gp, player_position.x + dx, player_position.y + dy) == 1) {
char new_location = grid_fetch(gp, player_position.x + dx, player_position.y + dy);
switch (new_location) {
case WALL:
break;
case TRAP:
grid_put_state(gp, STATE_VERLOREN);
enable_highlight(RED);
update_grid(gp, DEAD_PLAYER, player_position.x, player_position.y);
disable_highlight(RED);
break;
case EMPTY:
update_grid(gp, EMPTY, player_position.x, player_position.y);
update_grid(gp, LIVING_PLAYER, player_position.x + dx, player_position.y + dy);
break;
case MAZE_EXIT:
update_grid(gp, EMPTY, player_position.x, player_position.y);
grid_put_state(gp, STATE_GEWONNEN);
break;
}
refresh();
}
}
/*
* Speelt het spel met een gegeven rooster tot de toestand niet langer
* AAN_HET_SPELEN is.
*
* Input:
* gp: Een pointer naar het rooster.
*
* Side effects:
* Het rooster wordt ge-updated afhankelijk van de user input.
*/
static void speel_maze(grid *gp) {
switch (getch()) {
case KEY_UP: // fallthrough
case 'w':
case 'W':
maze_runner_move(gp, 0, -1);
break;
case KEY_DOWN: // fallthrough
case 's':
case 'S':
maze_runner_move(gp, 0, 1);
break;
case KEY_LEFT: // fallthrough
case 'a':
case 'A':
maze_runner_move(gp, -1, 0);
break;
case KEY_RIGHT: // fallthrough
case 'd':
case 'D':
maze_runner_move(gp, 1, 0);
break;
case 'p':
case KEY_BACKSPACE:
case KEY_ESCAPE:
grid_put_state(gp, STATE_QUIT);
break;
}
}
void maze_runner(void) {
// Voorbereiding.
grid *gp = get_maze();
if (gp == NULL) {
return;
}
show_grid(gp);
grid_put_state(gp, STATE_AAN_HET_SPELEN);
// Game zelf.
while (grid_fetch_state(gp) == STATE_AAN_HET_SPELEN) {
speel_maze(gp);
}
// Exit game.
game_exit_message(gp, (coordinate){0, grid_height(gp) + 2});
grid_cleanup(gp);
}

View File

@@ -0,0 +1,41 @@
/*
* Created by snapshot112 on 6/10/2025
*
* Naam: Jeroen Boxhoorn
* UvAnetID: 16333969
* Studie: BSC Informatica
*
* A game of maze runner build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling maze_runner();
*
* How to play the game:
* - You are the '*' character.
* - Use either WSAD or the arrow keys to navigate through the maze.
* - The exit of the maze is marked with a '$'.
* - Walls are '#'.
* - Traps are 'X'. These kill you.
*
* You can quit the program at any time by pressing CTRL + C.
*
* Have fun playing!
*/
#ifndef MINIGAME_MENU_MAZE_RUNNER_H
#define MINIGAME_MENU_MAZE_RUNNER_H
/*
* A game of maze runner build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling maze_runner();
*
* Side Effects:
* Clears the console and uses it to play a game of maze runner.
*
* Controls:
* use WSAD or arrow keys to move through the maze.
* Use BACKSPACE or ESCAPE to exit the game early.
*/
void maze_runner(void);
#endif //MINIGAME_MENU_MAZE_RUNNER_H

View File

@@ -0,0 +1,16 @@
/*
* Created by snapshot112 on 15/10/2025
*/
#include "minesweeper.h"
#include <ncurses.h>
#include "../../engine/grid_game_engine.h"
void minesweeper() {
clear();
mvprintw(0,0, "Minesweeper has not yet been created");
graceful_exit((coordinate){0, 3});
refresh();
}

View File

@@ -0,0 +1,30 @@
/*
* Created by snapshot112 on 15/10/2025
*
* A game of minesweeper build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling minesweeper();
*/
#ifndef MINIGAME_MENU_MINESWEEPER_H
#define MINIGAME_MENU_MINESWEEPER_H
/*
* A game of minesweeper build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling minesweeper();
*
* Side Effects:
* Clears the console and uses it to play a game of minesweeper.
*
* Instructions:
* use WSAD or arrow keys to select a grid square.
* use SPACEBAR to open the current square.
* use 'f' to mark/unmark a square.
* use BACKSPACE to exit the game early.
*
* marked squares can't be opened.
*/
void minesweeper(void);
#endif //MINIGAME_MENU_MINESWEEPER_H

View File

@@ -0,0 +1,187 @@
/*
* Created by snapshot112 on 15/10/2025
*/
#include "minigame_menu.h"
#include <ncurses.h>
#include <stdlib.h>
#include "../../engine/grid_game_engine.h"
#include "../manual/manual.h"
#include "../maze-runner/maze_runner.h"
#include "../minesweeper/minesweeper.h"
#include "../snake/snake.h"
#define AMOUNT_OF_MENU_OPTIONS 5
typedef enum {
GAME_MANUAL = 0,
GAME_MAZE_RUNNER = 1,
GAME_SNAKE = 2,
GAME_MINESWEEPER = 3,
GAME_QUIT = 4,
} game;
static game SELECTED_GAME = GAME_MANUAL;
static int OFFSET_Y = 5;
static int OFFSET_X = 5;
/*
* Launch a game from the menu.
*
* Input:
* menu: A pointer to the menu grid.
* game: The game you want to launch.
*/
static void launch_game(const game game) {
switch (game) {
case GAME_MANUAL:
manual((coordinate){0,0});
break;
case GAME_MAZE_RUNNER:
maze_runner();
break;
case GAME_SNAKE:
snake();
break;
case GAME_MINESWEEPER:
minesweeper();
break;
}
}
/*
* Highlight a valid menu option.
*
* Input:
* menu: A pointer to the menu grid.
* target: The menu option to highlight.
* offset_x: The x offset of the menu.
* offset_y: The y offset of the menu.
*
* Side effects:
* If a valid menu option is provided: It will be highlighted in green.
* If an invalid menu option is provided: Nothing happens
*/
static void menu_highlight(const grid *menu) {
switch (SELECTED_GAME) {
case GAME_MANUAL:
case GAME_MAZE_RUNNER:
case GAME_SNAKE:
case GAME_MINESWEEPER:
case GAME_QUIT:
attron(COLOR_PAIR(GREEN));
char* row = grid_fetch_row(menu, SELECTED_GAME);
mvprintw(OFFSET_Y + (int)SELECTED_GAME, OFFSET_X, "%s", row);
free(row);
attroff(COLOR_PAIR(GREEN));
}
}
/*
* Show the menu screen.
*
* Input:
* menu: A pointer to the menu grid.
* default_selection: The starting selection in the menu.
* offset_x: The x offset of the menu.
* offset_y: The y offset of the menu.
*
* Side Effects:
* Displays the menu
*/
static void show_menu(const grid *menu) {
erase();
show_grid_on_offset(menu, OFFSET_X, OFFSET_Y);
menu_highlight(menu);
refresh();
}
/*
* Select the game on a location determined by a given offset.
* Negative values go up and positive values go down.
* Out of bounds selections loop around.
*
* Input:
* selected_game: The currently selected game.
* offset: The amount offset the current selection by.
*
* Side effect:
* The game on the location of the given offset will be selected.
*/
static void menu_move(const int offset) {
SELECTED_GAME = modulo(SELECTED_GAME + offset, AMOUNT_OF_MENU_OPTIONS);
}
/*
* Navigate through the menu.
*
* Input:
* menu: A pointer to the menu grid.
*
* Output: A code that reflects the current menu state.
* 0: Continue running.
* 1: Exit the menu.
*
*
* Side Effect:
* Changes the SELECTED_GAME as needed and launches selected games on select.
*/
static int navigate_menu(void) {
switch (getch()) {
case KEY_UP:
case 'w':
case 'W':
menu_move(-1);
break;
case KEY_DOWN:
case 's':
case 'S':
menu_move(1);
break;
case KEY_ENTER:
case '\n':
case 'f':
case 'F':
case ' ':
if (SELECTED_GAME == GAME_QUIT) {
return 1;
}
launch_game(SELECTED_GAME);
break;
case KEY_BACKSPACE:
case KEY_ESCAPE:
return 1;
}
return 0;
}
/*
* Create the menu grid.
*
* Output:
* A pointer to the menu grid.
*/
static grid *initialize_menu(void) {
char menu[] = "How to play\n"
"Maze Runner\n"
" Snake \n"
"Minesweeper\n"
" Leave \n";
grid *rp = grid_create_from_string(menu);
return rp;
}
void minigame_menu(void) {
grid *menu = initialize_menu();
while (true) {
if (navigate_menu() == 1) {
break;
}
}
grid_cleanup(menu);
}

View File

@@ -0,0 +1,28 @@
/*
* Created by snapshot112 on 15/10/2025
*
* A minigame menu for games build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling menu();
*/
#ifndef MINIGAME_MENU_MINIGAME_MENU_H
#define MINIGAME_MENU_MINIGAME_MENU_H
/*
* A minigame menu for games build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling menu();
*
* Side Effects:
* Clears the console and uses it to display a minigame menu.
*
* Controls:
* 'w'/'arr_up': Next menu item.
* 's'/'arr_down': Previous menu item.
* 'f'/'ENTER': Select current menu item.
* 'BACKSPACE'/'ESC': Exit the menu.
*/
void minigame_menu(void);
#endif //MINIGAME_MENU_MINIGAME_MENU_H

449
src/games/snake/snake.c Normal file
View File

@@ -0,0 +1,449 @@
/*
* 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/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();
}

26
src/games/snake/snake.h Normal file
View File

@@ -0,0 +1,26 @@
/*
* Created by snapshot112 on 15/10/2025
*
* A game of maze runner build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling snake();
*/
#ifndef MINIGAME_MENU_SNAKE_H
#define MINIGAME_MENU_SNAKE_H
/*
* A game of snake build on the grid game engine.
*
* Please make sure to include and initialize the game engine before calling snake();
*
* Side Effects:
* Clears the console and uses it to play a game of snake.
*
* Controls:
* Use WSAD or arrow keys to redirect the snake.
* Use BACKSPACE or ESCAPE to exit the game early.
*/
void snake(void);
#endif //MINIGAME_MENU_SNAKE_H

34
src/main.c Normal file
View File

@@ -0,0 +1,34 @@
/*
* Created by snapshot112 on 15/10/2025
*
* Naam: Jeroen Boxhoorn
* UvAnetID: 16333969
* Studie: BSC Informatica
*
* A minigame menu that lets you play games on the grid game engine.
*
* Currently the following games are included:
* - maze runner
* - snake
* - minesweeper
*
* A user manual can be found in the assets or by selected it in the menu using ENTER or 'f'.
*/
#include "engine/engine/grid_game_engine.h"
#include "games/minigame-menu/minigame_menu.h"
/*
* Play minigame menu.
*
* Side effect:
* Minigame menu starts.
*/
int main(void) {
init_engine();
// Speel het spel.
minigame_menu();
cleanup_engine();
}