Adam Richardson's Site

Perplexer (A Simple Text Adventure)

Table of Contents

What is "Perplexer"?

  • This is a simple text adventure game that I made
  • It involves collecting a few keys to exit a maze
  • I was inspired by Infocom: The Documentary
  • I originally wrote the game in C
  • This project is remaking that C version in Lisp
  • Reading through the chapter on the text adventure game in Land of Lisp made me want to rewrite it in lisp

Project Goals

  • I wanted to compare using Common Lisp to implement a project I had first written in C
  • A text adventure game also seems like a good chance to try more literate programming with Emacs org mode
    • Because of this the html export of this doc does not contain the full picture of how this is put together
    • To see all the parts you should look at the org mode source file
  • While the game design is not the most extensible, the script and the game should be separated
  • The core program of the game should be able to handle any script so long as it is structured to match the needed data

Room Layout

  • This graph visualizes the connections between the rooms in the game
  • It doesn't spoil where the keys are though

perplexer_graph.png

Interface

  • This is an example of what the interface should look like
  • The game should have prompt where the user enters commands
Escape the perplexity
---------------------



You are underneath a light dangling from the ceiling by a string.
North, west and east are blocked by walls. You see a pile of
shredded up paper in the corner. You try to read it but are unable
to piece it together. Through the east wall you hear the muffled
sounds of a of a dot matrix printer. To the south you see a dark
corridor and faintly hear running water.
> help
Use commands north, south, east, west to explore. The first time
you enter a room you get a long description. After that, you will
get a short description. Use the look command to read the long
description again. Use the interact command to try to interact
with the room. Use the quit command to exit the game. To see this
message again type help.
> south
The room is pitch black and it is hard to make anything out. You
feel mist hitting your face from what sounds like a waterfall to
the south. To the east you hear wind rustling and see a distant
light.
>

Lisp Version

Interface

CL-USER> (perplexer)
PERPLEXER> look
You are underneath a light dangling from the ceiling by a string. North, west and east are blocked by walls. You see a pile of shredded up paper in the corner. You try to read it but are unable to piece it together. Through the east wall you hear the muffled sounds of a of a dot matrix printer. To the south you see a dark corridor and faintly hear running water.
PERPLEXER> walk south
The room is pitch black and it is hard to make anything out. You feel mist hitting your face from what sounds like a waterfall to the south. To the east you hear wind rustling and see a distant light.
PERPLEXER> walk south
You approach the falling water but despite being liquid it is far too cold to bear on your skin. You are unable to proceed.
PERPLEXER> walk east
You are at a central room and can move in each direction. You hear a crackling sound to the south. To the north you hear the muffled sound of a dot matrix printer. The east is an unlit corridor. You can hear what sounds like someone murmuring to themselves.
PERPLEXER> walk south
You see a small rectangular hole in the south wall. On the other side all you can see are flames.
PERPLEXER> interact
You pick up a branch from the floor and stick it in the hole. The branch is now a torch.
PERPLEXER> walk north
central room
PERPLEXER> walk north
You use your torch to burn a hole big enough to crawl through. You see a walled in room with a table in the center. On the table is a dot matrix printer constantly printing the same 10 digit number over and over again.
PERPLEXER>

Globals

