The User Interface (UI)

The principal UI is the game board. This consists of a tank, in which the fish live, eat, breed and die, and a control area. The control area contains a status display, which provides feedback about the health and well being of the fish, as well as a set of buttons for controlling the game. The UI is shown below:

This entire window is supported as a CDialog in CPhishDlg:

class CPhishDlg : public CDialog
{
// Construction
public:
   CPhishDlg( CWnd* pParent = NULL );   // standard constructor
   ~CPhishDlg();
   void Update();
   void ResetControls();
// Dialog Data
   //{{AFX_DATA(CPhishDlg)
   enum { IDD = IDD_PHISH_DIALOG };
   CListBox   dStatus;
   CButton   dSave;
   CButton   dLoad;
   CButton   dStartSim;
   CButton   dManageSpecies;
   CButton   dEndSim;
   CPhishWnd         dPhishWnd;
   //}}AFX_DATA

   // ClassWizard generated virtual function overrides
   //{{AFX_VIRTUAL(CPhishDlg)
   protected:
   virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
   //}}AFX_VIRTUAL

// Implementation
protected:
   void ResetSpecies();
   HICON m_hIcon;

   // Generated message map functions
   //{{AFX_MSG(CPhishDlg)
   virtual BOOL OnInitDialog();
   afx_msg void OnSysCommand( UINT nID, LPARAM lParam );
   afx_msg void OnPaint();
   afx_msg void OnLoadClick();
   afx_msg void OnSaveClick();
   afx_msg void OnManageSpeciesClick();
   afx_msg void OnStartSimClick();
   afx_msg void OnEndSimClick();
   afx_msg void OnTimer(UINT nIDEvent);
   afx_msg void OnAboutPhish();
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()
private:
   CFileDialog dFileOpen;
   CFileDialog dFileSave;
};

Note that this is a wizard-generated class with handlers for the various events, such as OnManageSpeciesClick. When the Manage Species button is pressed the OnManageSpeciesClick() method will be called which will, in turn, bring up the Manage Species Dialog:

This dialog is designed to allow the player to create and manage the various species of Phish. In the first version of this game, new species are not spontaneously generated by the game itself. That feature will not be difficult to add, but for now all species are created by the user prior to beginning the game.

Each species is represented by a tab in the property sheet shown above. The various attributes of a species are captured by values, although the actual value assigned to any individual fish is not necessarily the exact number for that species — it is within a specified range for that species. Thus, if the species has a strength of 20, individual fish might range from 15 - 25.

Examine the constructor for the PAnimal class:

int PAnimal::dNextSerialNumber = 0;

PAnimal::PAnimal( const TSpeciesPtr &Species, int XPos, int YPos )
: dCurrent(XPos, YPos), pSpecies(Species), dDead (false),
   dStarveTime( Species->FoodCap() ), dAge( rand()%10)
{
   Function f ( "PAnimal::PAnimal" );
   dLastMate = dAge;
   dSerialNumber = dNextSerialNumber++;
   dStrength = pSpecies->Strength() + rand() % 11 - 5;   // +-5
   LogValue (dSerialNumber);
}

The actual strength of the individual fish is determined by starting with the overall strength of the species and then setting it to a value plus or minus five points from that starting value. This is done with the line:

dStrength = pSpecies->Strength() + rand() % 11-5;// +-5

In a later iteration of this program, the range itself will be captured in the species — that is, some species will vary from the average more than others.

Strategy Pattern

When two fish are in the same location, the outcome of their chance encounter is determined by a strategy object, in this case a resolver. The PResolver class encapsulates the strategy of how conflicts are resolved.

Thus, one resolver might determine that when two fish encounter one another, the stronger eats the weaker. A more sophisticated resolver might implement a stochastic outcome: the stronger fish will usually win the battle, but not always. In one such implementation, the values of the two fish would establish a ratio of likely victory. Thus, if one fish had strength 20 and another 30, the first fish would win 40% of the battles and the latter fish would win 60%. A still more sophisticated resolver might take into account the length of time that had passed since each fish has eaten, mated or battled.

