Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Hunt the Wumpus

0.00/5 (No votes)
20 Jun 2014 1  
A Higher-Order-Programming Environment (HOPE) Example

Previous articles on the Higher Order Programming Environment:

HOPE - Higher Order Programming, an Introduction

HOPE - APOD Website Scraper A HOPE demonstration

Watch the video!

Source Code

Source code can be cloned/forked from https://github.com/cliftonm/HOPE, the game is in the "hunt-the-wumpus" branch as well as the "master" branch.

Playing the Game

Open the TypeSystems.sln solution and build the solution.  Run the TypeSystemExplorer project.  When the application loads, click on "File -> LoadApplet" and select "HuntTheWumpus.xml" from the bin\Debug folder.

Introduction

Hunt the Wumpus was created in 1973 by my late friend Gregory Yob.  The rules are very simple:

  1. You are in a cave system (a dodecahedron) such that each cave connects to three other caves. 
  2. Somewhere in the cave lives a Wumpus, with sucker feet to cling to the walls of bottomless pits and is too heavy to be picked up by super-bats, and he has big teeth and will eat you if disturbed, but mostly the Wumpus likes to sleep.
  3. In the cave system, three of the caves are bottomless pits.  Fall in one, and keep falling.
  4. Also, three of caves contain super-bats, which will carry you off to a random cave.  Super-bats might be in a cave with a bottomless pit, in which you are lucky, the super-bat will grab you as you are falling and transport you somewhere else.  But perhaps your doom will still be another bottomless pit, or worse, the cave where the Wumpus slumbers!
  5. The Wumpus hasn't taken a bath in a long time, so if you're in a cave adjoining the cave where the Wumpus slumbers, you will definitely smell him.
  6. Similarly, if an adjoining cave has a bottomless pit, you will feel the draft upon your cheek of the winds emerging from the infinite depths.
  7. As well, you will hear the flapping of super-bat wings in adjoining caves.
  8. You have 5 arrows.  When you think you know where the Wumpus is (or even if you don't) shoot an arrow into a room.  If you hit the Wumpus, who is rather thin skinned, he will die and you win the game.
  9. If you miss, the Wumpus, who is also a very light sleeper, will wake and move, perhaps into your cave, where he will eat you!
  10. Also, the arrows are sort of weightless, so if you miss, they will continue traveling randomly for a total of five caves, perhaps doubling back and shooting you!

When I was learning Ruby, I wrote a version of the game (sort of C'ish looking) in Ruby, which you can peer at here.  What we'll do today is implement the game using the Higher Order Programming Environment.  We will observe how the code is transformed into more of an actor-message system.  Each cave is an autonomous receptor and the messages (carriers) reflect state information of the cave: what's in the cave and what is adjoining.  You'll also see how easy it is to implement the arrow , player, and Wumpus movement in an actor system.

Caves

To begin with, we'll simply have 20 caves (a dodecahedron has 12 sides and 20 vertices, each vertex having three edges connecting to another vertex.)

We'll start with a boilerplate receptor:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Clifton.Receptor.Interfaces;
using Clifton.SemanticTypeSystem.Interfaces;

namespace HuntTheWumpus
{
  public class CaveReceptor : BaseReceptor
  {
    public override string Name { get { return "Cave"; } }

    public CaveReceptor(IReceptorSystem rsys)
        : base(rsys)
    {
    }

    public override void ProcessCarrier(ICarrier carrier)
    {
    }
  }
}

And we'll drag & drop 20 of these receptors assemblies, each an autonomous cave, onto the surface "skin" of the HOPE sandbox:

Figure 1: A Cave System

Where am I?

Next, we need to create the cave network.  To accomplish this, instead of managing an array of these actors somewhere, we instead have each actor request "where am I?" from a cave configuration receptor, and it will indicate interest in "you are here" messages.  Each receptor will give itself a unique ID that will be used to qualify the response message.  Let's first declare that each cave listens for an "HW_CaveLocation" (incoming carrier) and will emit an "HW_WhereAmI" carrier.  Each cave will also issue the "Where am I?" carrier:

public class CaveReceptor : BaseReceptor
{
  public override string Name { get { return "Cave"; } }

  protected Guid id;

  public CaveReceptor(IReceptorSystem rsys)
      : base(rsys)
  {
    receiveProtocols.Add("HW_YouAre");
    emitProtocols.Add("HW_WhereAmI");
  }

  public override void Initialize()
  {
    base.Initialize();
    id = Guid.NewGuid();
    CreateCarrier("HW_WhereAmI", (signal) => signal.ID = id);
  }

  public override void ProcessCarrier(ICarrier carrier)
  {
  }
}

Now, when we load the Hunt The Wumpus HOPE applet, each cave has a carrier next to it, waiting to be processed by some other receptor:

Figure 2: Carriers awaiting a receptor

You Are Here!

Now we'll implement the cave configuration receptor.  For the moment, we'll used a non-randomized cave pattern, which will make testing easier.  Borrowing from the Ruby code I wrote a couple years ago, we have a cave configuration receptor that initializes a cave system and responds to a "Where Am I?" carrier:

public class CaveConfigurationReceptor : BaseReceptor
{
  public override string Name { get { return "Cave Configuration"; } }

  protected int[][] caveMatrix = new int[][] 
  {
    new int[] {1, 5, 4}, new int[] { 0, 7, 2}, new int[] { 1, 9, 3}, new int[] { 2, 11, 4}, 
    new int[] { 3, 13, 0}, new int[] { 0, 14, 6}, new int[] { 5, 16, 7}, new int[] { 1, 6, 8}, 
    new int[] { 7, 9, 17}, new int[] { 2, 8, 10}, new int[] { 9, 11, 18}, new int[] {10, 3, 12}, 
    new int[] {19, 11, 13}, new int[] {14, 12, 4}, new int[] {13, 5, 15}, new int[] {14, 19, 16}, 
    new int[] { 6, 15, 17}, new int[] {16, 8, 18}, new int[] {10, 17, 19}, new int[] {12, 15, 18}
  };

  protected Dictionary<Guid, int> idToIndexMap;

  public CaveConfigurationReceptor(IReceptorSystem rsys)
      : base(rsys)
  {
      idToIndexMap = new Dictionary<Guid, int>();
    receiveProtocols.Add("HW_WhereAmI");
    emitProtocols.Add("HW_YouAre");
  }

  public override void ProcessCarrier(ICarrier carrier)
  {
    dynamic signal = carrier.Signal;

    // Assign the ID to an index in the cave system that represent a vertex.
    idToIndexMap[signal.ID] = idToIndexMap.Count;

    // If we've got all 20 caves, tell each cave who it is and where it is.
    // The order in which the carriers are received here and by the cave receptors
    // is irrelevant.
    if (idToIndexMap.Count == 20)
    {
      idToIndexMap.ForEach(kvp) =>
      {
        int idx = kvp.Value;
        CreateCarrier("HW_YouAre", (outSignal) =>
        {
          outSignal.ID = kvp.Key;
          outSignal.CaveNumber = idx;
          outSignal.AdjoiningCave1 = caveMatrix[idx][0];
          outSignal.AdjoiningCave2 = caveMatrix[idx][1];
          outSignal.AdjoiningCave3 = caveMatrix[idx][2];
        });
      });
    }
  }
}

Now when we drop this receptor assembly onto the HOPE skin, all those carriers go to the Cave Configuration receptor and it emits the "You Are..." carriers to each and every receptor.  Here's a screenshot showing what the response back to the cave receptors looks like:

<img border="0" height="352" src="786118/r3.png" width="533" />

Figure 3: Caves being configured

Of course, once configured, we don't need to the Cave Configuration receptor anymore, so it can go away (though we will revisit it later to create the bottomless pits, super-bats, Wumpus, and player locations.)  It can remove itself simply enough:

rsys.Remove(this);

upon which we are returned back to the visual state in figure 1.

However, once having received its configuration, caves can now talk to each other, specifically, their neighbors to find out what's going on (drafts, bat wings, smelly Wumpus, etc.)  So, each cave is going to create and listen for some new protocols:

public override void ProcessCarrier(ICarrier carrier)
{
  // Is it the "You are here" protocol?
  if (carrier.Protocol.DeclTypeName == "HW_YouAre")
  {
    // And is it meant for me? (poor man's filtering for now!)
    if (carrier.Signal.ID == id)
    {
      // Save our cave # and neighbor cave numbers.
      caveNumber = carrier.Signal.CaveNumber;
      caveNeighbors[0] = carrier.Signal.AdjoiningCave1;
      caveNeighbors[1] = carrier.Signal.AdjoiningCave2;
      caveNeighbors[2] = carrier.Signal.AdjoiningCave3;

      // Configure emitters and listeners.
      UpdateEmitters();
      UpdateListeners();
    }
  }
}

protected void UpdateEmitters()
{
  // I will make ask my neighbors to announce interesting things about themselves.
  caveNeighbors.ForEach(cn =>
  {
    AddEmitProtocol("HW_Announce" + cn);
  });
}

protected void UpdateListeners()
{
  // I will respond to an announcement request.
  AddReceiveProtocol("HW_Announce" + caveNumber);
}

Now we see our cave system because of the way receptors are creating and listening to particular carrriers:

Let's un-entangle that (unfortunately HOPE doesn't do that for us!):

