/*********************************************************
*
* hangm.c: Web-based Hangman game
*
* This app dynamically generates the HTML code for a web 
* based hangman game.  The words, along with clues, are 
* picked randomly from a text file.
*
* The game works with multiple "hangee" images,
* which makes it a bit more fun.  All data for the state 
* of the game is saved in the HTML sent to the browser.  
* That way, the server doesn't need to remember the state
* of the multiple games going on at once.  Plus, it uses
* only basic HTML tags (i.e., no java, etc) so it should
* work on most any platform or browser.
*
* Written by Eric 'Pi' Minbiole 
* (c) 2001
*
* Special thanks to Thomas Boutell for his cgic library,
* which is used to parse the CGI parameters.
* (Available at www.boutell.com)
*
**********************************************************/

#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include "cgic.h"

/* miscellaneous constants */
#define MAX_CLUE_SIZE 100
#define MAX_WORD_SIZE 100
#define MAX_CGI_PARAM_SIZE 1000
#define WORDS_FILE_NAME "hangman.txt"

/* global info about current game */
char word[MAX_WORD_SIZE+1];
char clue[MAX_CLUE_SIZE+1];
char guesses[26]; /* true or false for a...z */
char guesses_string[31];
int num_guesses;
int cur_word_number;
int game_over;
int g_imagenum = 0;

/* structure for loading in word/clue pairs from file */
typedef struct word_pair
{
  char word[MAX_WORD_SIZE+1];
  char clue[MAX_CLUE_SIZE+1];
} WORD_PAIR;

/* structure to store filenames, sizes, etc */
/* of the different images to be hanged */
#define MAX_IMAGES 3
typedef struct image_info
{
  const char *filename;
  const char *description;
  const char *difficulty;
  int width;
  int height;
  int top_width;
  int top_height;
  int side_width;
  int side_height;
  int bot_width;
  int bot_height;
  int max_missed_guesses;
} IMAGE_INFO;

/* the images */
IMAGE_INFO the_images[MAX_IMAGES] =
{
  "vitruvian", "Vitruvian Man", "Beginner",
  220, 221, /* img width & height */
  216, 39, 50, 229, 252, 31, /* gallows sizes */
  10, /* max mistakes */

  "david", "David", "Intermediate",
  120, 300, /* img width & height */
  123, 39, 51, 301, 252, 31, /* gallows sizes */
  6, /* max mistakes */
 
  "vdmilo", "Venus de Milo", "Advanced",
  119, 300, /* img width & height */
  144, 39, 51, 301, 252, 31, /* gallows sizes */
  4, /* max mistakes */
};

/* give up and exit */
void fatal_error(char *mess)
{
  fprintf(cgiOut, mess);
  fprintf(cgiOut, "</BODY></HTML>\n");
  exit(0);
}

/* this routine computes a checksum of an integer */
/* it is used to prevent people from modifying the word */
int get_checksum(unsigned int num)
{
  int i = 0;

  /* the following constants are arbitrary */
  int multiplier[] = { 42, 97, 3 };
  int checksum = 3;
  num += 2356;

  while (num > 0)
  {
    checksum += (num % 10) * multiplier[i];
    i++;
    if (i >= 3)
      i = 0;
    num /= 10;
  }

  /* round it to two(ish) digits */
  checksum %= 93;

  return (checksum);
}

/* given a line from the words file, parse it into the WORD_PAIR structure */
int parse_word_pair(char *buffer, WORD_PAIR *word_pair)
{
  char seps[] = "\t\n";
  char *token;

  /* read in the word */
  token = strtok( buffer, seps );
  if (token == NULL)
    return (0);
  strncpy(word_pair->word, token, MAX_WORD_SIZE);
  word_pair->word[MAX_WORD_SIZE - 1] = '\0';

  /* read in the clue */
  token = strtok( NULL, seps );
  if (token == NULL)
    return (0);
  strncpy(word_pair->clue, token, MAX_CLUE_SIZE);
  word_pair->clue[MAX_CLUE_SIZE - 1] = '\0';

  return (1);
}

