Handle a bare numeric token on the command line a bit more gracefully.
[open-adventure.git] / main.c
diff --git a/main.c b/main.c
index a0c6cbf7e36c7bbdd837a9490a55fa0c7144b156..198a4ccb55868c4505fd9dfb8252392bd0fcd664 100644 (file)
--- a/main.c
+++ b/main.c
@@ -9,9 +9,12 @@
  * 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*),
- * and for the offensive globals.  Applying the Structured Program
- * Theorem can be hard.
+ * 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
  */
 
 #include <stdlib.h>
@@ -86,7 +89,6 @@ int main(int argc, char *argv[])
                 fprintf(stderr,
                         "advent: can't open save file %s for read\n",
                         optarg);
-            signal(SIGINT, sig_handler);
             break;
 #endif
         default:
@@ -116,6 +118,10 @@ int main(int argc, char *argv[])
     } 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
 
     if (settings.logfp)
@@ -130,20 +136,6 @@ int main(int argc, char *argv[])
     terminate(quitgame);
 }
 
-static bool fallback_handler(char *buf)
-/* fallback handler for commands not handled by FORTRANish parser */
-{
-    long sv;
-    if (sscanf(buf, "seed %ld", &sv) == 1) {
-        set_seed(sv);
-        printf("Seed set to %ld\n", sv);
-        // autogenerated, so don't charge user time for it.
-        --game.turns;
-        return true;
-    }
-    return false;
-}
-
 /*  Check if this loc is eligible for any hints.  If been here long
  *  enough, display.  Ignore "HINTS" < 4 (special stuff, see database
  *  notes). */
@@ -178,9 +170,9 @@ static void checkhints(void)
                     game.hintlc[hint] = 0;
                     return;
                 case 3:        /* maze */
-                    if (game.atloc[game.loc] == 0 &&
-                        game.atloc[game.oldloc] == 0 &&
-                        game.atloc[game.oldlc2] == 0 &&
+                    if (game.atloc[game.loc] == NO_OBJECT &&
+                        game.atloc[game.oldloc] == NO_OBJECT &&
+                        game.atloc[game.oldlc2] == NO_OBJECT &&
                         game.holdng > 1)
                         break;
                     game.hintlc[hint] = 0;
@@ -198,9 +190,9 @@ static void checkhints(void)
                     game.hintlc[hint] = 0;
                     return;
                 case 7:        /* woods */
-                    if (game.atloc[game.loc] == 0 &&
-                        game.atloc[game.oldloc] == 0 &&
-                        game.atloc[game.oldlc2] == 0)
+                    if (game.atloc[game.loc] == NO_OBJECT &&
+                        game.atloc[game.oldloc] == NO_OBJECT &&
+                        game.atloc[game.oldlc2] == NO_OBJECT)
                         break;
                     return;
                 case 8:        /* ogre */
@@ -217,9 +209,8 @@ static void checkhints(void)
                         break;
                     game.hintlc[hint] = 0;
                     return;
-                default:
+                default: // LCOV_EXCL_LINE
                     BUG(HINT_NUMBER_EXCEEDS_GOTO_LIST); // LCOV_EXCL_LINE
-                    break;
                 }
 
                 /* Fall through to hint display */
@@ -240,14 +231,14 @@ static bool spotted_by_pirate(int i)
     if (i != PIRATE)
         return false;
 
-    /*  The pirate's spotted him.  He leaves him alone once we've
+    /*  The pirate's spotted him.  Pirate leaves him alone once we've
      *  found chest.  K counts if a treasure is here.  If not, and
      *  tally=1 for an unseen chest, let the pirate be spotted.  Note
      *  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=0). */
+     *  (game.prop[CHEST] == STATE_FOUND). */
     if (game.loc == game.chloc ||
-        game.prop[CHEST] >= 0)
+        game.prop[CHEST] != STATE_NOTFOUND)
         return true;
     int snarfed = 0;
     bool movechest = false, robplayer = false;