Much better!  And amazing what we can visualize with just a wee bit of code.

Pits, Bats, Wumpus, and You

Let's go back to the cave configuration receptor and place the three bottomless pits, the three caves filled with super-bats, the Wumpus, and you.

We'll add a bit more information for each cave when we configure the caves:

if (idToIndexMap.Count == 20)
{
  AssignPits();
  AssignBats();
  AssignWumpus();
  AssignPlayer();
  idToIndexMap.ForEach(kvp) =>
  {
    int idx = kvp.Value;
    CreateCarrier("HW_YouAre", (outSignal) =>
    {
      outSignal.ID = kvp.Key;
      outSignal.CaveNumber = idx;
      outSignal.AdjoiningCave1 = caveMatrix[idx][0];
      outSignal.AdjoiningCave2 = caveMatrix[idx][1];
      outSignal.AdjoiningCave3 = caveMatrix[idx][2];
      outSignal.HasBottomlessPit = pits.Contains(idx);
      outSignal.HasSuperBats = bats.Contains(idx);
      outSignal.HasWumpus = wumpus == idx;
      outSignal.HasPlayer = player == idx;
    });
  });

And of course, we need to make the assignments such that the player can live wherever he/she starts off, and we don't want two or more pits or bat swarms in the same cave:

// Pits can go anywhere except twice or thrice in the same cave.
protected void AssignPits()
{
  // Probably a better way to do this.
  pits.Add(rnd.Next(20));
  List<int> available = caveNumbers.Where(cn => !pits.Contains(cn)).ToList();
  pits.Add(available[rnd.Next(19)]);
  available = caveNumbers.Where(cn => !pits.Contains(cn)).ToList();
  pits.Add(available[rnd.Next(18)]);
}