/* read in the words file, and find the appropriate word/clue */
void load_guess_file(int desired)
{
  FILE *fp;
  char line_buffer[1026], *ptr;
  WORD_PAIR word_pair;
  int index = 0;
  int num_pairs = 0;
  int found = 0;
  double val;

  /* open the translation file */
  fp = fopen(WORDS_FILE_NAME, "r");
  if (NULL == fp)
  {
    fatal_error("Could not open file");
  }

  /* count number of pairs in file first */
  /* (or look for the word) */
  while ((ptr = fgets(line_buffer, 1024, fp)) != NULL)
  {
    line_buffer[1024] = '\0';
    if (parse_word_pair(line_buffer, &word_pair))
      num_pairs++;
  }
  if (num_pairs <= 0)
  {
    fatal_error("No words to use");
    fclose(fp);
  }

  if (desired < 0)
  {
    /* chose a word at random */
    srand((int)time(NULL));
    val = rand();
    val /= RAND_MAX;
    desired = (int)(val * num_pairs);
  }
  if (desired >= num_pairs)
    desired = num_pairs - 1; 
  cur_word_number = desired;

  rewind(fp);  /* start at beginning again */

  /* find correct word */
  while ((ptr = fgets(line_buffer, 1024, fp)) != NULL)
  {
    if (parse_word_pair(line_buffer, &word_pair));
    {
      if (index == desired)
      {
        strcpy(word, word_pair.word);
        strcpy(clue, word_pair.clue);
        found = 1;
        break;
      }
      index++;
    }
  }
  
  if (!found)
  {
    fatal_error("could not get word");
  }
}

#define CHECKSUM_SIZE 2
#define LINE_SIZE 4
char cgiparams[MAX_CGI_PARAM_SIZE + 1];

/* format the parameters that we'll send back to ourself */
/* with the next guess.  The parameters are in the formt: */
/* s=(unique session id)  Used to prevent browser caching */
/* w=(word number with checksum) */
/* g=(string of guesses) */
/* i=(image index) */
void FormCgiParams(char c)
{
  char extra_guess[2] = {0, 0};
  if ((c >= 'a') || (c <= 'z'))
  {
    /* append the extra char, if necessary */
    extra_guess[0] = c;
  }

  sprintf(cgiparams, "s=%04d&w=%0*d%0*d&g=%s%s&i=%d",
    time(NULL)%1000,
    LINE_SIZE, cur_word_number, 
    CHECKSUM_SIZE, get_checksum(cur_word_number),
    guesses_string, extra_guess, g_imagenum);

}

/* format the choices, A-Z.  Only make the unchosen ones clickable */
void DisplayValidChoices(void)
{
  int i;

  /* nice, big, bold font */
  fprintf(cgiOut, "<br><font size=\"4\"><b>\n");

  /* loop through all the letters */
  for (i = 0; i < 26; i++)
  {
    if ((guesses[i] == 0) && (game_over == 0))
    {
      FormCgiParams((char)(i + 'a'));
      fprintf(cgiOut, "<a href=\"http://www.pisymbol.com/cgi-bin/hangm?%s\">%c</a> \n", 
        cgiparams, i + 'A');
    }
    else
    {
      fprintf(cgiOut, "%c \n", i + 'A');
    }

	/* start 'n' on a new line */
	if (i == 12)
		fprintf(cgiOut, "<br>");
  }

  /* close the font tag */
  fprintf(cgiOut, "<br></b></font>\n");
}

/* read the word number from the string. */
/* make sure the checksum matches */
int ExtractWordNum(char *buf)
{
    int wordnum, checksum, i;
    char word_str[10];
    char checksum_str[10];

    /* make sure it's all numbers */
    for (i = 0; i < CHECKSUM_SIZE + LINE_SIZE; i++)
    {
      if (!isdigit(buf[i]))
        return(-1);
    }
    /* make sure it's correct len */
    if (buf[i] != '\0')
    {
      return (-1);
    }

    /* extract the checksum */
    memcpy(checksum_str, buf + LINE_SIZE, CHECKSUM_SIZE);
    checksum_str[CHECKSUM_SIZE] = '\0';
    checksum = atoi(checksum_str);

    /* extract the word num */
    memcpy(word_str, buf, LINE_SIZE);
    word_str[LINE_SIZE] = '\0';
    wordnum = atoi(word_str);

    /* debug info */
#if 0
    printf("wordnum: %d, checksum: %d, correct: %d.\n", wordnum, checksum, get_checksum(wordnum));
#endif

    /* verify */
    if (get_checksum(wordnum) != checksum)
    {
      return(-1);
    }

    return(wordnum);
}

int CountMissedGuesses(void)
{
  int correct_guesses = 0;
  unsigned int i, j;
  char c;

  /* loop through all guesses, finding mistakes */
  for (i = 0; i < strlen(guesses_string); i++)
  {
    c = guesses_string[i];
    for (j = 0; j < strlen(word); j++)
    {
      if (tolower(word[j]) == c)
      {
		/* this guess was correct */
        correct_guesses++;
        break;
      }
    }
  }
  return (num_guesses - correct_guesses);
}