Rooms

  • Names
    (defparameter *room-names*
      '((northwest "shredded paper pile room")
        (west "mist room")
        (southwest "green cracked room")
        (north "printer room")
        (central "central room")
        (south "flame access")
        (northeast "tree room")
        (east "zigzag hallway")
        (southest "perplexer room")
        (exit "end")))
    
    
  • Descriptions
    (defparameter *room-descriptions*
      '((northwest "You are underneath a light dangling from the ceiling by a string. North, west and east are blocked by walls. You see a pile of shredded up paper in the corner. You try to read it but are unable to piece it together. Through the east wall you hear the muffled sounds of a of a dot matrix printer. To the south you see a dark corridor and faintly hear running water.")
        (west "The room is pitch black and it is hard to make anything out. You feel mist hitting your face from what sounds like a waterfall to the south. To the east you hear wind rustling and see a distant light.")
        (southwest "The ground is broken and covered with cracks blasting the room with green light. You see a small red tablet with gold lettering on the floor. You are unable to make out the writing.")
        (north "You see a walled in room with a table in the center. On the table is a dot matrix printer constantly printing the same 10 digit number over and over again.")
        (central "You are at a central room and can move in each direction. You hear a crackling sound to the south. To the north you hear the muffled sound of a dot matrix printer. The east is an unlit corridor. You can hear what sounds like someone murmuring to themselves.")
        (south "You see a small rectangular hole in the south wall. On the other side all you can see are flames.")
        (northeast "You enter a bright room filled with mist. The walls are glass and you can see the sun is out. In the center of the room is an odd tree with giant leaves.")
        (east "You crouch through a narrow doorway to enter a cramped hallway. The walls are painted with a black and white zigzag pattern. To the east you feel a cold wall, dead end. To the north you hear the sounds of birds chirping. The murmuring seems to be coming from the south. It sounds like they are answering questions to a quiz show but you can't hear any other voice.")
        (southeast "Inside the room is a disheveled person pacing back and forth. You notice an earpiece in their ear and realize they are using that to communicate with someone. They seem to be trying to find the right words to please their captor to release them from the conversation. They do not notice you. Behind them you see an open window you could climb through and escape.")
        (exit "Outside you are greeted by a group of lab coat wearing scientists. One scientists starts slow clapping and they all join in quickly afterwards. The hoist you up on their shoulders and start singing. You are not sure what is happening or where they are taking you.\n\n\nThe end")))
    
  • Locked Rooms
    • The *locked-rooms* alist is a list of all the locked rooms and what the needed key is
    • The *room-unlock-descriptions* contains the messages for when a room is unlocked by the player
    • Similarly the *room-locked-descriptions* contains the messages for when a player attempts to enter a room but does not have the needed key
    (defparameter *locked-rooms*
      '((southwest umbrella)
        (north torch)
        (southeast passcode)
        (exit tablet)))
    
    (defparameter *room-unlock-descriptions*
      '((southwest "Using the large leaf from your bag you hold it by the stem over your head. Jogging you are able to quickly enter the room.")
        (north "You use your torch to burn a hole big enough to crawl through.")
        (southeast "")
        (exit "As you enter the room the gold letters on the red tablet begin to light up. You go to pull it out of your bag but it is hot to the touch so you drop it. The disheveled person stops in their tracks and walks towards the tablet. They seem utterly perplexed. While they are distracted you make a break for it and climb through the window.")))
    
    (defparameter *room-locked-descriptions*
      '((southwest "You approach the falling water but despite being liquid it is far too cold to bear on your skin. You are unable to proceed.")
        (north "You feel a wall different from the rest. Perhaps there is a way to destroy this obstacle.")
        (southeast "The murmuring is definitely coming from behind the door. You twist the knob and it doesn't move. You see a 10 digit combination lock above door knob. You are unable to proceed without the code.")
        (exit "Their pacing is preventing you from reaching the window.")))
    
  • Room Visited Status
    • The room visted status alist tracks whether or not a player has visited a room or not
    • If they have then then only the name is displayed
    • If they have not visited the room then the full description is displayed
    (defparameter *rooms-visited*
      '((northwest t)
        (west nil)
        (southwest nil)
        (north nil)
        (central nil)
        (south nil)
        (northeast nil)
        (east nil)
        (southest nil)
        (exit nil)))
    
    

Edges

  • The edges represent the doors connecting the various rooms together
(defparameter *edges*
  '((northwest
     (west south))
    (west
     (northwest north)
     (southwest south)
     (central east))
    (southwest
     (west north))
    (central
     (north north)
     (east east)
     (south south)
     (west west))
    (north
     (central south))
    (south
     (central north))
    (east
     (central west)
     (northeast north)
     (southeast south))
    (northeast
     (east south))
    (southeast
     (east north)
     (exit south))))

Current Room

  • This variable is the current location of the player
  • The game starts with the player in the northwest room
(defparameter *current-room* 'northwest)

Objects

  • The objects alist represents the set of keys in the game and their current locations
  • Each key can either be in the initial room or in the player's bag
(defparameter *objects* '(torch umbrella passcode tablet))

(defparameter *object-locations* '((torch south)
                                   (umbrella northeast)
                                   (passcode north)
                                   (tablet southwest)))

Interaction Descriptions

(defparameter *room-interact*
  '((south "You pick up a branch from the floor and stick it in the hole. The branch is now a torch.")
    (northeast "You pull one of the leaves off the tree and place it in your bag.")
    (north "You rip the top page and stick it in your bag.")
    (southwest "")))

Actions

Object At

