old code
I recently came across the source code of a browser-based game I wrote around 2005, when I would have been 15 years old. While the original database structure was unfortunately lost, I managed to piece together a working version by looking at existing queries in the source code. With this, I was able to get the game running again! In this post, we’ll look at what a naive PHP developer can create - and the mistakes they make.
why the web⌗
I can’t remember the original reason why I wanted to make a website, or how I got into web programming at all. I recall taking an HTML class in high school, but by that time, I was already proficient in HTML and even writing PHP, CSS, and some JavaScript.
I was able to retrieve, via archive.org and other methods, some old examples of web sites I made around this time. And, judging by them, I apparently wanted to make websites about forts in the woods that my friends and I built when we were kids. No pictures, because digital cameras were uncommon and expensive back then, but some had MS Paint style maps drawn of what we’ve built and where it is in the wilderness.
I do recall another force that brought me into PHP and web programming: online MMORPGs. Like many young teens at the time, I was addicted to the online game RuneScape. And I also recall setting up forums for the group of players I hung out with online - taking an open-source PHP forum - phpBB - and installing it on a free php/mysql web host. I recall installing mods - source code mods to add features - into the forum software. This wasn’t automated with plugins like modern software. The “mods” were source code patches. Often, if you had multiple mods, the diffs would conflict and you’d have to resolve differences manually. I think this is where I first started looking at PHP.
Beyond that, it was “cool” at the time on online forums to have a dynamic signature. Just like a signature in an email client, a signature is a message that gets attached to the bottom of every post you make. You could embed images, so it was possible to hotlink to an image that gets generated on the fly.
But back to RuneScape forums. The most common kind of dynamic signature was one that shows the level of your character in the video game. Players put work into games and feel proud of the achievements they make, and this is especially true about RuneScape as it is notorious for its grueling time commitments. It is not surprising that such a thing became a popular trend. Players want to show off their work, and a flashy image that does so under every post they make is a great way to do it.
Young me thought that was cool too, and young me thought it was even cooler if I could have a completely unique dynamic signature, unlike anyone else’s. It would be showing off my programming achievement in addition to whatever the signature itself displayed.
And I did. And it worked. And I learned about things like caching, even though I didn’t even know the word for it at the time. I found through trial and error that making my dynamic signature script call cURL and fetch information from RuneScape’s website was too slow. Even back then, they would automatically throttle and block scrapers like mine. And so caching became a necessity.
The addiction grew from there. Both the game addiction and the programming addiction. The rest is history, as they say.
Beyond RuneScape, I was also a fan of top-down monster-fighting RPGs such as the Dragon Quest/Warrior series. I recall making other games in the past with ready-made game making software such as Kyle’s Quest, but never anything involving code or starting from scratch. RuneScape’s 3D graphics were far too much for me to match, so I apparently chose a 2D top-down style comparable to these other games.
the game⌗