When we first considered the design for a strategy object, we thought we would implement it as a part of PSpecies, so that every object might have its own strategy for resolving conflict. For our first release, however, we need to simplify the design. As a first approximation, we decided to create the PResolver directly in PSimulator, allowing all species to share a common strategy:

The PResolver class is an abstract base class, which is implemented by various concrete classes. Each of these classes implements a particular set of policies, such as, "if fish1 is stronger than fish2, fish1 eats fish2."

class PResolver : public RCBody
{
   TSimulatorPtr pOwner;
public:
   PResolver( TSimulatorPtr Owner );
   virtual ~PResolver();
   virtual void Resolve ( TAnimalListIter first, TAnimalListIter last ) = 0;
};

Note that PResolver derives from RCBody. RCBody is a utility class that implements smart pointers. RCBody will be discussed later in this chapter. The thing to notice about this class is that there is only one significant method, Resolve(), which is a pure virtual function. Resolve() takes two iterators, each of which iterates over a collection of PAnimals. The collection classes and their iterators will be discussed later, in the description of the utility classes and the STL.

For this example, I've implemented two resolver classes: PResolver1 and PResolver2. PResolver1 is a very simple strategy — only when two fish are in the same place must their conflict be resolved. PResolver1 does this by saying that if they are of the same species they mate, otherwise the first one eats the second. Here is an excerpt from its Resolve() method:

      if ( CurrentVal.first == hfirst->first ) 
      {
         PAnimal &First  = *CurrentVal.second;
         PAnimal &Second = *hfirst->second;
         if ( First.SameSpecies(Second) ) 
         {
            First.Mate(Second);
         } 
         else 
         {
            First.Eat(Second);
         };
         hfirst++;
      };

The more sophisticated strategy from PResolver2 takes into account their relative strength:

      if ( CurrentVal.first == hfirst->first ) 
      {
         PAnimal &First  = *CurrentVal.second;
         PAnimal &Second = *hfirst->second;
         if ( First.SameSpecies (Second) ) 
         {
            First.Mate(Second);
         } 
         else 
         {
            int TotalStrength = First.Strength() + 
                              Second.Strength();
            if ( ( (rand() % TotalStrength ) + 1) < First.Strength() )
            {
               First.Eat(Second);
            } 
            else 
            {
               Second.Eat(First);
            };
         };
         hfirst++;
      };

In the first release version of Phish, it is this more sophisticated strategy that is chosen every time. This is a feature that could be made an option for the user to chose at the set up stage of the game.

Once again, if two fish are in the same location and if they are of the same species, then they mate. On the other hand, if they are not of the same species this time, then their strengths are compared to decide which fish eats which. TotalStrength is set to the combined strength of the two fish. A random number is generated and set to the range of 0 to TotalStrength (by use of the modulus operator). If this new value is less than the strength of the first fish, it eats the second, otherwise the second fish eats the first.

Let's look at an example to make this a little clearer. Assume that the first fish has a strength of 60 and the second has a strength of 40. Their combined value is 100. A random number is generated and computed modulus 100, and then 1 is added. This will generate a number between 1 and 100. This is then compared with the value for the first fish (60). Any value between 1 and 60 will cause the first fish to eat the second, while any value between 61 and 100 will cause the second fish to eat the first. Thus, the odds of the first fish winning the battle are 60:40.

In a commercial program, the algorithm can be made more sophisticated. More important, you can implement the resolver as a DLL (or COM object), which can be "dropped in" at runtime. This will allow users to write their own, more sophisticated PResolver classes.

Other Species Values

Returning to the property sheet, we find a number of other values of interest:

The Life Span of a species dictates that after a certain amount of time (so many rounds) the individual animals will die. Again, the particular life span of an individual fish is influenced, but not absolutely determined, by the life span of the species. You could think of this value as a life expectancy.

The Food Capacity dictates how often the fish will be hungry and the Food Value indicates how much nutrition an individual fish will supply to any other fish which eats it. Speed dictates how quickly the individual fish will move over time, and the Attention Span indicates how likely it is that the fish will change its direction of travel.

Attention span works much like strength, in that the value indicates a probability not a deterministic number of turns. Thus, when it is time to move, if the attention span is 95, there is a 95% chance the animal will move in the same direction it moved in the previous turn. Speed works in the same way, a speed of 95 indicates there is a 95% likelihood the fish will move in a given round.

