Ch-Ch-Ch-Changes

[ LiB ]

Now that you know about the major components of the game that have been added for this chapter, you can go on to the existing components that have been changed. There are many changes that need to be made to enable the new components to work, such as making sure players are properly added to and removed from rooms, adding the new room commands, and many other changes as well.

Entering and Leaving the Realm

Whenever a player is within the Game handler/state, that player's ID must be within a room's m_players list. The easiest way to make sure this happens is to add the player to the room list whenever he enters the state, and remove the player from his room list whenever he exits the state.

Luckily, we have two functions Game::Enter and Game::Leave that are called whenever a player enters or leaves the state!

Entering

So the first order of business is making sure that a player is always inside of a room whenever he is in the game state, by adding this line to the Game::Enter function. (I'm going to refrain from posting the whole function, since you've seen it earlier, and I need to conserve space for more interesting things.)

 p.CurrentRoom()->AddPlayer( p.ID() ); 

Within the function, p is a Player reference.

To make the game seem a little easier to play, the following is added to this function:

 if( p.Newbie() )   GotoTrain();  else               p.SendString( PrintRoom( p.CurrentRoom() ) );  

The first line of this code segment should be familiar to youit is from Chapter 8. It hasn't changed. The addition is bolded (second line). If a player is not a newbie, the room's current description is printed out to the player. (It isn't printed for newbies. The new player is immediately going into the training state, so the player doesn't need a room description yet.) The bolded code uses the Game::PrintRoom function, which I will show you in a little bit. And the bolded code gives a player a sense of his bearings whenever he enters the game.

Leaving

On the other hand, whenever a player is leaving the game state, he needs to be removed from the room that he is in. This line of code is added to the Game::Leave function:

 p.CurrentRoom()->RemovePlayer( p.ID() ); 

No other changes need to be made to this function.

New Room- Related Commands

There are a bunch of new room-related commands that have been added to the Game::Handler function:

look prints a description of the current room

north moves a player north

east moves a player east

south moves a player south

west moves a player west

get picks up an item from the ground

drop drops an item from a player's inventory

First, let's look at the look command:

 if( firstword == "look"  firstword == "l" ) {     p.SendString( PrintRoom( p.CurrentRoom() ) );     return; } 

If the user types in either look or just l , the contents of the room are sent to his connection using the PrintRoom function that I mentioned before.

All four movement commands are structurally similar, so I'm only going to show you the first one, for moving north:

 if( firstword == "north"  firstword == "n" ) {     Move( NORTH );     return; } 

If the user types north or just n , the Game::Move function is called to move a player in a specified direction. As you can probably imagine, south calls Move with a parameter of SOUTH , and so on.

Finally, there are the two item commands:

 if( firstword == "get"  firstword == "take" ) {     GetItem( RemoveWord( p_data, 0 ) );     return; } if( firstword == "drop" ) {     DropItem( RemoveWord( p_data, 0 ) );     return; } 

Both commands strip off the first word ( get , take or drop ), and then call either the GetItem or DropItem functions with the name of the item, so get sword calls GetItem("sword"). I discuss these functions in the next section.

New Room Functions

In the preceding sections, you saw four functions that you haven't seen before: Game::PrintRoom , Game::Move , Game::GetItem , and Game::DropItem . Along with those, there is one more function dealing with rooms Game::SendRoom which sends a string of text to everyone within a given room.

Printing Room Descriptions