@@ -294,7 +285,7 @@ static bool spotted_by_pirate(int i)
                 continue;
             if (!(treasure == PYRAMID && (game.loc == objects[PYRAMID].plac ||
                                           game.loc == objects[EMERALD].plac))) {
-                if (AT(treasure) && game.fixed[treasure] == 0)
+                if (AT(treasure) && game.fixed[treasure] == IS_FREE)
                     carry(treasure, game.loc);
                 if (TOTING(treasure))
                     drop(treasure, game.chloc);
@@ -309,22 +300,21 @@ static bool dwarfmove(void)
 /* Dwarves move.  Return true if player survives, false if he dies. */
 {
     int kk, stick, attack;
-    long tk[21];
+    loc_t tk[21];
 
     /*  Dwarf stuff.  See earlier comments for description of
      *  variables.  Remember sixth dwarf is pirate and is thus
      *  very different except for motion rules. */
 
-    /*  First off, don't let the dwarves follow him into a pit or
-     *  a wall.  Activate the whole mess the first time he gets as
-     *  far as the hall of mists (loc 15).  If game.newloc is
-     *  forbidden to pirate (in particular, if it's beyond the
+    /*  First off, don't let the dwarves follow him into a pit or a
+     *  wall.  Activate the whole mess the first time he gets as far
+     *  as the Hall of Mists (what INDEEP() tests).  If game.newloc
+     *  is forbidden to pirate (in particular, if it's beyond the
      *  troll bridge), bypass dwarf stuff.  That way pirate can't
      *  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 == 0 ||
+     *  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))
         return true;
@@ -337,7 +327,7 @@ static bool dwarfmove(void)
     }
 
     /*  When we encounter the first dwarf, we kill 0, 1, or 2 of
-     *  the 5 dwarves.  If any of the survivors is at loc,
+     *  the 5 dwarves.  If any of the survivors is at game.loc,
      *  replace him with the alternate. */
     if (game.dflag == 1) {
         if (!INDEEP(game.loc) ||
@@ -380,9 +370,10 @@ static bool dwarfmove(void)
         kk = tkey[game.dloc[i]];
         if (kk != 0)
             do {
-                game.newloc = travel[kk].dest;
+                enum desttype_t desttype = travel[kk].desttype;
+                game.newloc = travel[kk].destval;
                 /* Have we avoided a dwarf encounter? */
-                if (SPECIAL(game.newloc))
+                if (desttype != dest_goto)
                     continue;
                 else if (!INDEEP(game.newloc))
                     continue;
@@ -391,7 +382,8 @@ static bool dwarfmove(void)
                 else if (j > 1 && game.newloc == tk[j - 1])
                     continue;
                 else if (j >= DIM(tk) - 1)
-                    continue;
+                    /* This can't actually happen. */
+                    continue; // LCOV_EXCL_LINE
                 else if (game.newloc == game.dloc[i])
                     continue;
                 else if (FORCED(game.newloc))
@@ -428,9 +420,7 @@ static bool dwarfmove(void)
         }
     }
 
-    /*  Now we know what's happening.  Let's tell the poor sucker about it.
-     *  Note that various of the "knife" messages must have specific relative
-     *  positions in the rspeak database. */
+    /*  Now we know what's happening.  Let's tell the poor sucker about it. */
     if (game.dtotal == 0)
         return true;
     rspeak(game.dtotal == 1 ? DWARF_SINGLE : DWARF_PACK, game.dtotal);
@@ -443,7 +433,7 @@ static bool dwarfmove(void)
         rspeak(stick > 1 ? MULTIPLE_HITS : (stick == 1 ? ONE_HIT : NONE_HIT), stick);
     } else {
         rspeak(KNIFE_THROWN);
-        rspeak(MISSES_YOU);
+        rspeak(stick ? GETS_YOU : MISSES_YOU);
     }
     if (stick == 0)
         return true;
@@ -474,6 +464,8 @@ static bool dwarfmove(void)
 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;
@@ -482,8 +474,8 @@ static void croak(void)
          *  death and exit. */
         rspeak(DEATH_CLOSING);
         terminate(endgame);
-    } else if (game.numdie == NDEATHS ||
-               !yes(query, yes_response, arbitrary_messages[OK_MAN]))
+    } else if ( !yes(query, yes_response, arbitrary_messages[OK_MAN])
+                || game.numdie == NDEATHS)
         terminate(endgame);
     else {
         game.place[WATER] = game.place[OIL] = LOC_NOWHERE;
@@ -496,16 +488,18 @@ static void croak(void)
                 drop(i, (i == LAMP) ? LOC_START : game.oldlc2);
             }
         }
-        game.loc = LOC_BUILDING;
-        game.oldloc = game.loc;
+        game.oldloc = game.loc = game.newloc = LOC_BUILDING;
     }
 }
 