The UI is simple - buttons to move in 4 directions. A button to wait which refreshes the page. Page loads were a little slower in the 2000s, so I had a walking queue that used a little JavaScript to send many actions at once. I guess I wasn’t the greatest speller then - “que” not “queue”, heh. On the battle screen, options to attack and run. That’s it!
The game is technically an MMORPG: you see other players moving around the map with their name displayed over their heads. There are level leader boards to drive competition between players. Being a web game, being turn-based made the most sense. One page load equals one turn. However, the game had so few actual features - no chat, no inventory - it’s hard to put it in a category at all, besides that of being just a fun programming challenge for young me at the time.
I was never much of an artist, so the graphics were stolen from other games. I believe the trees and buildings and general scenery came from the Nintendo Entertainment System version of EarthBound. The player character itself came from A Day At Work for Mac OS. Other graphics, such as enemy monsters, were arbitrary images, such as the monster.com monster. The website itself that housed the game was from a now long defunct free template website.
The gameplay loop was simple. The starting area was safe. Go to a dangerous area by going the only way you can go, into the house and out the back door. Wander around until you encounter a monster. Fight it, die, and respawn in the starting area. Or win, and gain experience points. Wander and fight again, or go back to the starting area to heal. That’s it!
a closer look⌗
The game is overly simple, I have no disillusion about that. It is, however, technically a game engine. Content is decoupled from the engine. I can’t recall if I understood this concept at the time. Probably not. Or perhaps, though experimenting with RuneScape private servers, I had observed such a decoupling of content and engine and internalized it without even knowing the name of the technique. I’ll never know.
Just because it’s a game engine, doesn’t mean it’s a good game engine. I’ll talk about the funny parts first.
Being a mid 2000s PHP app written by a teenager, it is obviously full of security issues and other uncleanliness that would not surprise an experienced programmer.
Apparently I didn’t know about PHP sessions data yet, because I used raw cookies to handle user login. And by raw, I mean raw:
if($_POST['u']!="") {
$u=addslashes($_POST['u']);
$p=addslashes($_POST['p']);
if($u!="" && $p!="") {
$sql="SELECT * FROM mrpg_users WHERE name='$u'";
$result = mysql_query($sql);
while($row = mysql_fetch_array($result)) {
if($row['password']==$p) {
$message="You have been logged in. Proceed to the Game.
";
setcookie("mrpg-ethbnd", strtolower($u . "@seperator@" . $p), time()+525600);
} else {
$message="Invalid username or password.";
}
}
}
}The cookie name is hard-coded on line 10. It’s also hard-coded in every file. I knew about includes, there was one a few lines up in this snippet’s context, but I will never know why I didn’t use one for something like this.
But that’s only the beginning. Perhaps you noticed what the cookie is set to on line 10 as well. It’s the literal
username and password separated by a hard-coded string, @seperator@. Very strange, and also reveals that raw passwords
were being stored in plain text. Terrible practice, but it did work. At least I understood the separator token had to be
something players were unlikely to type.
I cannot recall why I chose 525600 seconds as the cookie expiration time, either. 525,600 is the number of minutes in
a 365 day year, so I think a reasonable guess is that I wanted a year and thought the duration was in minutes. But as
written, the cookie expiration ends up being slightly longer than 6 days.
How this cookie was read isn’t much better. In every game file, the same snippet was repeated again:
$credsYo=$_COOKIE['mrpg-ethbnd'];
$credsYo=explode("@seperator@", $credsYo);
$u=strtolower($credsYo[0]);
$p=strtolower($credsYo[1]);
if($u=="") {
header("Location: index.php?login");
}
$result = mysql_query("SELECT * FROM mrpg_users WHERE name='$u'");
while($row = mysql_fetch_array($result)) {
if($row['password']!=$p) {
header("Location: index.php");
exit;
}
}So many problems in so little code. The same hard-coded cookie name on line 1. With the same hard-coded separator token
on line 2. We only support lowercase passwords, apparently, because of the strtolower() on line 5. Attempting to read
index 1 from the cookie credentials probably causes an error if the cookie isn’t set at all. It looks like we redirect
the browser on line 8 with the header() call, but the script keeps running because there is no exit;. If there’s no
cookie, we look up a blank user with a blank password on line 11. We check the plaintext password vs the database
plaintext password on line 13.
But if all of this passes, we have actually verified which user is logged in. Barely.
This is probably also vulnerable to SQL injection via the username as there is no attempt made to sanitize $u the
username before inserting it directly into a SQL query. This vulnerable snippet
appears everywhere!
Many other places in the code rely on addslashes() for SQL injection protection. While somewhat sufficient for plain
ASCII, it is likely this does not block SQL injection as this sanitation method isn’t fully sufficient. It can fail due
to text encoding misalignment and is ignorant of the SQL server’s supported escaping modes.
These are the most egregious problems. Other lesser issues are more naiveties than actual problems
The way the “walkable” status of the map is done is amusing:
$walkable[56]=array(0,0,0,0,0,0,1,0,0,0,0,0,0,0,0);
$walkable[57]=array(0,0,0,0,1,1,1,1,1,0,0,0,0,0,0);
$walkable[58]=array(0,0,0,1,1,1,1,1,1,1,0,0,0,0,0);
$walkable[59]=array(0,0,1,1,1,1,1,1,1,1,1,1,0,0,0);
$walkable[60]=array(0,0,0,1,1,1,1,1,1,1,1,1,0,0,0);
$walkable[61]=array(0,1,1,1,1,1,1,1,1,1,1,1,1,0,0);
$walkable[62]=array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);This is an excerpt, but the above linked file has much more content just like this. A wall of textg. And what this wall
is, is a giant array of 1s and 0s that specify whether each coordinate in the game can be walked into. The array’s
indexes are the world map’s coordinates.
This isn’t too bad, actually. Probably fairly efficient, if it were to grow. And it’s simple.
However, one of my pet peeves shows up in this file too:
if($walkable[$y][$x]==1) {
return true;
} else {
return false;
}This should be simply return $walkable[$y][$x]==1;! No need to if and else! Just return the boolean! Argh!
When rendering the map, the game needs to fetch a list of the other players so it may know where to draw them on the map. The code that does this simply fetches all users!
$result = mysql_query("SELECT name,x,y,lastact,dir FROM mrpg_users");If this game ever saw a serious amount of players - which it did not - this would be slow as many many players images would need to be rendered, even those invisible off-screen. And as more players joined, the map would render slower and slower. A location filter should be used here, that excludes players too far away to be worth rendering.
The map render script was otherwise somewhat slick. It utilized PHP’s GD library, which is a set of APIs for manipulating images. You can load images, combine them, write text, crop, scale, etc. Effectively, my game’s map rendering process is that it queried the DB for players, calculated the map pixel position based on the current player, and drew each player on the map. Pretty simple.
Other screens, such as battling monsters, had separate render scripts that retrieved and displayed information in the same manner. Image-based instead of web and text based is an odd choice, but I guess my early experience with dynamic forum signatures had me liking the GD library.
Lastly, digging through the source code also revealed that each character had a wide variety of different properties …most of which never actually got used for anything:
imageString($im, 4, 50, 100, "PP: " . $row['pp'], $white);
imageString($im, 4, 50, 140, "Offense: " . $row['offense'], $white);
imageString($im, 4, 50, 180, "Defense: " . $row['defense'], $white);
imageString($im, 4, 50, 220, "Fight: " . $row['fight'], $white);
imageString($im, 4, 340, 100, "Speed: " . $row['speed'], $white);
imageString($im, 4, 340, 140, "Wisdom: " . $row['wisdom'], $white);
imageString($im, 4, 340, 180, "Strength: " . $row['strength'], $white);
imageString($im, 4, 340, 220, "Force: " . $row['force'], $white);
imageString($im, 5, 200, 260, "Level: " . level(2, $exp['level']), $white);
imageString($im, 5, 200, 275, "EXP: " . $exp['level'], $white);
imageString($im, 5, 190, 10, "HP: " . $row['hp'] . "/" . $row['maxhp'], $green);HP, EXP, Offense and Strength were used, but the others did nothing. All of them increase when leveling up, but still are never read.
epilogue⌗
I hope I did an understandable job of explaining how the internals of my naive game work, and that the problems called out that I wrote into it aren’t too offensive - remember, I was 15!
I wrote a Docker Compose file should you want to run a copy of the game. In addition to the code vulnerabilities - which I will not be fixing because they add charm! - the Docker images used are fairly outdated as well. So, it would be a bad idea to put this on the public internet. But if you want to play, it’s available.
Source code is available on my Git instance:
Think you achieved a high score in my game? Let me know directly.