Uncategorized

Offline for a while

Building Browsergames will be going offline for a few days - I’m going to be transferring the site(and all related files) to a new server.

Thursday, July 24th, 2008 Uncategorized No Comments

Building Browsergames: Reducing Repetition (Perl)

While working on your own browsergame project, one of the things that will slow you down the most is re-writing the exact same code. Unfortunately, we’ve been doing that with our stats code - as reader sepp helpfully pointed out.

In the programming world, there is a concept known as ‘DRY’ - which stands for “Don’t Repeat Yourself”. Applying DRY to your code means that you should never have to write code to do almost the same thing twice - you should be writing code flexible enough to do both. Using a DRY approach can drastically cut down our development time, in addition to improving our code’s maintainability(because there’s less of it to worry about). So today, we’re going to convert our stats code from being 3 different pieces of code to a single piece of DRY code.

So far, we have 3 helper modules that retrieve our stats for us - each from different tables and columns, but for the most part they’re the exact same code. Here’s a refresher on what our stats::getStat function looks like:

sub getStat {
	my ($statName,$userID) = @_;
	use config;
	my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
	createIfNotExists($statName,$userID);
	my $sth = $dbh->prepare("SELECT value FROM user_stats WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND user_id = ?");
	$sth->execute($statName,$statName,$userID);
	my $value;
	$sth->bind_columns(\$value);
	$sth->fetch;
	return $value;
}

While that would be fine if we were only going to use this code for retrieving user stats, it’s organically grown into 3 different places with the exact same code - which is bad. Let’s rewrite the code slightly, so that any of our stats code can call it:

