606 lines
21 KiB
C
606 lines
21 KiB
C
/*
|
|
* serpent.c
|
|
*
|
|
* Copyright 2024 Darius Drake
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
/* Libraries */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <getopt.h>
|
|
#include <stdbool.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <ncurses.h>
|
|
#include "serpent.h"
|
|
/* */
|
|
|
|
/* Global variables */
|
|
bool isAlive = true; /* variable to check if snake is alive */
|
|
bool isPaused = false; /* variable to check if the game is paused */
|
|
const unsigned int minSpeed = 100; /* default speed of the game (lower is faster) */
|
|
const unsigned int maxSpeed = 70; /* max speed of the game (lower is faster) */
|
|
unsigned int speed = minSpeed; /* variable speed of the game (lower is faster) */
|
|
unsigned int terminalRows; /* variable for storing the terminal rows */
|
|
unsigned int terminalCols; /* variable for storing the terminal columns */
|
|
int startY; /* initial Y position of the window */
|
|
int startX; /* initial X position of the window */
|
|
unsigned int score; /* game score */
|
|
/* */
|
|
|
|
/* Initialize structs */
|
|
Snake *snake;
|
|
Apple *apple;
|
|
/* */
|
|
|
|
int main (int argc, char **argv) {
|
|
/* Command line parsing */
|
|
int option;
|
|
|
|
static const char* shortOptions = "chv";
|
|
static struct option longOptions[] = {
|
|
{"show-controls", no_argument, NULL, 'c'},
|
|
{"help", no_argument, NULL, 'h'},
|
|
{"version", no_argument, NULL, 'v'},
|
|
{NULL, 0, NULL, 0}
|
|
};
|
|
|
|
while ((option = getopt_long (argc, argv, shortOptions, longOptions, NULL)) != -1) {
|
|
switch (option) {
|
|
case 'c':
|
|
argControls();
|
|
return 0;
|
|
case 'h':
|
|
argHelp();
|
|
return 0;
|
|
case 'v':
|
|
argVersion();
|
|
return 0;
|
|
case '?':
|
|
fprintf(stderr, "Use '-h, --help' for help.\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if ((initializeGame()) == 1) return 1;
|
|
|
|
cleanup();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Function responsible of initializing the snake strucutre */
|
|
Snake *startSnake() {
|
|
/* Allocate memory for a new snake */
|
|
Snake *new_snake = malloc(sizeof(Snake));
|
|
|
|
/* Allocate memory for the head of the snake */
|
|
SnakeNode *head = malloc(sizeof(SnakeNode));
|
|
|
|
/* Initialize the snake's head and tail to the allocated head node */
|
|
new_snake->head = head;
|
|
new_snake->tail = head;
|
|
|
|
/* Set the head's previous and next pointers to NULL, as it's the only node in the beginning */
|
|
head->prev = NULL;
|
|
head->next = NULL;
|
|
|
|
/* Set the initial position of the snake's head at the center of the board */
|
|
head->pX = SCREEN_WIDTH / 2;
|
|
head->pY = SCREEN_HEIGHT / 2;
|
|
|
|
/* Initialize a node pointer to the head for iterating through the snake's body */
|
|
SnakeNode *node = head;
|
|
|
|
/* Create additional nodes to form the initial snake body */
|
|
for (int i = 1; i < START_SNAKE_SIZE; i++) {
|
|
/* Add a new node to the snake, updating the node pointer */
|
|
appendSnakeNode(new_snake);
|
|
node = node->next;
|
|
|
|
/* Copy the position of the previous node to maintain a straight line */
|
|
node->pX = node->prev->pX;
|
|
node->pY = node->prev->pY + 1;
|
|
}
|
|
|
|
/* Set the initial direction of the snake to move upward */
|
|
new_snake->direction = UP;
|
|
|
|
/* Return the initialized snake */
|
|
return new_snake;
|
|
}
|
|
|
|
/* Function responsible of initializing the apple structure */
|
|
Apple *startApple() {
|
|
/* Allocate memory for a new apple */
|
|
Apple *new_apple = malloc(sizeof(Apple));
|
|
|
|
/* Seed the random number generator with the current time */
|
|
srand(time(NULL));
|
|
|
|
/* Generate random coordinates for the new apple, ensuring it does not overlap with the snake */
|
|
do {
|
|
new_apple->pX = (random() % (SCREEN_WIDTH - 2)) + 1;
|
|
new_apple->pY = (random() % (SCREEN_HEIGHT - 2)) + 1;
|
|
} while (snakeCollision(new_apple->pX, new_apple->pY, false));
|
|
|
|
/* Return the initialized apple */
|
|
return new_apple;
|
|
}
|
|
|
|
/* Function responsible of adding a new node to the tail of the snake */
|
|
void appendSnakeNode(Snake *new_snake) {
|
|
/* Allocate memory for a new snake node */
|
|
SnakeNode *node_ptr = malloc(sizeof(SnakeNode));
|
|
|
|
/* Set the previous pointer of the new node to the current tail of the snake */
|
|
node_ptr->prev = new_snake->tail;
|
|
|
|
/* The new node is the last in the linked list, so its next pointer is NULL */
|
|
node_ptr->next = NULL;
|
|
|
|
/* Connect the current tail's next pointer to the new node */
|
|
new_snake->tail->next = node_ptr;
|
|
|
|
/* Update the snake's tail to point to the new node, making it the new tail */
|
|
new_snake->tail = node_ptr;
|
|
}
|
|
|
|
/* Function responsible of freeing the memory for the snake structure */
|
|
void freeSnake() {
|
|
/* Initialize pointers to traverse the snake linked list */
|
|
SnakeNode *snake_current = snake->head;
|
|
SnakeNode *snake_next = snake->head->next;
|
|
|
|
/* Traverse the snake linked list and free each node */
|
|
while (snake_next != NULL) {
|
|
/* Free the current node */
|
|
free(snake_current);
|
|
|
|
/* Move to the next node */
|
|
snake_current = snake_next;
|
|
snake_next = snake_next->next;
|
|
}
|
|
|
|
/* Free the last node */
|
|
free(snake_current);
|
|
|
|
/* Free the snake structure itself */
|
|
free(snake);
|
|
|
|
/* Set the global snake pointer to NULL to avoid dangling references */
|
|
snake = NULL;
|
|
}
|
|
|
|
/* Function responsible of saving the size of the snake */
|
|
int snakeSize() {
|
|
/* Initialize a pointer to traverse the snake linked list */
|
|
SnakeNode *snake_ptr = snake->head;
|
|
|
|
/* Initialize a counter to keep track of the number of nodes in the snake linked list */
|
|
int counter = 0;
|
|
|
|
/* Traverse the snake linked list and count each node */
|
|
while (snake_ptr != NULL) {
|
|
counter++;
|
|
|
|
/* Move to the next node */
|
|
snake_ptr = snake_ptr->next;
|
|
}
|
|
|
|
/* Return the total size of the snake, which is the number of nodes in the linked list */
|
|
return counter;
|
|
}
|
|
|
|
/* Function responsible of handling the snake's movement */
|
|
void updateSnake() {
|
|
/* Check if the snake is moving (either horizontally or vertically) */
|
|
if ((ABS(snake->direction == LEFT ? -1 : snake->direction == RIGHT ? 1 : 0) > 0) ||
|
|
(ABS(snake->direction == UP ? -1 : snake->direction == DOWN ? 1 : 0) > 0)) {
|
|
/* Calculate the new coordinates for the head and tail of the snake */
|
|
int new_head_x = snake->head->pX + (snake->direction == LEFT ? -1 : snake->direction == RIGHT ? 1 : 0);
|
|
int new_head_y = snake->head->pY + (snake->direction == UP ? -1 : snake->direction == DOWN ? 1 : 0);
|
|
int new_tail_x = snake->tail->pX + (snake->direction == LEFT ? -1 : snake->direction == RIGHT ? 1 : 0);
|
|
int new_tail_y = snake->tail->pY + (snake->direction == UP ? -1 : snake->direction == DOWN ? 1 : 0);
|
|
|
|
/* Check if the new head position does not overlap with an apple */
|
|
if (!appleCollision(new_head_x, new_head_y)) {
|
|
/* Move the tail to the new head position */
|
|
snake->tail->pX = new_head_x;
|
|
snake->tail->pY = new_head_y;
|
|
|
|
/* Adjust the linked list to maintain the snake's continuity */
|
|
snake->tail->next = snake->head;
|
|
snake->head->prev = snake->tail;
|
|
snake->tail = snake->tail->prev;
|
|
snake->tail->next = NULL;
|
|
snake->head->prev->prev = NULL;
|
|
snake->head = snake->head->prev;
|
|
} else {
|
|
/* If the head overlaps with an apple (the snake ate an apple), move the apple to a new position */
|
|
updateApple();
|
|
|
|
/* Increase the speed if it hasn't reached the maximum */
|
|
if (speed >= maxSpeed) {
|
|
speed -= 1;
|
|
}
|
|
|
|
/* Create a new head node and update its position */
|
|
SnakeNode *new_head = malloc(sizeof(SnakeNode));
|
|
new_head->pX = new_head_x;
|
|
new_head->pY = new_head_y;
|
|
new_head->prev = NULL;
|
|
new_head->next = snake->head;
|
|
|
|
/* Update the linked list to include the new head */
|
|
snake->head->prev = new_head;
|
|
snake->head = new_head;
|
|
|
|
/* Add a new node to the snake's body */
|
|
appendSnakeNode(snake);
|
|
|
|
/* Set the position for the tail node */
|
|
snake->tail->pX = new_tail_x;
|
|
snake->tail->pY = new_tail_y;
|
|
}
|
|
|
|
/* Check for collision with the game borders or itself */
|
|
if (snakeCollision(new_head_x, new_head_y, false) ||
|
|
(new_head_x == 0) || (new_head_x == SCREEN_WIDTH - 1) ||
|
|
(new_head_y == 0) || (new_head_y == SCREEN_HEIGHT - 1)) {
|
|
/* If there is a collision, set the snake as not alive */
|
|
isAlive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Function responsible of moving the apple to a new position */
|
|
void updateApple() {
|
|
/* Variables to store the new coordinates for the apple */
|
|
int new_x, new_y;
|
|
|
|
/* Generate new random coordinates for the apple, ensuring it does not overlap with the snake */
|
|
do {
|
|
new_x = (random() % (SCREEN_WIDTH - 2)) + 1;
|
|
new_y = (random() % (SCREEN_HEIGHT - 2)) + 1;
|
|
} while (snakeCollision(new_x, new_y, true));
|
|
|
|
/* Update the position of the existing apple to the new coordinates */
|
|
apple->pX = new_x;
|
|
apple->pY = new_y;
|
|
}
|
|
|
|
/* Function responsible of checking if the snake collided with itself */
|
|
bool snakeCollision(int x, int y, bool excludeHead) {
|
|
/* Initialize a pointer to traverse the snake linked list */
|
|
SnakeNode *snake_ptr;
|
|
|
|
/* Determine the starting point in the linked list based on whether the head should be excluded */
|
|
if (excludeHead) {
|
|
snake_ptr = snake->head;
|
|
} else {
|
|
snake_ptr = snake->head->next;
|
|
}
|
|
|
|
/* Traverse the snake linked list */
|
|
while (snake_ptr != NULL) {
|
|
/* Check if the current node's position matches the specified coordinates */
|
|
if (snake_ptr->pX == x && snake_ptr->pY == y) {
|
|
/* The snake occupies the specified position */
|
|
return true;
|
|
}
|
|
|
|
/* Move to the next node */
|
|
snake_ptr = snake_ptr->next;
|
|
}
|
|
|
|
/* The snake does not occupy the specified position */
|
|
return false;
|
|
}
|
|
|
|
/* Function to chech if the snake ate an apple */
|
|
bool appleCollision(int x, int y) {
|
|
/* Check if the specified coordinates match the position of the apple */
|
|
if (apple->pX == x && apple->pY == y) {
|
|
/* The apple occupies the specified position */
|
|
return true;
|
|
}
|
|
|
|
/* The apple does not occupy the specified position */
|
|
return false;
|
|
}
|
|
|
|
/* Function responsible of handling user input */
|
|
void handleInput(int key) {
|
|
/* Handle different key inputs to change the snake's direction */
|
|
switch (key) {
|
|
case KEY_UP:
|
|
if (isPaused) isPaused = !isPaused;
|
|
/* If the snake is not currently moving down, change its direction to up */
|
|
if (snake->direction != DOWN) snake->direction = UP;
|
|
break;
|
|
case KEY_DOWN:
|
|
if (isPaused) isPaused = !isPaused;
|
|
/* If the snake is not currently moving up, change its direction to down */
|
|
if (snake->direction != UP) snake->direction = DOWN;
|
|
break;
|
|
case KEY_RIGHT:
|
|
if (isPaused) isPaused = !isPaused;
|
|
/* If the snake is not currently moving left, change its direction to right */
|
|
if (snake->direction != LEFT) snake->direction = RIGHT;
|
|
break;
|
|
case KEY_LEFT:
|
|
if (isPaused) isPaused = !isPaused;
|
|
/* If the snake is not currently moving right, change its direction to left */
|
|
if (snake->direction != RIGHT) snake->direction = LEFT;
|
|
break;
|
|
case 'p':
|
|
isPaused = !isPaused;
|
|
break;
|
|
default:
|
|
/* Do nothing for other keys */
|
|
refresh();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Function responsible of drawing each object in the screen */
|
|
void drawGame() {
|
|
/* Clear the terminal screen */
|
|
erase();
|
|
|
|
/* Create a ncurses window for the game board */
|
|
WINDOW *gameBoard = newwin(SCREEN_HEIGHT, SCREEN_WIDTH, startY, startX);
|
|
box(gameBoard, 0, 0);
|
|
refresh();
|
|
wrefresh(gameBoard);
|
|
|
|
/* Draw the snake's head on the game board */
|
|
SnakeNode *snake_ptr = snake->head;
|
|
switch (snake->direction) {
|
|
case LEFT:
|
|
mvaddch(snake_ptr->pY + startY, snake_ptr->pX + startX, SNAKE_HEAD_L);
|
|
break;
|
|
case RIGHT:
|
|
mvaddch(snake_ptr->pY + startY, snake_ptr->pX + startX, SNAKE_HEAD_R);
|
|
break;
|
|
case UP:
|
|
mvaddch(snake_ptr->pY + startY, snake_ptr->pX + startX, SNAKE_HEAD_U);
|
|
break;
|
|
case DOWN:
|
|
mvaddch(snake_ptr->pY + startY, snake_ptr->pX + startX, SNAKE_HEAD_D);
|
|
break;
|
|
}
|
|
|
|
/* Move to the next node */
|
|
snake_ptr = snake_ptr->next;
|
|
|
|
/* Draw the snake's body */
|
|
while(snake_ptr != NULL) {
|
|
mvaddch(snake_ptr->pY + startY, snake_ptr->pX + startX, SNAKE_BODY);
|
|
/* Move to the next node */
|
|
snake_ptr = snake_ptr->next;
|
|
}
|
|
|
|
/* Draw the apple on the game board */
|
|
mvaddch(apple->pY + startY, apple->pX + startX, FOOD);
|
|
|
|
/* Display the game score */
|
|
mvprintw(startY, startX + 1, "Score: %d", snakeSize() - START_SNAKE_SIZE);
|
|
}
|
|
|
|
/* Game loop function */
|
|
void gameLoop() {
|
|
/* Take arrow key inputs */
|
|
int c = getch();
|
|
if (c != ERR) {
|
|
handleInput(c);
|
|
}
|
|
|
|
// Check if the game is not paused before updating the snake position
|
|
if (!isPaused) {
|
|
updateSnake();
|
|
}
|
|
|
|
/* Redraw the frame */
|
|
drawGame();
|
|
|
|
/* Refresh the window */
|
|
refresh();
|
|
|
|
/* Introduce a delay for the game loop */
|
|
usleep(speed * 800L);
|
|
}
|
|
|
|
/* Function responsible of initializing the game */
|
|
int initializeGame() {
|
|
/* Initialize window settings with ncurses */
|
|
initscr();
|
|
cbreak();
|
|
noecho();
|
|
keypad(stdscr, TRUE);
|
|
curs_set(0);
|
|
|
|
/* Detect if the terminal window is smaller than the board */
|
|
if (SCREEN_HEIGHT > LINES || SCREEN_WIDTH > COLS) {
|
|
endwin();
|
|
fprintf(stderr, "ERROR: The terminal window needs to be larger.\n");
|
|
return 1;
|
|
}
|
|
|
|
nodelay(stdscr, TRUE);
|
|
|
|
/* Set starting X and Y coordinates to center ncurses windows */
|
|
getmaxyx(stdscr, terminalRows, terminalCols);
|
|
startY = (terminalRows - SCREEN_HEIGHT) / 2;
|
|
startX = (terminalCols - SCREEN_WIDTH) / 2;
|
|
|
|
/* Initialize the snake doubly-linked list */
|
|
snake = startSnake();
|
|
|
|
/* Initialize the apple */
|
|
apple = startApple();
|
|
|
|
/* Run the game */
|
|
run();
|
|
return 0;
|
|
}
|
|
|
|
/* Starting point of the game */
|
|
void run() {
|
|
int choice;
|
|
bool isRunning = true;
|
|
|
|
/* Create an ncurses window for the main menu */
|
|
WINDOW * menuScreen = newwin(SCREEN_HEIGHT, SCREEN_WIDTH, startY, startX);
|
|
refresh();
|
|
wrefresh(menuScreen);
|
|
|
|
while (isRunning) {
|
|
/* Display main menu */
|
|
mainMenu(menuScreen, 1);
|
|
|
|
/* Input validation loop */
|
|
do {
|
|
/* Use wgetch to get a single character */
|
|
choice = wgetch(menuScreen);
|
|
|
|
} while (choice < '1' || choice > '3');
|
|
|
|
switch (choice) {
|
|
case '1':
|
|
if (!isAlive) {
|
|
cleanup();
|
|
snake = startSnake();
|
|
apple = startApple();
|
|
isAlive = true;
|
|
speed = minSpeed;
|
|
}
|
|
/* Start the game loop */
|
|
while (isAlive) {
|
|
gameLoop();
|
|
}
|
|
score = snakeSize() - START_SNAKE_SIZE;
|
|
mainMenu(menuScreen, 3);
|
|
break;
|
|
case '2':
|
|
/* Show controls */
|
|
mainMenu(menuScreen, 2);
|
|
continue;
|
|
break;
|
|
case '3':
|
|
/* Exit the game */
|
|
isRunning = false;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Function to display the main menu */
|
|
void mainMenu(WINDOW *menuScreen, int menuType) {
|
|
int menuY = (SCREEN_HEIGHT - 5) / 5;
|
|
int menuX = (SCREEN_WIDTH - 30) / 2;
|
|
|
|
wclear(menuScreen);
|
|
box(menuScreen, 0, 0);
|
|
mvwprintw(menuScreen, menuY - 1, menuX - 2, " ____ ");
|
|
mvwprintw(menuScreen, menuY, menuX - 2, " ________________________/ O \\___/");
|
|
mvwprintw(menuScreen, menuY + 1, menuX - 2, "<_____________________________/ \\");
|
|
mvwprintw(menuScreen, menuY + 2, menuX - 2, " __ _ ");
|
|
mvwprintw(menuScreen, menuY + 3, menuX - 2, "/ _\\ ___ _ __ _ __ ___ _ __ | |_ ");
|
|
mvwprintw(menuScreen, menuY + 4, menuX - 2, "\\ \\ / _ \\ '__| '_ \\ / _ \\ '_ \\| __|");
|
|
mvwprintw(menuScreen, menuY + 5, menuX - 2, "_\\ \\ __/ | | |_) | __/ | | | |_ ");
|
|
mvwprintw(menuScreen, menuY + 6, menuX - 2, "\\__/\\___|_| | .__/ \\___|_| |_|\\__|");
|
|
mvwprintw(menuScreen, menuY + 7, menuX - 2, " |_| ");
|
|
|
|
switch (menuType) {
|
|
case 1:
|
|
/* Print the main menu inside the menuScreen window */
|
|
mvwprintw(menuScreen, menuY + 9, menuX, "\tMain Menu");
|
|
mvwprintw(menuScreen, menuY + 10, menuX, "\t 1. Start Game");
|
|
mvwprintw(menuScreen, menuY + 11, menuX, "\t 2. Show Controls");
|
|
mvwprintw(menuScreen, menuY + 12, menuX, "\t 3. Exit Game");
|
|
mvwprintw(menuScreen, menuY + 13, menuX, "\tPress a key [1-3]...");
|
|
wrefresh(menuScreen);
|
|
break;
|
|
case 2:
|
|
/* Display controls */
|
|
mvwprintw(menuScreen, menuY + 9, menuX, "\tControls");
|
|
mvwprintw(menuScreen, menuY + 10, menuX, "\t Arrow Up: Move Up");
|
|
mvwprintw(menuScreen, menuY + 11, menuX, "\t Arrow Down: Move Down");
|
|
mvwprintw(menuScreen, menuY + 12, menuX, "\t Arrow Left: Move Left");
|
|
mvwprintw(menuScreen, menuY + 13, menuX, "\t Arrow Right: Move Right");
|
|
mvwprintw(menuScreen, menuY + 13, menuX, "\t p: Pause the game");
|
|
mvwprintw(menuScreen, menuY + 14, menuX, "\tPress a key to go back...");
|
|
wrefresh(menuScreen);
|
|
wgetch(menuScreen);
|
|
break;
|
|
case 3:
|
|
/* Display the final score on the main menu */
|
|
mvwprintw(menuScreen, menuY + 12, menuX, "\tGAME OVER");
|
|
mvwprintw(menuScreen, menuY + 13, menuX, "\tFinal Score: %d", score);
|
|
mvwprintw(menuScreen, menuY + 14, menuX, "\tPress a key to go back...");
|
|
wgetch(menuScreen);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Function responsible of cleaning the memory */
|
|
void cleanup() {
|
|
/* Free memory allocated by snake */
|
|
freeSnake();
|
|
|
|
/* Free memory allocated by apple */
|
|
free(apple);
|
|
|
|
/* End ncurses window */
|
|
endwin();
|
|
}
|
|
|
|
/* Function for displaying the game controls in the command line */
|
|
void argControls() {
|
|
printf("%s version: %.1lf\n", NAME, VERSION);
|
|
printf("Controls:\n");
|
|
printf("\tArrow Up: Move Up\n");
|
|
printf("\tArrow Down: Move Down\n");
|
|
printf("\tArrow Left: Move Left\n ");
|
|
printf("\tArrow Right: Move Right\n");
|
|
printf("\tp: Pause the game\n");
|
|
}
|
|
|
|
/* Function to display the help message in the command line */
|
|
void argHelp() {
|
|
printf("Usage: %s [OPTIONS]\n", NAME);
|
|
printf("Play the all time classic snake game in the console.\n\n");
|
|
printf("Options:\n");
|
|
printf("\t-c, --show-controls Show the controls for the game.\n");
|
|
printf("\t-h, --help Display this help message and exit.\n");
|
|
printf("\t-v, --version Display version and exit.\n");
|
|
}
|
|
|
|
/* Function to display the version in the command line */
|
|
void argVersion() {
|
|
printf("%s. version: %.1lf\n", NAME, VERSION);
|
|
printf("Author: Darius Drake\n");
|
|
printf("License: GPL v3\n");
|
|
printf("Contribute:\n");
|
|
printf(" The source code is available on GitHub -> https://github.com/d4r1us-drk/serpent\n");
|
|
printf(" Feel free to contribute with ideas, issues or pull requests.\n");
|
|
}
|