;; Borrowed from Land of Lisp
(defun objects-at (loc objs obj-locs)
    (labels ((at-loc-p (obj)
               (eq (cadr (assoc obj obj-locs)) loc)))
      (remove-if-not #'at-loc-p objs)))

Visit Node

(defun location-visited-p (loc loc-visited)
  (cadr (assoc loc loc-visited)))

(defun visit-location (loc loc-visited)
  (setf (cadr (assoc loc loc-visited)) t))

Describe / Look

  • The describe function will return the string that is the long description for a room
  • It is a pure function so it doesn't use any globals
  • The look function wraps the describe function to use the globals *current-room* and *rooms*
  • The quick look function will return the full description if the room hasn't been visited, otherwise it will return the name
(defun look ()
  (cadr (assoc *current-room* *room-descriptions*)))

(defun quick-look ()
  (if (location-visited-p *current-room* *rooms-visited*)
      (cadr (assoc *current-room* *room-names*))
      (cadr (assoc *current-room* *room-descriptions*))))

Can Enter?

  • This predicate will return true if a room is unlocked or locked and the needed key is in the player's bag
(defun can-enter-p (loc)
  (flet ((is-unlocked-p (loc)
           (not (assoc loc *locked-rooms*)))
         (needed-key-p (loc)
           (cadr (assoc loc *locked-rooms*)))
         (key-in-bag-p (key)
           (find key (objects-at 'bag *objects* *object-locations*))))

    (or (is-unlocked-p loc)
        (key-in-bag-p (needed-key-p loc)))))

Walk

  • This function takes a direction as an argument and potentially changes the *current-room* global to this new location
  • The move must be valid however, so if there isn't an edge between the rooms the player will not be able to travel to the room
  • If the edge is valid and locked then the key object must be in the player's bag
(defun walk (direction)
  (let ((next (find direction
                    (cdr (assoc *current-room* *edges*))
                    :key #'cadr)))
    (if next
        (if (can-enter-p (car next))
            (progn
              (flet ((is-room-locked-p (loc)
                       (assoc loc *locked-rooms*)))
                (visit-location *current-room* *rooms-visited*)
                (setf *current-room* (car next))
                (if (is-room-locked-p (car next))
                    (progn
                      (setf *locked-rooms* (remove-if #'(lambda (loc)
                                                          (eq loc (car next)))
                                                      *locked-rooms*))
                      (concatenate 'string
                                   (cadr (assoc *current-room*
                                               *room-unlock-descriptions*))
                                   " "
                                   (quick-look)))
                    (quick-look))))
            (cadr (assoc (car next) *room-locked-descriptions*)))
        "A cold wall prevents you from moving in this direction")))

Inventory

  • The inventory function will list all items that are located on the player
  • The has item predicate will return nil if the item is not in the inventory
(defun inventory ()
  (objects-at 'bag *objects* *object-locations*))

(defun has-object (object)
  (find object (inventory)))

Name

(defun name-location (loc loc-names)
  (cadr (assoc loc loc-names)))

Interact

  • The interact function uses the global *current-room* and adds any items in the room into the players bag
  • It will also display the room interaction message
(defun interact ()
  (flet ((has-key-p (loc)
           (objects-at loc *objects* *object-locations*)))
    (if (has-key-p *current-room*)
        (progn
          (let ((obj (car (objects-at *current-room* *objects*
                                      *object-locations*))))
            (setf (cadr (assoc obj *object-locations*)) 'bag)
            (cadr (assoc *current-room* *room-interact*))))
        "Nothing happened")))

Game REPL

  • These functions are inspired by the text engine from Land of Lisp
  • That create a REPL that is the interface for the game

Read

;; Borrowed from Land of Lisp
(defun game-read ()
  (let ((cmd (read-from-string
              (concatenate 'string "(" (read-line) ")"))))
    (flet ((quote-it (x)
             (list 'quote x)))
      (cons (car cmd) (mapcar #'quote-it (cdr cmd))))))

Eval

(defparameter *allowed-commands* '(look walk interact inventory))

(defun game-eval (sexp)
  (if (member (car sexp) *allowed-commands*)
      (eval sexp)
      "I can't do that"))

Print

(defun game-print (text)
  (princ text)
  (fresh-line))

REPL

(defun perplexer ()
  (princ "PERPLEXER> ")
  (let ((cmd (game-read)))
    (unless (eq (car cmd) 'quit)
      (game-print (game-eval cmd))
      (perplexer))))

C Version

Prompt

Prompting the User

  • Interaction with the user happens through a custom prompt
  • The prompt takes as arguments the prompt message, a validator function, pointer for the result of what the user typed, and a void pointer to some user data
  • The prompt will continue to show the message and ask the user for input until the validator function returns a value other than zero
  • The prompt function uses fgets to read in no more than PROMPT_MAX number of characters
    • It then checks to see if the input string has a newline character in it, if not continue the loop and try again
  • The user data argument gets passed into the validator function to allow the validator to use state from outside the prompt function
    • This was modeled after how the user events work in SDL2
  • Once the validator approves the input the result is returned
  • This function is handy since it encapsulates the mechanics of getting input from the user
  • The programmer only needs to worry about what is valid and what is not valid input
void promptUser (const char *msg, int (*validator)(const char *, void *),
                 char *result, void *userData) {
  char input[PROMPT_MAX], *p;
  int isValid = 0;

  do {
    isValid = 0;
    printf("%s", msg);

    fgets(input, sizeof(input), stdin);
    if ((p = strchr(input, '\n')) == NULL) {
      continue;
    }

    *p = '\0';

    isValid = validator(input, userData);
  } while (isValid == 0);

  strcpy(result, input);
}

Print Width

  • This function is used to print a string with a max number of columns
  • It checks to make sure the current word doesn't exceed the max before printing it on the current line
  • This is handy for ensuring the presentation of the text looks good on very wide terminals
void printWidth(const char *msg, int width) {
  int col = 0;
  for (int i = 0; i < DESC_MAX && msg[i] != '\0'; i++) {
    if (msg[i] == ' ') {
      int nextSpace = 1;
      while (i + nextSpace < DESC_MAX &&
             msg[i + nextSpace] != ' ' &&
             msg[i + nextSpace] != '\0') {
        nextSpace++;
      }

      if (col + nextSpace > width) {
        putchar('\n');
        col = 0;
      } else {
        putchar(' ');
      }
    } else {
      putchar(msg[i]);
      col++;
    }
  }

  putchar('\n');
}

Adventure

  • The "engine" of the game is called adventure

Room Data Model

  • The game is divided into a series of rooms
  • Rooms can contain a variety of text:
    • desc - This is the main description of the room
    • locationName - This is a short description of the room
    • unlockDesc - This is what is displayed when the room is unlocked
    • lockedDesc - This is what is displayed when attempting to enter this room without the key
    • interactDesc - If the room has a key in it this text is displayed when the player uses the interact command
  • The rooms can potentially contain or require keys
  • Each room has a set of 4 pointers to other rooms in the cardinal directions
  • If the direction pointer is null that indicates a dead end
struct room {
  const char desc[DESC_MAX];
  const char locationName[30];
  const char unlockDesc[DESC_MAX];
  const char lockedDesc[DESC_MAX];
  const char interactDesc[DESC_MAX];
  int requiredKey;
  int containedKey;
  int unlocked;
  int visited;
  struct room *north;
  struct room *south;
  struct room *east;
  struct room *west;
};

Move Input Validator

  • The player is allow to only type a handful of commands into the prompt
  • Since the prompt function allows custom validators, adventure provides one for playing a text adventure game
  • This function loops through all the possible actions and their short commands
  • If it is unable to find the input string in the valid strings it rejects the input
int isMoveValid (const char *move, void *userData) {
  static const char * const validInput[] = {
    "north", "n",
    "North", "N",
    "south", "s",
    "South", "S",
    "east", "e",
    "East", "E",
    "west", "w",
    "West", "W",
    "look", "l",
    "Look", "L",
    "bag", "b",
    "Bag", "B",
    "interact", "i",
    "Interact", "I",
    "help", "h",
    "Help", "H",
    "quit", "q",
    "Quit", "Q"
  };

  for (int i = 0; i < 36; i++) {
    if (strcmp(validInput[i], move) == 0) {
      return 1;
    }
  }

  return 0;
}

Moving through rooms

  • The move function will return the adjacent room based on the action (north, south, east or west)
  • If the adjacent room in that direction is null then the original room is returned
struct room * move (struct room *r, enum action a) {
  if (r == NULL) {
    return r;
  }

  struct room *next;

  switch (a) {
  case North:
    next = r->north;
    break;
  case South:
    next = r->south;
    break;
  case East:
    next = r->east;
    break;
  case West:
    next = r->west;
    break;
  default:
    return r;
  }

  if (next == NULL) {
    return r;
  }

  return next;
}

Actions

  • Actions are either moving around, looking, interacting with the room or looking in your bag
enum action {
  North,
  South,
  East,
  West,
  Look,
  Bag,
  Interact,
  Help,
  Quit
};

Game Loop

  • The loop of adventure is very similar to a REPL
  • Prompt the User for an Action
    • The game uses the > character as the prompt
    • The game supports upper or lower case for commands
    • To streamline the conditional handling we convert any upper case letters to the lower case versions
    • An easy way to do is is to add 32 to the character since all lower case letters are 32 away from their upper case
    promptUser("> ", isMoveValid, moveInput, NULL);
    char firstCh = moveInput[0];
    if (firstCh >= 'A' && firstCh <= 'Z') {
      firstCh += 32; // Make it lowercase
     }
    
  • Converting the Action String to Enum
    • This uses the lowercase letter of the valid action the user typed with the enum action
    enum action a;
    switch (firstCh) {
     case 'n':
       a = North;
       break;
     case 's':
       a = South;
       break;
     case 'e':
       a = East;
       break;
     case 'w':
       a = West;
       break;
     case 'l':
       a = Look;
       break;
     case 'b':
       a = Bag;
       break;
     case 'i':
       a = Interact;
       break;
     case 'h':
       a = Help;
       break;
     case 'q':
       a = Quit;
       break;
     }
    
  • Handling the Action
    • Look
      • In the case of the look action all we need to do is print the description of the current room
      printWidth(currentRoom->desc, DESC_WIDTH);
      
    • Move
      • Call the move function to get the next room
        nextRoom = move(currentRoom, a);
        
      • If the nextRoom is the same as the currentRoom it is a dead end
        if (nextRoom == currentRoom) {
          printWidth(deadEnd, DESC_WIDTH);
         }
        
      • If the room requires a key, is it in the bag?
        • This iterates through the keys in tha bag
        • If it finds the required key it sets the state of that room to unlocked
        • It also prints the unlock description
        • If the key is not in the bag it prints the locked description
        int moveOk = 0;
        if (nextRoom->requiredKey != 0) {
          for (int i = 0; i < bagLen; i++) {
            if (nextRoom->requiredKey == bag[i]) {
              if (nextRoom->unlocked == 0) {
                printWidth(nextRoom->unlockDesc, DESC_WIDTH);
              }
              moveOk = 1;
              nextRoom->unlocked = 1;
              break;
            }
          }
        
          if (moveOk == 0) {
            printWidth(nextRoom->lockedDesc, DESC_WIDTH);
          }
         } else {
          moveOk = 1;
         }
        
      • Print the description when entering the new room
        • If the room has been visited already we just display the location name
        • If it is the first time visiting the room we want to show the full description
        if (nextRoom->visited == 0) {
          printWidth(nextRoom->desc, DESC_WIDTH);
         } else {
          printWidth(nextRoom->locationName, DESC_WIDTH);
         }
        
      • Advance the currentRoom to the nextRoom
        • Also ensure the room is marked as visited
        currentRoom = nextRoom;
        currentRoom->visited = 1;
        
      • Deciding on whether or not to go to the next room
        • Before entering a room this function checks to see if you have the needed key
        • If you have never been to the room before it will display the long description, otherwise it will display the short
        • Once the user has reached the exit room the game ends
    • Inventory / Bag
      if (bagLen == 0) {
        printf("Your bag is empty\n");
       } else {
        printf("In your bag you find:\n");
        for (int i = 0; i < bagLen; i++) {
          if (bag[i] >= 0 && bag[i] < keyCount ) {
            printf("\t- %s\n", keyDesc[bag[i]]);
          } else {
            printf("\t- Error\n");
          }
        }
       }
      
    • Interact
      • This will check if the room contains a key
      • If it does it will print the interaction description
      • The key will be added to the bag and the room contained key will be set to none
      if (currentRoom->containedKey != 0) {
        bag[bagLen] = currentRoom->containedKey;
        bagLen++;
        currentRoom->containedKey = 0;
      
        printWidth(currentRoom->interactDesc, DESC_WIDTH);
       } else {
        printWidth("Nothing happened", DESC_WIDTH);
       }
      
      
    • Help
      • When the user types the help command we just redisplay the help message
      printWidth("Use commands north, south, east, west to explore. The first time you enter a room you get a long description. After that, you will get a short description. Use the look command to read the long description again. Use the interact command to try to interact with the room. Use the quit command to exit the game. To see this message again type help.", DESC_WIDTH);
      
    • Quit
      • To handle the quit action we just display a message and return from the adventure loop
      printf("Goodbye!\n");
      return;
      

Perplexer

  • The perplexer game code mostly consists of setting up the room data structures
  • A local enum is created for all the possible keys in the game
  • If a room needs a key or contains a key the enum value is used
  • A maze is built connecting the rooms together and the starting node is passed to the adventure function
  • Utilizing org babel tangle and weave features are great for text based games
    • You can edit the descriptions in a dedicated text block, then weave that into the code blocks
int main (int argc, char **argv) {
  enum key { None, Torch, Umbrella, Passcode, Tablet };
  int keyCount = 5;
  const char *keyDesc[] = { "None", "Torch", "Umbrella", "Passcode", "Tablet" };

  struct room nw = {
    .desc = "You are underneath a light dangling from the ceiling by a string. North, west and east are blocked by walls. You see a pile of shredded up paper in the corner. You try to read it but are unable to piece it together. Through the east wall you hear the muffled sounds of a of a dot matrix printer. To the south you see a dark corridor and faintly hear running water.",
    .locationName = "shredded paper pile room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room n = {
    .desc = "You see a walled in room with a table in the center. On the table is a dot matrix printer constantly printing the same 10 digit number over and over again.",
    .locationName = "printer room",
    .unlockDesc = "You use your torch to burn a hole big enough to crawl through.",
    .lockedDesc = "You feel a wall different from the rest. Perhaps there is a way to destroy this obstacle.",
    .interactDesc = "You rip the top page and stick it in your bag.",
    .requiredKey = Torch,
    .containedKey = Passcode
  };

  struct room ne = {
    .desc = "You enter a bright room filled with mist. The walls are glass and you can see the sun is out. In the center of the room is an odd tree with giant leaves.",
    .locationName = "tree room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "You pull one of the leaves off the tree and place it in your bag.",
    .requiredKey = None,
    .containedKey = Umbrella
  };

  struct room w = {
    .desc = "The room is pitch black and it is hard to make anything out. You feel mist hitting your face from what sounds like a waterfall to the south. To the east you hear wind rustling and see a distant light.",
    .locationName = "mist room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room c = {
    .desc = "You are at a central room and can move in each direction. You hear a crackling sound to the south. To the north you hear the muffled sound of a dot matrix printer. The east is an unlit corridor. You can hear what sounds like someone murmuring to themselves.",
    .locationName = "central room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room e = {
    .desc = "You crouch through a narrow doorway to enter a cramped hallway. The walls are painted with a black and white zigzag pattern. To the east you feel a cold wall, dead end. To the north you hear the sounds of birds chirping. The murmuring seems to be coming from the south. It sounds like they are answering questions to a quiz show but you can't hear any other voice.",
    .locationName = "zigzag hallway",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room sw = {
    .desc = "The ground is broken and covered with cracks blasting the room with green light. You see a small red tablet with gold lettering on the floor. You are unable to make out the writing.",
    .locationName = "green cracked room",
    .unlockDesc = "Using the large leaf from your bag you hold it by the stem over your head. Jogging you are able to quickly enter the room.",
    .lockedDesc = "You approach the falling water but despite being liquid it is far too cold to bear on your skin. You are unable to proceed.",
    .interactDesc = "You place the tablet in your bag.",
    .requiredKey = Umbrella,
    .containedKey = Tablet
  };

  struct room s = {
    .desc = "You see a small rectangular hole in the south wall. On the other side all you can see are flames.",
    .locationName = "flame access",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "You pick up a branch from the floor and stick it in the hole. The branch is now a torch.",
    .requiredKey = None,
    .containedKey = Torch
  };

  struct room se = {
    .desc = "Inside the room is a disheveled person pacing back and forth. You notice an earpiece in their ear and realize they are using that to communicate with someone. They seem to be trying to find the right words to please their captor to release them from the conversation. They do not notice you. Behind them you see an open window you could climb through and escape.",
    .locationName = "perplexer room",
    .unlockDesc = "You review the code from the dot matrix printer and put it in the combination lock. You hear a click and can now turn the door knob.",
    .lockedDesc = "The murmuring is definitely coming from behind the door. You twist the knob and it doesn't move. You see a 10 digit combination lock above door knob. You are unable to proceed without the code.",
    .interactDesc = "",
    .requiredKey = Passcode,
    .containedKey = None
  };

  struct room exitRoom = {
    .desc = "Outside you are greeted by a group of lab coat wearing scientists. One scientists starts slow clapping and they all join in quickly afterwards. The hoist you up on their shoulders and start singing. You are not sure what is happening or where they are taking you.\n\n\nThe end",
    .locationName = "end",
    .unlockDesc = "As you enter the room the gold letters on the red tablet begin to light up. You go to pull it out of your bag but it is hot to the touch so you drop it. The disheveled person stops in their tracks and walks towards the tablet. They seem utterly perplexed. While they are distracted you make a break for it and climb through the window.",
    .lockedDesc = "Their pacing is preventing you from reaching the window.",
    .interactDesc = "",
    .requiredKey = Tablet,
    .containedKey = None
  };

  nw.south = &w;

  n.south = &c;

  ne.south = &e;

  w.north = &nw;
  w.south = &sw;
  w.east  = &c;

  c.north = &n;
  c.east = &e;
  c.south = &s;
  c.west = &w;

  e.north = &ne;
  e.south = &se;
  e.west = &c;

  sw.north = &w;

  s.north = &c;

  se.north = &e;
  se.south = &exitRoom;
  se.east = &exitRoom;

  printf("Escape the perplexity\n---------------------\n");

  printf("\n\n\n");

  adventure(&nw, &exitRoom, "A cold wall prevents you from moving in this direction", keyCount, keyDesc);

  return 0;
}

Complete Code Listing

constants.h

#ifndef _CONSTANTS_H_
#define _CONSTANTS_H_

#define PROMPT_MAX 50
#define DESC_MAX 500

#endif

prompt.h

#ifndef _PROMPT_H_
#define _PROMPT_H_

#include <stdint.h>

#include "constants.h"

void promptUser (const char *, int (*)(const char *, void *), char *, void *);
int termSetEcho (int echo);
void printWidth (const char *, int);

#endif

prompt.c

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

#include "prompt.h"

void promptUser (const char *msg, int (*validator)(const char *, void *),
                 char *result, void *userData) {
  char input[PROMPT_MAX], *p;
  int isValid = 0;

  do {
    isValid = 0;
    printf("%s", msg);

    fgets(input, sizeof(input), stdin);
    if ((p = strchr(input, '\n')) == NULL) {
      continue;
    }

    *p = '\0';

    isValid = validator(input, userData);
  } while (isValid == 0);

  strcpy(result, input);
}


int termSetEcho (int echo) {
  struct termios termInfo;
  int r = tcgetattr(STDIN_FILENO, &termInfo);
  if (r > 0) {
    return r;
  }

  if (echo) {
    termInfo.c_lflag |= ECHO;
  } else {
    termInfo.c_lflag &= ~ECHO;
  }

  return tcsetattr(STDIN_FILENO, TCSANOW, &termInfo);
  }

void printWidth(const char *msg, int width) {
  int col = 0;
  for (int i = 0; i < DESC_MAX && msg[i] != '\0'; i++) {
    if (msg[i] == ' ') {
      int nextSpace = 1;
      while (i + nextSpace < DESC_MAX &&
             msg[i + nextSpace] != ' ' &&
             msg[i + nextSpace] != '\0') {
        nextSpace++;
      }

      if (col + nextSpace > width) {
        putchar('\n');
        col = 0;
      } else {
        putchar(' ');
      }
    } else {
      putchar(msg[i]);
      col++;
    }
  }

  putchar('\n');
}

adventure.h

#ifndef _ADVENTURE_H_
#define _ADVENTURE_H_

#include "constants.h"

struct room {
  const char desc[DESC_MAX];
  const char locationName[30];
  const char unlockDesc[DESC_MAX];
  const char lockedDesc[DESC_MAX];
  const char interactDesc[DESC_MAX];
  int requiredKey;
  int containedKey;
  int unlocked;
  int visited;
  struct room *north;
  struct room *south;
  struct room *east;
  struct room *west;
};


enum action {
  North,
  South,
  East,
  West,
  Look,
  Bag,
  Interact,
  Help,
  Quit
};

int isMoveValid (const char *, void *);
struct room * move (struct room *, enum action);
void adventure (struct room *, struct room *, const char *, int, const char **);

#endif

adventure.c

#include <stdio.h>
#include <string.h>

#include "prompt.h"
#include "adventure.h"

#define DESC_WIDTH 55

int isMoveValid (const char *move, void *userData) {
  static const char * const validInput[] = {
    "north", "n",
    "North", "N",
    "south", "s",
    "South", "S",
    "east", "e",
    "East", "E",
    "west", "w",
    "West", "W",
    "look", "l",
    "Look", "L",
    "bag", "b",
    "Bag", "B",
    "interact", "i",
    "Interact", "I",
    "help", "h",
    "Help", "H",
    "quit", "q",
    "Quit", "Q"
  };

  for (int i = 0; i < 36; i++) {
    if (strcmp(validInput[i], move) == 0) {
      return 1;
    }
  }

  return 0;
}

struct room * move (struct room *r, enum action a) {
  if (r == NULL) {
    return r;
  }

  struct room *next;

  switch (a) {
  case North:
    next = r->north;
    break;
  case South:
    next = r->south;
    break;
  case East:
    next = r->east;
    break;
  case West:
    next = r->west;
    break;
  default:
    return r;
  }

  if (next == NULL) {
    return r;
  }

  return next;
}


void adventure (struct room *startRoom, struct room *exitRoom,
                const char *deadEnd, int keyCount, const char **keyDesc) {
  struct room *currentRoom = startRoom;
  struct room *nextRoom = NULL;

  int bag[9] = { };
  int bagLen = 0;
  char moveInput[10] = { };

  printWidth(currentRoom->desc, DESC_WIDTH);
  currentRoom->visited = 1;
  do {
    promptUser("> ", isMoveValid, moveInput, NULL);
    char firstCh = moveInput[0];
    if (firstCh >= 'A' && firstCh <= 'Z') {
      firstCh += 32; // Make it lowercase
     }

                      enum action a;
                      switch (firstCh) {
                       case 'n':
                         a = North;
                         break;
                       case 's':
                         a = South;
                         break;
                       case 'e':
                         a = East;
                         break;
                       case 'w':
                         a = West;
                         break;
                       case 'l':
                         a = Look;
                         break;
                       case 'b':
                         a = Bag;
                         break;
                       case 'i':
                         a = Interact;
                         break;
                       case 'h':
                         a = Help;
                         break;
                       case 'q':
                         a = Quit;
                         break;
                       }

      switch (a) {
      case Look:
        printWidth(currentRoom->desc, DESC_WIDTH);
          break;
      case Bag:
        if (bagLen == 0) {
          printf("Your bag is empty\n");
         } else {
          printf("In your bag you find:\n");
          for (int i = 0; i < bagLen; i++) {
            if (bag[i] >= 0 && bag[i] < keyCount ) {
              printf("\t- %s\n", keyDesc[bag[i]]);
            } else {
              printf("\t- Error\n");
            }
          }
         }
          break;
      case Interact:
        if (currentRoom->containedKey != 0) {
          bag[bagLen] = currentRoom->containedKey;
          bagLen++;
          currentRoom->containedKey = 0;

          printWidth(currentRoom->interactDesc, DESC_WIDTH);
         } else {
          printWidth("Nothing happened", DESC_WIDTH);
         }

          break;
      case Help:
        printWidth("Use commands north, south, east, west to explore. The first time you enter a room you get a long description. After that, you will get a short description. Use the look command to read the long description again. Use the interact command to try to interact with the room. Use the quit command to exit the game. To see this message again type help.", DESC_WIDTH);
          break;
      case Quit:
        printf("Goodbye!\n");
        return;
          break;
      case North:
      case South:
      case East:
      case West:
        nextRoom = move(currentRoom, a);

        if (nextRoom == currentRoom) {
          printWidth(deadEnd, DESC_WIDTH);
         } else {
            int moveOk = 0;
            if (nextRoom->requiredKey != 0) {
              for (int i = 0; i < bagLen; i++) {
                if (nextRoom->requiredKey == bag[i]) {
                  if (nextRoom->unlocked == 0) {
                printWidth(nextRoom->unlockDesc, DESC_WIDTH);
                  }
                  moveOk = 1;
                  nextRoom->unlocked = 1;
                  break;
                }
              }

              if (moveOk == 0) {
                printWidth(nextRoom->lockedDesc, DESC_WIDTH);
              }
             } else {
              moveOk = 1;
             }
              if (moveOk) {
                if (nextRoom->visited == 0) {
                  printWidth(nextRoom->desc, DESC_WIDTH);
                 } else {
                  printWidth(nextRoom->locationName, DESC_WIDTH);
                 }
                currentRoom = nextRoom;
                currentRoom->visited = 1;

                  }
          }
          break;
      }
  } while(currentRoom != exitRoom);
}

main.c

#include <stdio.h>

#include "adventure.h"
#include "prompt.h"

int main (int argc, char **argv) {
  enum key { None, Torch, Umbrella, Passcode, Tablet };
  int keyCount = 5;
  const char *keyDesc[] = { "None", "Torch", "Umbrella", "Passcode", "Tablet" };

  struct room nw = {
    .desc = "You are underneath a light dangling from the ceiling by a string. North, west and east are blocked by walls. You see a pile of shredded up paper in the corner. You try to read it but are unable to piece it together. Through the east wall you hear the muffled sounds of a of a dot matrix printer. To the south you see a dark corridor and faintly hear running water.",
    .locationName = "shredded paper pile room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room n = {
    .desc = "You see a walled in room with a table in the center. On the table is a dot matrix printer constantly printing the same 10 digit number over and over again.",
    .locationName = "printer room",
    .unlockDesc = "You use your torch to burn a hole big enough to crawl through.",
    .lockedDesc = "You feel a wall different from the rest. Perhaps there is a way to destroy this obstacle.",
    .interactDesc = "You rip the top page and stick it in your bag.",
    .requiredKey = Torch,
    .containedKey = Passcode
  };

  struct room ne = {
    .desc = "You enter a bright room filled with mist. The walls are glass and you can see the sun is out. In the center of the room is an odd tree with giant leaves.",
    .locationName = "tree room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "You pull one of the leaves off the tree and place it in your bag.",
    .requiredKey = None,
    .containedKey = Umbrella
  };

  struct room w = {
    .desc = "The room is pitch black and it is hard to make anything out. You feel mist hitting your face from what sounds like a waterfall to the south. To the east you hear wind rustling and see a distant light.",
    .locationName = "mist room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room c = {
    .desc = "You are at a central room and can move in each direction. You hear a crackling sound to the south. To the north you hear the muffled sound of a dot matrix printer. The east is an unlit corridor. You can hear what sounds like someone murmuring to themselves.",
    .locationName = "central room",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room e = {
    .desc = "You crouch through a narrow doorway to enter a cramped hallway. The walls are painted with a black and white zigzag pattern. To the east you feel a cold wall, dead end. To the north you hear the sounds of birds chirping. The murmuring seems to be coming from the south. It sounds like they are answering questions to a quiz show but you can't hear any other voice.",
    .locationName = "zigzag hallway",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "",
    .requiredKey = None,
    .containedKey = None
  };

  struct room sw = {
    .desc = "The ground is broken and covered with cracks blasting the room with green light. You see a small red tablet with gold lettering on the floor. You are unable to make out the writing.",
    .locationName = "green cracked room",
    .unlockDesc = "Using the large leaf from your bag you hold it by the stem over your head. Jogging you are able to quickly enter the room.",
    .lockedDesc = "You approach the falling water but despite being liquid it is far too cold to bear on your skin. You are unable to proceed.",
    .interactDesc = "You place the tablet in your bag.",
    .requiredKey = Umbrella,
    .containedKey = Tablet
  };

  struct room s = {
    .desc = "You see a small rectangular hole in the south wall. On the other side all you can see are flames.",
    .locationName = "flame access",
    .unlockDesc = "",
    .lockedDesc = "",
    .interactDesc = "You pick up a branch from the floor and stick it in the hole. The branch is now a torch.",
    .requiredKey = None,
    .containedKey = Torch
  };

  struct room se = {
    .desc = "Inside the room is a disheveled person pacing back and forth. You notice an earpiece in their ear and realize they are using that to communicate with someone. They seem to be trying to find the right words to please their captor to release them from the conversation. They do not notice you. Behind them you see an open window you could climb through and escape.",
    .locationName = "perplexer room",
    .unlockDesc = "You review the code from the dot matrix printer and put it in the combination lock. You hear a click and can now turn the door knob.",
    .lockedDesc = "The murmuring is definitely coming from behind the door. You twist the knob and it doesn't move. You see a 10 digit combination lock above door knob. You are unable to proceed without the code.",
    .interactDesc = "",
    .requiredKey = Passcode,
    .containedKey = None
  };

  struct room exitRoom = {
    .desc = "Outside you are greeted by a group of lab coat wearing scientists. One scientists starts slow clapping and they all join in quickly afterwards. The hoist you up on their shoulders and start singing. You are not sure what is happening or where they are taking you.\n\n\nThe end",
    .locationName = "end",
    .unlockDesc = "As you enter the room the gold letters on the red tablet begin to light up. You go to pull it out of your bag but it is hot to the touch so you drop it. The disheveled person stops in their tracks and walks towards the tablet. They seem utterly perplexed. While they are distracted you make a break for it and climb through the window.",
    .lockedDesc = "Their pacing is preventing you from reaching the window.",
    .interactDesc = "",
    .requiredKey = Tablet,
    .containedKey = None
  };

  nw.south = &w;

  n.south = &c;

  ne.south = &e;

  w.north = &nw;
  w.south = &sw;
  w.east  = &c;

  c.north = &n;
  c.east = &e;
  c.south = &s;
  c.west = &w;

  e.north = &ne;
  e.south = &se;
  e.west = &c;

  sw.north = &w;

  s.north = &c;

  se.north = &e;
  se.south = &exitRoom;
  se.east = &exitRoom;

  printf("Escape the perplexity\n---------------------\n");

  printf("\n\n\n");

  adventure(&nw, &exitRoom, "A cold wall prevents you from moving in this direction", keyCount, keyDesc);

  return 0;
}