PHP - Interfict planning
From LXF Wiki
(Original version written by Paul Hudson for LXF issue 57.)
There've been many installments of this tutorial series, but only now do we really start getting practical...
Congratulations! If you've read all the PHP tutorials to date, you've learnt enough about PHP to be able to get out there and feel confident programming pretty much anything. It's now time for us to create a full PHP project - something you can follow through from start to finish, that pushes your skills right to the edge and hopefully beyond, and also gives us something that works really well that we can all be proud of.
Following on from the success of our Trout Wars tutorial in the magazine, the project I've chosen to produce is an interactive fiction site. If you ever read the old "Choose your own Adventure" (CYOA) books when you were younger, you'll remember you would read a page in the book and at the bottom if would say something like, "if you want to turn left, turn to page 50" and "if you want to fight the ogre, turn to page 59". On the other hand there are fully fledged role-playing games (RPGs) such as Dungeons & Dragons.
Interactive fiction falls into a half-way house between the two. That is, there is a fixed storyline like the CYOA books, but like full RPGs you have a character that can pick up objects and use them, talk to characters, and more. What we're going to construct is a world creation toolkit that allows writers to create an adventure for others to play. This is potentially a huge project depending on how much control you want to give the adventure writers, so be prepared for a lot of work! Fortunately, the end result is an exciting system that people can have a lot of fun with.
What we're going to produce is big. If you thought Burger King's Extra Double Jumbo Whopper was big, think again: what we're going to produce is /big/. When producing a game system, giving people the flexibility to make what they want out of it is key: while we could make a system in a day, it would almost certainly not be flexible enough to let adventure writers truly bring their games alive.
In order that you fully understand the design of the system, the entirety of this tutorial is going to be devoted to the layout and interaction of the tables. It is absolutely key that you understand how the tables interact, otherwise you'll run into problems later if you get confused. The focus is always going to be PHP and SQL, so the design presented here may change if I end up spending inordinate thousands of words trying to explain how something works!
To begin with we're going to have seventeen basic tables. Stop crying and relax: they are all useful, important tables that are quite easily understood. Linux Format's Art Editor, Julian, has done a sterling job of putting our table design into one big picture ///// LOCATION /////, but I still need to explain what each table and field does. Note that each table has an ID field for its primary key to uniquely identify it in that table and to act as a foreign key in other tables.
Before we get started, you need to know two things: the people who create the adventures will be referred to as games masters (GMs), and each character playing an adventure has a set of skills and attributes that defines how good they are at various things. Each character will have a rating for: Strength, Intellect, Energy, and Agility.
This stores information about users who have created accounts on the server. You might want to add more fields, such as name, age, demographics, etc.
- Username: This stores the username of each user. Sounds obvious, I know, but it's important because although users will log in using their email address and password, their name will be shown on the site using the Username.
- Password: A standard password field. This won't be encrypted or hashed, although you're welcome to write this functionality simply by calling sha1().
- EmailAddress: This won't be verified at all, but you're welcome to add this. For example, adding an extra field called Confirm that was a VARCHAR(40) would allow you to run sha1() on their email address + a call to time() and have that as their confirmation number. This then gets emailed to their email address, and the user needs to enter that into the site in order to activate their account. So, "$confirm = sha1($email + time())" should be fine.
- DateJoined: This is the date their account was created, and isn't required. It's useful for tracking how popular the site is, though.
This stores a little information about guests to the site. As each person playing the game needs to have an account, they must either be logged-in users (with an entry in the users table) or guests. If they are guests, they will have an entry created in here.
- CreationTime: As Guests won't be able to log in to their account to continue playing where they left off, storing the time their guest account was created means we can easily clean up the system of all guest games that were started more than X days ago. As each game could well be quite a bit of information, it's important to clean up like this.
These are actual adventures created by users for others to play. People must be logged in to create an adventure.
- Name: The name of the adventure, eg "Tux's Revenge" or "Return of the Ping". This is just to give players a way to refer to the game.
- Creator: ID number of the user who created the game.
- Difficulty: How hard the game is. This won't actually affect how the game plays - it's there for the creator to let potential players know what they are letting themselves in for. 1 is Easy, 3 is Medium, and 5 is hard. There are gaps in there in case we later wish to add something between Easy and Medium or Medium and Hard.
- DateCreated: Just for information purposes really
- Info: A text description of the adventure to whet players' appetites and encourage them to try it out.
- MinCharLevel: Each character has a "level" that defines how powerful they are. By default, characters start out at level 1, but by setting a higher number in here GMs can force new characters to at least be (for example) level 10, and thus more powerful. This in turn means they can make the adventure harder.
- PointsAdjust: Each character has a set number of character points to spend adjusting their basic attributes, and GMs can either increase those points to make characters better or decrease them to make them worse.
- RandomEncounters: Percentage chance of a random encounter each time a player changes location. Random encounters are unscripted attacks on players that the engine will automatically create to keep players on their toes. Setting this to 0 disables random encounters.
- RandomEncounterStrength: This defines how strong enemies should be when they are met in a random encounter. 1 is Easy (should be very easy), 3 is Medium (should hurt the player a little), 5 is Hard (player should survive, but only just), 7 is Hardest (equal in strength to the player), 9 is Impossible (greater in strength than the player)
- Status: The status of the adventure. 1 is "Just created", 2 is "Playable", 3 is "Down temporarily", 4 is "Down long term". Only status 2 allows others to play the game. 3 and 4 are there so that the GMs can make changes to the game without causing problems for players.
- Length: Like Difficulty this is just an indication from the GM to the players. 1 will be shortest, 9 will be longest.
This is where characters are stored. Note that characters are separate from users, as each user (each physical member of the site) can have many characters (fictional players in games). Also note that each character can only be in one game at one time.
- Owner: ID number of the user that owns this character.
- Name: Name of the character, eg "Bork the Indestructible"
- Age: Age of the character. For information only.
- Sex: Sex of the character. For information only. 1 is Male, 2 is Female.
- Race: Race of the character. This can affect the minimum and maximum ages of the character and may also affect their attributes.
- Class: Character class of this character, eg Fighter or Mage. This affects their attributes as well as their potential for spell casting and healing.
- Level: Experience level of this character. This determines how powerful the character is.
- ExperiencePoints: Each time a charater kills a creature or performs an action they might receive some experience points. Experience points determine the level of the player.
- Info: Free text for the player to describe their character. For information only.
- Game: ID of the game this character is currently in.
- Location: ID of the room this character is currently in. More on this soon.
- GameState: ID of the game state this character is currently in. More on this soon, although -1 means "This player has yet to start playing their game."
- Strength, Intellect, Energy, Agility: The four basic attributes that determine how good a character is at doing various things. Increased strength means the character is more dangerous when fighting, increased intellect means they can learn more, increased energy means they can take more damage, and increased agility means they are more likely to hit people in combat and dodge enemy blows.
- Vitality, VitalityMax: VitalityMax is the maximum amount of health this character can have, whereas Vitality is the current level of energy this character has.
- Mana, ManaMax: ManaMax is the maximum amount of mana (spellcasting power) this character can have, whereas Mana is the actual amount of spellcasting power the character currently has.
- Hunger, HungerMax: HungerMax is the maximum amount of time this character can survive before starving, whereas Hunger is how close the player is to starvation currently.
GMs can define the races they want players to be able to choose from for their characters in a given adventure. For example, if I created an adventure based around Lord of the Rings I might use the races Hobbit, Elf, Dwarf, Human, Ent, etc.
- Game: ID of the game these races are for. Note that a -1 in this field means "this race is available in all games that don't specify their own races" - we'll be providing a stock selection of races that are there by default.
- Name: Name of the race
- Info: Free-text description of the race to explain some history and back-story to players.
- PointsAdjust: GMs can have this race grant a character point bonus (or penalty) to players.
- StrengthAdjust, IntellectAdjust, EnergyAdjust, AgilityAdjust, VitalityMaxAdjust, ManaMaxAdjust, HungerMaxAdjust: These allow the GM to provide bonuses or penalties to these character attributes for this class.
- MinAge, MaxAge: How young and old characters of this class can be.
These are essentially vocations or "jobs" that define what players do best. They can be as generic (eg "Fighter") or as specific (eg "Berserker", "Knight", "Stormtrooper") as GMs want.
- Game: ID number of the game this class belongs to. Like races, if this is set to -1 (which GMs will not be able to do) this class will be available in all games where no special classes have been defined.
- Name: The name of this class.
- Info: Free-text description of this class to make it more alive to players.
- StrengthMin, IntellectMin, EnergyMin, AgilityMin: Minimum attributes that characters must meet in order to be a part of this class. Average scores for the attributes are 10.
- SpellCasting: How strong characters of this class can be at spellcasting. 0 is nothing (they will never have any skill), 1 is "Very weak", 2 is "Poor", 3 is "Average", 4 is "Good", 5 is "Powerful", 6 is "Master mage", 7 is "Legendary magician".
- Healing: How strong characters of this class can be at healing. 0 is nothing, 1 is "Very weak", 2 is "Poor", 3 is "Average", 4 is "Good", 5 is "Powerful", 6 is "Master healer" and 7 is "Legendary healer".
- FightingAdjust: This allows GMs to directly affect how good characters of this class will be at fighting. So, while a Magician class might have a 4 in spellcasting it might also have a -3 in FightingAdjust, whereas a Fighter class might have a 0 in spellcasting and healing but a +10 in FightingAdjust.
- ExperienceLevels: This defines how easy it should be for people of this class to go up levels. As levels are decided by how many experience points a character has, this figure is applied as a percentage of a basic experience point table. That is, a setting of 0 means a character goes up levels at the standard rate, whereas a setting of 100 means the points required to go up levels are doubled, making progress much slower.
This defines "chapters" in an adventure. For example, in the game Monkey Island (the original) you had to complete "The Three Trials": find the idol in the Governor's mansion, find the treasure (although it's just a T-Shirt (apologies if you've never played the game and I've just spoiled the fun!)), and defeat the sword master. As you complete each one, the game changes state - you get a cut-scene where more information is presented about things happening elsewhere, people say different things to you etc. That's what our game states are: when the game state is changed, players are given some text describing what's changed, and the game can react differently.
- Game: The game ID that this game state belongs to.
- Name: The name of the game state (players don't see this)
- Info: Free-text description shown to the player when they enter this game state.
NOTE: Each game needs at least one game state called START. This is activated when players first start the adventure.
This stores all the items that can be in an adventure. It does not define where the items are in a live game currently being played by a character - this is covered later.
- Game: The ID number of the game this item belongs to.
- Name: Full name of the item, eg "Sword of Doom"
- ShortDesc: Vague description of this item, eg "Rusty sword". This is used when the item is on the ground.
- Type: What this item actually is. 1 is Weapon, 2 is Shield, 3 is Helment, 4 is Armour, 5 is Ring, 6 is Scroll, 7 is Potion, 8 is Food, and 50 is Other.
- DamageMin, DamageMax: How much damage this item can do when used. If a weapon, this dictates how much it hurts the enemy. If a potion, this dictates how much it heals the wearer. If a ring this dictates how much it hurts enemies. If food, a negative figure dictates how much hunger it removes.
- Info: Free-text description of the object. This is used when the item is examined by the player.
- UseWrong: Text to be printed when the object is used in a place where it has no use, eg "You can't use your scroll here".
- UseRight: Text to be printed when the item is used in the place where it is supposed to be used, eg "You throw the rock through the window, leaving a large hole you can climb through."
- UseRightTrigger: What to do when the object is used correctly. Triggers are discussed soon.
- ExperienceUseRight: How many experience points should be awarded when the item is used properly.
- DeleteOnUse: Set to 1 if this item be removed from the game when used (eg a potion gets drunk and thus cannot be used again)
- DropOnUse: Set to 1 if this item should be dropped by the player when used. For example, "using" a rock can mean throwing it, so it gets dropped but not destroyed - the player can fetch it again.
- LocationFound: Where the item is found.
- LocationUsed: Where the item is used. Set to -1 if this item can be used anywhere.
- SellWorth: How much the thing is worth when sold. 0 means it cannot be sold.
- FightingAdjust: When the item is fought with, this affects how well the player fights. For example, a magical sword might make its bearer a better fighting.
- DefenceAdjust: If this is greater than 0, this item adds protection to the character that makes them safer against enemies.
- MinLevel: This allows GMs to stop powerful items being used by lower-level characters. They can still hold the item, they just can't use it.
- CreateWhenState: This allows GMs to set up items to be created when a certain game state is set. For example, once the character returns home after killing the ghost from the previous example, they might find a note stuck to their front door by a dagger that wasn't there before.
- AutoStart: If set to 1, this item is automatically given to players who play this adventure as soon as they start. This is helpful to give players basic kit such as a bit of armour, a weapon, or, in more imaginative cases, a medallion left to them by the father they never knew that, if the players can /just/ complete some adventure, will reveal all sorts of information....... You get the point.
This stores where items in each game actually are. For a given game, there is only one entry in the "items" table for it, but as there can be many players playing the game at the same time we need this table also.
- Item: ID in the items table for this item.
- LocationCurrent: Where this item is (locations are explained shortly), or -1 if the character has picked it up.
- CharacterOwner: ID in the character table for who owns this item. Note that "owns" is not the same as "picked up". For example, the point of the adventure might be to collect the Amulet of Yendor, which is located thousands of light years away in a distant solar system and will take years of adventuring to find. When the player starts playing, the amulet is copied from the "items" table into "itemslive", given their character ID as the CharacterOwner so that it cannot be seen or touched by other players, and placed in the distant location. This can then be picked up once the player finally reaches their destination.
A "room" is the term we'll be using for a location in the adventure. This can be as general as "The forest" or as specific as "The dark corner behind the cooker in the kitchen" - it's down to the GMs how they plan out their rooms. Each room can link to other rooms so that players can move to and fro freely, although note that a link is not bidirectional. That is, if room A links to room B, room B does not necessarily link back to room A. This is helpful because the link might be players going down a trap door that closes, or, in more advanced adventures, GMs might create a clone of room A that's subtly different from the original (we'll call the clone room C) so that players can move from room A to room B, see that they can go back to room A (it's actually room C, but it looks like room A), and thus fall into a trap if they go back. Note that room that is created first is where players start the adventure.
- Game: ID of the game this room belongs to.
- Name: Name of the room. This appears at the top of the page so that players know where they are.
- Info: Free-text description of the room that describes the surroundings. This won't include information about what items or people are in this room, as this is handled automatically. Note that GMs will be able to use a simple programming language in this text to make the room more dynamic. More on that soon.
- SafeToRest: Set to 1 if characters can sleep here. Sometimes denying people the ability to sleep (if the place is too noisy or too dangerous) can be a helpful plot device as sleeping regains health.
- SafeFromEncounters: If the RandomEncounters game per cent is greater than 0, random encounters can happen. However, if this value is set to 1, characters moving into this room cannot be hit by a random encounter because the room is considered "safe".
- SafeToAttack: If this is set to 1, the player may not attack any of the AI players in this room.
- CallTrigger: Trigger to call each time this room is entered. Triggers are discussed soon.
- CallTriggerOnce: Trigger to call the very first time this room is entered and never again.
This table defines the connections between rooms.
- FromRoom, ToRoom: IDs in the "rooms" table that defines the link. For example, FromRoom of 3 and ToRoom of 10 means that a player can move from room 3 to room 10. As noted already, this does not mean that a player can move from room 10 to room 3 - a separate row needs to be entered for that.
- ConditionType, ConditionVar: This link can be shown only when a certain condition is satisfied (more on this soon)
This table defines the various conditions for use in the "links" table. For example, a condition might be "Is a variable set?". If a GM uses this condition for the ConditionType in a link, they can specify the name of the variable to check for the ConditionVar. Variables are discussed soon! (yes, this might be confusing by now!) The conditiontypes are pre-defined and cannot be altered by GMs. This condition-based room linking is helpful, because it allows GMs to set up situations where (for example) players can only enter a room if they have already done something else, such as pick up an item.
- Name: name of the condition to check. This is never seen by anyone but the GM, and only then really as a mnemonic for the ID number because the ID number is faster for the database to work with.
This stands for Non-Player Characters and this is a pretty distinct subsection of the engine that will require work once we get close enough to it! For now, the table is quite simple.
- Game: ID of the game this NPC belongs to.
- Name: Name of the NPC, eg Jessica Atreides.
- Info: Description of the NPC to be printed out when they are examined by the player.
- Sex: 1 is Male, 2 is Female.
- Race: ID in the "races" table for the race of this NPC.
- Class: ID in the "classes" table for the class of this NPC.
- Level: How powerful this NPC is.
- LocationStart: Where this NPC is
At last, this is what really makes the game interesting. An is something that affects the state of the game or the character. For example, it might set a variable, subtract health from the character, or teleport them to another room. Events cannot be called directly - they are bundled into groups called triggers, and it is these triggers that are called.
- Trigger: ID of the trigger in the "triggers" table that this event belongs to.
- EventType: ID in the "eventtypes" table that defines what this event does.
- Variable: Some events need an extra data field to make their change. This is discussed shortly.
This stores the various possible event types in a convenient place. My initial implementation of the code has various values hardcoded as opposed to in a special table like these event types. This will be something I'll be solving en route!
- Name: Name of the event type. This is just for reference during adventure creation. Event types will include SET VARIABLE to set a variable, UNSET VARIABLE to do the opposite, TOGGLE VARIABLE to set a variable if it is unset and unset it if it is set, ADD HEALTH and SUBTRACT HEALTH to heal or hurt the player, and TELEPORT PLAYER to move the character to a new room in the game. Each of these require a Variable to be entered in the "events" table in order to make sense. For example, SET VARIABLE needs the name of the variable to be set (variables don't have values, they are either set or unset), ADD HEALTH needs to know how much health to add, etc.
This table stores the actual triggers that can be called from the game. These are just collections of events and have no inherent magic.
- Game: The ID of the game this trigger belongs to.
- Name: Name of the trigger for internal use by the GM.
This table stores variables that have been set by the game. For example, if a guard outside of a palace has been put to sleep by a sleeping draught, a variable such as PALACE_GUARD_ASLEEP should be put in here so that the game knows what to show. Note that variables starting with _ are not allowed, as this table is also used to handle the CallTriggerOnce field of the "rooms" table - when a room has a trigger that should only be called once, a variable is entered into this table called __IF_ROOM_ENTERED_<ROOM ID> so that it won't be called again.
- CharacterID: ID in the "characters" table for the character that owns this variable.
- Variable: Name of variable that is set.
Where's the code?
As I said at the beginning, the whole purpose of this instalment has been to detail the various tables that our game engine will use. Please pay close attention to the diagram of how the tables interact - cut it out and keep it if it will help you - because that explains 90% of how the system works. Some things, particularly NPCs, fighting, magic, etc, are yet to be planned out. Fortunately these don't really affect how the actual adventure creation works, so we're fine for quite some time yet!
As this is such a big project, there are almost certainly various areas that the system can be improved. You're welcome to write in with suggestions if there's something you'd like to see implemented, or if you'd just like to know how it /could/ be implemented in your own code. Also, please keep in mind that this is a very complex system we're going to produce: don't worry if you get confused here and there; that's why we have forums on our web site!