void PAnimal::Move () {
   dAge++;
   if ( dAge > pSpecies->LifeSpan() ) 
   {
      dLast = dCurrent;   
      Die();
      return;
   }
   if ( -- dStarveTime < 1 ) 
   {
      dLast = dCurrent;   
      Die();
      return;
   }
   if ( ( ( rand() % 100 ) + 1 ) > pSpecies->Speed() ) 
   {
      return;
   }
   dLast = dCurrent;
   if ( ( ( rand() % 100 ) + 1 ) >= pSpecies->AttentionSpan() ) 
   {
      dCurrent.RandomDir();
   }
   dCurrent.Move();
}

When a fish is told to move it first looks to see if it has died of old age. If not, it looks to see if it has died of starvation. These are implemented as separate if statements. This allows us, at a later time, to write debugging code which logs the cause of death. Once we have determined that the fish is alive, the question is whether it should move in this turn, and if so, then how fast. A random number between 1 and 100 is generated and compared to the speed (which is always a value between 1 and 100). Thus if the speed is 70, 70% of the time the random number will be smaller and the fish will move.

You might choose to implement speed as a set of discrete values (rather than a range of 1 - 100). You might use a drop down list box with choices such as: slothful, slow, moderate, quick, speedy. Each would be assigned a value (for example, 5, 20, 40, 60, 80). The internal resolution need not change, but the UI might be more intuitive.

Finally, another random number between 1 and 100 is generated to decide if the direction will change. If so, a random direction is chosen, otherwise the fish will move in the same direction it has been moving. When a fish hits the wall of the tank it bounces off in the opposite direction; that is, the angle of incidence is equal to the angle of reflection.

dCurrent is a member variable of PAnimal which holds a PLocation object. The PLocation object encapsulates all information about the physical location of a fish and its current direction (expressed as a primary compass direction of North, Northeast, East, Southeast and so forth). The code for the Plocation class is shown below:

class PLocation {
private:
   union 
   {
      struct 
      {
         uint16   dXPos;
         uint16   dYPos;
      };
      uint32      dPos;
   };
   uint16         dDir;
   const static string Names[];
public:
   typedef pair<uint16, uint16> TPoint;
   void Move();   
   const string &DirName() const;
   bool Correct ( const PLocation &, const PLocation &);
   void SetDirName ( const string &Name );
   TPoint CtrPixels () const;
   TPoint NorthWestPixels() const;
   TPoint SouthEastPixels() const;
   void   FromPixels( const TPoint &Extent );
   void   FromPixels( int x, int y );   

   static int Sides();
   int DirCount() const { return Sides(); };

//inlines
   PLocation ( int XPos = -1, int YPos = -1)
      : dXPos(XPos), dYPos(YPos)
   {
      RandomDir();
   };  
   void RandomDir ()
   {
      dDir = rand()%Sides ();
   };   
   int      Width() const         {return 11;};
   int      Height() const         {return 11;};
   uint16  Dir   ()   const         {return dDir;};
   uint16   XPos()   const         {return dXPos;};
   uint16   YPos()   const         {return dYPos;};
   uint32   Pos   ()   const         {return dPos;};
   void   XPos(uint16 XPos)      {dXPos = XPos;};
   void   YPos(uint16 YPos)      {dYPos = YPos;};
   void   Dir   (uint16 Dir   )      {dDir=Dir;};
   bool Equals      ( const PLocation &rhs ) const
   {
      return (dDir==rhs.dDir) && (dPos==rhs.dPos);
   };
   PLocation &IncX () 
   {
      ++dXPos;
      return *this;
   };
   PLocation &IncY () 
   {
      ++dYPos;
      return *this;
   };
   PLocation &DecX () 
   {
      --dXPos;
      return *this;
   };
   PLocation &DecY () 
   {
      --dYPos;
      return *this;
   };
   static PLocation Random ( const PLocation &MaxLoc )
   {
      return PLocation (rand()%MaxLoc.dXPos, rand()%MaxLoc.dYPos);
   }; 
};

The final values in the species management property sheet are Initial Population and Color. The initial population dictates how many of each species will be created when you push Start Simulation button. The color indicates how the Phish will be rendered on the screen, and can be adjusted by clicking on the color swatch, which brings up the standard color dialog.

