PHP - Interfict - how it works

From LXF Wiki

(Difference between revisions)
Revision as of 13:58, 18 Nov 2007
Guy (Talk | contribs)

← Go to previous diff
Current revision
M-Saunders (Talk | contribs)
Reverted edit of LadomLicac, changed back to last version by Guy

Current revision

Table of contents

Practical PHP

(Original version written by Paul Hudson for LXF issue 58.)

In the previous tutorial we looked at the schemata for more than a dozen tables. This time we show you how it actually works...

php58-snapshot2.png-thumb.png (
The W3C Validator service is a hard taskmaster that will point out every little error in your XHTML.
php58-snapshot3.png-thumb.png (
To get started we'll be writing our SQL by hand - the GUI tools will come later.

When planning anything complex, I always like to start off with schemata: blueprints for how it will work. If it's a finely planned holiday, I use booking times, web site printouts, books on the areas being visited, and lists of things I want to see and do. If it's assembling a computer, I like to take things out of boxes, play around with them individually, and plan out how I intend to put things together.

If it's a database, however, I create all the SQL I think I'll need for the entire system, mentally linking together how the tables will work, /before/ I write a line of code to work with those tables. The upsides to this are that you forsee many problems right at the beginning and solve them, you can form contracts at early stages (that is, "every W rows in table X must have Y rows in table Z") so that when it comes to the coding stage it's second nature, and also the final application usually ends up being better than one where coding is started.

In the same way that premature optimisation is the root of all evil, premature cutting of features is the root of all lame software - by planning the database first and laying out precisely how I want the system to work in an ideal environment, I then essentially throw down a coding gauntlet to myself, saying, "go on, then - make /that/".

Last issue what we had was these table schemata: we've defined most of the tables that will be there in the final build of the program, as well as defined most of the rows in those tables, giving us quite a complete system. I didn't have much time to explain how things work, though, which is what I'll be starting with this issue. After that we'll also be putting together the web site part of the project: the little touches of HTML that top and tail the PHP output to make it look nice.

How stuff works

The complex diagram printed last issue really /does/ make sense after a little while. Remember, though, that there are no connection lines to the "games" table's ID field - when I added that in the diagram became too messy to read. This is because the "games" table is pretty much the core of the system: rows in most of the other tables tie themselves to an individual game either directly (by having a game ID as a foreign key in their table) or indirectly (by linking to a row in another table that in turn links to game ID). An example of direct linking is the "races" table: the Game field there links to ID in the "games" table. For indirect linking, note how events in the "events" table don't have a game ID, but they do have a field Trigger that links to the "triggers" table, which in turn links to the "games" table through its Game field.

This is because all the rooms, all the items, all the races, all the classes, all the triggers, etc, are active for just one game, which means they all need to know which game they should be attached to. At the querying end, it will usually be beneficial to filter by the Game ID before any other fields so that the vast majority of fields can be ignored from the off.

The "gamestates" table might be a source of confusion as its role is largely for advancing the storyline. By default, new characters to a game will have a gamestate of -1, so the first thing the game will do is search for the gamestate with the name "START" in the "gamestates" table for the current game, and set the character's gamestate value to the ID of that row. As the player has now started, the game should also create any items that have AutoStart set to 1 in the items table. After that, items should be created either on explicit command from the games master (GM) or when their CreateWhenState value matches the gamestate. Gamestates can be switched by the GM as and when they please. When the gamestate changes, the text in the Info field of gamestates will be printed out to advance the storyline (eg: "You killed the evil skeleton king, and as his magic wears off the wall behind him disappears and you can see treasure behind there"). This text is only printed out once - when the gamestate switch takes place - and the items are created at the same time.

All the items for a game are in the "items" table, with the correct Game value set to link it to the current adventure. The "itemslive" table is used to store items that are actually available to a player in the game: each player in each adventure has their own set of entries in the "itemslive" table. If an item is in the "itemslive" table, it means that either the player has picked it up or they can see it in the world. Items not in the "itemslive" table are either inaccessible at this point in the game or they have been used already. If, for example, at the end of an adventure the player has to pick up a special amulet, that item will probably be put into the "itemslive" table at the beginning as it is accessible to the player. However, if they player needs to get that amulet by killing a certain creature or doing some other action - something that means the amulet should not be available otherwise - it should not be placed into the itemslive table until the player has completed those actions. Note that there's no link in "itemslive" to the current game. Instead, it links to the character that owns the item (even if they have yet to pick it up), which in turn links to the current game. LocationCurrent points to the ID of the room in the "rooms" table where the item can be found, or -1 if the player has already picked it up. The "itemslive" table is probably the hardest to understand without actually seeing the code - we'll go over it thoroughly later.

The "variables" table is something that might fox some of you, so I'll go over it quickly. In order to allow GMs more control over what players can and cannot do in a given situation, they can toggle variables about characters based upon their actions in the game. These variables don't have values, they are either "there" or "not there". For example, consider a situation where a player needs to get permission from the mayor of a town to enter the king's palace. The GM would have some scripting in the palace entry area saying, "if permission has been received, let them enter, if not, say so." This is accomplished with variables: when the permission is received, the GM would set a variable like "GOT_PALACE_PERMISSION", which is what is checked against at the palace entry.

Last issue I explained that links are what join rooms together, and also that links between rooms are one-way: a link from room A to room B doesn't mean that the player can get from room B to room A unless a second link for that purpose has been created. These links will be shown at the bottom of each page for the current room so that players can see where they can go from the current location. The ConditionType and ConditionVar fields are there to make GM's lives easier, as when used properly they can restrict display of a link until the condition has been fulfilled. Entering the palace from the previous example would require a link from outside the palace into the palace itself, but that link should only be shown when the "GOT_PALACE_PERMISSION variable has been set. The scripting side is there to allow GMs to print out different text based upon the current condition of the game (eg: "A guard at the palace says, 'You can't come in here - you don't have permission!"), but as links need to be automatically generated by the system the GMs don't have the ability to put in links themselves. In this case, the GM would use the scripting to make characters fill in the story, and use the condition variables in "links" to make the movement options match the storyline.

Finally, let me explain briefly how the users situation is designed to work. There are two tables available for ownership of characters: the "users" table and the "guests" table. The latter is easiest, so we'll start there. People really do need to sign up in order to create games because otherwise there's no way to verify them as owners of an adventure (and thus allow them to edit it), there's no real reason that people must sign up in order to /play/ adventures. Sure, if people want to have their characters around when they come back next time they need to create an account, but by allowing guests to jump in and play at any time we're greatly expanding the numbers of users that will play. So, the guests table stores temporary account IDs along with the time that record was created. The latter field is there so we can pick out all guests that were created a long time ago and clean them up: one character in a game is likely to have many rows in "itemslive", some in "variables", etc, so it's best to clear them up as regularly as possible. The "users" table on the other hand is there to store permanent members of the site who have full access.


One of the curious things about working with PHP is that before learning most people consider themselves at the peak of the HTML "programming" community: they've mastered CSS, frames, and tables, have learnt through trial and error what works and what doesn't, and some have even figured out some degree of XML. After a few months of PHP, though, this all changes: HTML almost becomes irrelevant, as the vast majority of work (and interesting programming) is going on at the server side with PHP forming the core of the site. Whenever I go back to HTML and have to make a site work, it disgusts me - all those hours wasted trying to make a site look the same in Netscape Navigator and Internet Explorer!

Still, in order to get the main part of our site (the PHP code) working we first need to put the design shell together that makes it look halfway-decent for our users. This isn't a HTML tutorial, though, so I'll leave you to read through my HTML and pick out whatever interests you - you're welcome to ask questions. There are some things that need mention, though. First up, the site is split into header.php, footer.php, and stdlib.php. The first file loads up everything up to the page-specific words, the second file loads up everything after the page-specific words, and the last file contains all the functions, variables, and settings needed to get a page to work. A site as complex as this will have many custom functions, many of which will only be minor variations on functionality already in PHP - as long as it saves time and makes the code less error-prone, it's welcome. As a result of this segregation of content, a given page on the site will include all three of those files, but only contain content specific to itself in its file. Later on these files will specify settings before including stdlib.php so that specific action can be taken - perhaps they need to be logged in to view the page, for example.

At the top of header.php is the DTD for our pages:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "">

That sets us up for full XHTML compatibility. You should really be using this as second nature, but all too often you'll find you slip back into HTML by accident - I do it all the time. Adding this at the top means we can later run the entire site through a compatibility checker to ensure we are compatible with the XHTML spec. The W3C engine is great for this, and even points out where incompatibilities lie - more on this in a much later issue.

In the HTML you'll notice a large sidebar containing lots of hardcoded values for a given character. This is just in there as a placeholder: I'm still toying with the idea of having a graphical readout like this, and will probably silently drop it sometime soon. This is all static right now, so don't worry about it.

At the bottom of header.php is a line of code that's particularly helpful:

<?php if ($IF_WARNINGS != "") { echo "<B>$IF_WARNINGS</B>"; } ?>

The $IF_WARNINGS variable is declared as being blank in stdlib.php, and is printed out after the name of the page but before any page body text from within header.php. It's also in bold, which should make it stand out compared to the rest of the page quite nicely. The idea of this variable is to allow our PHP pages to add important information here and have it rendered neatly on the page. For example, when a player picks up a sword, we could just have code like this:

echo "You picked up the sword!";

But who's to say where that will be printed out? At that point the header might not have been printed out, or perhaps all the body copy has been printed out as the information gets placed right at the bottom where it can barely be noticed. The $IF_WARNINGS variable is always printed out at the most noticeable position on the page, in bold, which means it's where our pages should print out important information such as picking up items, changing of game state, etc.

The rest of the site is layout fuzz that you're welcome to re-write yourself. As page-specific content is kept wholly separate from the layout around it, it's very easy to redo the design to something that suits your taste more - I've never been entirely fond of white text on a black background, but I didn't want to spend too much time focusing on the HTML! If you have better ideas for the design - and don't mind contributing it to the public domain - do let us know.

The first adventure

There are two parts to any system like this: that which creates the data, and that which uses it. For Interfict (the name of our project - see the box "It lives!"), the creation of data is where GMs create items, design races and classes, lay out triggers and events, etc, and the using of data is where players actually go through adventures. The actual point of the site is to let people play interactive fiction games: the ability to create those games is a necessary by-product of that point. What this means is that the majority of our work should go into the playing part of it - creating the adventure is really just a wrapper around adding rows directly to a database.

So, to get started, what we're going to do is hand-write all the SQL necessary to create an exceedingly simple adventure: a few rooms to walk around, a couple of items, and one or two simple triggers. This will then be inserted directly into the database as if it had been created using a fancy PHP interface. Our first programming task will be to allow players to walk inside that world - we get the playing part done first. Once that's complete, we'll put together the interface for actually creating and editing worlds so that others can create adventures without explicit knowledge of SQL. With that done we can go back and add any touch-ups to the play engine to make it more advanced and generally finish it off, but at this point there'll be enough for everyone to take part in the site and enjoy it.

We'll be looking at the implementation of the very basic play engine next month, and adding more advanced features the month after - things like scripting will wait until then. After that, we'll move onto the creation system, which is largely just repetition: forms for adding races, forms for adding classes, etc, etc. Don't worry, I'll be providing code for all these pages on either your coverdisc or on the LXF web site: no one likes doing the dull bits!

Working on your own

Although this tutorial is written so that you should be able to follow it from start to finish without problem, it's my hope that you'll already have plans for your own fork of my codebase. Although there are several neat features I will be including, there are many we simply won't have space for - occasionally I'll mention some and give hints as to how to proceed, but you're on your own after that. Sometimes I might even mention something, say I've implemented it in the code, but not bothered to document it. That's for features I considered important enough to include, but not important enough to use up some of my limited words on! If you have patches that you're willing to release under a suitable licence, you're welcome to submit them for inclusion - and perhaps even discussion! - here. If it's longer than 20 lines, please do provide some documentation: I don't relish the prospect of deciphering someone else's crusty PHP code.

It lives!

As this is to be a fully working project, I've taken the liberty of registering the domain name where I'll upload the latest working version of the site on a regular basis. I encourage you to go to the site and check it out, as it will give you the best idea where the tutorial is headed over the coming issues. If you get the time, try playing an adventure there - or, better, creating your own - to see how it all works. If you have any suggestions for improvements, they are of course always welcome!

php58-snapshot1.png-thumb.png (
The design of the web site is pretty basic, but the code is the main focus.