-static bool traveleq(long a, long b)
+static bool traveleq(int a, int b)
 /* Are two travel entries equal for purposes of skip after failed condition? */
 {
-    return (travel[a].cond == travel[b].cond)
-           && (travel[a].dest == travel[b].dest);
+    return (travel[a].condtype == travel[b].condtype)
+           && (travel[a].condarg1 == travel[b].condarg1)
+           && (travel[a].condarg2 == travel[b].condarg2)
+           && (travel[a].desttype == travel[b].desttype)
+           && (travel[a].destval == travel[b].destval);
 }
 
 /*  Given the current location in "game.loc", and a motion verb number in
@@ -533,39 +527,39 @@ static void playermove( int motion)
             motion = game.oldlc2;
         game.oldlc2 = game.oldloc;
         game.oldloc = game.loc;
-        int spk = 0;
-        if (motion == game.loc)
-            spk = FORGOT_PATH;
-        if (CNDBIT(game.loc, COND_NOBACK))
-            spk = TWIST_TURN;
-        if (spk == 0) {
-            int te_tmp = 0;
-            for (;;) {
-                scratchloc = travel[travel_entry].dest;
-                if (scratchloc != motion) {
-                    if (!SPECIAL(scratchloc)) {
-                        if (FORCED(scratchloc) && travel[tkey[scratchloc]].dest == motion)
-                            te_tmp = travel_entry;
-                    }
-                    if (!travel[travel_entry].stop) {
-                        ++travel_entry;        /* go to next travel entry for this location */
-                        continue;
-                    }
-                    /* we've reached the end of travel entries for game.loc */
-                    travel_entry = te_tmp;
-                    if (travel_entry == 0) {
-                        rspeak(NOT_CONNECTED);
-                        return;
-                    }
-                }
+        if (CNDBIT(game.loc, COND_NOBACK)) {
+            rspeak(TWIST_TURN);
+            return;
+        }
+        if (motion == game.loc) {
+            rspeak(FORGOT_PATH);
+            return;
+        }
 
-                motion = travel[travel_entry].motion;
-                travel_entry = tkey[game.loc];
-                break; /* fall through to ordinary travel */
+        int te_tmp = 0;
+        for (;;) {
+            enum desttype_t desttype = travel[travel_entry].desttype;
+            scratchloc = travel[travel_entry].destval;
+            if (desttype != dest_goto || scratchloc != motion) {
+                if (desttype == dest_goto) {
+                    if (FORCED(scratchloc) && travel[tkey[scratchloc]].destval == motion)
+                        te_tmp = travel_entry;
+                }
+                if (!travel[travel_entry].stop) {
+                    ++travel_entry;    /* go to next travel entry for this location */
+                    continue;
+                }
+                /* we've reached the end of travel entries for game.loc */
+                travel_entry = te_tmp;
+                if (travel_entry == 0) {
+                    rspeak(NOT_CONNECTED);
+                    return;
+                }
             }
-        } else {
-            rspeak(spk);
-            return;
+
+            motion = travel[travel_entry].motion;
+            travel_entry = tkey[game.loc];
+            break; /* fall through to ordinary travel */
         }
     } else if (motion == LOOK) {
         /*  Look.  Can't give more detail.  Pretend it wasn't dark
@@ -596,25 +590,38 @@ static void playermove( int motion)
         if (travel[travel_entry].stop) {
             /*  Couldn't find an entry matching the motion word passed
              *  in.  Various messages depending on word given. */
-            int spk = CANT_APPLY;
-            if (motion >= EAST && motion <= NW)
-                spk = BAD_DIRECTION;
-            if (motion == UP ||
-                motion == DOWN)
-                spk = BAD_DIRECTION;
-            if (motion == FORWARD ||
-                motion == LEFT ||
-                motion == RIGHT)
-                spk = UNSURE_FACING;
-            if (motion == OUTSIDE ||
-                motion == INSIDE)
-                spk = NO_INOUT_HERE;
-            if (motion == XYZZY ||
-                motion == PLUGH)
-                spk = NOTHING_HAPPENS;
-            if (motion == CRAWL)
-                spk = WHICH_WAY;
-            rspeak(spk);
+            switch (motion) {
+            case EAST:
+            case WEST:
+            case SOUTH:
+            case NORTH:
+            case NE:
+            case NW:
+            case SW:
+            case SE:
+            case UP:
+            case DOWN:
+                rspeak(BAD_DIRECTION);
+                break;
+            case FORWARD:
+            case LEFT:
+            case RIGHT:
+                rspeak(UNSURE_FACING);
+                break;
+            case OUTSIDE:
+            case INSIDE:
+                rspeak(NO_INOUT_HERE);
+                break;
+            case XYZZY:
+            case PLUGH:
+                rspeak(NOTHING_HAPPENS);
+                break;
+            case CRAWL:
+                rspeak(WHICH_WAY);
+                break;
+            default:
+                rspeak(CANT_APPLY);
+            }
             return;
         }
         ++travel_entry;
