Standard Template Library (STL)

Phish is awash in collections, all of which are implemented using the STL. The two collections which are used most extensively in Phish are the vector and map. We'll also look at the pair which holds two related values. Vectors are sequence collections and are described in the persistence chapter (Chapter 6). Maps are associative collections: a map associates a key with a value. These matched keys and values are implemented as pairs. You can manipulate pairs either within maps or simply as objects in their own right.

The PSimulator class uses all three of these collections, along with iterators for the vector and the map. To understand how they are used, we start by exploring the simulator. Look at the diagram below:

The PPhishTank is essentially the application itself. It has a display and a simulator. The display is responsible for the user interface, and the simulator is responsible for the state of the game.

The PSimulator is where all the action is, and this class is worth examining in some detail. It keeps a collection of every PSpecies object known to the game. Each PSpecies object will be held in an RCHandle:

typedef RCHandle<PSpecies>               TSpeciesPtr;

This declaration allows us to use the type TSpeciesPtr to refer to any object that acts as a pointer to a PSpecies object. Thus, you can declare a TSpeciesPtr, or you can declare a collection of them.

typedef map<string,TSpeciesPtr>          TSpeciesMap;

This line creates the type TSpeciesMap as a map of a string to a TSpeciesPtr. That is, it maps a character string to an RCHandle on a PSpecies object.

PSimulator keeps an instance of such a collection as a private member variable:

   mutable TSpeciesMap cSpecies;

cSpecies is the collection of species known to a given simulator. (Remember, we are using a notation whereby all collections are prefixed with the letter c.) At the beginning of the game this map will be initialized from the default species file, and the user can add to the map by creating new species, deleting species or editing existing species.

Loading The Defaults

When the game begins the CPhishDlg's initDialog() method is called by the application framework. Here's what it does in pseudo code:

BOOL CPhishDlg::OnInitDialog()
{
      CDialog::OnInitDialog(); // chain up to the base class

      //... ( set up the menus )

      //... ( set the icons )

      //... ( size the tank )

      ResetSpecies(); // load the default species
      ResetControls(); // enable and disable controls appropriately
}

The method ResetSpecies() is responsible for reading Default.phi— the default file which loads the species:

void CPhishDlg::ResetSpecies() 
{
   PPhishTank &Tank = dPhishWnd.Tank();
      //...
      try
      {
         Tank.LoadFile("Default.phi");
      } 
      //...
   TStringList Species =    Tank.SpeciesNames();
   TStringListIter first =    Species.begin();
   TStringListIter last  =    Species.end();
}

Tank's LoadFile() method opens the file into an ifstream and calls the PSimulator's ReadFrom() method passing in that stream. The ReadFrom() method is a bit complex, and the code for it is shown below:

const static string ClassName = typeid(PSimulator).name();

TSimulatorPtr PSimulator::ReadFrom ( istream &stream ) 
{
   TSimulatorPtr pSimulator(new PSimulator());
   int Count;
   string CName;
   char ch;
   stream >> ch;
   getline(stream,CName,',');
   if ( CName != ClassName ) 
   {
      throw exception ( "Corrupt input stream" );
   };
   stream >> Count >> ch;
   for (;Count;--Count) 
   {
      PSpecies::ReadFrom(stream)->SetSimulator(pSimulator);
      stream >> ch;
   };
   return pSimulator;
};

First, ClassName is declared and initialized with the string returned by calling the name() method on the object returned by the typeid operator when passed PSimulator. The net effect of this is that ClassName contains the string PSimulator.

Next, a new PSimulator is created and its address is returned as a pointer, stored in pSimulator. One character is read from the file and stored in the char variable, ch, and then a line is read up to the first comma, and stored in CName. CName is compared with ClassName to ensure we are reading what we expect. Here's the file that is being read:

(class PSimulator,3,(class PSpecies,Halibut,100,150,200,95,200,20,50,16711680),(class PSpecies,Minnow,96,98,97,95,100,99,94,8388863),(class PSpecies,Shark,100,150,200,95,500,200,50,4194368))

The first character, read into ch, is the parentheses. Thus, the string, CName, contains "class PSimulator". This will match the return value stored in ClassName, so the exception won't be thrown and we can proceed to create the various species objects.

The value, 3, is streamed into the variable Count, and the for loop will iterate three times to pick up each of the species. For each one, the PSpecies method, ReadFrom(), is called. This will return a PSpecies object, on which we call SetSimulator(), passing in the just created PSimulator. The net effect is that the species are created and inserted into the collection in the new PSimulator, which is then returned.

The PSpecies's ReadFrom() method is quite similar:

