+int word_count(char* s)
+{
+ char* copy = xstrdup(s);
+ char delims[] = " \t";
+ int count = 0;
+ char* word;
+
+ word = strtok(copy, delims);
+ while(word != NULL)
+ {
+ word = strtok(NULL, delims);
+ ++count;
+ }
+ free(copy);
+ return(count);
+}
+
+char* get_input()
+{
+ // Set up the prompt
+ char input_prompt[] = "> ";
+ if (!prompt)
+ input_prompt[0] = '\0';
+
+ // Print a blank line if game.blklin tells us to.
+ if (game.blklin == true)
+ printf("\n");
+
+ char* input;
+ while (true) {
+ if (editline)
+ input = linenoise(input_prompt);
+ else {
+ input = NULL;
+ size_t n = 0;
+ if (isatty(0))
+ // LCOV_EXCL_START
+ // Should be unreachable in tests, as they will use a non-interactive shell.
+ printf("%s", input_prompt);
+ // LCOV_EXCL_STOP
+ ssize_t numread = getline(&input, &n, stdin);
+ if (numread == -1) // Got EOF; return with it.
+ return(NULL);
+ }
+
+ if (input == NULL) // Got EOF; return with it.
+ return(input);
+ else if (input[0] == '#') // Ignore comments.
+ continue;
+ else // We have a 'normal' line; leave the loop.
+ break;
+ }
+
+ // Strip trailing newlines from the input
+ input[strcspn(input, "\n")] = 0;
+
+ linenoiseHistoryAdd(input);
+
+ if (!isatty(0))
+ echo_input(stdout, input_prompt, input);
+
+ if (logfp)
+ echo_input(logfp, input_prompt, input);
+
+ return (input);
+}
+
+bool silent_yes()
+{
+ char* reply;
+ bool outcome;
+
+ for (;;) {
+ reply = get_input();
+ if (reply == NULL) {
+ // LCOV_EXCL_START
+ // Should be unreachable. Reply should never be NULL
+ linenoiseFree(reply);
+ exit(EXIT_SUCCESS);
+ // LCOV_EXCL_STOP
+ }
+
+ char* firstword = (char*) xmalloc(strlen(reply)+1);
+ sscanf(reply, "%s", firstword);
+
+ for (int i = 0; i < (int)strlen(firstword); ++i)
+ firstword[i] = tolower(firstword[i]);
+
+ int yes = strncmp("yes", firstword, sizeof("yes") - 1);
+ int y = strncmp("y", firstword, sizeof("y") - 1);
+ int no = strncmp("no", firstword, sizeof("no") - 1);
+ int n = strncmp("n", firstword, sizeof("n") - 1);
+
+ free(firstword);
+
+ if (yes == 0 || y == 0) {
+ outcome = true;
+ break;
+ } else if (no == 0 || n == 0) {
+ outcome = false;
+ break;
+ } else
+ rspeak(PLEASE_ANSWER);
+ }
+ linenoiseFree(reply);
+ return (outcome);
+}
+
+
+bool yes(const char* question, const char* yes_response, const char* no_response)
+/* Print message X, wait for yes/no answer. If yes, print Y and return true;
+ * if no, print Z and return false. */
+{
+ char* reply;
+ bool outcome;
+
+ for (;;) {
+ speak(question);
+
+ reply = get_input();
+ if (reply == NULL) {
+ // LCOV_EXCL_START
+ // Should be unreachable. Reply should never be NULL
+ linenoiseFree(reply);
+ exit(EXIT_SUCCESS);
+ // LCOV_EXCL_STOP
+ }
+
+ char* firstword = (char*) xmalloc(strlen(reply)+1);
+ sscanf(reply, "%s", firstword);
+
+ for (int i = 0; i < (int)strlen(firstword); ++i)
+ firstword[i] = tolower(firstword[i]);
+
+ int yes = strncmp("yes", firstword, sizeof("yes") - 1);
+ int y = strncmp("y", firstword, sizeof("y") - 1);
+ int no = strncmp("no", firstword, sizeof("no") - 1);
+ int n = strncmp("n", firstword, sizeof("n") - 1);
+
+ free(firstword);
+
+ if (yes == 0 || y == 0) {
+ speak(yes_response);
+ outcome = true;
+ break;
+ } else if (no == 0 || n == 0) {
+ speak(no_response);
+ outcome = false;
+ break;
+ } else
+ rspeak(PLEASE_ANSWER);
+ }
+ linenoiseFree(reply);
+ return (outcome);
+}
+
+/* Line-parsing routines (GETTXT, MAKEWD, PUTTXT, SHFTXT) */
+
+long GETTXT(bool skip, bool onewrd, bool upper)
+/* Take characters from an input line and pack them into 30-bit words.
+ * Skip says to skip leading blanks. ONEWRD says stop if we come to a
+ * blank. UPPER says to map all letters to uppercase. If we reach the
+ * end of the line, the word is filled up with blanks (which encode as 0's).
+ * If we're already at end of line when TEXT is called, we return -1. */
+{
+ long text;
+ static long splitting = -1;
+
+ if (LNPOSN != splitting)
+ splitting = -1;
+ text = -1;
+ while (true) {
+ if (LNPOSN > LNLENG)
+ return (text);
+ if ((!skip) || INLINE[LNPOSN] != 0)
+ break;
+ ++LNPOSN;
+ }
+
+ text = 0;
+ for (int I = 1; I <= TOKLEN; I++) {
+ text = text * 64;
+ if (LNPOSN > LNLENG || (onewrd && INLINE[LNPOSN] == 0))
+ continue;
+ char current = INLINE[LNPOSN];
+ if (current < ascii_to_advent['%']) {
+ splitting = -1;
+ if (upper && current >= ascii_to_advent['a'])
+ current = current - 26;
+ text = text + current;
+ ++LNPOSN;
+ continue;
+ }
+ if (splitting != LNPOSN) {
+ text = text + ascii_to_advent['%'];
+ splitting = LNPOSN;
+ continue;
+ }
+
+ text = text + current - ascii_to_advent['%'];
+ splitting = -1;
+ ++LNPOSN;
+ }
+
+ return text;
+}
+
+token_t MAKEWD(long letters)
+/* Combine TOKLEN (currently 5) uppercase letters (represented by
+ * pairs of decimal digits in lettrs) to form a 30-bit value matching
+ * the one that GETTXT would return given those characters plus
+ * trailing blanks. Caution: lettrs will overflow 31 bits if
+ * 5-letter word starts with V-Z. As a kludgey workaround, you can
+ * increment a letter by 5 by adding 50 to the next pair of
+ * digits. */
+{
+ long i = 1, word = 0;