@@ -623,31 +630,31 @@ static void playermove( int motion)
     /* (ESR) We've found a destination that goes with the motion verb.
      * Next we need to check any conditional(s) on this destination, and
      * possibly on following entries. */
-    /* FIXME: Magic numbers related to move opcodes */
     do {
         for (;;) { /* L12 loop */
             for (;;) {
-                long cond = travel[travel_entry].cond;
-                long arg = MOD(cond, 100);
-                if (!SPECIAL(cond)) {
+                enum condtype_t condtype = travel[travel_entry].condtype;
+                long condarg1 = travel[travel_entry].condarg1;
+                long condarg2 = travel[travel_entry].condarg2;
+                if (condtype < cond_not) {
                     /* YAML N and [pct N] conditionals */
-                    if (cond <= 100) {
-                        if (cond == 0 ||
-                            PCT(cond))
+                    if (condtype == cond_goto || condtype == cond_pct) {
+                        if (condarg1 == 0 ||
+                            PCT(condarg1))
                             break;
                         /* else fall through */
                     }
                     /* YAML [with OBJ] clause */
-                    if (TOTING(arg) ||
-                        (cond > 200 && AT(arg)))
+                    else if (TOTING(condarg1) ||
+                             (condtype == cond_with && AT(condarg1)))
                         break;
                     /* else fall through to check [not OBJ STATE] */
-                } else if (game.prop[arg] != cond / 100 - 3)
+                } else if (game.prop[condarg1] != condarg2)
                     break;
 
                 /* We arrive here on conditional failure.
                  * Skip to next non-matching destination */
-                long te_tmp = travel_entry;
+                int te_tmp = travel_entry;
                 do {
                     if (travel[te_tmp].stop)
                         BUG(CONDITIONAL_TRAVEL_ENTRY_WITH_NO_ALTERATION); // LCOV_EXCL_LINE
@@ -658,17 +665,17 @@ static void playermove( int motion)
             }
 
             /* Found an eligible rule, now execute it */
-            game.newloc = travel[travel_entry].dest;
-            if (!SPECIAL(game.newloc))
+            enum desttype_t desttype = travel[travel_entry].desttype;
+            game.newloc = travel[travel_entry].destval;
+            if (desttype == dest_goto)
                 return;
 
-            if (game.newloc > 500) {
+            if (desttype == dest_speak) {
                 /* Execute a speak rule */
-                rspeak(L_SPEAK(game.newloc));
+                rspeak(game.newloc);
                 game.newloc = game.loc;
                 return;
             } else {
-                game.newloc -= SPECIALBASE;
                 switch (game.newloc) {
                 case 1:
                     /* Special travel 1.  Plover-alcove passage.  Can carry only
@@ -676,8 +683,8 @@ static void playermove( int motion)
                      * entries going through passage, which can never be used
                      * for actual motion, but can be spotted by "go back". */
                     game.newloc = (game.loc == LOC_PLOVER)
-                       ? LOC_ALCOVE
-                       : LOC_PLOVER;
+                                  ? LOC_ALCOVE
+                                  : LOC_PLOVER;
                     if (game.holdng > 1 ||
                         (game.holdng == 1 && !TOTING(EMERALD))) {
                         game.newloc = game.loc;
@@ -691,14 +698,16 @@ static void playermove( int motion)
                      * to get it out.  Having dropped it, go back and
                      * pretend he wasn't carrying it after all. */
                     drop(EMERALD, game.loc);
-                    int te_tmp = travel_entry;
-                    do {
-                        if (travel[te_tmp].stop)
-                            BUG(CONDITIONAL_TRAVEL_ENTRY_WITH_NO_ALTERATION); // LCOV_EXCL_LINE
-                        ++te_tmp;
-                    } while
-                    (traveleq(travel_entry, te_tmp));
-                    travel_entry = te_tmp;
+                    {
+                        int te_tmp = travel_entry;
+                        do {
+                            if (travel[te_tmp].stop)
+                                BUG(CONDITIONAL_TRAVEL_ENTRY_WITH_NO_ALTERATION); // LCOV_EXCL_LINE
+                            ++te_tmp;
+                        } while
+                        (traveleq(travel_entry, te_tmp));
+                        travel_entry = te_tmp;
+                    }
                     continue; /* goto L12 */
                 case 3:
                     /* Special travel 3.  Troll bridge.  Must be done
@@ -706,15 +715,16 @@ static void playermove( int motion)
                      * wander across and encounter the bear.  (They
                      * won't follow the player there because that
                      * region is forbidden to the pirate.)  If
-                     * game.prop(TROLL)=1, he's crossed since paying,
-                     * so step out and block him.  (standard travel
-                     * entries check for game.prop(TROLL)=0.)  Special
-                     * stuff for bear. */
+                     * game.prop[TROLL]=TROLL_PAIDONCE, he's crossed
+                     * since paying, so step out and block him.
+                     * (standard travel entries check for
+                     * game.prop[TROLL]=TROLL_UNPAID.)  Special stuff
+                     * for bear. */
                     if (game.prop[TROLL] == TROLL_PAIDONCE) {
                         pspeak(TROLL, look, TROLL_PAIDONCE, true);
                         game.prop[TROLL] = TROLL_UNPAID;
-                        move(TROLL2, 0);
-                        move(TROLL2 + NOBJECTS, 0);
+                        move(TROLL2, LOC_NOWHERE);
+                        move(TROLL2 + NOBJECTS, IS_FREE);
                         move(TROLL, objects[TROLL].plac);
                         move(TROLL + NOBJECTS, objects[TROLL].fixd);
                         juggle(CHASM);
@@ -726,17 +736,16 @@ static void playermove( int motion)
                             game.prop[TROLL] = TROLL_PAIDONCE;
                         if (!TOTING(BEAR))
                             return;
-                        rspeak(BRIDGE_COLLAPSE);
-                        game.prop[CHASM] = BRIDGE_WRECKED;
+                        state_change(CHASM, BRIDGE_WRECKED);
                         game.prop[TROLL] = TROLL_GONE;
                         drop(BEAR, game.newloc);
-                        game.fixed[BEAR] = -1;
+                        game.fixed[BEAR] = IS_FIXED;
                         game.prop[BEAR] = BEAR_DEAD;
                         game.oldlc2 = game.newloc;
                         croak();
                         return;
                     }
-                default:
+                default: // LCOV_EXCL_LINE
                     BUG(SPECIAL_TRAVEL_500_GT_L_GT_300_EXCEEDS_GOTO_LIST); // LCOV_EXCL_LINE
                 }
             }
@@ -766,6 +775,15 @@ static bool closecheck(void)
  *  problems arise from the use of negative prop numbers to suppress
  *  the object descriptions until he's actually moved the objects. */
 {
+    /* If a turn threshold has been met, apply penalties and tell
+     * the player about it. */
+    for (int i = 0; i < NTHRESHOLDS; ++i) {
+        if (game.turns == turn_thresholds[i].threshold + 1) {
+            game.trnluz += turn_thresholds[i].point_loss;
+            speak(turn_thresholds[i].message);
+        }
+    }
+
     /*  Don't tick game.clock1 unless well into cave (and not at Y2). */
     if (game.tally == 0 && INDEEP(game.loc) && game.loc != LOC_Y2)
         --game.clock1;
@@ -788,19 +806,19 @@ static bool closecheck(void)
         game.prop[FISSURE] = UNBRIDGED;
         for (int i = 1; i <= NDWARVES; i++) {
             game.dseen[i] = false;
-            game.dloc[i] = 0;
+            game.dloc[i] = LOC_NOWHERE;
         }
-        move(TROLL, 0);
-        move(TROLL + NOBJECTS, 0);
+        move(TROLL, LOC_NOWHERE);
+        move(TROLL + NOBJECTS, IS_FREE);
         move(TROLL2, objects[TROLL].plac);
         move(TROLL2 + NOBJECTS, objects[TROLL].fixd);
         juggle(CHASM);
         if (game.prop[BEAR] != BEAR_DEAD)
             DESTROY(BEAR);
         game.prop[CHAIN] = CHAIN_HEAP;
-        game.fixed[CHAIN] = CHAIN_HEAP;
-        game.prop[AXE] = 0;
-        game.fixed[AXE] = 0;
+        game.fixed[CHAIN] = IS_FREE;
+        game.prop[AXE] = AXE_HERE;
+        game.fixed[AXE] = IS_FREE;
         rspeak(CAVE_CLOSING);
         game.clock1 = -1;
         game.closng = true;
@@ -824,10 +842,10 @@ static bool closecheck(void)
          *  could cause trouble, such as the keys).  We describe the
          *  flash of light and trundle back. */
         game.prop[BOTTLE] = put(BOTTLE, LOC_NE, EMPTY_BOTTLE);
-        game.prop[PLANT] = put(PLANT, LOC_NE, 0);
-        game.prop[OYSTER] = put(OYSTER, LOC_NE, 0);
-        game.prop[LAMP] = put(LAMP, LOC_NE, 0);
-        game.prop[ROD] = put(ROD, LOC_NE, 0);
+        game.prop[PLANT] = put(PLANT, LOC_NE, PLANT_THIRSTY);
+        game.prop[OYSTER] = put(OYSTER, LOC_NE, STATE_FOUND);
+        game.prop[LAMP] = put(LAMP, LOC_NE, LAMP_DARK);
+        game.prop[ROD] = put(ROD, LOC_NE, STATE_FOUND);
         game.prop[DWARF] = put(DWARF, LOC_NE, 0);
         game.loc = LOC_NE;
         game.oldloc = LOC_NE;
@@ -837,13 +855,13 @@ static bool closecheck(void)
         put(GRATE, LOC_SW, 0);
         put(SIGN, LOC_SW, 0);
         game.prop[SIGN] = ENDGAME_SIGN;
-        game.prop[SNAKE] = put(SNAKE, LOC_SW, 1);
-        game.prop[BIRD] = put(BIRD, LOC_SW, 1);
-        game.prop[CAGE] = put(CAGE, LOC_SW, 0);
-        game.prop[ROD2] = put(ROD2, LOC_SW, 0);
-        game.prop[PILLOW] = put(PILLOW, LOC_SW, 0);
+        game.prop[SNAKE] = put(SNAKE, LOC_SW, SNAKE_CHASED);
+        game.prop[BIRD] = put(BIRD, LOC_SW, BIRD_CAGED);
+        game.prop[CAGE] = put(CAGE, LOC_SW, STATE_FOUND);
+        game.prop[ROD2] = put(ROD2, LOC_SW, STATE_FOUND);
+        game.prop[PILLOW] = put(PILLOW, LOC_SW, STATE_FOUND);
 
-        game.prop[MIRROR] = put(MIRROR, LOC_NE, 0);
+        game.prop[MIRROR] = put(MIRROR, LOC_NE, STATE_FOUND);
         game.fixed[MIRROR] = LOC_SW;
 
         for (int i = 1; i <= NOBJECTS; i++) {
@@ -869,30 +887,37 @@ static void lampcheck(void)
      *  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.  Eve after it goes
+     *  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 && HERE(BATTERY) && game.prop[BATTERY] == FRESH_BATTERIES && HERE(LAMP)) {
-        rspeak(REPLACE_BATTERIES);
-        game.prop[BATTERY] = DEAD_BATTERIES;
-        if (TOTING(BATTERY))
-            drop(BATTERY, game.loc);
-        game.limit += BATTERYLIFE;
-        game.lmwarn = false;
-    } else if (game.limit == 0) {
+    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);
-    } else if (game.limit <= WARNTIME) {
-        if (!game.lmwarn && HERE(LAMP)) {
-            game.lmwarn = true;
-            int spk = GET_BATTERIES;
-            if (game.place[BATTERY] == LOC_NOWHERE)
-                spk = LAMP_DIM;
-            if (game.prop[BATTERY] == DEAD_BATTERIES)
-                spk = MISSING_BATTERIES;
-            rspeak(spk);
-        }
     }
 }
 
@@ -908,7 +933,7 @@ static void listobjects(void)
     if (!DARK(game.loc)) {
         ++game.abbrev[game.loc];
         for (int i = game.atloc[game.loc]; i != 0; i = game.link[i]) {
-            long obj = i;
+            obj_t obj = i;
             if (obj > NOBJECTS)
                 obj = obj - NOBJECTS;
             if (obj == STEPS && TOTING(NUGGET))
@@ -939,9 +964,9 @@ static void listobjects(void)
             }
             int kk = game.prop[obj];
             if (obj == STEPS)
-               kk = (game.loc == game.fixed[STEPS])
-                   ? STEPS_UP
-                   : STEPS_DOWN;
+                kk = (game.loc == game.fixed[STEPS])
+                     ? STEPS_UP
+                     : STEPS_DOWN;
             pspeak(obj, look, kk, true);
         }
     }
@@ -950,10 +975,7 @@ static void listobjects(void)
 static bool do_command()
 /* Get and execute a command */
 {
-    long kmod, defn;
-    static long igo = 0;
-    static struct command_t command;
-    command.verb = 0;
+    static command_t command;
 
     /*  Can't leave cave once it's closing (except by main office). */
     if (OUTSID(game.newloc) && game.newloc != 0 && game.closng) {
@@ -1007,19 +1029,16 @@ static bool do_command()
         speak(msg);
         if (FORCED(game.loc)) {
             playermove(HERE);
-           return true;
+            return true;
         }
         if (game.loc == LOC_Y2 && PCT(25) && !game.closng)
             rspeak(SAYS_PLUGH);
 
         listobjects();
 
-L2012:
-        command.verb = 0;
+Lclearobj:
         game.oldobj = command.obj;
-        command.obj = 0;
 
-L2600:
         checkhints();
 
         /*  If closing time, check for any objects being toted with
@@ -1038,129 +1057,136 @@ L2600:
         if (game.knfloc > 0 && game.knfloc != game.loc)
             game.knfloc = 0;
 
-        /* This is where we get a new command from the user */
-        char* input;
-        char inputbuf[LINESIZE];
-
-        for (;;) {
-            input = get_input();
-            if (input == NULL)
-                return (false);
-            if (word_count(input) > 2) {
-                rspeak(TWO_WORDS);
-                continue;
-            }
-            if (strcmp(input, "") != 0)
-                break;
-        }
-
-        strncpy(inputbuf, input, LINESIZE - 1);
-        free(input);
-
-        tokenize(inputbuf, &command);
+        /* 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
 
-       char word1[TOKLEN+1];
-        char word2[TOKLEN+1];
-        packed_to_token(command.wd1, word1);
-        packed_to_token(command.wd2, word2);
-        command.id1 = get_vocab_id(word1);
-        command.id2 = get_vocab_id(word2);
+        /* 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;
 
-        /*  Every input, check "game.foobar" flag.  If zero, nothing's
-         *  going on.  If pos, make neg.  If neg, he skipped a word,
-         *  so make it zero. */
-L2607:
-        game.foobar = (game.foobar > 0 ? -game.foobar : 0);
+#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;
 
-        /* If a turn threshold has been met, apply penalties and tell
-         * the player about it. */
-        for (int i = 0; i < NTHRESHOLDS; ++i) {
-            if (game.turns == turn_thresholds[i].threshold + 1) {
-                game.trnluz += turn_thresholds[i].point_loss;
-                speak(turn_thresholds[i].message);
-            }
-        }
-
-        if (command.verb == SAY && command.id2 != WORD_NOT_FOUND && command.id2 != WORD_EMPTY)
-            command.verb = 0;
-        if (command.verb == SAY) {
-            command.part = transitive;
-            goto Laction;
-        }
         if (closecheck()) {
             if (game.closed)
                 return true;
         } else
             lampcheck();
 
-        if (command.id1 == ENTER && (command.id2 == STREAM ||
-                            command.id2 == PROMOTE_WORD(WATER))) {
-            if (LIQLOC(game.loc) == WATER) {
+        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 {
+            else
                 rspeak(WHERE_QUERY);
-            }
-            goto L2012;
+
+            goto Lclearobj;
         }
-        if (command.id1 == ENTER && command.id2 != WORD_NOT_FOUND && command.id2 != WORD_EMPTY) {
-            /* command.wd1 = command.wd2; */
-            /* wordclear(&command.wd2); */
-         command.id1 = command.id2;
-         command.id2 = WORD_EMPTY;
-        } else {
-            /* FIXME: Magic numbers related to vocabulary */
-            if (!((command.id1 != PROMOTE_WORD(WATER) && command.id1 != PROMOTE_WORD(OIL)) ||
-                  (command.id2 != PROMOTE_WORD(PLANT) && command.id2 != PROMOTE_WORD(DOOR)))) {
-                if (AT(DEMOTE_WORD(command.id2)))
-                    command.wd2 = token_to_packed("POUR");
+
+        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;
+                }
+                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 ((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;
+            }
+
+            /* 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;
             }
-            if (command.id1 == PROMOTE_WORD(CAGE) && command.id2 == PROMOTE_WORD(BIRD) && HERE(CAGE) && HERE(BIRD))
-                command.wd1 = token_to_packed("CATCH");
         }
-L2620:
-        if (wordeq(command.wd1, token_to_packed("WEST"))) {
-            ++game.iwest;
-            if (game.iwest == 10)
+
+Lookup:
+        if (strncasecmp(command.word[0].raw, "west", sizeof("west")) == 0) {
+            if (++game.iwest == 10)
                 rspeak(W_IS_WEST);
         }
-        if (wordeq(command.wd1, token_to_packed("GO")) && !wordempty(command.wd2)) {
-            if (++igo == 10)
+        if (strncasecmp(command.word[0].raw, "go", sizeof("go")) == 0 && command.word[1].id != WORD_EMPTY) {
+            if (++game.igo == 10)
                 rspeak(GO_UNNEEDED);
         }
-Lookup:
-        packed_to_token(command.wd1, word1);
-        defn = get_vocab_id(word1);
-        if (defn == WORD_NOT_FOUND) {
+        if (command.word[0].id == WORD_NOT_FOUND) {
             /* Gee, I don't understand. */
-            if (fallback_handler(inputbuf))
-                continue;
-            sspeak(DONT_KNOW, command.raw1);
-            goto L2600;
+            sspeak(DONT_KNOW, command.word[0].raw);
+            goto Lclearobj;
         }
-        /* FIXME: magic numbers related to vocabulary */
-        kmod = MOD(defn, 1000);
-        switch (defn / 1000) {
-        case 0:
-            playermove(kmod);
-           return true;
-        case 1:
+        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 = kmod;
+            command.obj = command.word[0].id;
             break;
-        case 2:
-            command.part = intransitive;
-            command.verb = kmod;
+        case ACTION:
+            if (command.word[1].type == NUMERIC)
+                command.part = transitive;
+            else
+                command.part = intransitive;
+            command.verb = command.word[0].id;
             break;
-        case 3:
-            speak(specials[kmod].message);
-            goto L2012;
-        default:
+        case NUMERIC: // LCOV_EXCL_LINE
+           if (!settings.oldstyle) {
+               sspeak(DONT_KNOW, command.word[0].raw);
+               goto Lclearobj;
+           }
+        default: // LCOV_EXCL_LINE
             BUG(VOCABULARY_TYPE_N_OVER_1000_NOT_BETWEEN_0_AND_3); // LCOV_EXCL_LINE
         }
-
-Laction:
-        switch (action(&command)) {
+        switch (action(command)) {
         case GO_TERMINATE:
             return true;
         case GO_MOVE:
@@ -1168,33 +1194,28 @@ Laction:
             return true;
         case GO_TOP:
             continue;  /* back to top of main interpreter loop */
-        case GO_CLEAROBJ:
-            goto L2012;
-        case GO_CHECKHINT:
-            goto L2600;
-        case GO_CHECKFOO:
-            goto L2607;
-        case GO_LOOKUP:
-            goto Lookup;
         case GO_WORD2:
+#ifdef GDEBUG
+            printf("Word shift\n");
+#endif /* GDEBUG */
             /* Get second word for analysis. */
-            command.wd1 = command.wd2;
-           strcpy(command.raw1, command.raw2);
-            wordclear(&command.wd2);
-           command.raw2[0] = '\0';
-            goto L2620;
+            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.raw1[0] = toupper(command.raw1[0]);
-            sspeak(DO_WHAT, command.raw1);
+            command.word[0].raw[0] = toupper(command.word[0].raw[0]);
+            sspeak(DO_WHAT, command.word[0].raw);
             command.obj = 0;
-            goto L2600;
+        // Fallthrough
+        case GO_CLEAROBJ:
+            goto Lclearobj;
         case GO_DWARFWAKE:
             /*  Oh dear, he's disturbed the dwarves. */
             rspeak(DWARVES_AWAKEN);
             terminate(endgame);
-        default:
+        default: // LCOV_EXCL_LINE
             BUG(ACTION_RETURNED_PHASE_CODE_BEYOND_END_OF_SWITCH); // LCOV_EXCL_LINE
         }
     }