/* if all the letters in the word were guessed, */
/* the player won */
int PlayerWon(void)
{
  unsigned int i;
  char c;

  for (i = 0; i < strlen(word); i++)
  {
    c = tolower(word[i]);
    if ((c >= 'a') && (c <= 'z'))
    {
      if (guesses[c - 'a'] == 0)
        return(0); /* found a missing letter */
    }
  }
  return (1);
}

/* Loads the game from the parameters passed in the URL, if possible */
/* if not possible, start a new game */
void LoadGame(IMAGE_INFO **imgptr, int *MissedGuesses)
{
  int i;
  int valid_saved_game = 0;
  int result1, result2;
  unsigned int wordnum = 0;
  char word_string[20];
  char c;

  /* get the image number, and default to "David" */
  cgiFormIntegerBounded("i", &g_imagenum, 0, MAX_IMAGES - 1, 1);
  *imgptr = &the_images[g_imagenum];

  result1 = cgiFormStringNoNewlines("w", word_string, 20);
  result2 = cgiFormStringNoNewlines("g", guesses_string, 30);
  if ((result1 == cgiFormSuccess) && (result2 == cgiFormSuccess))
  {
    valid_saved_game = 1;

    num_guesses = strlen(guesses_string);
    if (num_guesses < 1)
      valid_saved_game = 0;

    wordnum = ExtractWordNum(word_string);
    if (wordnum < 0)
      valid_saved_game = 0;

    if (valid_saved_game)
    {
      /* try to load this word */
      load_guess_file(wordnum);
    }

	/* fill out the table of guesses */
    for (i = 0; (i < num_guesses) && (valid_saved_game); i++)
    {
      c = guesses_string[i];
      if ((c > 'z') || (c < 'a'))
      {
		/* this is not a valid choice */
        valid_saved_game = 0;
        break;
      }
      guesses[c - 'a'] = 1;
    }
  }

  if (!valid_saved_game)
  {
	/* we don't have a valid game.  Start a new one */
    /* get a random word from the file */
    load_guess_file(-1);
    
    /* reset the guesses */
    for (i = 0; i < 26; i++)
      guesses[i] = 0;
    num_guesses = 0;
    guesses_string[0] = 0;
  }

  /* see if we've had too many mistakes */
  *MissedGuesses = CountMissedGuesses();
  if (*MissedGuesses >= (*imgptr)->max_missed_guesses)
    game_over = 1;
}

void DisplayGameInfo(IMAGE_INFO *imgptr)
{
  char c;
  unsigned int count = 0, j;
  int mistakes_left = 0;
  fprintf(cgiOut, "<br>Clue: %s<br>\n", clue);

  /* tell 'em if they won/lost */
  if (PlayerWon())
  {
    fprintf(cgiOut, "<font size=\"4\" color=\"#008800\">You Win!</font><br>&nbsp;\n");
    game_over = 1;
    return;
  }
  if (game_over)
  {
    fprintf(cgiOut, "<font size=\"4\" color=\"#ff0000\">Game Over</font><br>&nbsp;\n");
    return;
  }

  /* see if the last guess was correct */
  if (strlen(guesses_string) > 0)
  {
	/* count the occurences of the last guess */
    c = guesses_string[strlen(guesses_string) - 1];
    for (j = 0; j < strlen(word); j++)
    {
      if (tolower(word[j]) == c)
      {
        count++;
      }
    }

	/* tell 'em how many were found */
    c = toupper(c);
    if (count == 1)
    {
      fprintf(cgiOut, "Yes, there is one %c. ", c);
    }
    else if (count > 1)
    {
      fprintf(cgiOut, "Yes, there are %d %c's. ", count, c);
    }
    else
    {
      fprintf(cgiOut, "Sorry, there were no %c's. ", c);
    }
  }

  /* display the number of mistakes left */
  mistakes_left = imgptr->max_missed_guesses - CountMissedGuesses();
  fprintf(cgiOut, "You have %d mistake%s left.<br>&nbsp;<br>\n", 
    mistakes_left, mistakes_left == 1 ? "" : "s");
}

