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.
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
.
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.
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 PAnimal
s. 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 PAnimal
s 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.