The PrintRoom function is a long and boring one; it just goes through the contents of a room, formatting and coloring things as it goes along. I'm going to show you bits and pieces of it, but on the whole, it's not an important function.

 string Game::PrintRoom( room p_room ) {     string desc = "\r\n" + bold + white + p_room->Name() + "\r\n";     string temp;     int count;     desc += bold + magenta + p_room->Description() + "\r\n"; 

The function starts out by printing (to a string) the name of the room in white, and then the description of the room in magenta. The next part of the code prints out the exits from the room:

 desc += bold + green + "exits: ";     for( int d = 0; d < NUMDIRECTIONS; d++ ) {         if( p_room->Adjacent( d ) != 0 )             desc += DIRECTIONSTRINGS[d] + "  ";     }     desc += "\r\n"; 

It checks to see if there is an exit in each direction, and if so, it prints out the name of the direction of the exit. The next part is item and money printing:

 temp = bold + yellow + "You see: ";     count = 0;     if( p_room->Money() > 0 ) {         count++;         temp += "$" + tostring( p_room->Money() ) + ", ";  // print money     }     std::list<item>::iterator itemitr = p_room->Items().begin();     while( itemitr != p_room->Items().end() ) {         count++;         temp += (*itemitr)->Name() + ", ";                 // print item         ++itemitr;     } 

The function checks if there is money on the ground, and if so, it's printed. The code block after that loops through every item in the room, and prints each description.

The next block of code chops off the last two characters of the item string (if there are any items in the room at all) and appends the item string to desc :

 if( count > 0 ) {         temp.erase( temp.size() - 2, 2 );         desc += temp + "\r\n";     } 

Why are the last two characters chopped off? Well, if you had some items in a room, it would end up looking like this: You see: Sword, Knife, Axe, , with an extra comma and a space at the end. This is just a minor annoyance, but it is an annoyance nonetheless, so this code fixes it.

I'm not going to show you the rest of the function, since the next part of the function is essentially the same as the item printing, except that it prints the people in the room instead.

Here's an example of what a room listing looks like:

Town Square You are in the town square. This is the central meeting place for the realm. exits: NORTH EAST SOUTH WEST You see: Rusty Knife, Heavy Longsword, Jeweled Dagger People: mithrandir, Washu, Tyraziel

In the next chapter, I revisit this function to add enemy printing as well.

Moving Between Rooms

There is a helper function as part of the Game handler that moves a player in one direction.

There are a number of things it needs to accomplish to successfully move a player in a direction, as you'll see from the function:

 void Game::Move( int p_direction ) {     Player& p = *m_player;     room next = p.CurrentRoom()->Adjacent( p_direction );     room previous = p.CurrentRoom(); 

The first thing the helper function does is get a reference to the player ( p ) and then get the room ID of the player's current room ( previous ) and the room to which the player is moving ( next ).

 if( next == 0 ) {         SendRoom( red + p.Name() + " bumps into the wall to the " +                   DIRECTIONSTRINGS[p_direction] + "!!!",                   p.CurrentRoom() );         return;     } 

If there is no exit in the direction the player wants to go, everyone is told that the player bumped into the wall (much to his embarrassment), and the function ends.

At this point, you know that there is a room in the direction the player wants to go, so you need to remove the player from that room, and tell everyone he left:

 previous->RemovePlayer( p.ID() );     SendRoom( green + p.Name() + " leaves to the " +               DIRECTIONSTRINGS[p_direction] + ".",               previous );     SendRoom( green + p.Name() + " enters from the " +               DIRECTIONSTRINGS[OppositeDirection(p_direction)] + ".",               next );     p.SendString( green + "You walk " + DIRECTIONSTRINGS[p_direction] + "." );     p.CurrentRoom() = next;     next->AddPlayer( p.ID() ); 

The previous code segment also tells everyone in the new room that the player has entered it, and tells the player that he moved to a different room. The player's current room is reset to next , and the player is added to the room's player list.

 p.SendString( PrintRoom( next ) ); } 

The last bit of code shows the room's description to the player, so he can navigate easily without constantly looking around. This approach is okay for small MUDs like this, but for a larger MUD, you might consider having two different room descriptions, one long and one short; when players move around, the short description displays, but when they explicitly type look , the long description displays.

Getting Items

Picking up items from the floor is a complicated task; there are so many different things that you need to check.

The first part of the function checks to see if the player wants to pick up any money that's on the ground. This is done by checking if the player typed in the character '$' as the first letter of the parameter string:

 void Game::GetItem( string p_item ) {     Player& p = *m_player;         // get player reference     if( p_item[0] == '$' ) {       // check if player wants money         p_item.erase( 0, 1 );      // chop off the '$'         money m = BasicLib::totype<money>( p_item );  // get amount desired 

If the player did indeed type '$' , the '$' is erased from the string, and the amount of money the player wants to pick up is converted into m . Next, the program checks that there's enough money on the ground to satisfy the greedy player:

 if( m > p.CurrentRoom()->Money() ) {  // make sure enough money exists             p.SendString( red + bold + "There isn't that much here!" );         }         else {             p.Money() += m;                   // add money to player             p.CurrentRoom()->Money() -= m;    // subtract money from floor             SendRoom( cyan + bold + p.Name() + " picks up $" +                       tostring( m ) + ".", p.CurrentRoom() );         }         return;     } 

If there isn't enough money, then obviously the player can't pick it up, and the player is told so. On the other hand, if there is enough money, the amount of money the player wants is added to his character, subtracted from the room, and the entire room is told about how much money the player took. Then the function returns.

NOTE

Because the game thinks that if you type in get $<whatever> you want money, it never thinks that an item starts with the character $ . There fore, you should never name items starting with the letter $ , because the game will never detect them when you want to get or drop them. Why anyone would want to, I don't know.

NOTE

This method of keeping money separate from regular items is really just a hack. In real life, it's more logical to represent a pile of coins as a bunch of coins . Unfortunately there is no easy way to solve this kind of dual treatment of items and money, simply because the game would find it extremely difficult to treat a few thousand coins in a single room as separate objects. I explore a different way to tackle this problem in the BetterMUD.

On the other hand, if the first character isn't $ , the game assumes that the player is trying to get an item:

 item i = p.CurrentRoom()->FindItem( p_item );     if( i == 0 ) {               // check if item exists in room         p.SendString( red + bold + "You don't see that here!" );         return;     }     if( !p.PickUpItem( i ) ) {   // try to pick up item         p.SendString( red + bold + "You can't carry that much!" );         return;     }     p.CurrentRoom()->RemoveItem( i );  // remove item from room     SendRoom( cyan + bold + p.Name() + " picks up " + i->Name() + ".",               p.CurrentRoom() ); } 

First the room is searched to see if it contains an item named p_item (a string), and the ID of that item is stored in i . If the search turns up dry, the room doesn't have an item matching the name. The player is told so, and the function returns.

Then, the player's PickUpItem function is called, attempting to pick up the item. If it returns false , that means that the player didn't have enough room for the item, and thus the item isn't picked up.

Finally, if the function passes both tests, the item is removed from the room, and the room is told that the player picked up the item.

Dropping Items

Dropping an item is essentially the opposite process from getting an item; if you're dropping money, the game code makes sure you have enough money, and then drops it. If you're dropping an item, the game code makes sure you have the item, and then drops it into the room using the Room::AddItem function (which makes sure that there are at most 32 items in a room at any given time). Because the code is similar, I don't want to waste precious space in this chapter. I hope you'll forgive me for moving on.

Sending Text to a Room

To send text to a room, you need to be able to find every player in that room and send text to their connections. This is where the m_players list inside every Room comes in handy. It would strain your MUD to have a function that searches every player in the database to see if he is in a specific room, and then sends text to him; so instead, I use the list of people already in a room:

 void Game::SendRoom( string p_text, room p_room ) {     std::for_each( p_room->Players().begin(),                    p_room->Players().end(),                    playersend( p_text ) ); } 

Here, I utilize the std::for_each algorithm and the playersend functor (from Chapter 8) to send p_text to every player in room p_room . This works because the playersend functor was designed to work on both containers of Room s, and containers of Room* s (or datatypes that work just like Room* s, such as the room database pointer class). See Chapter 8 for an in-depth discussion on this, if you haven't already.

Commands Related to New Store

In addition to the movement commands, there are three new store commands:

list lists everything a store sells and buys

buy <item> buys an item from the store

sell <item> sells an item to the store

These commands are added into the Game::Handle function. Here's the part that handles store listing:

 if( firstword == "list" ) {         if( p.CurrentRoom()->Type() != STORE ) {             p.SendString( red + bold + "You're not in a store!" );             return;         }         p.SendString( StoreList( p.CurrentRoom()->Data() ) );         return;     } 

This code makes sure you're in a store first, and then it calls the Game::StoreList function (which you haven't seen yet), passing in the ID of the store (contained within the current rooms' Data() variable), to get a list of the items in the store.

Buying items is similar:

 if( firstword == "buy" ) {         if( p.CurrentRoom()->Type() != STORE ) {             p.SendString( red + bold + "You're not in a store!" );             return;         }         Buy( RemoveWord( p_data, 0 ) );         return;     } 

This time the code calls the Game::Buy command with the name of the item you wish to purchase (by removing the "buy" from the string). The "sell" command is equivalent, except that it calls Game::Sell instead.

The StoreList Function

I don't know about you, but I think that code that prints information is boring. Unfortunately, MUDs are packed with them, and the Game::StoreList function is one of them.

Here's the function declaration:

 string Game::StoreList( entityid p_store ); 

Essentially the function takes the ID of a store, and prints out the items that are available to buy or sell. The function is a large loop inside, utilizing the Store 's iterator functions, and I don't want to take up space showing you the code. Instead, I'll show you just a sample listing of a store:

 -------------------------------------------------------------  Welcome to Bob's Weapon Shop! -------------------------------------------------------------  Item                            Price -------------------------------------------------------------  Rusty Knife                     5  Knife                           15  Dagger                          40  Shortsword                      50 ------------------------------------------------------------- Ta-da! 

Buying Items

It requires roughly the same amount of checking to buy items as to pick them up. The function to buy items should check that the store has the item the player wants, check if the player has enough money to pay for the item, and check if the player has enough room to carry it. Here is the code:

 void Game::Buy( const string& p_item ) {     Player& p = *m_player;          // get player     Store& s = StoreDatabase::get( p.CurrentRoom()->Data() );  // get store     item i = s.find( p_item );      // find if store has item     if( i == 0 ) {                  // store doesn't have item         p.SendString( red + bold + "Sorry, we don't have that item!" );         return;     }     if( p.Money() < i->Price() ) { // see if player has enough money         p.SendString( red + bold + "Sorry, but you can't afford that!" );         return;     }     if( !p.PickUpItem( i ) ) {     // see if player can carry it         p.SendString( red + bold + "Sorry, but you can't carry that much!" );         return;     }     p.Money() -= i->Price();       // subtract money     SendRoom( cyan + bold + p.Name() + " buys a " + i->Name(),               p.CurrentRoom() ); } 

That wasn't so difficult, was it?

Selling Items

Selling an item in a store is a little different from buying an item. The selling process first finds the item the player wants to sell, and then finds out if the store wants to buy it, and finally sells it.

 void Game::Sell( const string& p_item ) {     Player& p = *m_player;                   // get player     Store& s = StoreDatabase::get( p.CurrentRoom()->Data() );  // get store     int index = p.GetItemIndex( p_item );    // get inventory index of item     if( index == -1 ) {                      // make sure player has item         p.SendString( red + bold + "Sorry, you don't have that!" );         return;     }     item i = p.GetItem( index );             // get the ID of the item     if( !s.has( i ) ) {                      // see if store sells it         p.SendString( red + bold + "Sorry, we don't want that item!" );         return;     }     p.DropItem( index );                     // remove item from inventory     p.Money() += i->Price();                 // add price     SendRoom( cyan + bold + p.Name() + " sells a " + i->Name(),               p.CurrentRoom() ); } 

As you can see from this code and the buying code you examined previously, stores in SimpleMUD are, well, simple. In a more realistic game, stores would have limits to the amount of items they have to be bought and sold. I'll get to this kind of stuff when going into BetterMUD.

New Training Room Commands

Finally, the last two commands added to the game in this chapter are

train trains a player to the next level when he has enough experience

editstats edits a player's stats and allocates statpoints to his attributes

The good news is that these commands are pretty easy to implement. Here's the "train" command (added to Game::Handle ):

 if( firstword == "train" ) {     if( p.CurrentRoom()->Type() != TRAININGROOM ) {         p.SendString( red + bold + "You cannot train here!" );         return;     }     if( p.Train() ) {         p.SendString( green + bold + "You are now level " +                       tostring( p.Level() ) );     }     else {         p.SendString( red + bold +                       "You don't have enough experience to train!" );     }     return; } 

Obviously, a player can only train inside of a training room, so the function makes sure the player can do that first. If the player is inside a training room, the Player::Train function is called, which returns a Boolean determining if the player has successfully trained. In either case, the player is told if he went to the next level. If the player trained successfully, the Player::Train function automatically adds his new bonuses to his character.

The editstats command simply takes you into the training state:

 if( firstword == "editstats" ) {     if( p.CurrentRoom()->Type() != TRAININGROOM ) {         p.SendString( red + bold + "You cannot edit your stats here!" );         return;     }     GotoTrain();     return; } 

Once again, the command checks to see if the player is in a training room, and if the player is not, the command prints out an error to the player, and quits out of the function. Otherwise, the player is approved to move, and Game::GotoTrain is called, putting the player into the training state.

Database Reloading

In Chapter 8, I showed you how to reload the item database and the player database within the game. In this chapter, I'm going to continue, and show you how to add a room template and store reloading from within the game, so that you don't have to stop and re-run the game every time you change some datafiles.

This is the code that is added to the Game::Handle function (inside the block of code that handles the reload command):

 else if( db == "rooms" ) {     RoomDatabase::LoadTemplates();     p.SendString( bold + cyan + "Room Template Database Reloaded!" ); } else if( db == "stores" ) {     StoreDatabase::Load();     p.SendString( bold + cyan + "Store Database Reloaded!" ); } 

Therefore, the two new reloading commands are

reload rooms reloads all the room template data from disk

reload stores reloads all the store data

I don't allow you to reload volatile room data, simply because editing that data is a very dangerous thing to do while the MUD is running. Imagine this scenario:

You take the /maps/default.data that the MUD saved previously (you'll see this in the next chapter), and start editing it. Meanwhile, someone comes into that room, and accidentally drops a valuable item. Then, you finish editing the file, and reload it. The poor player finds out that his precious item has been lost forever, because you just overwrote it by reloading the database.

NOTE

The " disappearing item" problem is only in SimpleMUD, and that's due to its limited design. You see, rooms keep track of items within them on disk, and that's not such a great idea in the grand scheme of things. In reality, items should keep track of where they are, but that kind of system was a bit complex for SimpleMUD. I use this kind of a system in the BetterMUD, however.

Miscellaneous Changes

This section describes a few minor changes.

Removing the Old Room Definition

For example, this line, created in Chapter 8 inside of /SimpleMUD/Player.h needs to be removed:

 typedef entityid room;  // REMOVE THIS LATER 

I just went ahead and deleted it.

Default Commands

In Chapter 8, SimpleMUD would interpret every unrecognized command a player typed in as a global chat. In that effect, just saying something like hello would cause your player to chat hello to everyone in the game. Now that the game supports room-based talking, however, the old line

 SendGame( bold + p.Name() + " chats: " + p_data ); 

is changed to this instead

 SendRoom( p.Name() + " says: " + p_data, p.CurrentRoom() ); 

Now, whenever you say something in a room, it is sent only to whomever is in that room, and the only way to send global messages is to use the chat or : commands now.

Using and Removing Items

Whenever a player uses or removes an item (this functionality was introduced in Chapter 8), he's obviously performing an action that everyone in the room can see, so why not make the game more realistic, and add code to the Game::UseItem and Game::RemoveItem functions to send messages to the room the player is in?

For example, here's the large switch statement from Game::UseItem with the newly added parts in bold:

 switch( itm.Type() ) { case WEAPON:     p.UseWeapon( i );  SendRoom( green + bold + p.Name() + " arms a " + itm.Name(),   p.CurrentRoom() );  return true; case ARMOR:     p.UseArmor( i );  SendRoom( green + bold + p.Name() + " puts on a " + itm.Name(),   p.CurrentRoom() );  return true; case HEALING:     p.AddBonuses( itm.ID() );     p.AddHitpoints( BasicLib::RandomInt( itm.Min(), itm.Max() ) );     p.DropItem( i );  SendRoom( green + bold + p.Name() + " uses a " + itm.Name(),   p.CurrentRoom() );  return true; } 

It's little things like this that make the game more fun. People can watch a player as he arms his "Magic Sword Of Super Powers" and be in awe, or laugh at his "Rusty Knife."

Changes to the Main Module

Finally, now that you've got everything else programmed, you can make the final two changes. Demo09-01.cpp contains the same code as Demo08-01.cpp (from Chapter 8), with two minor additions (in bold):

 int main() {     try {     ItemDatabase::Load();     PlayerDatabase::Load();  RoomDatabase::LoadTemplates();   RoomDatabase::LoadData();   StoreDatabase::Load();  <SNIP>     PlayerDatabase::Save();  RoomDatabase::SaveData();  } 

Basically, all you need to do is load the room and store databases when the program begins, and save the room database back out when the program ends. That's it!

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net