Implementing Multiple Areas (Perl)

Now that our database is prepared for multiple areas, we are going to build multiple areas into our exploration/combat system.

To start off, check out version 47 of our code from the Google Code Project:

svn checkout http://building-browsergames-tutorial.googlecode.com/svn/trunk/php/pbbg tutorial -r 47

With a fresh checkout of the code, we can get started. The first thing we will do is copy the code from forest.cgi over to explore.cgi, which is where we will now be storing our exploration code. Once that’s done, we will need to change the way that our script retrieves the current monster for a player to fight:

131
132
133
134
135
136
137
138
139
140
my $area_id = $query->url_param('area');
$sth = $dbh->prepare("SELECT monster FROM area_monsters WHERE area = ? ORDER BY RAND() LIMIT 1");
$sth->execute($area_id);
my $monster_id;
$sth->bind_columns(\$monster_id);
$sth->fetch;
$sth = $dbh->prepare("SELECT name FROM monsters WHERE id = ?");
$sth->execute($monster_id);
$sth->bind_columns(\$parameters{monster});
$sth->fetch;

Because we will be intermingling arguments passed through both GET and POST for our code, we have to use the url_param method – we will also need to make a slight change to how we determine whether we’re being POST’d to or not:

19
if($query->request_method() eq 'POST') {

With our code changes made, we need to put areas and monsters in areas into our database for our code to retrieve:

INSERT INTO areas(name) VALUES ('Forest');
INSERT INTO areas(name) VALUES ('Woods');
INSERT INTO area_monsters(area,monster) VALUES (1,1);
INSERT INTO area_monsters(area,monster) VALUES (1,2);
INSERT INTO area_monsters(area,monster) VALUES (2,3);

Once the data is inserted, our changes are (mostly) complete. Try visiting explore.cgi?area=1, and refreshing a few times – you should only ever see monsters appearing that belong in the area with the ID of 1.

While you are testing that, you may have noticed that the title tag for our page still says ‘The Forest’ – we should probably modify our code to also retrieve the name of the area we’re visiting:

142
143
144
145
146
147
148
149
my $area_id = $arguments{'area'};
$sth = $dbh->prepare("SELECT name FROM areas WHERE id = ?");
$sth->execute($area_id);
$sth->bind_columns(\$parameters{area_name});
$sth->fetch;
 
my $template = HTML::Template->new(
		filename	=>	'explore.tmpl',

With that code added, it’s easy enough to tweak our template(renamed from forest.tmpl to explore.tmpl) to display the current area in it’s <title> tag, in addition to changing where our combat form POSTs to:

3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    	<title>The <!--tmpl_var name='area_name'--></title>
    </head>
    <body>
    	<tmpl_unless name='combat'>
    		<p>You've encountered a <!--tmpl_var name='monster'-->!</p>
    		<form action='explore.cgi?area=<!--tmpl_var name='area'-->' method='post'>
    			<input type='submit' name='action' value='Attack' /> or 
    			<input type='submit' name='action' value='Run Away' />
    			<input type='hidden' name='monster' value='<!--tmpl_var name='monster'-->' />
    		</form>
    	<tmpl_else>
    		<ul>
    		<tmpl_loop name='combat'>
    			<li><strong><!--tmpl_var name='attacker'--></strong> attacks <!--tmpl_var name='defender'--> for <!--tmpl_var name='damage'--> damage!</li>
    		</tmpl_loop>
    		</ul>
    		<tmpl_if name='won'>
    			<p>You killed <strong><!--tmpl_var name='monster'--></strong>! You gained <strong><!--tmpl_var name='gold'--></strong> gold, and <strong><!--tmpl_var name='exp'--></strong> experience.</p>
    			<tmpl_if name='level_up'>
        			<p><strong>You gained a level!</strong></p>
    			</tmpl_if>
    			<p>You found a <strong><!--tmpl_var name='item'--></strong>!</p>
    			<p><a href='explore.cgi?area=<!--tmpl_var name='area'-->'>Explore Again</a></p>

With our template changes finished, we’re done! Your game can now support multiple different areas that players can encounter enemies in – all you have to do is link them to explore.cgi?area=<area_id>, where <area_id> is an ID for an area in the database. Here’s what our new code looks like:

explore.cgi:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(warningsToBrowser fatalsToBrowser);
use Data::Dumper;
use HTML::Template;
use DBI;
use config;
use weaponstats;
 
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;
 
if($query->request_method() eq 'POST') {
	if($arguments{action} eq 'Attack') {
		# fighting the monster	
		use stats;
		use monsterstats;
		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;
		my %player = (
			name		=>	$username,
			attack		=>	stats::getStat('atk',$userID),
			defence		=>	stats::getStat('def',$userID),
			curhp		=>	stats::getStat('curhp',$userID)
		);
		my $phand = stats::getStat('phand',$userID);
		$player{attack} += weaponstats::getWeaponStat('atk',$phand);
		use armorstats;
		my @armor = qw(atorso ahead alegs aright aleft);
		foreach my $key(@armor) {
			my $id = stats::getStat($key,$userID);
			my $defence = armorstats::getArmorStat('defence',$id);
			$player{defence} += $defence;
		}		
		$sth = $dbh->prepare("SELECT id FROM monsters WHERE name = ?");
		$sth->execute($arguments{monster});
		my $monsterID;
		$sth->bind_columns(\$monsterID);
		$sth->fetch;
		my %monster = (
			name		=>	$arguments{monster},
			attack		=>	monsterstats::getMonsterStat('atk',$monsterID),
			defence		=>	monsterstats::getMonsterStat('def',$monsterID),
			curhp		=>	monsterstats::getMonsterStat('maxhp',$monsterID)
		);
		my @combat;
		my $turns = 0;
		my ($attacker,$defender);
		while($player{curhp} > 0 && $monster{curhp} > 0) {
			my %attack;
			if($turns % 2 != 0) {
				$attacker = \%monster;
				$defender = \%player;
			} else {
				$attacker = \%player;
				$defender = \%monster;
			}
			my $damage = 0;
			if($attacker->{attack} > $defender->{defence}) {
				$damage = $attacker->{attack} - $defender->{defence};	
			}
			my %attack = (
				attacker => $attacker->{name},
				defender => $defender->{name},
				damage => $damage
			);
			$defender->{curhp} -= $damage;
			push @combat, \%attack;
			$turns++;
		}
		stats::setStat('curhp',$userID,$player{curhp});
		if($player{curhp} > 0) {
			# player won
			stats::setStat('gc',$userID,stats::getStat('gc',$userID) + monsterstats::getMonsterStat('gc',$monsterID));
			$parameters{won} = 1;
			$parameters{gold} = monsterstats::getMonsterStat('gc',$monsterID);
			my $rand = int(rand(99))+1;
			$sth = $dbh->prepare("SELECT item_id FROM monster_items WHERE monster_id = ? AND rarity >= ? ORDER BY RAND() LIMIT 1");
			$sth->execute($monsterID,$rand);
			my $itemID;
			$sth->bind_columns(\$itemID);
			$sth->fetch;
			$sth = $dbh->prepare("SELECT count(id) FROM user_items WHERE user_id = ? AND item_id = ?");
			$sth->execute($userID,$itemID);
			my $count;
			$sth->bind_columns(\$count);
			$sth->fetch;
			if($count > 0) {
				# already has one of the item
				$sth = $dbh->prepare("UPDATE user_items SET quantity = quantity + 1 WHERE user_id = ? AND item_id = ?");
			} else {
				# has none of the item - new row
				$sth = $dbh->prepare("INSERT INTO user_items(quantity,user_id,item_id) VALUES (1,?,?)");
			}
			$sth->execute($userID,$itemID);
			$sth = $dbh->prepare("SELECT name FROM items WHERE id = ?");
			$sth->execute($itemID);
			$sth->bind_columns(\$parameters{item});
			$sth->fetch;
 
            my $monster_exp = monsterstats::getMonsterStat('exp',$monsterID);
            $parameters{exp} = $monster_exp;
            my $exp_rem = stats::getStat('exp_rem',$userID);
            $exp_rem -= $monster_exp;
            if($exp_rem <= 0) {
                $exp_rem = 100;
                $parameters{level_up} = 1;
            }
            stats::setStat('exp_rem',$userID,$exp_rem);   		
		} else {
			# monster won	
			$parameters{lost} = 1;
		}
		$parameters{combat} = \@combat;
	} else {
		# running away - back to the index page!
		print $query->redirect('index.cgi');
	}	
} else {
    my $area_id = $arguments{'area'};
    $sth = $dbh->prepare("SELECT monster FROM area_monsters WHERE area = ? ORDER BY RAND() LIMIT 1");
    $sth->execute($area_id);
    my $monster_id;
    $sth->bind_columns(\$monster_id);
    $sth->fetch;
    $sth = $dbh->prepare("SELECT name FROM monsters WHERE id = ?");
    $sth->execute($monster_id);
    $sth->bind_columns(\$parameters{monster});
    $sth->fetch;
}
 
my $area_id = $query->url_param('area');
$sth = $dbh->prepare("SELECT name FROM areas WHERE id = ?");
$sth->execute($area_id);
$sth->bind_columns(\$parameters{area_name});
$sth->fetch;
 
my $template = HTML::Template->new(
		filename	=>	'explore.tmpl',
		associate	=>	$query,
	);
$template->param(%parameters);
print $query->header(), $template->output;

explore.tmpl:

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
<html>
<head>
	<title>The <!--tmpl_var name='area_name'--></title>
</head>
<body>
	<tmpl_unless name='combat'>
		<p>You've encountered a <!--tmpl_var name='monster'-->!</p>
		<form action='explore.cgi?area=<!--tmpl_var name='area'-->' method='post'>
			<input type='submit' name='action' value='Attack' /> or 
			<input type='submit' name='action' value='Run Away' />
			<input type='hidden' name='monster' value='<!--tmpl_var name='monster'-->' />
		</form>
	<tmpl_else>
		<ul>
		<tmpl_loop name='combat'>
			<li><strong><!--tmpl_var name='attacker'--></strong> attacks <!--tmpl_var name='defender'--> for <!--tmpl_var name='damage'--> damage!</li>
		</tmpl_loop>
		</ul>
		<tmpl_if name='won'>
			<p>You killed <strong><!--tmpl_var name='monster'--></strong>! You gained <strong><!--tmpl_var name='gold'--></strong> gold, and <strong><!--tmpl_var name='exp'--></strong> experience.</p>
			<tmpl_if name='level_up'>
    			<p><strong>You gained a level!</strong></p>
			</tmpl_if>
			<p>You found a <strong><!--tmpl_var name='item'--></strong>!</p>
			<p><a href='explore.cgi?area=<!--tmpl_var name='area'-->'>Explore Again</a></p>
		</tmpl_if>
		<tmpl_if name='lost'>
			<p>You were killed by <strong><!--tmpl_var name='monster'--></strong>.</p>
		</tmpl_if>
		<p><a href='index.cgi'>Back to main</a></p>
	</tmpl_unless>
</body>
</html>

Extra Credit

  • Make the explore page verify that the area ID passed to explore.cgi actaully exists within the database(hint: SELECT COUNT).

Wish there was more?

I'm considering writing an ebook - click here.

.

Luke is the primary editor of Building Browsergames, and has written a large portion of the articles that you read here. He generally has no idea what to say when asked to write about himself in the third person.

Tags: , , , ,

Monday, January 19th, 2009 buildingbrowsergames, medieval
blog comments powered by Disqus

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.

Sponsors

Got Something to Say?

Send an e-mail to luke@buildingbrowsergames.com, or get in touch through Twitter at http://twitter.com/bbrowsergames