X-Git-Url: https://jxself.org/git/?a=blobdiff_plain;f=main.c;h=cdc83bafddeb15d090ff9ecd5a62c8c2e270316b;hb=829c13d1d56e398e5b47186c31b2e2146f26dc6a;hp=c1ebe0e0583bb6410b86102f8b5733aa42fcf1aa;hpb=f01b3267c87c6d5670aa47e586fde0d622952bae;p=open-adventure.git diff --git a/main.c b/main.c index c1ebe0e..cdc83ba 100644 --- a/main.c +++ b/main.c @@ -1,20 +1,7 @@ /* - * There used to be a note that said this: - * - * The author - Don Woods - apologises for the style of the code; it - * is a result of running the original Fortran IV source through a - * home-brew Fortran-to-C converter. - * - * Now that the code has been restructured into something much closer - * to idiomatic C, the following is more appropriate: - * - * ESR apologizes for the remaing gotos (now confined to one function - * in this file - there used to be over 350 of them, *everywhere*). - * Applying the Structured Program Theorem can be hard. - * - * Copyright (c) 1977, 2005 by Will Crowther and Don Woods - * Copyright (c) 2017 by Eric S. Raymond - * SPDX-License-Identifier: BSD-2-clause + * SPDX-FileCopyrightText: 1977, 2005 by Will Crowther and Don Woods + * SPDX-FileCopyrightText: 2017 by Eric S. Raymond + * SPDX-License-Identifier: BSD-2-Clause */ #include @@ -24,11 +11,25 @@ #include #include #include +#include +#include #include "advent.h" #include "dungeon.h" #define DIM(a) (sizeof(a)/sizeof(a[0])) +#if defined ADVENT_AUTOSAVE +static FILE* autosave_fp; +void autosave(void) +{ + if (autosave_fp != NULL) { + rewind(autosave_fp); + savefile(autosave_fp); + fflush(autosave_fp); + } +} +#endif + // LCOV_EXCL_START // exclude from coverage analysis because it requires interactivity to test static void sig_handler(int signo) @@ -37,106 +38,66 @@ static void sig_handler(int signo) if (settings.logfp != NULL) fflush(settings.logfp); } + +#if defined ADVENT_AUTOSAVE + if (signo == SIGHUP || signo == SIGTERM) + autosave(); +#endif exit(EXIT_FAILURE); } // LCOV_EXCL_STOP -/* - * MAIN PROGRAM - * - * Adventure (rev 2: 20 treasures) - * History: Original idea & 5-treasure version (adventures) by Willie Crowther - * 15-treasure version (adventure) by Don Woods, April-June 1977 - * 20-treasure version (rev 2) by Don Woods, August 1978 - * Errata fixed: 78/12/25 - * Revived 2017 as Open Adventure. - */ - -static bool do_command(void); - -int main(int argc, char *argv[]) +char *myreadline(const char *prompt) { - int ch; - - /* Options. */ - -#ifndef ADVENT_NOSAVE - const char* opts = "l:or:"; - const char* usage = "Usage: %s [-l logfilename] [-o] [-r restorefilename]\n"; - FILE *rfp = NULL; -#else - const char* opts = "l:o"; - const char* usage = "Usage: %s [-l logfilename] [-o]\n"; -#endif - while ((ch = getopt(argc, argv, opts)) != EOF) { - switch (ch) { - case 'l': - settings.logfp = fopen(optarg, "w"); - if (settings.logfp == NULL) - fprintf(stderr, - "advent: can't open logfile %s for write\n", - optarg); - signal(SIGINT, sig_handler); - break; - case 'o': - settings.oldstyle = true; - settings.prompt = false; - break; -#ifndef ADVENT_NOSAVE - case 'r': - rfp = fopen(optarg, "r"); - if (rfp == NULL) - fprintf(stderr, - "advent: can't open save file %s for read\n", - optarg); - break; -#endif - default: - fprintf(stderr, - usage, argv[0]); - fprintf(stderr, - " -l create a log file of your game named as specified'\n"); - fprintf(stderr, - " -o 'oldstyle' (no prompt, no command editing, displays 'Initialising...')\n"); -#ifndef ADVENT_NOSAVE - fprintf(stderr, - " -r restore from specified saved game file\n"); -#endif - exit(EXIT_FAILURE); - break; - } + /* + * This function isn't required for gameplay, readline() straight + * up would suffice for that. It's where we interpret command-line + * logfiles for testing purposes. + */ + /* Normal case - no script arguments */ + if (settings.argc == 0) { + char *ln = readline(prompt); + if (ln == NULL) { + fputs(prompt, stdout); + } + return ln; } - /* Initialize game variables */ - long seedval = initialise(); + char *buf = malloc(LINESIZE + 1); + for (;;) { + if (settings.scriptfp == NULL || feof(settings.scriptfp)) { + if (settings.optind >= settings.argc) { + free(buf); + return NULL; + } -#ifndef ADVENT_NOSAVE - if (!rfp) { - game.novice = yes(arbitrary_messages[WELCOME_YOU], arbitrary_messages[CAVE_NEARBY], arbitrary_messages[NO_MESSAGE]); - if (game.novice) - game.limit = NOVICELIMIT; - } else { - restore(rfp); - } -#else - game.novice = yes(arbitrary_messages[WELCOME_YOU], arbitrary_messages[CAVE_NEARBY], arbitrary_messages[NO_MESSAGE]); - if (game.novice) - game.limit = NOVICELIMIT; -#endif + char *next = settings.argv[settings.optind++]; - if (settings.logfp) - fprintf(settings.logfp, "seed %ld\n", seedval); + if (settings.scriptfp != NULL && feof(settings.scriptfp)) + fclose(settings.scriptfp); + if (strcmp(next, "-") == 0) + settings.scriptfp = stdin; // LCOV_EXCL_LINE + else + settings.scriptfp = fopen(next, "r"); + } - /* interpret commands until EOF or interrupt */ - for (;;) { - if (!do_command()) - break; + if (isatty(fileno(settings.scriptfp))) { + free(buf); // LCOV_EXCL_LINE + return readline(prompt); // LCOV_EXCL_LINE + } else { + char *ln = fgets(buf, LINESIZE, settings.scriptfp); + if (ln != NULL) { + fputs(prompt, stdout); + fputs(ln, stdout); + return ln; + } + } } - /* show score and exit */ - terminate(quitgame); + + return NULL; } -/* Check if this loc is eligible for any hints. If been here long +/* Check if this loc is eligible for any hints. If been here int * enough, display. Ignore "HINTS" < 4 (special stuff, see database * notes). */ static void checkhints(void) @@ -148,7 +109,7 @@ static void checkhints(void) if (!CNDBIT(game.loc, hint + 1 + COND_HBASE)) game.hintlc[hint] = -1; ++game.hintlc[hint]; - /* Come here if he's been long enough at required loc(s) for some + /* Come here if he's been int enough at required loc(s) for some * unused hint. */ if (game.hintlc[hint] >= hints[hint].turns) { int i; @@ -210,15 +171,16 @@ static void checkhints(void) game.hintlc[hint] = 0; return; default: // LCOV_EXCL_LINE + // Should never happen BUG(HINT_NUMBER_EXCEEDS_GOTO_LIST); // LCOV_EXCL_LINE } /* Fall through to hint display */ game.hintlc[hint] = 0; - if (!yes(hints[hint].question, arbitrary_messages[NO_MESSAGE], arbitrary_messages[OK_MAN])) + if (!yes_or_no(hints[hint].question, arbitrary_messages[NO_MESSAGE], arbitrary_messages[OK_MAN])) return; rspeak(HINT_COST, hints[hint].penalty, hints[hint].penalty); - game.hinted[hint] = yes(arbitrary_messages[WANT_HINT], hints[hint].hint, arbitrary_messages[OK_MAN]); + game.hinted[hint] = yes_or_no(arbitrary_messages[WANT_HINT], hints[hint].hint, arbitrary_messages[OK_MAN]); if (game.hinted[hint] && game.limit > WARNTIME) game.limit += WARNTIME * hints[hint].penalty; } @@ -237,8 +199,7 @@ static bool spotted_by_pirate(int i) * that game.place[CHEST] = LOC_NOWHERE might mean that he's thrown * it to the troll, but in that case he's seen the chest * (game.prop[CHEST] == STATE_FOUND). */ - if (game.loc == game.chloc || - game.prop[CHEST] != STATE_NOTFOUND) + if (game.loc == game.chloc || game.prop[CHEST] != STATE_NOTFOUND) return true; int snarfed = 0; bool movechest = false, robplayer = false; @@ -251,8 +212,7 @@ static bool spotted_by_pirate(int i) game.loc == objects[EMERALD].plac)) { continue; } - if (TOTING(treasure) || - HERE(treasure)) + if (TOTING(treasure) || HERE(treasure)) ++snarfed; if (TOTING(treasure)) { movechest = true; @@ -314,9 +274,7 @@ static bool dwarfmove(void) * steal return toll, and dwarves can't meet the bear. Also * means dwarves won't follow him into dead end in maze, but * c'est la vie. They'll wait for him outside the dead end. */ - if (game.loc == LOC_NOWHERE || - FORCED(game.loc) || - CNDBIT(game.newloc, COND_NOARRR)) + if (game.loc == LOC_NOWHERE || FORCED(game.loc) || CNDBIT(game.newloc, COND_NOARRR)) return true; /* Dwarf activity level ratchets up */ @@ -331,8 +289,7 @@ static bool dwarfmove(void) * replace him with the alternate. */ if (game.dflag == 1) { if (!INDEEP(game.loc) || - (PCT(95) && (!CNDBIT(game.loc, COND_NOBACK) || - PCT(85)))) + (PCT(95) && (!CNDBIT(game.loc, COND_NOBACK) || PCT(85)))) return true; game.dflag = 2; for (int i = 1; i <= 2; i++) { @@ -460,24 +417,28 @@ static bool dwarfmove(void) * building (and heaven help him if he tries to xyzzy back into the * cave without the lamp!). game.oldloc is zapped so he can't just * "retreat". */ - static void croak(void) /* Okay, he's dead. Let's get on with it. */ { - if (game.numdie < 0) - game.numdie = 0; const char* query = obituaries[game.numdie].query; const char* yes_response = obituaries[game.numdie].yes_response; + ++game.numdie; + if (game.closng) { /* He died during closing time. No resurrection. Tally up a * death and exit. */ rspeak(DEATH_CLOSING); terminate(endgame); - } else if ( !yes(query, yes_response, arbitrary_messages[OK_MAN]) - || game.numdie == NDEATHS) + } else if (!yes_or_no(query, yes_response, arbitrary_messages[OK_MAN]) + || game.numdie == NDEATHS) { + /* Player is asked if he wants to try again. If not, or if + * he's already used all of his lives, we end the game */ terminate(endgame); - else { + } else { + /* If player wishes to continue, we empty the liquids in the + * user's inventory, turn off the lamp, and drop all items + * where he died. */ game.place[WATER] = game.place[OIL] = LOC_NOWHERE; if (TOTING(LAMP)) game.prop[LAMP] = LAMP_DARK; @@ -492,6 +453,28 @@ static void croak(void) } } +static void describe_location(void) +/* Describe the location to the user */ +{ + const char* msg = locations[game.loc].description.small; + + if (MOD(game.abbrev[game.loc], game.abbnum) == 0 || msg == NO_MESSAGE) + msg = locations[game.loc].description.big; + + if (!FORCED(game.loc) && DARK(game.loc)) { + msg = arbitrary_messages[PITCH_DARK]; + } + + if (TOTING(BEAR)) + rspeak(TAME_BEAR); + + speak(msg); + + if (game.loc == LOC_Y2 && PCT(25) && !game.closng) + rspeak(SAYS_PLUGH); +} + + static bool traveleq(int a, int b) /* Are two travel entries equal for purposes of skip after failed condition? */ { @@ -509,8 +492,7 @@ static bool traveleq(int a, int b) * does, game.newloc will be limbo, and game.oldloc will be what killed * him, so we need game.oldlc2, which is the last place he was * safe.) */ - -static void playermove( int motion) +static void playermove(int motion) { int scratchloc, travel_entry = tkey[game.loc]; game.newloc = game.loc; @@ -584,8 +566,7 @@ static void playermove( int motion) /* Look for a way to fulfil the motion verb passed in - travel_entry indexes * the beginning of the motion entries for here (game.loc). */ for (;;) { - if (T_TERMINATE(travel[travel_entry]) || - travel[travel_entry].motion == motion) + if ((travel[travel_entry].motion == HERE) || travel[travel_entry].motion == motion) break; if (travel[travel_entry].stop) { /* Couldn't find an entry matching the motion word passed @@ -634,19 +615,17 @@ static void playermove( int motion) for (;;) { /* L12 loop */ for (;;) { enum condtype_t condtype = travel[travel_entry].condtype; - long condarg1 = travel[travel_entry].condarg1; - long condarg2 = travel[travel_entry].condarg2; + int condarg1 = travel[travel_entry].condarg1; + int condarg2 = travel[travel_entry].condarg2; if (condtype < cond_not) { /* YAML N and [pct N] conditionals */ if (condtype == cond_goto || condtype == cond_pct) { - if (condarg1 == 0 || - PCT(condarg1)) + if (condarg1 == 0 || PCT(condarg1)) break; /* else fall through */ } /* YAML [with OBJ] clause */ - else if (TOTING(condarg1) || - (condtype == cond_with && AT(condarg1))) + else if (TOTING(condarg1) || (condtype == cond_with && AT(condarg1))) break; /* else fall through to check [not OBJ STATE] */ } else if (game.prop[condarg1] != condarg2) @@ -685,8 +664,7 @@ static void playermove( int motion) game.newloc = (game.loc == LOC_PLOVER) ? LOC_ALCOVE : LOC_PLOVER; - if (game.holdng > 1 || - (game.holdng == 1 && !TOTING(EMERALD))) { + if (game.holdng > 1 || (game.holdng == 1 && !TOTING(EMERALD))) { game.newloc = game.loc; rspeak(MUST_DROP); } @@ -721,9 +699,9 @@ static void playermove( int motion) * game.prop[TROLL]=TROLL_UNPAID.) Special stuff * for bear. */ if (game.prop[TROLL] == TROLL_PAIDONCE) { - pspeak(TROLL, look, TROLL_PAIDONCE, true); + pspeak(TROLL, look, true, TROLL_PAIDONCE); game.prop[TROLL] = TROLL_UNPAID; - move(TROLL2, LOC_NOWHERE); + DESTROY(TROLL2); move(TROLL2 + NOBJECTS, IS_FREE); move(TROLL, objects[TROLL].plac); move(TROLL + NOBJECTS, objects[TROLL].fixd); @@ -755,6 +733,50 @@ static void playermove( int motion) (false); } +static void lampcheck(void) +/* Check game limit and lamp timers */ +{ + if (game.prop[LAMP] == LAMP_BRIGHT) + --game.limit; + + /* Another way we can force an end to things is by having the + * lamp give out. When it gets close, we come here to warn him. + * First following arm checks if the lamp and fresh batteries are + * here, in which case we replace the batteries and continue. + * Second is for other cases of lamp dying. Even after it goes + * out, he can explore outside for a while if desired. */ + if (game.limit <= WARNTIME) { + if (HERE(BATTERY) && game.prop[BATTERY] == FRESH_BATTERIES && HERE(LAMP)) { + rspeak(REPLACE_BATTERIES); + game.prop[BATTERY] = DEAD_BATTERIES; +#ifdef __unused__ + /* This code from the original game seems to have been faulty. + * No tests ever passed the guard, and with the guard removed + * the game hangs when the lamp limit is reached. + */ + if (TOTING(BATTERY)) + drop(BATTERY, game.loc); +#endif + game.limit += BATTERYLIFE; + game.lmwarn = false; + } else if (!game.lmwarn && HERE(LAMP)) { + game.lmwarn = true; + if (game.prop[BATTERY] == DEAD_BATTERIES) + rspeak(MISSING_BATTERIES); + else if (game.place[BATTERY] == LOC_NOWHERE) + rspeak(LAMP_DIM); + else + rspeak(GET_BATTERIES); + } + } + if (game.limit == 0) { + game.limit = -1; + game.prop[LAMP] = LAMP_DARK; + if (HERE(LAMP)) + rspeak(LAMP_OUT); + } +} + static bool closecheck(void) /* Handle the closing of the cave. The cave closes "clock1" turns * after the last treasure has been located (including the pirate's @@ -808,7 +830,7 @@ static bool closecheck(void) game.dseen[i] = false; game.dloc[i] = LOC_NOWHERE; } - move(TROLL, LOC_NOWHERE); + DESTROY(TROLL); move(TROLL + NOBJECTS, IS_FREE); move(TROLL2, objects[TROLL].plac); move(TROLL2 + NOBJECTS, objects[TROLL].fixd); @@ -822,7 +844,7 @@ static bool closecheck(void) rspeak(CAVE_CLOSING); game.clock1 = -1; game.closng = true; - return true; + return game.closed; } else if (game.clock1 < 0) --game.clock2; if (game.clock2 == 0) { @@ -871,56 +893,13 @@ static bool closecheck(void) rspeak(CAVE_CLOSED); game.closed = true; - return true; + return game.closed; } + lampcheck(); return false; } -static void lampcheck(void) -/* Check game limit and lamp timers */ -{ - if (game.prop[LAMP] == LAMP_BRIGHT) - --game.limit; - - /* Another way we can force an end to things is by having the - * lamp give out. When it gets close, we come here to warn him. - * First following arm checks if the lamp and fresh batteries are - * here, in which case we replace the batteries and continue. - * Second is for other cases of lamp dying. Even after it goes - * out, he can explore outside for a while if desired. */ - if (game.limit <= WARNTIME) { - if (HERE(BATTERY) && game.prop[BATTERY] == FRESH_BATTERIES && HERE(LAMP)) { - rspeak(REPLACE_BATTERIES); - game.prop[BATTERY] = DEAD_BATTERIES; -#ifdef __unused__ - /* This code from the original game seems to have been faulty. - * No tests ever passed the guard, and with the guard removed - * the game hangs when the lamp limit is reached. - */ - if (TOTING(BATTERY)) - drop(BATTERY, game.loc); -#endif - game.limit += BATTERYLIFE; - game.lmwarn = false; - } else if (!game.lmwarn && HERE(LAMP)) { - game.lmwarn = true; - if (game.prop[BATTERY] == DEAD_BATTERIES) - rspeak(MISSING_BATTERIES); - else if (game.place[BATTERY] == LOC_NOWHERE) - rspeak(LAMP_DIM); - else - rspeak(GET_BATTERIES); - } - } - if (game.limit == 0) { - game.limit = -1; - game.prop[LAMP] = LAMP_DARK; - if (HERE(LAMP)) - rspeak(LAMP_OUT); - } -} - static void listobjects(void) /* Print out descriptions of objects at this location. If * not closing and property value is negative, tally off @@ -946,6 +925,8 @@ static void listobjects(void) game.prop[RUG] = RUG_DRAGON; if (obj == CHAIN) game.prop[CHAIN] = CHAINING_BEAR; + if (obj == EGGS) + game.seenbigwords = true; --game.tally; /* Note: There used to be a test here to see whether the * player had blown it so badly that he could never ever see @@ -967,16 +948,87 @@ static void listobjects(void) kk = (game.loc == game.fixed[STEPS]) ? STEPS_UP : STEPS_DOWN; - pspeak(obj, look, kk, true); + pspeak(obj, look, true, kk); } } } -static bool do_command() -/* Get and execute a command */ +static bool preprocess_command(command_t *command) +/* Pre-processes a command input to see if we need to tease out a few specific cases: + * - "enter water" or "enter stream": + * weird specific case that gets the user wet, and then kicks us back to get another command + * - : + * Irregular form of input, but should be allowed. We switch back to form for + * further processing. + * - "grate": + * If in location with grate, we move to that grate. If we're in a number of other places, + * we move to the entrance. + * - "water plant", "oil plant", "water door", "oil door": + * Change to "pour water" or "pour oil" based on context + * - "cage bird": + * If bird is present, we change to "carry bird" + * + * Returns true if pre-processing is complete, and we're ready to move to the primary command + * processing, false otherwise. */ { - static command_t command; + if (command->word[0].type == MOTION && command->word[0].id == ENTER + && (command->word[1].id == STREAM || command->word[1].id == WATER)) { + if (LIQLOC(game.loc) == WATER) + rspeak(FEET_WET); + else + rspeak(WHERE_QUERY); + } else { + if (command->word[0].type == OBJECT) { + /* From OV to VO form */ + if (command->word[1].type == ACTION) { + command_word_t stage = command->word[0]; + command->word[0] = command->word[1]; + command->word[1] = stage; + } + + if (command->word[0].id == GRATE) { + command->word[0].type = MOTION; + if (game.loc == LOC_START || + game.loc == LOC_VALLEY || + game.loc == LOC_SLIT) { + command->word[0].id = DEPRESSION; + } + if (game.loc == LOC_COBBLE || + game.loc == LOC_DEBRIS || + game.loc == LOC_AWKWARD || + game.loc == LOC_BIRDCHAMBER || + game.loc == LOC_PITTOP) { + command->word[0].id = ENTRANCE; + } + } + if ((command->word[0].id == WATER || command->word[0].id == OIL) && + (command->word[1].id == PLANT || command->word[1].id == DOOR)) { + if (AT(command->word[1].id)) { + command->word[1] = command->word[0]; + command->word[0].id = POUR; + command->word[0].type = ACTION; + strncpy(command->word[0].raw, "pour", LINESIZE - 1); + } + } + if (command->word[0].id == CAGE && command->word[1].id == BIRD && HERE(CAGE) && HERE(BIRD)) { + command->word[0].id = CARRY; + command->word[0].type = ACTION; + } + } + + /* If no word type is given for the first word, we assume it's a motion. */ + if (command->word[0].type == NO_WORD_TYPE) + command->word[0].type = MOTION; + + command->state = PREPROCESSED; + return true; + } + return false; +} +static bool do_move(void) +/* Actually execute the move to the new location and dwarf movement */ +{ /* Can't leave cave once it's closing (except by main office). */ if (OUTSID(game.newloc) && game.newloc != 0 && game.closng) { rspeak(EXIT_CLOSED); @@ -1004,217 +1056,316 @@ static bool do_command() if (!dwarfmove()) croak(); - /* Describe the current location and (maybe) get next command. */ + if (game.loc == LOC_NOWHERE) + croak(); + + /* The easiest way to get killed is to fall into a pit in + * pitch darkness. */ + if (!FORCED(game.loc) && DARK(game.loc) && game.wzdark && PCT(PIT_KILL_PROB)) { + rspeak(PIT_FALL); + game.oldlc2 = game.loc; + croak(); + return false; + } + + return true; +} + +static bool do_command(void) +/* Get and execute a command */ +{ + static command_t command; + clear_command(&command); + + /* Describe the current location and (maybe) get next command. */ + while (command.state != EXECUTED) { + describe_location(); - for (;;) { - if (game.loc == 0) - croak(); - const char* msg = locations[game.loc].description.small; - if (MOD(game.abbrev[game.loc], game.abbnum) == 0 || - msg == 0) - msg = locations[game.loc].description.big; - if (!FORCED(game.loc) && DARK(game.loc)) { - /* The easiest way to get killed is to fall into a pit in - * pitch darkness. */ - if (game.wzdark && PCT(35)) { - rspeak(PIT_FALL); - game.oldlc2 = game.loc; - croak(); - continue; /* back to top of main interpreter loop */ - } - msg = arbitrary_messages[PITCH_DARK]; - } - if (TOTING(BEAR)) - rspeak(TAME_BEAR); - speak(msg); if (FORCED(game.loc)) { playermove(HERE); return true; } - if (game.loc == LOC_Y2 && PCT(25) && !game.closng) - rspeak(SAYS_PLUGH); listobjects(); -Lclearobj: - game.oldobj = command.obj; - - checkhints(); - - /* If closing time, check for any objects being toted with - * game.prop < 0 and stash them. This way objects won't be - * described until they've been picked up and put down - * separate from their respective piles. */ - if (game.closed) { - if (game.prop[OYSTER] < 0 && TOTING(OYSTER)) - pspeak(OYSTER, look, 1, true); - for (size_t i = 1; i <= NOBJECTS; i++) { - if (TOTING(i) && game.prop[i] < 0) - game.prop[i] = STASHED(i); + /* Command not yet given; keep getting commands from user + * until valid command is both given and executed. */ + clear_command(&command); + while (command.state <= GIVEN) { + + if (game.closed) { + /* If closing time, check for any objects being toted with + * game.prop < 0 and stash them. This way objects won't be + * described until they've been picked up and put down + * separate from their respective piles. */ + if (game.prop[OYSTER] < 0 && TOTING(OYSTER)) + pspeak(OYSTER, look, true, 1); + for (size_t i = 1; i <= NOBJECTS; i++) { + if (TOTING(i) && game.prop[i] < 0) + game.prop[i] = STASHED(i); + } } - } - game.wzdark = DARK(game.loc); - if (game.knfloc > 0 && game.knfloc != game.loc) - game.knfloc = 0; - - /* Preserve state from last command for reuse when required */ - command_t preserve = command; - - // Get command input from user - if (!get_command_input(&command)) - return false; -#ifdef GDEBUG - /* Needs to stay synced with enum word_type_t */ - const char *types[] = {"NO_WORD_TYPE", "MOTION", "OBJECT", "ACTION", "NUMERIC"}; - /* needs to stay synced with enum speechpart */ - const char *roles[] = {"unknown", "intransitive", "transitive"}; - printf("Preserve: role = %s type1 = %s, id1 = %ld, type2 = %s, id2 = %ld\n", - roles[preserve.part], - types[preserve.word[0].type], - preserve.word[0].id, - types[preserve.word[1].type], - preserve.word[1].id); - printf("Command: role = %s type1 = %s, id1 = %ld, type2 = %s, id2 = %ld\n", - roles[command.part], - types[command.word[0].type], - command.word[0].id, - types[command.word[1].type], - command.word[1].id); -#endif - - /* Handle of objectless action followed by actionless object */ - if (preserve.word[0].type == ACTION && preserve.word[1].type == NO_WORD_TYPE && command.word[1].id == 0) - command.verb = preserve.verb; - -#ifdef BROKEN - /* Handling of actionless object followed by objectless action */ - if (preserve.word[0].type == OBJECT && preserve.word[1].type == NO_WORD_TYPE && command.word[1].id == 0 && command.word[0].id == CARRY) - command.obj = preserve.obj; -#endif /* BROKEN */ - - ++game.turns; + /* Check to see if the room is dark. If the knife is here, + * and it's dark, the knife permanently disappears */ + game.wzdark = DARK(game.loc); + if (game.knfloc != LOC_NOWHERE && game.knfloc != game.loc) + game.knfloc = LOC_NOWHERE; + + /* Check some for hints, get input from user, increment + * turn, and pre-process commands. Keep going until + * pre-processing is done. */ + while ( command.state < PREPROCESSED ) { + checkhints(); + + /* Get command input from user */ + if (!get_command_input(&command)) + return false; + + /* Every input, check "foobar" flag. If zero, nothing's going + * on. If pos, make neg. If neg, he skipped a word, so make it + * zero. + */ + game.foobar = (game.foobar > WORD_EMPTY) ? -game.foobar : WORD_EMPTY; + + ++game.turns; + preprocess_command(&command); + } - if (closecheck()) { - if (game.closed) + /* check if game is closed, and exit if it is */ + if (closecheck() ) return true; - } else - lampcheck(); - if (command.word[0].type == MOTION && command.word[0].id == ENTER - && (command.word[1].id == STREAM || command.word[1].id == WATER)) { - if (LIQLOC(game.loc) == WATER) - rspeak(FEET_WET); - else - rspeak(WHERE_QUERY); + /* loop until all words in command are processed */ + while (command.state == PREPROCESSED ) { + command.state = PROCESSING; - goto Lclearobj; - } + if (command.word[0].id == WORD_NOT_FOUND) { + /* Gee, I don't understand. */ + sspeak(DONT_KNOW, command.word[0].raw); + clear_command(&command); + continue; + } - if (command.word[0].type == OBJECT) { - if (command.word[0].id == GRATE) { - command.word[0].type = MOTION; - if (game.loc == LOC_START || - game.loc == LOC_VALLEY || - game.loc == LOC_SLIT) { - command.word[0].id = DEPRESSION; + /* Give user hints of shortcuts */ + if (strncasecmp(command.word[0].raw, "west", sizeof("west")) == 0) { + if (++game.iwest == 10) + rspeak(W_IS_WEST); } - if (game.loc == LOC_COBBLE || - game.loc == LOC_DEBRIS || - game.loc == LOC_AWKWARD || - game.loc == LOC_BIRD || - game.loc == LOC_PITTOP) { - command.word[0].id = ENTRANCE; + if (strncasecmp(command.word[0].raw, "go", sizeof("go")) == 0 && command.word[1].id != WORD_EMPTY) { + if (++game.igo == 10) + rspeak(GO_UNNEEDED); } - } - if ((command.word[0].id == WATER || command.word[0].id == OIL) && (command.word[1].id == PLANT || command.word[1].id == DOOR)) { - if (AT(command.word[1].id)) { - command.word[1] = command.word[0]; - command.word[0].id = POUR; - command.word[0].type = ACTION; - strncpy(command.word[0].raw, "pour", LINESIZE - 1); + + switch (command.word[0].type) { + case MOTION: + playermove(command.word[0].id); + command.state = EXECUTED; + continue; + case OBJECT: + command.part = unknown; + command.obj = command.word[0].id; + break; + case ACTION: + if (command.word[1].type == NUMERIC) + command.part = transitive; + else + command.part = intransitive; + command.verb = command.word[0].id; + break; + case NUMERIC: + if (!settings.oldstyle) { + sspeak(DONT_KNOW, command.word[0].raw); + clear_command(&command); + continue; + } + break;// LCOV_EXCL_LINE + default: // LCOV_EXCL_LINE + case NO_WORD_TYPE: // LCOV_EXCL_LINE + BUG(VOCABULARY_TYPE_N_OVER_1000_NOT_BETWEEN_0_AND_3); // LCOV_EXCL_LINE } - } - if (command.word[0].id == CAGE && command.word[1].id == BIRD && HERE(CAGE) && HERE(BIRD)) { - command.word[0].id = CARRY; - command.word[0].type = ACTION; - } - /* From OV to VO form */ - if (command.word[0].type == OBJECT && command.word[1].type == ACTION) { - command_word_t stage = command.word[0]; - command.word[0] = command.word[1]; - command.word[1] = stage; - } - } + switch (action(command)) { + case GO_TERMINATE: + command.state = EXECUTED; + break; + case GO_MOVE: + playermove(NUL); + command.state = EXECUTED; + break; + case GO_WORD2: +#ifdef GDEBUG + printf("Word shift\n"); +#endif /* GDEBUG */ + /* Get second word for analysis. */ + command.word[0] = command.word[1]; + command.word[1] = empty_command_word; + command.state = PREPROCESSED; + break; + case GO_UNKNOWN: + /* Random intransitive verbs come here. Clear obj just in case + * (see attack()). */ + command.word[0].raw[0] = toupper(command.word[0].raw[0]); + sspeak(DO_WHAT, command.word[0].raw); + command.obj = NO_OBJECT; + + /* object cleared; we need to go back to the preprocessing step */ + command.state = GIVEN; + break; + case GO_CHECKHINT: // FIXME: re-name to be more contextual; this was previously a label + command.state = GIVEN; + break; + case GO_DWARFWAKE: + /* Oh dear, he's disturbed the dwarves. */ + rspeak(DWARVES_AWAKEN); + terminate(endgame); + case GO_CLEAROBJ: // FIXME: re-name to be more contextual; this was previously a label + clear_command(&command); + break; + case GO_TOP: // FIXME: re-name to be more contextual; this was previously a label + break; + default: // LCOV_EXCL_LINE + BUG(ACTION_RETURNED_PHASE_CODE_BEYOND_END_OF_SWITCH); // LCOV_EXCL_LINE + } + } /* while command has not been fully processed */ + } /* while command is not yet given */ + } /* while command is not executed */ -Lookup: - if (strncasecmp(command.word[0].raw, "west", sizeof("west")) == 0) { - if (++game.iwest == 10) - rspeak(W_IS_WEST); - } - if (strncasecmp(command.word[0].raw, "go", sizeof("go")) == 0 && command.word[1].id != WORD_EMPTY) { - if (++game.igo == 10) - rspeak(GO_UNNEEDED); - } - if (command.word[0].id == WORD_NOT_FOUND) { - /* Gee, I don't understand. */ - sspeak(DONT_KNOW, command.word[0].raw); - goto Lclearobj; - } - switch (command.word[0].type) { - case NO_WORD_TYPE: // FIXME: treating NO_WORD_TYPE as a motion word is confusing - case MOTION: - playermove(command.word[0].id); - return true; - case OBJECT: - command.part = unknown; - command.obj = command.word[0].id; + /* command completely executed; we return true. */ + return true; +} + +/* + * MAIN PROGRAM + * + * Adventure (rev 2: 20 treasures) + * History: Original idea & 5-treasure version (adventures) by Willie Crowther + * 15-treasure version (adventure) by Don Woods, April-June 1977 + * 20-treasure version (rev 2) by Don Woods, August 1978 + * Errata fixed: 78/12/25 + * Revived 2017 as Open Adventure. + */ + +int main(int argc, char *argv[]) +{ + int ch; + + /* Options. */ + +#if defined ADVENT_AUTOSAVE + const char* opts = "dl:oa:"; + const char* usage = "Usage: %s [-l logfilename] [-o] [-a filename] [script...]\n"; + FILE *rfp = NULL; + const char* autosave_filename = NULL; +#elif !defined ADVENT_NOSAVE + const char* opts = "dl:or:"; + const char* usage = "Usage: %s [-l logfilename] [-o] [-r restorefilename] [script...]\n"; + FILE *rfp = NULL; +#else + const char* opts = "dl:o"; + const char* usage = "Usage: %s [-l logfilename] [-o] [script...]\n"; +#endif + while ((ch = getopt(argc, argv, opts)) != EOF) { + switch (ch) { + case 'd': // LCOV_EXCL_LINE + settings.debug +=1; // LCOV_EXCL_LINE + break; // LCOV_EXCL_LINE + case 'l': + settings.logfp = fopen(optarg, "w"); + if (settings.logfp == NULL) + fprintf(stderr, + "advent: can't open logfile %s for write\n", + optarg); + signal(SIGINT, sig_handler); break; - case ACTION: - if (command.word[1].type == NUMERIC) - command.part = transitive; - else - command.part = intransitive; - command.verb = command.word[0].id; + case 'o': + settings.oldstyle = true; + settings.prompt = false; + break; +#ifdef ADVENT_AUTOSAVE + case 'a': + rfp = fopen(optarg, READ_MODE); + autosave_filename = optarg; + signal(SIGHUP, sig_handler); + signal(SIGTERM, sig_handler); + break; +#elif !defined ADVENT_NOSAVE + case 'r': + rfp = fopen(optarg, "r"); + if (rfp == NULL) + fprintf(stderr, + "advent: can't open save file %s for read\n", + optarg); + break; +#endif + default: + fprintf(stderr, + usage, argv[0]); + fprintf(stderr, + " -l create a log file of your game named as specified'\n"); + fprintf(stderr, + " -o 'oldstyle' (no prompt, no command editing, displays 'Initialising...')\n"); +#if defined ADVENT_AUTOSAVE + fprintf(stderr, + " -a automatic save/restore from specified saved game file\n"); +#elif !defined ADVENT_NOSAVE + fprintf(stderr, + " -r restore from specified saved game file\n"); +#endif + exit(EXIT_FAILURE); break; - case NUMERIC: // LCOV_EXCL_LINE - default: // LCOV_EXCL_LINE - BUG(VOCABULARY_TYPE_N_OVER_1000_NOT_BETWEEN_0_AND_3); // LCOV_EXCL_LINE } - switch (action(command)) { - case GO_TERMINATE: - return true; - case GO_MOVE: - playermove(NUL); - return true; - case GO_TOP: - continue; /* back to top of main interpreter loop */ - case GO_WORD2: -#ifdef GDEBUG - printf("Word shift\n"); -#endif /* GDEBUG */ - /* Get second word for analysis. */ - command.word[0] = command.word[1]; - command.word[1] = empty_command_word; - goto Lookup; - case GO_UNKNOWN: - /* Random intransitive verbs come here. Clear obj just in case - * (see attack()). */ - command.word[0].raw[0] = toupper(command.word[0].raw[0]); - sspeak(DO_WHAT, command.word[0].raw); - command.obj = 0; - // Fallthrough - case GO_CLEAROBJ: - goto Lclearobj; - case GO_DWARFWAKE: - /* Oh dear, he's disturbed the dwarves. */ - rspeak(DWARVES_AWAKEN); - terminate(endgame); - default: // LCOV_EXCL_LINE - BUG(ACTION_RETURNED_PHASE_CODE_BEYOND_END_OF_SWITCH); // LCOV_EXCL_LINE + } + + /* copy invocation line part after switches */ + settings.argc = argc - optind; + settings.argv = argv + optind; + settings.optind = 0; + + /* Initialize game variables */ + int seedval = initialise(); + +#if !defined ADVENT_NOSAVE + if (!rfp) { + game.novice = yes_or_no(arbitrary_messages[WELCOME_YOU], arbitrary_messages[CAVE_NEARBY], arbitrary_messages[NO_MESSAGE]); + if (game.novice) + game.limit = NOVICELIMIT; + } else { + restore(rfp); +#if defined ADVENT_AUTOSAVE + score(scoregame); +#endif + } +#if defined ADVENT_AUTOSAVE + if (autosave_filename != NULL) { + if ((autosave_fp = fopen(autosave_filename, WRITE_MODE)) == NULL) { + perror(autosave_filename); + return EXIT_FAILURE; } + autosave(); } +#endif +#else + game.novice = yes_or_no(arbitrary_messages[WELCOME_YOU], arbitrary_messages[CAVE_NEARBY], arbitrary_messages[NO_MESSAGE]); + if (game.novice) + game.limit = NOVICELIMIT; +#endif + + if (settings.logfp) + fprintf(settings.logfp, "seed %d\n", seedval); + + /* interpret commands until EOF or interrupt */ + for (;;) { + // if we're supposed to move, move + if (!do_move()) + continue; + + // get command + if (!do_command()) + break; + } + /* show score and exit */ + terminate(quitgame); } /* end */