TSpeciesPtr PSpecies::ReadFrom ( istream &stream ) 
{
   string   CName;
   string   Name;
   int      Speed;
   int      FoodCap;
   int      FoodValue;
   int      AttentionSpan;
   int      LifeSpan;
   int      Strength;
   int      Population;
   RGBColor Color;
   char ch;
   stream >> ch;
   getline(stream,CName,',');
   getline(stream,Name,',');   
   stream >> Speed;
   ch = 0;
   stream >> ch;
   stream >> FoodCap;
   ch = 0;
   stream >> ch;
   stream >> FoodValue;
   ch = 0;
   stream >> ch;
   stream >> AttentionSpan;
   ch = 0;
   stream >> ch;
   stream >> LifeSpan;
   ch = 0;
   stream >> ch;
   stream >> Strength;
   ch = 0;
   stream >> ch;
   stream >> Population;
   ch = 0;
   stream >> ch;
   stream >> Color;
   stream >> ch;
   return TSpeciesPtr ( new PSpecies (Name, Speed, FoodCap, FoodValue,
                                        AttentionSpan, LifeSpan, Strength, 
                                        Population, Color) );
};

Each value is read out of the file and stashed in a local variable. These are then used as parameters to the constructor of a species, which is returned. Once the species is returned, the method SetSimulator() of the PSpecies object is called:

void PSpecies::SetSimulator ( RCHandle<PSimulator> &Simulator ) 
{
   assert(!pSimulator);
   pSimulator = Simulator;
   Simulator->AddSpecies(RCHandle<PSpecies>(this));
};

The species is set to point to the PSimulator object, to which it will now add itself. It then calls the PSimulator's AddSpecies() method:

void PSimulator::AddSpecies ( const TSpeciesPtr &Species ) 
{
   cSpecies.insert(TSpeciesMapVal(Species->Name(), Species));
};

cSpecies is, you may remember, the member variable of PSimulator which holds a map of PSpecies objects. We call insert() on this collection, passing in the name of the species and the PSpecies object itself. TSpeciesMapVal is a typedef of TSpeciesMap::value_type. A value_type is a typedef supplied by the standard library:

typedef pair<const Key, T> value_type

The net effect is that the species is read from the file, created and inserted into the collection in the PSimulator.

Starting the Simulation

When the user clicks the Start Simulation button on the dialog, it calls the CPhishWnd's StartSimulation() method. This in turn calls the PPhishTank's StartSimulation() method — an in-line method that resizes the display and then calls the PSimulator's StartSimulation() method. This is shown below:

void PSimulator::StartSimulation ()
{
   bool Invalid = false;
   int Count = 0;
   dRound = 0;

   TSpeciesMapIter first,last;
   first = cSpecies.begin();
   last  = cSpecies.end();

   int count = 0;

   for ( ;first != last; ++first ) 
   {
         count += first->second->Population();
         first->second->CreateInitial( dBottomRight );
   }
   dStatus.Reset(count);
   dRunning = true;
};

Two iterators, first and last, are created against the collection of PSpecies. The TSpeciesMapIter type is a typedef:

typedef RCHandle<PSpecies>            TSpeciesPtr;
typedef map<string,TSpeciesPtr>      TSpeciesMap;
typedef TSpeciesMap::iterator         TSpeciesMapIter;

Thus, the iterator is against a map, which matches strings to PSpecies smart pointers. cSpecies is the collection of PSpecies objects held by the PSimulator itself. The iterators first and last are initialized with the first and last objects in the collection.

The for loop iterates through the collection. Here's how this works. The iterator is against a map, and thus returns a pair. The pair consists of first (the string name of the species) and second (the PSpecies object itself). We ask first (the first iterator) for second (the PSpecies object itself) and call Population() and then CreateInitial() on that object.

The actual work of populating the tank is delegated to the species itself, in the call to CreateInitial() (passing in the location of the bottom-right pixel of the display):

void PSpecies::CreateInitial ( const PLocation &MaxLoc ) 
{
   for ( int i = 0; i < dPopulation; i++ ) 
   {
      pSimulator->AddPhish(TAnimalPtr
            (new PAnimal (this, PLocation::Random( MaxLoc ))));
   }

}

This creates a new PAnimal object, passing in the PSpecies (as the this pointer) and the results of calling Random(), a static method on PLocation. The Random() method looks like this:

static PLocation Random ( const PLocation &MaxLoc )
{
    return PLocation ( rand() % MaxLoc.dXPos, rand() % MaxLoc.dYPos);
};

This returns a location whose x and y values are within the bounds set by MaxLoc (maximum location). The net result is that a new animal is created at a random location, and the pointer is passed to the PSimulator's AddPhish() method, which adds the newly created PAnimal object to the PSimulator's cSpecies collection.