Kill Your Own Children

I once worked for Larry Weiss, who was at the time Executive Vice President of Citibank and Director of the Development Division. Larry told me that all developers fall in love with their own ideas and innovations, and that the really great developers are not afraid to "kill their own children" — to set aside their pet ideas or innovations when they no longer make sense.

When we first considered Phish, we wanted to create a way to allow the user to set the starting number of fish for each species. The device we created was a combination combo box and slider:

The idea was that you would drop down the combo box to reveal the various species and as you chose them the slider would reflect how many fish would be created for each. As you changed your selection the slider would show the current selection for each species.

To support this, we created the CSynchroComboBox class:

class CSynchroComboBox : public CComboBox
{
   auto_ptr<TIntSource> pSource;
// Construction
public:
   CSynchroComboBox( TIntSource *Source );

// Attributes
public:
   void SyncValue();
// Operations
public:

// Overrides
   // ClassWizard generated virtual function overrides
   //{{AFX_VIRTUAL(CSynchroComboBox)
   //}}AFX_VIRTUAL

// Implementation
public:
   virtual ~CSynchroComboBox();   
   // Generated message map functions
protected:
   //{{AFX_MSG(CSynchroComboBox)
   afx_msg void OnCloseup();
   afx_msg void OnDblclk();
   afx_msg void OnDropdown();
   afx_msg void OnEditchange();
   afx_msg void OnEditupdate();
   afx_msg void OnErrspace();
   afx_msg void OnKillfocus();
   afx_msg void OnSelchange();
   afx_msg void OnSelendcancel();
   afx_msg void OnSelendok();
   afx_msg void OnSetfocus();
   afx_msg HBRUSH CtlColor(CDC* pDC, UINT nCtlColor);
   afx_msg void ParentNotify(UINT message, LPARAM lParam);
   //}}AFX_MSG

   DECLARE_MESSAGE_MAP()
};

This class is derived from CComboBox and keeps a smart pointer to an integer source. An integer source is any control which evaluates to an integer. TIntSource is an abstract base type providing an interface for any control which could set and return an integer value. A CSlider is such an integer source.

class TIntSource
{
public:
   TIntSource(){};
   virtual ~TIntSource(){};
   virtual int value() const = 0;
   virtual void value (int) = 0;
};

To make the code as flexible as possible, we designed a derived and parameterized type, TIntSourceAdapter:

template<class T>
class TIntSourceAdaptor : public TIntSource 
{
public:
   typedef int (T::*TGetFn)()const;
   typedef void (T::*TSetFn)(int);
private:
   T &         dSource;
   TGetFn      dGetter;
   TSetFn      dSetter;

public:
   TIntSourceAdaptor( T &Source, TGetFn Getter, TSetFn Setter ) 
      : dSource(Source), dGetter(Getter), dSetter(Setter)
   {
   }
   int value() const 
   {
      return (dSource.*dGetter)();
   }
   void value ( int rhs ) 
   {
      (dSource.*dSetter)(rhs);
   }
};

This class implements TIntSource, and adds a reference to the source object, as well as two pointers to member functions. The first pointer-to-member function is TGetFn, which returns int, takes no parameters and is constant — the typical signature for an accessor get function. The second, TSetFn returns void but takes an int as its parameter — the typical set function.

The constructor takes as parameters the source object and the two pointer-to-member functions and initializes the member variables. Calling the function value(), which overrides the pure virtual function in the base class, calls the appropriate accessor function.

With this design, we can make the CSlider control a TIntSourceAdaptor, and thus a TIntSource, which can thereby be attached to the CSynchroComboBox. Each entry in the CSynchroComboBox is a string to display, together with the value extracted from the CSlider. As one changes, the other is updated.

This design was elegant, object-oriented and quite appealing. Except that the customer, who dictated the UI (that would be me) changed his mind. It just didn't work psychologically; the customer couldn't make the connection between the combo box and the slider. It was tempting to keep it just because it was "cool", but we opted to toss the entire design and move the control of the number of fish of each species off the main interface and back to the property sheets.

© 1998 by Wrox Press. All rights reserved.