Building Browsergames: Buying Armor (Perl)

For all that we’ve built weapons into our game, we’re still missing armor! Today we will be starting on the code for our armor system.

To start off, check out a copy of the source code for our tutorial:

svn checkout http://building-browsergames-tutorial.googlecode.com/svn/trunk/perl/pbbg tutorial -r 30
With that finished, we’re ready to start writing some code.

Based on our poll from earlier, there are going to be 5 armor slots – head, torso, legs, right arm, and left arm. That’s a lot of stats – but not altogether difficult to add to our stats table:

INSERT INTO stats(display_name,short_name) VALUES
	('Armor - Head','ahead'),
	('Armor - Torso','atorso'),
	('Armor - Legs','alegs'),
	('Armor - Right Arm','aright'),
	('Armor - Left Arm','aleft'),
	('Item Armor Slot','aslot');

If you’ve been paying attention at all, you’ll have noticed that there are actually six stats being inserted above – the sixth one being Item Armor Slot. This stat was added so that we can keep track of which armor slot an item should end up in – we will take advantage of it later.

In order to display things, we’re going to need a template. With that in mind, we’ll create armor-shop.tmpl:

<html>
<head>
	<title>The Armor Shop</title>
</head>
<body>
	<p>Welcome to the Armor Shop.</p>
	<p><a href='index.cgi'>Back to main</a></p>
	<h3>Current Armor:</h3>
	<ul>
		<li>
			Head:
			<tmpl_if name='ahead'>
				<!-- tmpl_var name='ahead'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='ahead' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Torso:
			<tmpl_if name='atorso'>
				<!-- tmpl_var name='atorso'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='atorso' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Legs:
			<tmpl_if name='alegs'>
				<!-- tmpl_var name='alegs'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='alegs' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Right Arm:
			<tmpl_if name='aright'>
				<!-- tmpl_var name='aright'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='aright' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Left Arm:
			<tmpl_if name='aleft'>
				<!-- tmpl_var name='aleft'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='aleft' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
	</ul>
	<p>You may purchase any of the armor listed below.</p>
	<tmpl_if name='error'>
		<p style='color:red'><!--tmpl_var name='error'--></p>
	</tmpl_if>
	<tmpl_if name='message'>
		<p style='color:green'><!--tmpl_var name='message'--></p>
	</tmpl_if>
	<ul>
		<tmpl_loop name='armor'>
			<li>
				<strong><!--tmpl_var name='name'--></strong> - <em><!--tmpl_var name='price'--> gold coins</em>
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='armor-id' value='<!--tmpl_var name="id"-->' />
					<input type='submit' value='Buy' />
				</form>
			</li>
		</tmpl_loop>
	</ul>
</body>
</html>

If that code looks at all similar to you, that’s because it is – it’s almost entirely copied from the weapon shop template code. While I prefer to write templates for each individual piece of functionality/view, no matter how similar they are, it’s entirely up to you – and you could probably refactor the two templates into one – although I’ll leave that as an exercise for the reader.

Seeing as there will probably be a lot more armor in our game compared to weapons(5 slots to fill instead of just 1 or 2), we’ll display a little bit more armor – and retrieve 10 different pieces using this query:

SELECT DISTINCT(id), name, price FROM items WHERE type = ‘Armor’ ORDER BY RAND() LIMIT 10;
We’ve figured out how to display our armor shop, and how to retrieve the items that will be for sale inside of it – but we still haven’t written the page that will actually handle all of our functionality. It’s time to build armor-shop.cgi:

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use HTML::Template;
use DBI;
use config;
 
my $query = new CGI;
my %arguments = $query->Vars;
 
my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
my $sth;
my %parameters;
 
use stats;
 
my $cookie = $query->cookie('username+password');
my ($username) = split(/\+/,$cookie);
$sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)");
$sth->execute($username);
my $userID;
$sth->bind_columns(\$userID);
$sth->fetch;
 
$sth = $dbh->prepare("SELECT DISTINCT(id), name, price FROM items WHERE type = 'Armor' ORDER BY RAND() LIMIT 10");
$sth->execute();
my @armor = ();
while(my $row = $sth->fetchrow_hashref) {
	push @armor, $row;
}
$parameters{armor} = \@armor;
my @stats = qw(atorso ahead alegs aright aleft);
$sth = $dbh->prepare("SELECT name FROM items WHERE id = ?");
foreach my $key (@stats) {
	my $id = stats::getStat($key,$userID);
	$sth->execute($id);
	my $name;
	$sth->bind_columns(\$name);
	if($sth->fetch) {
		$parameters{$key} = $name;
	}
 
}
 
my $template = HTML::Template->new(
		filename	=>	'armor-shop.tmpl',
		associate	=>	$query,
	);
$template->param(%parameters);
print $query->header(), $template->output;

This code is extremely similar to the weapons shop, too – because at the core of it, all we’re doing for weapons or armor or any other type of item is shuffling items around. However, you may notice one fairly large difference between this and weapon shop code: there doesn’t seem to be any code to display the user’s current armor!