void DisplayHangmanImages(IMAGE_INFO *imgptr, int MissedGuesses)
{
  /* show the top of the gallows */
  fprintf(cgiOut, "<img src=\"../games/hangm/%stop.gif\" align=\"left\" width=\"%d\" height=\"%d\">\n", 
	  imgptr->filename, imgptr->top_width, imgptr->top_height);
  fprintf(cgiOut, "<br clear=\"all\">\n");

  /* show the vertical section of the gallows */
  fprintf(cgiOut, "<img src=\"../games/hangm/%sside.gif\" align=\"left\" width=\"%d\" height=\"%d\">\n", 
    imgptr->filename, imgptr->side_width, imgptr->side_height);

  /* put the appropriate pic inside the gallows */
  if (MissedGuesses > 0)
  {
    fprintf(cgiOut, "<img src=\"../games/hangm/%s%d.gif\" align=\"left\" width=\"%d\" height=\"%d\">\n", 
      imgptr->filename, MissedGuesses - 1, imgptr->width, imgptr->height);
  }
  fprintf(cgiOut, "<br clear=\"all\">\n");

  /* show the bottom of the gallows */
  fprintf(cgiOut, "<img src=\"../games/hangm/%sbot.gif\" align=\"left\" width=\"%d\" height=\"%d\">\n", 
    imgptr->filename, imgptr->bot_width, imgptr->bot_height);
}

void DisplayNewGameForm(void)
{
  int i;
  IMAGE_INFO *imgptr;

  /* start the form */
  fprintf(cgiOut, "<form method=\"get\" action=\"/cgi-bin/hangm\">\n");

  /* add a select box, with a choice for each image */
  fprintf(cgiOut, "<hr><select name=\"i\">\n");
  for (i = 0; i < MAX_IMAGES; i++)
  {
    imgptr = &the_images[i];
    fprintf(cgiOut, "<option value=\"%d\"%s>%s (%s)\n", 
      i, (i == g_imagenum) ? " selected" : "", imgptr->description, imgptr->difficulty);
  }
  fprintf(cgiOut, "</select>\n");

  /* add a hidden field with a pseudorandom session id.  This keeps browsers from caching the page */
  fprintf(cgiOut, "<input type=\"hidden\" name=\"s\" value=\"%d\">\n", time(NULL) % 1000);

  /* add a "New Game" button and close the form */
  fprintf(cgiOut, "<input type=\"submit\" value=\"New Game\"></form>\n");
}

/* format the word in its current state, */
/* using dashes for unguessed letters */
void FormatWord(void)
{
  unsigned int i;
  char c;
  char str[100];

  /* really big, bold font */
  fprintf(cgiOut, "<br><font size=\"6\"><b>&nbsp;\n");

  /* loop through the entire word */
  for (i = 0; i < strlen(word); i++)
  {
    c = word[i];
    c = tolower(c);
    if ((c >= 'a') && (c <= 'z'))
    {
	  /* it's a letter: display the letter or a dash */
      if ((guesses[c - 'a']) || (game_over))
        sprintf(str, "%c", word[i]);
      else
        strcpy(str, "_&nbsp;");
    }
    else if (c == ' ')
    {
	  /* output the space */
      strcpy(str, " &nbsp;");
    }
    else
    {
	  /* other character: just echo it */
      sprintf(str, "%c", c);
    }
    fprintf(cgiOut, "%s", str);
  }

  /* close out the font */
  fprintf(cgiOut, "<br></b></font>\n");
}

/* main entry point */
int cgiMain() 
{
  int MissedGuesses = 0;
  IMAGE_INFO *imgptr;
#if DEBUG
  /* Load a saved CGI scenario if we're debugging */
  cgiReadEnvironment("capcgi.dat");
#endif
  cgiHeaderContentType("text/html");
  fprintf(cgiOut, "<HTML><HEAD>\n");
  fprintf(cgiOut, "<TITLE>Online Hangman</TITLE></HEAD>\n");
  fprintf(cgiOut, "<BODY BACKGROUND=\"../fun.gif\"><CENTER><H2>Online Hangman</H2></CENTER>\n");

  /* load the current game from the params */
  LoadGame(&imgptr, &MissedGuesses);

  /* start a table: left = picture, right = game info */
  fprintf(cgiOut, "<table border=\"0\"><tr><td>\n");

  /* format the four pics that make up the hangman */
  DisplayHangmanImages(imgptr, MissedGuesses);

  /* move to the right half of the table & display the word & game info */
  fprintf(cgiOut, "</td><td>\n");
  FormatWord();
  DisplayGameInfo(imgptr);
  DisplayValidChoices();

  /* display the form */
  DisplayNewGameForm();
    
  /* add some additional links */
  fprintf(cgiOut, "<center><br><A HREF=\"../games/hangm/hangm_about.htm\">About the game</A>\n");
  fprintf(cgiOut, "<br><A HREF=\"../index.htm\">Home</A></center>\n");

  /* finish the table and html */
  fprintf(cgiOut, "</td></tr></table>\n</BODY></HTML>\n");

  /* exit success */
  return 0;
}
