555 lines
16 KiB
C
555 lines
16 KiB
C
#ifdef _WIN32 //For 32 and 64 bits
|
|
/* pdcurses include */
|
|
#include <curses.h>
|
|
#else
|
|
/* Linux ncurses include */
|
|
#include <ncurses.h>
|
|
#endif
|
|
#include "hangman.h" //includes time.h
|
|
#include <stdlib.h> //atexit
|
|
#include <string.h>
|
|
#include <ctype.h> //tolower
|
|
#include <getopt.h>
|
|
#include "prng.h"
|
|
#include <unistd.h> //usleep
|
|
#include "figures.h"
|
|
#include <signal.h>
|
|
|
|
|
|
void handle_winch(int sig) {
|
|
/* This function handles the console resizing */
|
|
endwin();
|
|
clear();
|
|
refresh();
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
|
|
const char *short_options = "w:hf:ct";
|
|
struct option long_options[] = {
|
|
{"word", required_argument, NULL, 'w'},
|
|
{"help", no_argument, NULL, 'h'},
|
|
{"file", required_argument, NULL, 'f'},
|
|
{"credits", no_argument, NULL, 'c'},
|
|
{"troll", no_argument, NULL, 't'}
|
|
};
|
|
int c, startscr = 1; /* Show startscreen by default */
|
|
char filename[255];
|
|
game_state gs = {0};
|
|
hitfeed hf = {{{0}}}; // 3 braces are needed to not get a warning
|
|
|
|
filename[0] = '\0';
|
|
|
|
/* Initialization */
|
|
initscr();
|
|
atexit(quitProgram);
|
|
|
|
//Windows resize handler
|
|
struct sigaction sa;
|
|
memset(&sa, 0, sizeof(struct sigaction));
|
|
sa.sa_handler = handle_winch;
|
|
sigaction(SIGWINCH, &sa, NULL);
|
|
|
|
start_color(); //Enables multi color mode
|
|
|
|
/* Initialize custom color pairs */
|
|
/* Color pair index must be > 0 */
|
|
/* init_pair(index, foreground color, background color); */
|
|
/* Color pairs are called by attron(COLOR_PAIR(1)); */
|
|
init_pair(1, COLOR_CYAN, COLOR_BLACK); //Start screen
|
|
init_pair(2, COLOR_GREEN, COLOR_BLACK); //
|
|
init_pair(3, COLOR_RED, COLOR_BLACK); //Hitfeed
|
|
|
|
initCoordinates(&gs);
|
|
gs.allowedMoves = DEFAULTTRIES;
|
|
|
|
keypad(stdscr, FALSE);
|
|
nonl(); //No new line. Prevents the new line when hitting enter
|
|
|
|
curs_set(0);
|
|
while ( (c = getopt_long(argc, argv, short_options, long_options, NULL)) != -1 ) {
|
|
switch (c) {
|
|
case 'w':
|
|
sprintf(gs.guessWord, "%s", optarg);
|
|
startscr = 0;
|
|
break;
|
|
case 'h':
|
|
showHelp(&gs);
|
|
return 0;
|
|
break;
|
|
case 'f':
|
|
/* Set filename */
|
|
sprintf(filename, "%s", optarg);
|
|
startscr = 0;
|
|
break;
|
|
case 'c':
|
|
|
|
break;
|
|
case 't':
|
|
/* troll option */
|
|
gs.trollEnabled = 1;
|
|
break;
|
|
default:
|
|
mvprintw(gs.centery, gs.centerx - 9, "Invalid arguments.");
|
|
getch();
|
|
showHelp(&gs);
|
|
return 0;
|
|
break;
|
|
}
|
|
}
|
|
InitializePRNG(time(NULL)); /* Initialize random number generator */
|
|
|
|
if (startscr) {
|
|
showStartScreen(&gs);
|
|
}
|
|
|
|
/* start game */
|
|
initGuessWord(&gs, filename);
|
|
initAlphabet(&gs);
|
|
drawAlphabet(&gs, 0);
|
|
drawFigure(&gs, 0);
|
|
startGame(&gs, &hf);
|
|
|
|
nl(); //enable newline again
|
|
while (getch() != 10);
|
|
clear();
|
|
return 0;
|
|
}
|
|
|
|
void startGame(game_state *gs, hitfeed *hf) {
|
|
noecho(); /* Don't show the player input */
|
|
time(&gs->startTime);
|
|
while (checkWin(gs)) {
|
|
initCoordinates(gs); //Necessary for window resizing
|
|
updateScreen(gs);
|
|
playerInput(gs, hf);
|
|
printHitFeed(gs, hf);
|
|
}
|
|
}
|
|
|
|
void quitProgram(void) {
|
|
endwin();
|
|
}
|
|
|
|
void showStartScreen(game_state *gs) {
|
|
enColor(1);
|
|
mvprintw(gs->centery - 1, gs->centerx - 15, "Welcome to hangman in ncurses.");
|
|
mvprintw(gs->centery, gs->centerx - 5, "Have fun!");
|
|
mvprintw(gs->centery + 1, gs->centerx - 17, "https://gitlab.com/STRUCTiX/hangman");
|
|
disColor(1);
|
|
refresh();
|
|
getch();
|
|
clear();
|
|
}
|
|
|
|
void updateScreen(game_state *gs) {
|
|
mvprintw(0, 1, "Remaining wrong guesses: %i", (gs->allowedMoves - gs->moves));
|
|
mvprintw(gs->maxy, 1, "Press ctrl + c to exit.");
|
|
drawGuessWord(gs);
|
|
mvprintw(1, 1, "Wrong characters: %s", gs->wrongCharacters);
|
|
refresh();
|
|
}
|
|
|
|
void initCoordinates(game_state *gs) {
|
|
getmaxyx(stdscr, gs->maxy, gs->maxx);
|
|
gs->maxy -= 1;
|
|
gs->maxx -= 1;
|
|
gs->centery = gs->maxy / 2;
|
|
gs->centerx = gs->maxx / 2;
|
|
}
|
|
|
|
void initGuessWord(game_state *gs, char *filename) {
|
|
int i;
|
|
mvprintw(0, 1, "Please enter your word: ");
|
|
if (strlen(gs->guessWord) == 0) { /* Word can be set by arguments */
|
|
/* If a file argument was specified then try to read a random line from the file,
|
|
if this is not successful or if there is no file argument use the manual input. */
|
|
if (strlen(filename) == 0 || readRandomLine(filename, gs->guessWord) != 0) { // readRandomLine is only excecuted when strlen(filename) != 0
|
|
/* Manual input */
|
|
curs_set(1);
|
|
getnstr(gs->guessWord, MAXWORDLENGTH - 1); /* Reads the guessWord with a limit */
|
|
curs_set(0);
|
|
}
|
|
}
|
|
|
|
/* Make String all lowercase */
|
|
toLowerCase(gs->guessWord);
|
|
gs->wordLength = strlen(gs->guessWord);
|
|
|
|
for (i = 0; i < gs->wordLength; i++) {
|
|
if (gs->guessWord[i] == ' ') { //Check for spaces if it's a sentence
|
|
gs->currentWord[i] = ' ';
|
|
} else {
|
|
gs->currentWord[i] = '_';
|
|
}
|
|
}
|
|
clear(); //clear the screen
|
|
}
|
|
|
|
void initAlphabet(game_state *gs) {
|
|
strcpy(gs->alphabet, "abcdefghijklmnopqrstuvwxyz");
|
|
|
|
for (unsigned int i = 0; i < strlen(gs->guessWord); i++) {
|
|
if (isdigit(gs->guessWord[i]) > 0) {
|
|
strcat(gs->alphabet, "1234567890");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void drawAlphabet(game_state *gs, char usedchar) {
|
|
int start = centerDiff(gs->centerx, gs->alphabet);
|
|
int i;
|
|
if (usedchar != 0) { /* 0 is used for initialization */
|
|
for (i = 0; i < (int)strlen(gs->alphabet); i++) {
|
|
if (gs->alphabet[i] == usedchar) {
|
|
gs->alphabet[i] = '_';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
mvprintw(1, start, "%s", gs->alphabet);
|
|
}
|
|
|
|
void toLowerCase(char *str) {
|
|
int length = strlen(str);
|
|
int i;
|
|
for (i = 0; i < length; i++) {
|
|
str[i] = tolower(str[i]);
|
|
}
|
|
}
|
|
|
|
void drawGuessWord(game_state *gs) {
|
|
int startpos = gs->centerx - gs->wordLength;
|
|
int i, wordpos = 0;
|
|
int switchspace = 0;
|
|
int xcounter = startpos; /* counter of the x position on screen */
|
|
int ycounter = 0; /* counter of the y position on sceen (row) */
|
|
int tempstartpos = startpos; /* the calculated start position to draw the characters */
|
|
int rows = 1; /* number of rows */
|
|
|
|
if (gs->wordLength * 2 > gs->maxx - LINEBREAK) {
|
|
/* The word will be longer then the max. linesize with offset. */
|
|
rows = 2;
|
|
while (1) {
|
|
if ((gs->wordLength * 2) / rows < gs->maxx - LINEBREAK) {
|
|
/* splitted the word into equal sizes and found
|
|
the right amount of rows. */
|
|
break;
|
|
}
|
|
rows++;
|
|
}
|
|
gs->wordRows = rows; //set the number of rows for clearing the screen later
|
|
tempstartpos = xcounter = gs->centerx - (gs->wordLength / rows);
|
|
}
|
|
for (i = startpos; i < startpos + (gs->wordLength * 2); i++) {
|
|
if (xcounter >= gs->centerx + (gs->wordLength / rows)) {
|
|
/* end of the current row. Next line and revert the x to start */
|
|
ycounter++;
|
|
xcounter = tempstartpos;
|
|
}
|
|
if (switchspace) {
|
|
/* this will place a space */
|
|
mvprintw(gs->centery + ycounter, xcounter, " ");
|
|
switchspace = 0;
|
|
} else {
|
|
mvprintw(gs->centery + ycounter, xcounter, "%c", gs->currentWord[wordpos++]);
|
|
switchspace = 1;
|
|
}
|
|
xcounter++;
|
|
}
|
|
refresh();
|
|
}
|
|
|
|
int playerInput(game_state *gs, hitfeed *hf) {
|
|
int inp;
|
|
int i, found = 0;
|
|
inp = getch();
|
|
if (inp != KEY_UP && inp != KEY_DOWN && inp != KEY_LEFT && inp != KEY_RIGHT) {
|
|
for (i = 0; i < gs->wordLength; i++) {
|
|
if (inp == gs->guessWord[i]) {
|
|
found++;
|
|
//break;
|
|
}
|
|
}
|
|
if (found) {
|
|
/* found a valid character */
|
|
if (fillCurrentWord(gs, inp)) {
|
|
//gs->moves++;
|
|
gs->guesses++;
|
|
trollHitScreen(gs, hf, found);
|
|
trollHandleImpressive(hf, gs, found); //Prints a message with a little animation
|
|
}
|
|
} else {
|
|
/* no valid character found */
|
|
if (stackWrongCharacter(gs, inp)) {
|
|
gs->moves++;
|
|
gs->guesses++;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int fillCurrentWord(game_state *gs, char validchar) {
|
|
int i, alreadyUsed = 0;
|
|
for (i = 0; i < gs->wordLength; i++) {
|
|
if (gs->currentWord[i] == validchar) {
|
|
alreadyUsed = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!alreadyUsed) {
|
|
for (i = 0; i < gs->wordLength; i++) {
|
|
if (gs->guessWord[i] == validchar) {
|
|
gs->currentWord[i] = validchar;
|
|
}
|
|
}
|
|
drawAlphabet(gs, validchar);
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int stackWrongCharacter(game_state *gs, char wrongchar) {
|
|
for (unsigned i = 0; i < strlen(gs->wrongCharacters); i++) {
|
|
if (gs->wrongCharacters[i] == wrongchar) {
|
|
return 0; // already used
|
|
}
|
|
}
|
|
|
|
// not yet used
|
|
gs->wrongCharacters[strlen(gs->wrongCharacters)] = wrongchar;
|
|
drawAlphabet(gs, wrongchar);
|
|
drawFigure(gs, 1);
|
|
|
|
return 1;
|
|
}
|
|
|
|
int checkWin(game_state *gs) {
|
|
if (strcmp(gs->guessWord, gs->currentWord) != 0 && gs->moves < gs->allowedMoves) {
|
|
/* next move */
|
|
return 1;
|
|
} else {
|
|
/* game end: decide if game is won or lost */
|
|
time(&gs->endTime);
|
|
printGameStats(gs);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void printGameStats(game_state *gs) {
|
|
//clear();
|
|
int i, z;
|
|
|
|
if (!gs->trollEnabled) {
|
|
for (z = 0; z <= gs->wordRows; z++) { //<= takes the last row too
|
|
for (i = 0; i < gs->maxx; i++) {
|
|
mvprintw(gs->centery + z, i, " ");
|
|
//mvprintw(gs->centery + 1, i, " ");
|
|
}
|
|
}
|
|
} else {
|
|
for (z = 0; z <= gs->wordRows; z++) {
|
|
animateLineClear(gs, gs->centery + z, 0); //-1 to speed up the animation
|
|
}
|
|
}
|
|
|
|
if (gs->moves >= gs->allowedMoves) {
|
|
mvprintw(gs->centery, gs->centerx - 10, "Game lost. Solution:");
|
|
mvprintw(gs->centery + 1, gs->centerx - (gs->wordLength / 2), gs->guessWord);
|
|
} else {
|
|
char message[100];
|
|
|
|
sprintf(message, "Game won! Total guesses: %i", gs->guesses);
|
|
mvprintw(gs->centery, gs->centerx - (strlen(message) / 2), message);
|
|
|
|
sprintf(message, "Wrong guesses: %i, right/wrong ratio: ", gs->moves);
|
|
if (gs->moves != 0) {
|
|
sprintf(message + strlen(message), "%.2f", ((double)gs->guesses - (double)gs->moves) / (double)gs->moves);
|
|
} else {
|
|
sprintf(message + strlen(message), "max");
|
|
}
|
|
|
|
sprintf(message + strlen(message), ", time: %llusec", (unsigned long long)(gs->endTime - gs->startTime));
|
|
mvprintw(gs->centery + 1, gs->centerx - (strlen(message) / 2), message);
|
|
mvprintw(gs->centery + 2, gs->centerx - (gs->wordLength / 2), gs->guessWord);
|
|
|
|
}
|
|
refresh();
|
|
}
|
|
|
|
void showHelp(game_state *gs) {
|
|
char *wordstring = "-w or --word: Enter the word or sentence as an argument";
|
|
char *helpstring = "-h or --help: Show this page";
|
|
mvprintw(gs->centery, centerDiff(gs->centerx, wordstring), wordstring);
|
|
mvprintw(gs->centery + 1, centerDiff(gs->centerx, helpstring), helpstring);
|
|
getch();
|
|
}
|
|
|
|
int centerDiff(int coordinate, char *str) {
|
|
int len = strlen(str);
|
|
return coordinate - (len / 2); /* Integer division */
|
|
}
|
|
|
|
int readRandomLine(char *file, char *result) {
|
|
FILE *fp;
|
|
int count = 0, wordlength = 0;
|
|
char line[MAXWORDLENGTH];
|
|
|
|
fp = fopen(file, "r");
|
|
if (fp == NULL) {
|
|
return 1;
|
|
}
|
|
|
|
srand(time(NULL));
|
|
while (fgets(line, MAXWORDLENGTH, fp) != NULL) {
|
|
count++;
|
|
if ((rand() / (float)RAND_MAX) <= (1.0 / count)) {
|
|
strcpy(result, line);
|
|
}
|
|
}
|
|
/* remove \n at the end of the line */
|
|
wordlength = strlen(result);
|
|
result[wordlength - 1] = '\0';
|
|
fclose(fp);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void trollHitScreen(game_state *gs, hitfeed *hf, int hits) {
|
|
if (gs->trollEnabled) {
|
|
char *strings[] = {"Double Hit", "Triple Hit", "Multi Hit",
|
|
"Ultra Hit", "Monster Hit", "Rampage",
|
|
"Unstoppable", "Wicked sick", "Godlike"};
|
|
int selection[] = {2, 3, 4, 5, 6, 7, 8, 9, 10};
|
|
int i, found = 0;
|
|
for (i = 0; i < 9; i++) {
|
|
if (selection[i] == hits) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (hits > 10) {
|
|
found = 1;
|
|
i = 8;
|
|
}
|
|
if (found) {
|
|
flash(); /* flash the screen */
|
|
mvprintw(5, centerDiff(gs->centerx, strings[i]), strings[i]);
|
|
refresh();
|
|
/* Put the new streak to the hit feed */
|
|
addHitToFeed(hf, strings[i], hits);
|
|
animateLineClear(gs, 5, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void addHitToFeed(hitfeed *hf, char *streak, int hit) {
|
|
/* Shift the array to put the new string to the first slot */
|
|
int i;
|
|
for (i = HITFEEDSLOTS - 1; i > 0; i--) {
|
|
strcpy(hf->history[i], hf->history[i - 1]);
|
|
}
|
|
/* Set the new first item */
|
|
strcpy(hf->history[0], streak);
|
|
if (hit > hf->besthit) {
|
|
strcpy(hf->beststreak, streak);
|
|
hf->besthit = hit;
|
|
}
|
|
}
|
|
|
|
void printHitFeed(game_state *gs, hitfeed *hf) {
|
|
if (gs->trollEnabled) {
|
|
/* print the stats at the bootom right corner */
|
|
int newMaxy = gs->maxy - HITFEEDSLOTS;
|
|
int newMaxx = gs->maxx - (gs->centerx / 2);
|
|
int i, z;
|
|
enColor(3); //Enable red color
|
|
for (i = -1; i < HITFEEDSLOTS; i++) {
|
|
for (z = 0; z <= gs->maxx - newMaxx; z++) {
|
|
mvprintw(newMaxy + i, newMaxx + z, " ");
|
|
}
|
|
}
|
|
for (i = 0; i < HITFEEDSLOTS; i++) {
|
|
mvprintw(newMaxy + i, newMaxx, hf->history[i]);
|
|
}
|
|
/* Print best score above history feed */
|
|
if (hf->besthit != 0) {
|
|
/* There is a hit. So a best score can be displayed */
|
|
char message[100];
|
|
sprintf(message, "Best: %s (%i hits)", hf->beststreak, hf->besthit);
|
|
mvprintw(newMaxy - 1, newMaxx, message);
|
|
}
|
|
disColor(3); //Disable red color
|
|
}
|
|
}
|
|
|
|
void trollHandleImpressive(hitfeed *hf, game_state *gs, int hits) {
|
|
if (hits >= IMPRESSIVEHIT) {
|
|
hf->impstreakcounter++; //Increase the counter
|
|
} else {
|
|
/* There was a hit beneath the hit threshold.
|
|
The streak is vanished */
|
|
hf->impstreakcounter = 0;
|
|
}
|
|
if (gs->trollEnabled && hf->impstreakcounter >= IMPRESSIVESTREAK) {
|
|
/* Execute animation and reset the counter */
|
|
int i;
|
|
for (i = 0; i < 5; i++) {
|
|
mvprintw(5, gs->centerx - 5, "IMPRESSIVE!");
|
|
refresh();
|
|
usleep(100000);
|
|
mvprintw(5, gs->centerx - 5, " ");
|
|
refresh();
|
|
usleep(100000);
|
|
}
|
|
hf->impstreakcounter = 0;
|
|
}
|
|
}
|
|
|
|
void drawFigure(game_state *gs, int drawNext) {
|
|
int row = 6;
|
|
int length = 8;
|
|
int i;
|
|
static int state = 0;
|
|
if (!state) {
|
|
for (i = 0; i < row; i++) {
|
|
mvprintw(i, gs->maxx - length - 1, "%s", base_figure[i]);
|
|
}
|
|
state++;
|
|
} else {
|
|
/* Draw the stages */
|
|
if (drawNext) {
|
|
mvprintw(stages[state - 1] + 1, gs->maxx - 4, "%s", figure[state - 1]);
|
|
state++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void animateLineClear(game_state *gs, int line, int offsetMultiplier) {
|
|
/* sleep and vanish */
|
|
int j = 0, k = gs->maxx, usec;
|
|
usec = ((ANIM_DURATION * 1000000) + (100000 * offsetMultiplier)) / gs->maxx;
|
|
|
|
for (j = 0; j <= gs->centerx; j++, k--) {
|
|
mvprintw(line, j, "!");
|
|
mvprintw(line, k, "!");
|
|
refresh();
|
|
usleep(usec);
|
|
mvprintw(line, j, " ");
|
|
mvprintw(line, k, " ");
|
|
}
|
|
}
|
|
|
|
|
|
void enColor(int color) {
|
|
attron(COLOR_PAIR(color));
|
|
}
|
|
|
|
|
|
void disColor(int color) {
|
|
attroff(COLOR_PAIR(color));
|
|
}
|