Playing the Game

For the first version, the entire game is played in "rounds", and each round is set off by a timer tick. The CPhishWnd class establishes a timer, and responds to that timer in its OnTimer() method:

void CPhishWnd::OnTimer(UINT nIDEvent) 
{
   bool Done = !dTank.Tick();
   Invalidate();
   CStatic::OnTimer(nIDEvent);
   if ( Done ) 
   {
      KillTimer(0);
   };
}

This simple logic calls the PPhishTank's Tick() method. If it gets back false, it will kill the timer. The Tick() method delegates responsibility to the PSimulator, passing along a pointer to the display. The PSimulator's Tick() method manages each round of the game, as illustrated by code below. I will show small excerpts of the code from Tick(), one at a time, and then discuss each section as I go through. The first excerpt is shown below:

bool PSimulator::Tick( PADisplay &Display ) 
{
   //...
   dRound++;
   static RCHandle<PResolver> pResolver = new PResolver2(this);

The method begins by creating a PResolver1 (strategy) object to resolve conflicts when two fish are in the same location.

   TAnimalListIter first, last;
   first = cAnimals.begin();
   last  = cAnimals.end();
   if ( first == last ) 
   {
      EndSimulation();
      return false;
   }
   while ( first != last ) 
   {
      (*first)->Move();
      (*first)->CorrectPos(dTopLeft,dBottomRight);
      first++;
   }

An iterator is created for the collection of animals, cAnimals, held by PSimulator. If the first and last animals in the iterator are identical, then all of the animals must have died and the simulation is over. Assuming that is not true, then each fish is told to move. After it moves, its position is corrected to ensure that it stays in the tank (bouncing off the glass walls of the Phish tank).

   if ( cAnimals.size() > 1 ) 
   {
      pResolver->Resolve(cAnimals.begin(), cAnimals.end());
   }

Once all the fish have moved, the entire set is resolved. Note that in this version the resolver is hard-wired; as mentioned earlier, but this will be more flexible in future versions. A simple first improvement would be to set the resolver based on a value in a dialog box or in a .ini file. Eventually the system should register a resolver object using the standard COM interface protocols.

   first = cAnimals.begin();
   last  = cAnimals.end();
   while ( first != last ) 
   {
      PAnimal & Current = **first;   // Cache this for later
      bool Moved = Current.Moved();   // This too
      bool Dead  = Current.Dead();   // This too.
      if ( Moved || Dead ) 
      {
         Current.Erase(Display);
         if ( Dead ) 
         {
            // Pre-Decrement last, since it points to the 
            // imaginary animal past the end of the array, 
            // or to the first dead animal

            --last;
            TAnimalPtr::Swap( *first, *last ); 

            // *first is now a differant animal, try again.  

            continue;            
         }

         else 

         {
            first++;
                  // We now know we don't have to revisit *first
                  // And we know it has moved.
            Current.Draw(Display);
         }
      } 
      else 
      {
         first++;
      }
   }

This code is fairly straightforward. The outer while loop iterates through the collection of PAnimals. If the PAnimal object has moved or is dead, we must erase it so that it can be redrawn. If it is dead, we swap it with the last PAnimal, thus bubbling all the dead animals to the end of the array where they can be chopped off all at one fell swoop.

   first = last;   // This is the first dead animal
   last  = cAnimals.end();
   if ( first != last ) {
      cAnimals.erase(first,last);
   };
   return true;
};

Each of the animals has now been moved and resolved, and all the corpses have been removed. It is time to redraw. The call to Invalidate() in the CPhishWnd causes the PSimulator to redraw. The PSimulator tells all the PAnimals to draw themselves, and they in turn delegate the responsibility to the PSpecies, passing in their current location and the display device in which to draw themselves.

void PSpecies::Draw(PADisplay & Display, const PLocation &Location)
{
   TImagePtr &Image = dImageMap[Location.DirName()];
   if ( !Image ) {
      Image = Display.CreateBitmap(Location.DirName(),dColor);
   };
   Display.Draw(Location,Image);
}

The PSpecies keeps a map of the images of the species pointing in various directions; the location's DirName returns the direction which is used as an offset into that map. The location and Image are then given to the CDisplay.

void CDisplay::Draw(const PLocation &Loc, const TImagePtr &Icon)
{
   TPoint Point = Loc.NorthWestPixels();
   Draw(Point.first, Point.second, Icon);
}

This method converts the PLocation object into a pair TPoint object, and then calls its own Draw() method, passing in the starting and ending points and the Icon to draw. This Draw() method gets the bitmap and calls BitBlt() to render the image in the correct location.

© 1998 by Wrox Press. All rights reserved.