Roguelikes have been in the spotlight recently, with games like Dungeons of Dredmor, Spelunky, The Binding of Isaac, and FTL reaching wide audiences and receiving critical acclaim. Long enjoyed by hardcore players in a tiny niche, roguelike elements in various combinations now help bring more depth and replayability to many existing genres.
In this tutorial, you will learn how to make a traditional roguelike using JavaScript and the HTML 5 game engine Phaser. By the end, you will have a fully-functional simple roguelike game, playable in your browser! (For our purposes a traditional roguelike is defined as a single-player, randomized, turn-based dungeon-crawler with permadeath.)
Note: Although the code in this tutorial uses JavaScript, HTML, and Phaser, you should be able to use the same technique and concepts in almost any other coding language and game engine.
Getting Ready
For this tutorial, you will need a text editor and a browser. I use Notepad++, and I prefer Google Chrome for its extensive developer tools, but the workflow will be pretty much the same with any text editor and browser you choose.
You should then download the source files and start with the init
folder; this contains Phaser and the basic HTML and JS files for our game. We will write our game code in the currently empty rl.js
file.
The index.html
file simply loads Phaser and our aforementioned game code file:
<!DOCTYPE html><head><title>roguelike tutorial</title><script src="phaser.min.js"></script><script src="rl.js"></script></head></html>
Initialization and Definitions
For the time being, we'll use ASCII graphics for our roguelike—in the future, we could replace these with bitmap graphics, but for now, using simple ASCII makes our lives easier.
Let's define some constants for the font size, the dimensions of our map (that is, the level), and how many actors spawn in it:
// font size var FONT = 32; // map dimensions var ROWS = 10; var COLS = 15; // number of actors per level, including player var ACTORS = 10;
Let's also initialize Phaser and listen for keyboard key-up events, as we will be creating a turn based game and will want to act once for every key stroke:
// initialize phaser, call create() once done var game = new Phaser.Game(COLS * FONT * 0.6, ROWS * FONT, Phaser.AUTO, null, { create: create }); function create() { // init keyboard commands game.input.keyboard.addCallbacks(null, null, onKeyUp); } function onKeyUp(event) { switch (event.keyCode) { case Keyboard.LEFT: case Keyboard.RIGHT: case Keyboard.UP: case Keyboard.DOWN: } }
Since default monospace fonts tend to be about 60% as wide as they are high, we've initialized the canvas size to be 0.6 * the font size * the number of columns
. We're also telling Phaser that it should call our create()
function immediately after it's finished initialising, at which point we initialize the keyboard controls.
You can view the game so far here—not that there's much to see!
The Map
The tile map represents our play area: a discrete (as opposed to continuous) 2D array of tiles, or cells, each represented by an ASCII character that can signify either a wall (#
: blocks movement) or floor (.
: doesn't block movement):
// the structure of the map var map;
Let's use the simplest form of procedural generation to create our maps: randomly deciding which cell should contain a wall and which a floor:
function initMap() { // create a new random map map = []; for (var y = 0; y < ROWS; y++) { var newRow = []; for (var x = 0; x < COLS; x++) { if (Math.random() > 0.8) newRow.push('#'); else newRow.push('.'); } map.push(newRow); } }
This should give us a map where 80% of the cells are walls and the rest are floors.
We initialize the new map for our game in the create()
function, immediately after setting up the keyboard event listeners:
function create() { // init keyboard commands game.input.keyboard.addCallbacks(null, null, onKeyUp); // initialize map initMap(); }
You can view the demo here—although, again, there's nothing to see, as we haven't rendered the map yet.
The Screen
It's time to draw our map! Our screen will be a 2D array of text elements, each containing a single character:
// the ascii display, as a 2d array of characters var screen;
Drawing the map will fill in the screen's content with the map's values, since both are simple ASCII characters:
function drawMap() { for (y in 0...ROWS) for (x in 0...COLS) screen[y][x].content = map[y][x]; }
Finally, before we draw the map we have to initialize the screen. We go back to our create()
function:
function create() { // init keyboard commands game.input.keyboard.addCallbacks(null, null, onKeyUp); // initialize map initMap(); // initialize screen screen = []; for (y in 0...ROWS) { var newRow = []; screen.push(newRow); for (x in 0...COLS) newRow.push( initCell('', x, y) ); } drawMap(); } function initCell(char:String, x:Int, y:Int):phaser.Text { // add a single cell in a given position to the ascii display var style = { font: FONT + "px monospace", fill:"#fff"}; return game.add.text(FONT*0.6*x, FONT*y, char, style); }
You should now see a random map displayed when you run the project.
Actors
Next in line are the actors: our player character, and the enemies they must defeat. Each actor will be an object with three fields: x
and y
for its location in the map, and hp
for its hit points.
We keep all actors in the actorList
array (the first element of which is the player). We also keep an associative array with the actors' locations as keys for quick searching, so that we don't have to iterate over the entire actor list to find which actor occupies a certain location; this will help us when we code the movement and combat.
// a list of all actors; 0 is the player var player; var actorList; var livingEnemies; // points to each actor in its position, for quick searching var actorMap;
We create all our actors and assign a random free position in the map to each:
function randomInt(max) { return Math.floor(Math.random() * max); } function initActors() { // create actors at random locations actorList = []; actorMap = {}; for (var e=0; e<ACTORS; e++) { // create new actor var actor = { x:0, y:0, hp:e == 0?3:1 }; do { // pick a random position that is both a floor and not occupied actor.y=randomInt(ROWS); actor.x=randomInt(COLS); } while ( map[actor.y][actor.x] == '#' || actorMap[actor.y + "_" + actor.x] != null ); // add references to the actor to the actors list & map actorMap[actor.y + "_" + actor.x]= actor; actorList.push(actor); } // the player is the first actor in the list player = actorList[0]; livingEnemies = ACTORS-1; }
It's time to show the actors! We're going to draw all the enemies as e
and the player character as its number of hitpoints:
function drawActors() { for (var a in actorList) { if (actorList[a].hp > 0) screen[actorList[a].y][actorList[a].x].content = a == 0?''+player.hp:'e'; } }
We make use of the functions we just wrote to initialize and draw all actors in our create()
function:
function create() { ... // initialize actors initActors(); ... drawActors(); }
We can now see our player character and enemies spread out in the level!
Blocking and Walkable Tiles
We need to make sure that our actors aren't running off the screen and through walls, so let's add this simple check to see in which directions a given actor can walk:
function canGo(actor,dir) { return actor.x+dir.x >= 0 && actor.x+dir.x <= COLS - 1 && actor.y+dir.y >= 0 && actor.y+dir.y <= ROWS - 1 && map[actor.y+dir.y][actor.x +dir.x] == '.'; }
Movement and Combat
We've finally arrived at some interaction: movement and combat! Since, in classic roguelikes, the basic attack is triggered by moving into another actor, we handle both of these at the same spot, our moveTo()
function, which takes an actor and a direction (the direction is the desired difference in x
and y
to the position the actor steps in):
function moveTo(actor, dir) { // check if actor can move in the given direction if (!canGo(actor,dir)) return false; // moves actor to the new location var newKey = (actor.y + dir.y) +'_' + (actor.x + dir.x); // if the destination tile has an actor in it if (actorMap[newKey] != null) { //decrement hitpoints of the actor at the destination tile var victim = actorMap[newKey]; victim.hp--; // if it's dead remove its reference if (victim.hp == 0) { actorMap[newKey]= null; actorList[actorList.indexOf(victim)]=null; if(victim!=player) { livingEnemies--; if (livingEnemies == 0) { // victory message var victory = game.add.text(game.world.centerX, game.world.centerY, 'Victory!\nCtrl+r to restart', { fill : '#2e2', align: "center" } ); victory.anchor.setTo(0.5,0.5); } } } } else { // remove reference to the actor's old position actorMap[actor.y + '_' + actor.x]= null; // update position actor.y+=dir.y; actor.x+=dir.x; // add reference to the actor's new position actorMap[actor.y + '_' + actor.x]=actor; } return true; }
Basically:
- We make sure the actor is trying to move into a valid position.
- If there is another actor in that position, we attack it (and kill it if its HP count reaches 0).
- If there isn't another actor in the new position, we move there.
Notice that we also show a simple victory message once the last enemy has been killed, and return false
or true
depending on whether or not we managed to perform a valid move.
Now, let's go back to our onKeyUp()
function and alter it so that, every time the user presses a key, we erase the previous actor's positions from the screen (by drawing the map on top), move the player character to the new location, and then redraw the actors:
function onKeyUp(event) { // draw map to overwrite previous actors positions drawMap(); // act on player input var acted = false; switch (event.keyCode) { case Phaser.Keyboard.LEFT: acted = moveTo(player, {x:-1, y:0}); break; case Phaser.Keyboard.RIGHT: acted = moveTo(player,{x:1, y:0}); break; case Phaser.Keyboard.UP: acted = moveTo(player, {x:0, y:-1}); break; case Phaser.Keyboard.DOWN: acted = moveTo(player, {x:0, y:1}); break; } // draw actors in new positions drawActors(); }
We will soon use the acted
variable to know if the enemies should act after each player input.
Basic Artificial Intelligence
Now that our player character is moving and attacking, let's even the odds by making the enemies act according to very simple path finding as long as the player is six steps or fewer from them. (If the player is further away, the enemy walks randomly.)
Notice that our attack code doesn't care who the actor is attacking; this means that, if you align them just right, the enemies will attack each other while trying to pursue the player character, Doom-style!
function aiAct(actor) { var directions = [ { x: -1, y:0 }, { x:1, y:0 }, { x:0, y: -1 }, { x:0, y:1 } ]; var dx = player.x - actor.x; var dy = player.y - actor.y; // if player is far away, walk randomly if (Math.abs(dx) + Math.abs(dy) > 6) // try to walk in random directions until you succeed once while (!moveTo(actor, directions[randomInt(directions.length)])) { }; // otherwise walk towards player if (Math.abs(dx) > Math.abs(dy)) { if (dx < 0) { // left moveTo(actor, directions[0]); } else { // right moveTo(actor, directions[1]); } } else { if (dy < 0) { // up moveTo(actor, directions[2]); } else { // down moveTo(actor, directions[3]); } } if (player.hp < 1) { // game over message var gameOver = game.add.text(game.world.centerX, game.world.centerY, 'Game Over\nCtrl+r to restart', { fill : '#e22', align: "center" } ); gameOver.anchor.setTo(0.5,0.5); } }
We've also added a game over message, which is shown if one of the enemies kills the player.
Now all that's left to do is make the enemies act every time the player moves, which requires adding the following to the end of our onKeyUp()
functions, right before drawing the actors in their new position:
function onKeyUp(event) { ... // enemies act every time the player does if (acted) for (var enemy in actorList) { // skip the player if(enemy==0) continue; var e = actorList[enemy]; if (e != null) aiAct(e); } // draw actors in new positions drawActors(); }
Bonus: Haxe Version
I originally wrote this tutorial in a Haxe, a great multi-platform language that compiles to JavaScript (among other languages). Although I translated the version above by hand as to make sure we get idiosyncratic JavaScript, if, like me, you prefer Haxe to JavaScript, you can find the Haxe version in the haxe
folder of the source download.
You need to first install the haxe compiler and can use whatever text editor you wish and compile the haxe code by calling haxe build.hxml
or double-clicking the build.hxml
file. I also included a FlashDevelop project if you prefer a nice IDE to a text editor and command line; just open rl.hxproj
and press F5 to run.
Summary
That's it! We now have a complete simple roguelike, with random map generation, movement, combat, AI and both win and lose conditions.
Here are some ideas for new features you can add to your game:
- multiple levels
- power ups
- inventory
- consumables
- equipment
Enjoy!