However, this isn’t actually true – all we’ve done is convert our retrieval logic to something that’s a little more generic when it retrieves things. Lines 34-45 contain all of the code we need to display as many stats we want – although we’re currently only displaying the current armor for the user.

It’s a fairly simple piece of code, really. We begin by defining the keys within our stats system for the different stats, and then looping through those keys. At that point, our code is the same as it’s always been – we just retrieve the name of our item, and then store it into our template. It’s pretty simple, and pretty handy – instead of writing 50+ lines of code(10 lines per armor slot), we’ve written just 10 that will handle it all for us. We can get away with doing this because of the way that we’ve set up the keys in both our stats system and our template – because they’re the same, we can retrieve from the database and store the data back into the template using the same key.

As cool as that is, though, we haven’t gotten to the actual point of our armor shop – buying armor! Therefore, we now need to build that logic. But before we can build that, we need to create armorstats.pm, so that we can retrieve stats for individual pieces of armor:

 

package armorstats;
use DBI;
 
use statsDRY;
 
sub getArmorStat {
	my ($statName,$userID) = @_;
	return statsDRY::getStatDRY('Item',$statName,$userID);
}
 
1;

With that finished, we can now write our code to purchase armor:

 

if(%arguments) {
	use armorstats;
	my $armorID = $arguments{'armor-id'};
	$sth = $dbh->prepare("SELECT price FROM items WHERE id = ?");
	$sth->execute($armorID);
	my $cost;
	$sth->bind_columns(\$cost);
	$sth->fetch;
	my $gold = stats::getStat('gc',$userID);
	if ($gold > $cost) {
		my $slot = armorstats::getArmorStat('aslot',$armorID);
		my $equipped = stats::getStat($slot,$userID);
		if(!$equipped) {
			stats::setStat($slot,$userID,$armorID);
			stats::setStat('gc',$userID,($gold - $cost));
			$parameters{'message'} = 'You purchased and equipped the new armor.';
		} else {
			# they already have something equipped - display an error message
			$parameters{'error'} = 'You are already wearing a piece of that kind of armor! You will need to sell your current armor before you can buy new armor.';
		}
	} else {
		$parameters{'error'} = 'You cannot afford that piece of armor.';
	}
}

And with that added, users can purchase armor to wear. As you can see, we are using our aslot stat to keep track of which armor slot a specific piece of armor goes in – that way, we can reduce the amount of code that we have to write to handle more than one different armor slot.

So now players can buy armor – but they can’t quite sell it yet. We’ll add in the code to handle what happens when a user clicks the ’sell’ button next to a piece of armor they’re wearing:

 

if($arguments{sell}) {
		my $armorID = stats::getStat($arguments{sell},$userID);
		$sth = $dbh->prepare("SELECT price FROM items WHERE id = ?");
		$sth->execute($armorID);
		my $price;
		$sth->bind_columns(\$price);
		$sth->fetch;
		my $gold = stats::getStat('gc',$userID);
		stats::setStat('gc',$userID,($gold + $price));
		stats::setStat($arguments{sell},$userID,'');
	} else {

And with that, users can now buy and sell armor. The last change we need to make is to index.tmpl, where we’ll add the ‘Armor Shop’ link:

<p><a href='armor-shop.cgi'>The Armor Shop</a></p>

And that’s all there is to it! Here’s the code for armor-shop.cgi:

 

#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use HTML::Template;
use DBI;
use config;
 
my $query = new CGI;
my %arguments = $query->Vars;
 
my $dbh = DBI->connect("DBI:mysql:$config{dbName}:$config{dbHost}",$config{dbUser},$config{dbPass},{RaiseError => 1});
my $sth;
my %parameters;
 
use stats;
 
my $cookie = $query->cookie('username+password');
my ($username) = split(/\+/,$cookie);
$sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)");
$sth->execute($username);
my $userID;
$sth->bind_columns(\$userID);
$sth->fetch;
 
if(%arguments) {
	use armorstats;
	if($arguments{sell}) {
		my $armorID = stats::getStat($arguments{sell},$userID);
		$sth = $dbh->prepare("SELECT price FROM items WHERE id = ?");
		$sth->execute($armorID);
		my $price;
		$sth->bind_columns(\$price);
		$sth->fetch;
		my $gold = stats::getStat('gc',$userID);
		stats::setStat('gc',$userID,($gold + $price));
		stats::setStat($arguments{sell},$userID,'');
	} else {
		my $armorID = $arguments{'armor-id'};
		$sth = $dbh->prepare("SELECT price FROM items WHERE id = ?");
		$sth->execute($armorID);
		my $cost;
		$sth->bind_columns(\$cost);
		$sth->fetch;
		my $gold = stats::getStat('gc',$userID);
		if ($gold > $cost) {
			my $slot = armorstats::getArmorStat('aslot',$armorID);
			my $equipped = stats::getStat($slot,$userID);
			if(!$equipped) {
				stats::setStat($slot,$userID,$armorID);
				stats::setStat('gc',$userID,($gold - $cost));
				$parameters{'message'} = 'You purchased and equipped the new armor.';
			} else {
				# they already have something equipped - display an error message
				$parameters{'error'} = 'You are already wearing a piece of that kind of armor! You will need to sell your current armor before you can buy new armor.';
			}
		} else {
			$parameters{'error'} = 'You cannot afford that piece of armor.';
		}
	}
}
 
