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).
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.