// Bats can go anywhere except twice or thrice in the same cave.
protected void AssignBats()
{
  bats.Add(rnd.Next(20));
  List<int> available = caveNumbers.Where(cn => !bats.Contains(cn)).ToList();
  bats.Add(available[rnd.Next(19)]);
  available = caveNumbers.Where(cn => !bats.Contains(cn)).ToList();
  bats.Add(available[rnd.Next(18)]);
}

// The Wumpus can go anywhere.
protected void AssignWumpus()
{
  wumpus = rnd.Next(20);
}

// The player must start off in a location where he/she can live.
protected void AssignPlayer()
{
  List<int> available = caveNumbers.Where(cn => (cn != wumpus) && !bats.Contains(cn) && !pits.Contains(cn)).ToList();
  player = available[rnd.Next(available.Count)];
}  

Now each cave saves its state:

hasBottomlessPit = carrier.Signal.HasBottomlessPit;
hasSuperBats = carrier.Signal.HasSuperBats;
hasWumpus = carrier.Signal.HasWumpus;
hasPlayer = carrier.Signal.HasPlayer;

Caves, Again

As you can see, most of the work is in the game setup.  Now that we know where everything is, the first order of business is that the cave with the player asks its neighbors about themselves. 

Now, at this point, you might be asking yourself, why doesn't each cave just store the information about its neighbors?  This is a good question, but the salient point is, a cave only knows about itself: what is has and to what caves it connects.  It is a boundary violation, if you will, to have a cave know about other caves.  What if we want to have the bats fly around from cave to cave?  Certainly the Wumpus can get up and move.  Do we want to update every cave with all the knowledge about all the other caves?  Certainly not.