$sth = $dbh->prepare("SELECT DISTINCT(id), name, price FROM items WHERE type = 'Armor' ORDER BY RAND() LIMIT 10");
$sth->execute();
my @armor = ();
while(my $row = $sth->fetchrow_hashref) {
	push @armor, $row;
}
$parameters{armor} = \@armor;
my @stats = qw(atorso ahead alegs aright aleft);
$sth = $dbh->prepare("SELECT name FROM items WHERE id = ?");
foreach my $key (@stats) {
	my $id = stats::getStat($key,$userID);
	$sth->execute($id);
	my $name;
	$sth->bind_columns(\$name);
	if($sth->fetch) {
		$parameters{$key} = $name;
	}
 
}
 
my $template = HTML::Template->new(
		filename	=>	'armor-shop.tmpl',
		associate	=>	$query,
	);
$template->param(%parameters);
print $query->header(), $template->output;

And here’s our template file(armor-shop.tmpl):

 

<html>
<head>
	<title>The Armor Shop</title>
</head>
<body>
	<p>Welcome to the Armor Shop.</p>
	<p><a href='index.cgi'>Back to main</a></p>
	<h3>Current Armor:</h3>
	<ul>
		<li>
			Head:
			<tmpl_if name='ahead'>
				<!-- tmpl_var name='ahead'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='ahead' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Torso:
			<tmpl_if name='atorso'>
				<!-- tmpl_var name='atorso'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='atorso' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Legs:
			<tmpl_if name='alegs'>
				<!-- tmpl_var name='alegs'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='alegs' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Right Arm:
			<tmpl_if name='aright'>
				<!-- tmpl_var name='aright'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='aright' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
		<li>
			Left Arm:
			<tmpl_if name='aleft'>
				<!-- tmpl_var name='aleft'-->
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='sell' value='aleft' />
					<input type='submit' value='Sell' />
				</form>
			<tmpl_else>
				None
			</tmpl_if>
		</li>
	</ul>
	<p>You may purchase any of the armor listed below.</p>
	<tmpl_if name='error'>
		<p style='color:red'><!--tmpl_var name='error'--></p>
	</tmpl_if>
	<tmpl_if name='message'>
		<p style='color:green'><!--tmpl_var name='message'--></p>
	</tmpl_if>
	<ul>
		<tmpl_loop name='armor'>
			<li>
				<strong><!--tmpl_var name='name'--></strong> - <em><!--tmpl_var name='price'--> gold coins</em>
				<form action='armor-shop.cgi' method='post'>
					<input type='hidden' name='armor-id' value='<!--tmpl_var name="id"-->' />
					<input type='submit' value='Buy' />
				</form>
			</li>
		</tmpl_loop>
	</ul>
</body>
</html>

If you’re having issues getting some armor into your shop to play with, here’s a quick SQL query you can run to insert some sample armors:

INSERT INTO items(name,type,price) VALUES ('Sample Helmet','Armor',10);
	INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name='aslot'),(SELECT id FROM items WHERE name='Sample Helmet'),'ahead','Item');
INSERT INTO items(name,type,price) VALUES ('Sample Torso','Armor',10);
	INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name='aslot'),(SELECT id FROM items WHERE name='Sample Torso'),'atorso','Item');
INSERT INTO items(name,type,price) VALUES ('Sample Legs','Armor',10);
	INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name='aslot'),(SELECT id FROM items WHERE name='Sample Legs'),'alegs','Item');
INSERT INTO items(name,type,price) VALUES ('Sample Right Arm','Armor',10);
	INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name='aslot'),(SELECT id FROM items WHERE name='Sample Right Arm'),'aright','Item');
INSERT INTO items(name,type,price) VALUES ('Sample Left Arm','Armor',10);
	INSERT INTO entity_stats(stat_id,entity_id,value,entity_type) VALUES ((SELECT id FROM stats WHERE short_name='aslot'),(SELECT id FROM items WHERE name='Sample Left Arm'),'aleft','Item');

Extra Credit

  1. Refactor the weapon shop code to use the same code as the armor shop for displaying current equipment.
  2. Refactor the weapon/armor shop templates so that both pages can use the same template.
  3. Refactor both pieces of code, so that there is only one template and one code file for weapons and armor.

There was a small bug found in the weapon stat’s retrieval code during the writing of this entry – make sure to update your checked out version!