sub getStatDRY {
	my ($tableName,$columnName,$statName,$userID) = @_;
	use config;
	my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
	createIfNotExistsDRY($tableName,$columnName,$statName,$userID);
	my $sth = $dbh->prepare("SELECT value FROM $tableName WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statName,$statName,$userID);
	my $value;
	$sth->bind_columns(\$value);
	$sth->fetch;
	return $value;
}

And with that done, we can now use that function to retrieve any of our stats - not just user stats. All we have to do is pass in the table and column name for the stat table, in addition to the information for which stat value we want to retrieve.

Doing it this way works because our stats tables are laid out in virtually the same way - they all have a name, and they’re virtually exactly the same, except for the name of the one column that tracks the ID number of whatever we are storing stats for. If our stats tables were very different from each other, this might not work quite as easily.

One thing that you need to notice and be aware of is the fact that neither the table name or the column name are escaped when we build our SQL query - which means that there’s potential for a SQL injection vulnerability there. You can get around this and keep your system safer by never using user input for the table or column names in your calls to this code.

We’ve seen how the change works for the getStat function - now let’s see how it goes for the other two:

sub setStatDRY {
	my ($tableName,$columnName,$statName,$userID,$statValue) = @_;
	use config;
	my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
	createIfNotExistsDRY($tableName,$columnName,$statName,$userID);
	my $sth = $dbh->prepare("UPDATE $tableName SET value = ? WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statValue,$statName,$statName,$userID);
}
 
sub createIfNotExistsDRY {
	my ($tableName,$columnName,$statName, $userID) = @_;	
	use config;
	my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
	my $sth = $dbh->prepare("SELECT count(value) FROM $tableName WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statName,$statName,$userID);
	my $count;
	$sth->bind_columns(\$count);
	$sth->fetch;
	if($count == 0) {
		# no entry for that stat/user combination - insert one with a value of 0
		$sth = $dbh->prepare("INSERT INTO $tableName(stat_id,$columnName,value) VALUES ((SELECT id FROM stats WHERE display_name = ? OR short_name = ?),?,?)");
		$sth->execute($statName,$statName,$userID,0);
	}	
}

As you can now see, there isn’t a big change being made for this - all we do is add two more arguments to our functions, and then modify the way we create our queries. But the gains we’re making in terms of development are huge.

You might be wondering why these functions now have the word ‘DRY’ at the end of their names - the reason for this is that we can now use these functions for one of the cooler aspects of DRY - being able to replace whole chunks of backend logic, without having to modify the primary codebase. If you save the current file as statsDRY.pm, you can now modify stats.pm to look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package stats;
use DBI;
 
use statsDRY;
 
use constant TABLE => 'user_stats';
use constant COLUMN => 'user_id';
 
sub getStat {
	my ($statName,$userID) = @_;
	return statsDRY::getStatDRY(TABLE,COLUMN,$statName,$userID);
}
 
sub setStat {
	my ($statName,$userID,$statValue) = @_;
	statsDRY::setStatDRY(TABLE,COLUMN,$statName,$userID,$statValue);
}
 
1;

And this is where you see the real gains of DRY - now, all of our stats logic is inside of statsDRY.pm, and if we ever need to modify it we’ll only need to modify that one file. We can change the way that all of our stats code works, from a single file - and those changes will seamlessly work accross item stats, user stats, monster stats, and any other stats we decide to implement in the future.

While our current codebase is relatively small, this isn’t a big deal - but what if you were making this change and had 20 different files calling stats::getStat()? You wouldn’t want to go through each and every one of them and modify the call - by moving our stats code out into its own module, we’ve made it so that you only have to edit a file or two, instead of having to edit every file that ever uses your stats code. That’s a real time-saver, and one of the great aspects of using the DRY approach to writing code.

There isn’t anything ‘new’ to this code, with the except of the two constants we set up - TABLE, and COLUMN. These two constants are what are passed to our DRY stats retrieval code, and creating them allows us to make another DRY change. Now, if you ever decide to modify where your user stats are stored, you only need to modify those two constants - and not go through every file that uses user_stats, changing it to the name of whatever table you switched to. While this will probably never happen, it’s still a good idea to have this in place - just in case.

Here’s what the finished statsDRY.pm looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package statsDRY;
 
use DBI;
 
sub getStatDRY {
	my ($tableName,$columnName,$statName,$userID) = @_;
	use config;
	my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
	createIfNotExistsDRY($tableName,$columnName,$statName,$userID);
	my $sth = $dbh->prepare("SELECT value FROM $tableName WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statName,$statName,$userID);
	my $value;
	$sth->bind_columns(\$value);
	$sth->fetch;
	return $value;
}
 
sub setStatDRY {
	my ($tableName,$columnName,$statName,$userID,$statValue) = @_;
	use config;
	my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
	createIfNotExistsDRY($tableName,$columnName,$statName,$userID);
	my $sth = $dbh->prepare("UPDATE $tableName SET value = ? WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statValue,$statName,$statName,$userID);
}
 
sub createIfNotExistsDRY {
	my ($tableName,$columnName,$statName, $userID) = @_;	
	use config;
	my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
	my $sth = $dbh->prepare("SELECT count(value) FROM $tableName WHERE stat_id = (SELECT id FROM stats WHERE display_name = ? OR short_name = ?) AND $columnName = ?");
	$sth->execute($statName,$statName,$userID);
	my $count;
	$sth->bind_columns(\$count);
	$sth->fetch;
	if($count == 0) {
		# no entry for that stat/user combination - insert one with a value of 0
		$sth = $dbh->prepare("INSERT INTO $tableName(stat_id,$columnName,value) VALUES ((SELECT id FROM stats WHERE display_name = ? OR short_name = ?),?,?)");
		$sth->execute($statName,$statName,$userID,0);
	}	
}
 
 
1;

I’ve also rewritten stats.pm to use this new code - you could do this for all your stats code, or(if you preferred, and are fairly certain your database structure won’t change) you could even swap all the code to just use the DRY functions instead of working off of the stats modules. Here’s what stats.pm looks like now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package stats;
use DBI;
 
use statsDRY;
 
use constant TABLE => 'user_stats';
use constant COLUMN => 'user_id';
 
sub getStat {
	my ($statName,$userID) = @_;
	return statsDRY::getStatDRY(TABLE,COLUMN,$statName,$userID);
}
 
sub setStat {
	my ($statName,$userID,$statValue) = @_;
	statsDRY::setStatDRY(TABLE,COLUMN,$statName,$userID,$statValue);
}
 
1;

Tuesday, July 8th, 2008 Uncategorized No Comments

Building Browsergames: Reducing Repetition (PHP)

One of the things that will slow you down and bore you the most about working on your own browsergame project is re-writing the exact same code. Unfortunately, that’s what we’ve been doing with our stats code - as reader sepp helpfully pointed out.

There’s a concept in the programming world known as ‘DRY’ - which stands for “Don’t Repeat Yourself”. What that means is that you should only ever write code to do something once - and if there are only slight variations between one piece of the code and another, you should merge them until the only differences are the ones that have to exist. By adhering to DRY principles, we can drastically cut down our development time, and make sure that we have less code overall to maintain(always a plus). Today, we’ll be using the concept of DRY to trim down our stats code.

So far, we’ve written three mostly-the-same-but-different pieces of stats code - one for item stats, one for monster stats, and one for user stats. The code for these pieces of logic are mostly the same - all that’s different between them is a few table and column names. Here’s what one of our getStat functions looks like:

function getStat($statName,$userID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExists($statName,$userID);
	$query = sprintf("SELECT value FROM user_stats WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND user_id = '%s'",
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($userID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}

While that’s fine for a single use, we’re now using almost exactly the same code in three different places - and three different files. We’re going to apply the DRY principle, and rewrite the code slightly so that any of our stats code can call it:

function getStatDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("SELECT value FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}

And with that, we can now use that single getStat function from any of our stats code - all we need to do is pass in the table and column names, in addition to which stats we want to retrieve.

Refactoring the code in this way works because our stat tables are all set up in the same way - one column has values, one column has the stat ID, and one column has the ID of whatever we’re tracking the stat for. If our tables were too varied, this might not work as easily.

If you look at the line where we create our $query variable using sprintf, you’ll notice that there are no quotes around the token for the table name - this is because MySQL won’t allow you to use quotes around your table name in select statements. You need to be aware that this has the potential to grow into a SQL injection vulnerability - so make sure that you aren’t using user input for your table name.

With that change made, let’s also make the change in our other functions:

function setStatDRY($tableName,$columnName,$statName,$trackingID,$value) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("UPDATE %s SET value = '%s' WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($value),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
}
 
function createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	$query = sprintf("SELECT count(value) FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($count) = mysql_fetch_row($result);
	if($count == 0) {
		// the stat doesn't exist; insert it into the database
		$query = sprintf("INSERT INTO %s(stat_id,%s,value) VALUES ((SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s'),'%s','%s')",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($trackingID),
		'0');
		mysql_query($query);
	}	
}

As you can see, it isn’t that big of a change - we just add the $tableName and $columnName arguments to our functions, and then modify the way that our queries are created.

You might be wondering why these functions have been showing up with the word ‘DRY’ after them - and that’s because I’m about to show you one of the cooler aspects of doing this. Now that we’ve created these versions of our functions, we can save them as stats-dry.php, and then modify our current stats code to work off of them. Here’s an example of what the new stats.php looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
 
require_once 'stats-dry.php';
define('TABLE','user_stats');
define('COLUMN','user_id');
 
function getStat($statName,$trackingID) {
	return getStatDRY(TABLE,COLUMN,$statName,$trackingID);
}
function setStat($statName,$trackingID,$value) {
	setStatDRY(TABLE,COLUMN,$statName,$trackingID,$value);
}
 
?>

And that’s where you start to see the real gains of DRY - now, all of our stats logic is inside stats-dry.php. We can go to our other code and change the way that our functions work, so that they call the DRY versions - without having to change any of the other code on the pages that are using it(like the index page, healer, forest, etc.). While our codebase is still relatively small and this could be accomplished relatively easily, this technique is valid for big or small codebases - it just gives you the flexibility to be able to do whatever you need to. And if your codebase is small enough that you feel like making the switch to only using the DRY code, you can remove the three extra stat libraries entirely.

One new thing that you may not understand with this code is the calls to define(). We are using define() to set up constants for our code - that way, if we ever decide that user stats should be stored inside user_stats_table instead of user_stats, or we decide to change the column name to something different, we only need to change it in the one place - another benefit of DRY.

Here’s what the finished code for stats-dry.php looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
 
function getStatDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("SELECT value FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($value) = mysql_fetch_row($result);
	return $value;		
}
 
function setStatDRY($tableName,$columnName,$statName,$trackingID,$value) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql');
	mysql_select_db($dbname);
	createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID);
	$query = sprintf("UPDATE %s SET value = '%s' WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($value),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
}
 
function createIfNotExistsDRY($tableName,$columnName,$statName,$trackingID) {
	include 'config.php';
	$conn = mysql_connect($dbhost,$dbuser,$dbpass)
		or die ('Error connecting to mysql:');
	mysql_select_db($dbname);
	$query = sprintf("SELECT count(value) FROM %s WHERE stat_id = (SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s') AND %s = '%s'",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($trackingID));
	$result = mysql_query($query);
	list($count) = mysql_fetch_row($result);
	if($count == 0) {
		// the stat doesn't exist; insert it into the database
		$query = sprintf("INSERT INTO %s(stat_id,%s,value) VALUES ((SELECT id FROM stats WHERE display_name = '%s' OR short_name = '%s'),'%s','%s')",
		mysql_real_escape_string($tableName),
		mysql_real_escape_string($columnName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($statName),
		mysql_real_escape_string($trackingID),
		'0');
		mysql_query($query);
	}	
}
 
?>

And here’s what the new code inside stats.php, which was used to retrieve our user’s stats, looks like. You will need to modify the other stats code yourself - although it shouldn’t be too difficult, and you could even just swap all the code to using the DRY functions instead(as long as you’re sure your database structure won’t change):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
 
require_once 'stats-dry.php';
define('TABLE','user_stats');
define('COLUMN','user_id');
 
function getStat($statName,$trackingID) {
	return getStatDRY(TABLE,COLUMN,$statName,$trackingID);
}
function setStat($statName,$trackingID,$value) {
	setStatDRY(TABLE,COLUMN,$statName,$trackingID,$value);
}
 
?>

Monday, July 7th, 2008 Uncategorized 2 Comments

So you want to build a browsergame, do you?

Well, this seems like a good place to start.

Right now it’s kind of sparse. But hopefully, soon, this website will be chock full of content that you’ll be able to use and share to help you get closer to building your own browsergame(or maybe even improve on one you’ve already built).

Good Luck!

Monday, April 14th, 2008 Uncategorized No Comments

About

Building Browsergames is a blog about browsergames(also known as PBBG's). It's geared towards the beginner to intermediate developer who has an interest in building their own browsergame.

Write for us!

Have you built a browsergame before, or do you have an opinion to share on the subject? Send an e-mail to buildingbrowsergames@gmail.com!