Back to the task at hand: let's ask or neighbors about themselves:

...
if (hasPlayer)
{
  AskAboutOurNeighbors();
  SayWhoIsNextToUs();
}
...

protected void AskAboutOurNeighbors()
{
  caveNeighbors.ForEach(cn => CreateCarrier("HW_Announce" + cn, (signal) => { }));
}

We supply "who is asking."

To respond, a cave issues information about itself in a "Text" carrier:

else if (carrier.Protocol.DeclTypeName.StartsWith("HW_Announce"))
{
  if (hasBottomlessPit)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "I feel a draft!");
  }

  if (hasSuperBats)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "I hear flapping!");
  }

  if (hasWumpus)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "I smell a Wumpus!");
  }
}

Notice we are preserving the autonomy of each cave receptor: it has an action associated with a protocol-of-interest, but the requester doesn't have any say in what that action is.

The cave that the player is in also emits some information about where the player is and what is connecting to us.  This only happens for the cave in which the player is currently occupying:

protected void SayWhoIsNextToUs()
{
  CreateCarrier("Text", (outSignal) => outSignal.Value = "You are in cave number "+caveNumber);
  CreateCarrier("Text", (outSignal) => outSignal.Value = "Passages lead to " + String.Join(", ", caveNeighbors.Select(cn => cn.ToString())));
}

When we drop a Text receptor (or a Text to Speech receptor, or something else that renders text), we suddenly discover where we are and what's near us (fortunately nothing nasty!). 

The game is coming alive!

Because every cave receptor can create a Text carrier, the visualizer draws this association between "producer" and "consumer."  But keep in mind that this is only a visual convenience, in reality, HOPE is very zen-like.  Carriers and receptors simply "are", and when something of interest is created, the receptors interested in that carrier can "digest" the signal embodied in the carrier.

Player Actions

We need a simple UI for handling player actions, and we'll start with movement so we can explore the cave system.  We'll create a receptor that pops open a window with some buttons and communicates with the cave (receptor) that contains the player. 

First off, the cave receptor that contains the player will emit a carrier with information the Player receptor to use in prompting the player:

protected void TalkToPlayer()
{
  CreateCarrier("HW_Player", (outSignal) =>
  {
    outSignal.CaveNumber = caveNumber;
    outSignal.AdjoiningCave1 = caveNeighbors[0];
    outSignal.AdjoiningCave2 = caveNeighbors[1];
    outSignal.AdjoiningCave3 = caveNeighbors[2];
  });
}

We can see this carrier emitted (the yellow triangle that I've circled in the screenshot) and even mouse over it to see the protocol and signal:

All it needs now is the receptor that is interested in this protocol and some simple UI setup to display the buttons:

namespace HuntTheWumpus
{
  public class PlayerReceptor : BaseReceptor
  {
    public override string Name { get { return "Player"; } }

    protected Form form;
    protected Button[] buttons;
    int currentCaveNumber;

    public PlayerReceptor(IReceptorSystem rsys)
        : base(rsys)
    {
      receiveProtocols.Add("HW_Player");
      form = new Form();
      form.Text = "Hunt The Wumpus";
      form.Location = new Point(600, 100);
      form.Size = new Size(320, 100);
      form.StartPosition = FormStartPosition.Manual;
      form.TopMost = true;

      buttons = new Button[3] { new Button(), new Button(), new Button() };
      buttons.ForEachWithIndex((b, idx) =>
      {
        b.Visible = false;
        b.Location = new Point(10 + idx * 100, 15);
        b.Size = new Size(80, 25);
        b.Click += OnMove;
        form.Controls.Add(b);
      });

      form.Show();
    }

    public override void ProcessCarrier(ICarrier carrier)
    {
      if (carrier.Protocol.DeclTypeName == "HW_Player")
      {
        dynamic signal = carrier.Signal;
        currentCaveNumber = signal.CaveNumber;
        buttons[0].Text = "Move to " + signal.AdjoiningCave1;
        buttons[0].Tag = (int)signal.AdjoiningCave1;
        buttons[1].Text = "Move to " + signal.AdjoiningCave2;
        buttons[1].Tag = (int)signal.AdjoiningCave2;
        buttons[2].Text = "Move to " + signal.AdjoiningCave3;
        buttons[2].Tag = (int)signal.AdjoiningCave3;
        buttons.ForEach(b => b.Visible = true);
      }
    }

    protected void OnMove(object sender, EventArgs e)
    {
    }
  }
}

Now we can select what cave we want to move to:

Notice how the player receptor is attached to the cave containing the player.

Next, we need to implement the move protocol.  This requires the cave to listen to a move action, adding another communication path:

When the player moves, we issue this carrier:

protected void OnMove(object sender, EventArgs e)
{
  int newCaveNumber = (int)((Button)sender).Tag;
  CreateCarrier("HW_MoveTo", (signal) => signal.NewCaveNumber = newCaveNumber);
}

Now, both the "HW_Player" protocol (created by caves) and the "HW_MoveTo" protocol (listened for by caves) is actually universal -- every cave creates and listens to these.  Each cave checks if it is the cave number the player is moving to:

else if (carrier.Protocol.DeclTypeName == "HW_MoveTo")
{
  if (carrier.Signal.NewCaveNumber == caveNumber)
  {
    hasPlayer = true;
    SayWhoIsNextToUs();
    AskAboutOurNeighbors();
    TalkToPlayer();
  }
  else
  {
    hasPlayer = false;
  }
}

If so, we emit some carriers that inform the player again what cave their in, what's nearby, and provide some information for the UI.

However, this does result in a messy cave system display:

Visualizing complex systems is another issue!  Also, this isn't well optimized--20 caves process this carrier, when only the cave the person is in and cave the person is moving to needs to process these carriers.  We'll look at qualifying carrier reception from data in a future article.

Regardless, we can now move about (it's a lot more impressive on the video.)  This is after moving to cave #1:

Oh my, a Wumpus, a bottomless pit, and super-bats are in an adjoining cave!  Well, we know it isn't cave #7, that's where we came from, so it must be either cave #0 or #2!

Moving into a Cave with Bad Things

Sometimes bad things happen to us:

protected bool CheckCaveState()
{
  bool ret = !(hasSuperBats || hasWumpus || hasBottomlessPit);

  if (hasSuperBats)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "Super-Bat Snatch!!!!!!!");
  }
  else if (hasBottomlessPit)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "aaaayyyYYYYEEEEEEE You fell into a bottomless pit!!!!!!!");
  }
  else if (hasWumpus)
  {
    // In the original game, I believe bumping into a wumpus woke him and he either ate you
    // or moved to another room.
    CreateCarrier("Text", (outSignal) => outSignal.Value = "gnom, gnom, crunch! You've been eaten by the Wumpus!!!!!!!");
  }

  return ret;
}

At the moment, our Wumpus hunter is immortal, so we can roam around the cave system without fear of death:

We'll also create a carrier for game state change that the Player receptor is listening for:

protected bool CheckCaveState()
{
  bool ret = !(hasSuperBats || hasWumpus || hasBottomlessPit);

  // Nothing save you from the Wumpus. Test first.
  if (hasWumpus)
  {
    // In the original game, I believe bumping into a wumpus woke him and he either ate you
    // or moved to another room.
    CreateCarrier("Text", (outSignal) => outSignal.Value = "gnom, gnom, crunch! You've been eaten by the Wumpus!!!!!!!");
    CreateCarrier("HW_GameState", (outSignal) => outSignal.PlayerEatenByWumpus = true);
  }
  else if (hasSuperBats)
  {
    // Bats will save you from a bottomless pit.
    CreateCarrier("Text", (outSignal) => outSignal.Value = "Super-Bat Snatch!!!!!!!");
    CreateCarrier("HW_GameState", (outSignal) => outSignal.SuperBatSnatch = true);
  }
  else if (hasBottomlessPit)
  {
    CreateCarrier("Text", (outSignal) => outSignal.Value = "AAAAAYYYYyyyyyeeeeeee You fell into a bottomless pit!!!!!!!");
    CreateCarrier("HW_GameState", (outSignal) => outSignal.PlayerFellIntoPit = true);
  }

  return ret;
}

HW_GameState is processed in the player receptor:

else if (carrier.Protocol.DeclTypeName == "HW_GameState")
{
  if (signal.PlayerEatenByWumpus || signal.PlayerFellIntoPit)
  {
    buttons.ForEach(b => b.Visible = false);
    lblPlayerIsDead.Visible = true;
  }
  else if (signal.SuperBatSnatch)
  {
    // Move to some random cave:
    CreateCarrier("HW_MoveTo", (outSignal) => outSignal.NewCaveNumber = rnd.Next(20));
  }
}

This isolates the cave receptor from the player receptor's handling of what happens on a caving event.

Our Wumpus hunter is no longer immortal:

Arrow Logic

Lastly, the player has the option of shooting into a room.  In the original game, you could specify the room number or room path, up to five rooms.  I'm going to keep it simple, allowing you to select only an adjacent room.  If you miss, the arrow will go on a random walk through 4 more caves, and maybe you'll get lucky!  This is implemented in the Cave receptor:

else if (carrier.Protocol.DeclTypeName == "HW_ShootInto")
{
  if (carrier.Signal.CaveNumber == caveNumber)
  {
    if (hasPlayer)
    {
      CreateCarrier("Text", (outSignal) => outSignal.Value = "Ouch! You shot yourself!!!!!!!!");
      CreateCarrier("HW_GameState", (outSignal) => outSignal.PlayerShotSelf = true);
    }
    // This is my cave the hunter is shooting into!
    else if (hasWumpus)
    {
      CreateCarrier("Text", (outSignal) => outSignal.Value = "Ouch! You shot the Wumpus!!!!!!!!");
      CreateCarrier("HW_GameState", (outSignal) => outSignal.WumpusIsDead = true);
    }
    else
    {
      int arrowLife = carrier.Signal.RemainingLife;
      --arrowLife;

      if (arrowLife > 0)
      {
        // The arrow continues to a random room.
        CreateCarrier("HW_ShootInto", (signal) =>
        {
          signal.CaveNumber = caveNeighbors[rnd.Next(3)];
          signal.RemainingLife = arrowLife;
        });
      }
    }
  }
}

And yes indeed, we can shoot ourselves:

And of course you can win by shooting the Wumpus:

Conclusion

In this article, I've demonstrated writing a simple text-based adventure game.  A few of the interesting points to the game are:

  1. The visualization system implicitly renders the game's cave topology because of how carriers are created and listened for between caves.
  2. The game is configured by a one-time configuration receptor
  3. The game illustrates dynamic configuration of listeners and emitter protocols -- initially, the cave system has no topology
  4. Receptors are like Actors: "in response to a message that it receives, an actor can make local decisions, create more actors, send more messages, and determine how to respond to the next message received."
  5. There is no particular reason why the receptors couldn't process carriers in parallel.
  6. Receptors enforce "island of isolation" programming, reducing entanglement between, in OOP, objects.
  7. Receptors are also components: "[they emphasize] the separation of concerns in respect of the wide-ranging functionality available throughout a given software system. It is a reuse-based approach to defining, implementing and composing loosely coupled independent components into systems."  For example, the text receptor could be replaced with the text-to-speech receptor and instead speak the game action.

While certainly not ready for prime-time web and desktop application development, hopefully these series of articles produces some mental "hmmm".

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here