Tuesday, August 30, 2011

roguelike tutorial 04: the player

Alright, let's get a hero so we can explore these caves.

We'll need something to represent our player and eventually monsters. They'll all have an x and y coordinate, a glyph, and a color. Since they will be interacting with the world, they should have a reference to that too.

package rltut;

import java.awt.Color;

public class Creature {
    private World world;

    public int x;
    public int y;

    private char glyph;
    public char glyph() { return glyph; }

    private Color color;
    public Color color() { return color; }

    public Creature(World world, char glyph, Color color){
        this.world = world;
        this.glyph = glyph;
        this.color = color;
    }
}

I made the x and y coordinate publicly accessible since they'll be used a lot, we don't need to constrain them or do anything when they change, and I'd rather not have to deal with getters and setters. Getters and setters are almost always a better idea than public fields (especially when using C# properties) but part of software engineering is knowing the rules and part is knowing when to break them. If this turns out to be a bad idea and we wish we used getters and setters instead, then it's not a big deal since most IDE's can automatically create getters and setters and rewrite your code to use those (Encapsulate Field or Generate Getters And Setters in Eclipse).

To implement all the different behaviors of all the different creatures, we could use a bunch of flags representing creature traits, or we could use subclassing, but let's use something that's usually more flexible: delegation. Each creature will have a reference to a CreatureAi and the creatures can let their ai decide what to do. Instead of using what's called constructor injection and passing the CreatureAi in the constructor like we do with the world, glyph, and color, which is usually a good way of doing things, we'll use setter injection.

private CreatureAi ai;
public void setCreatureAi(CreatureAi ai) { this.ai = ai; }



Since the caves we have so far aren't all connected, the player can only walk around in the open area he starts in. We could change how we build the world to make sure that all open cave floors are connected but there's a much easier way to make sure the player can explore everything; we'll let creatures dig through the walls.

public void dig(int wx, int wy) {
    world.dig(wx, wy);
}

And here's our addition to the World class allowing us to dig into cave walls.

public void dig(int x, int y) {
    if (tile(x,y).isDiggable())
        tiles[x][y] = Tile.FLOOR;
}

And the Tile class needs an isDiggable method. This way we don't even have to know what the tile is we can just care about if it can be dug through. If we later add new tiles, no-dig zones, or something else we just need to update this method.
public boolean isDiggable() {
    return this == Tile.WALL;
}

And that's the end of our "stuck in a tiny cave" problem. Any day I solve a tricky problem by adding a few little methods is a good day indeed.



Getting back to our Creature class, creatures will also move around in the world. What happens when they try to enter a new tile is up to the creature's ai.

public void moveBy(int mx, int my){
    ai.onEnter(x+mx, y+my, world.tile(x+mx, y+my));
}

I think were done with the Creature class for now so let's start on the CreatureAi. Remember when we created the setter for the creature's ai? We'll use that to wire up the creature and the creature's ai. The ai also needs to do deal with the creature trying to enter a new tile. We're going to have a specific ai for the player so it doesn't matter what you use here since we are just going to override it. Here's what I came up with:

package rltut;

public class CreatureAi {
    protected Creature creature;

    public CreatureAi(Creature creature){
        this.creature = creature;
        this.creature.setCreatureAi(this);
    }

    public void onEnter(int x, int y, Tile tile) { }
}

Remember when I said we're going to have a specific ai for the player a minute ago? Well here it is:

package rltut;

public class PlayerAi extends CreatureAi {

    public PlayerAi(Creature creature) {
    super(creature);
    }
}

We need to override the onEnter method to dig through walls and walk on ground tiles.

public void onEnter(int x, int y, Tile tile){
    if (tile.isGround()){
        creature.x = x;
        creature.y = y;
    } else if (tile.isDiggable()) {
        creature.dig(x, y);
    }
}

If your world has doors then you can make the player automatically open them by walking into them with code very similar to this.

Instead of checking the tile type directly we just ask if it can be walked on or dug through like with isDiggable. Here's our little addition to the Tile class to support that.

public boolean isGround() {
    return this != WALL && this != BOUNDS;
}

We've got a creature class and classes for creature ai — so far so good. We're going to create a lot of creatures that have the same values, all goblins will have a g glyph etc, and we need to make sure we always wire up the correct ai for each new creature. To centralize and hide all this assembly we'll create a class that's responsible for nothing else: the CreatureFactory. Using a factory means the other code doesn't have to deal with all this assembly each time a new creature is created.

package rltut;

import asciiPanel.AsciiPanel;

public class CreatureFactory {
    private World world;

    public CreatureFactory(World world){
        this.world = world;
    }
}

The only creature we're assembling so far is the player so that's the only method we need to add.

public Creature newPlayer(){
    Creature player = new Creature(world, '@', AsciiPanel.brightWhite);
    world.addAtEmptyLocation(player);
    new PlayerAi(player);
    return player;
}

Since the creature needs to start on some empty space and we don't really care which one, we'll add an addAtEmptyLocation method to the world class and let the world take care of that for us.

public void addAtEmptyLocation(Creature creature){
    int x;
    int y;

    do {
        x = (int)(Math.random() * width);
        y = (int)(Math.random() * height);
    }
    while (!tile(x,y).isGround());

    creature.x = x;
    creature.y = y;
}

All that's left is updating the PlayScreen to display and control our @ rather than scrolling on it's own. Our player is very similar to the current scrolling stuff so it won't be difficult to swap it out.

Remove the centerX and centerY variables and use a variable named player instead. Remove the scrollBy method and replace calls to scrollBy with calls to player.moveBy. (find and replace is your friend for things like this). Also replace references to centerX and centerY with player.x and player.y.

Now that we have a real live hero, we can display that rather than the 'X'.

terminal.write(player.glyph(), player.x - left, player.y - top, player.color());

If you missed any of these steps then the compiler or IDE should let you know. Just remember: error messages are your friend and will help you find what needs to be done.

Now you need to make a CreatureFactory and use it to create the player's creature. This can be done as the last step of the PlayScreen constructor.

CreatureFactory creatureFactory = new CreatureFactory(world);
player = creatureFactory.newPlayer();

Just about everything here was adding new classes or a few little methods. The only code we had to really muck around with was a fairly straightforward swap out of the temporary scrolling stuff. That may have seemed like a lot of work just to replace the X on the screen with an @, but we did a lot more than that and are in a really good position for moving on and adding other monsters.

download the code

Friday, August 26, 2011

roguelike tutorial 03: scrolling through random caves

Time to work on actual gameplay. Sort of. Well ... not really. A place for gameplay to happen. A world for our heroes, foes, and treasures.

Roguelikes happen somewhere. A somewhere made of floors, walls, rivers, trees, caves, doors, or whatever you can imagine. Since this is a tutorial to show the basics, we'll start with two kinds of environment tiles: cave floors and cave walls. I've found it's often useful to have another kind that represents out of bounds. That way instead of having to always check if something is out of bounds before checking the map about a specific tile, we can just ask and the map and it can tell us it's out of bounds and we can handle that however we want. If you're familiar with the NullObject design pattern then it's very similar; I guess you could call it an OutOfBoundsObject.

Since we're talking about tiles, let's have a Tile class. Each Tile needs to be displayed so we need a glyph to display and a color to display it with. Since we only have a few different tile types, and all tiles of the same type look and behave the same, we can represent the tiles as a java enum.

package rltut;

import java.awt.Color;
import asciiPanel.AsciiPanel;

public enum Tile {
    FLOOR((char)250, AsciiPanel.yellow),
    WALL((char)177, AsciiPanel.yellow),
    BOUNDS('x', AsciiPanel.brightBlack);

    private char glyph;
    public char glyph() { return glyph; }

    private Color color;
    public Color color() { return color; }

    Tile(char glyph, Color color){
        this.glyph = glyph;
        this.color = color;
    }
}

I like using extended ascii characters since AsciiPanel supports code page 437, but if you want to use '#' and '.', or something else entirely, go ahead. This is the place to do that.

Now that we have cave walls and floors, we need a World to hold them.

package rltut;

import java.awt.Color;

public class World {
    private Tile[][] tiles;
    private int width;
    public int width() { return width; }

    private int height;
    public int height() { return height; }

    public World(Tile[][] tiles){
        this.tiles = tiles;
        this.width = tiles.length;
        this.height = tiles[0].length;
    }
}

And now that we have a world made up of tiles we can add some methods to get details about them.

public Tile tile(int x, int y){
        if (x < 0 || x >= width || y < 0 || y >= height)
            return Tile.BOUNDS;
        else
            return tiles[x][y];
    }

public char glyph(int x, int y){
        return tile(x, y).glyph();
    }

public Color color(int x, int y){
        return tile(x, y).color();
    }

By checking for bounds here we don't need to worry about out of bounds errors and check everythime we ask the world about a location.

That's perfect for getting details about our world of tiles but we don't have a way of creating the tiles a World is made of. We could add a bunch of methods to create a World, but I like having the World class only responsible for the running of a world not creating it. Creating a new world is an entirely different and complicated subject that's only relevant at the beginning of a game and should be forgotten about right after we have a world to work with. Something else needs to create, or build, a world. And if we have something else who's only responsibility is building a new world, you could use the Builder pattern and call it a WorldBuilder.



To create a WorldBuilder you need a world size. Then you can call methods, in fluent style, to build up a world. Once you've specified how to build the world you want, you call the build method and you get a new World to play with.

package rltut;

public class WorldBuilder {
    private int width;
    private int height;
    private Tile[][] tiles;

    public WorldBuilder(int width, int height) {
        this.width = width;
        this.height = height;
        this.tiles = new Tile[width][height];
    }

    public World build() {
        return new World(tiles);
    }
}

The simplest interesting (i.e. randomized) world I know of is a world of caves. I came up with a basic algorithm to build randomized caves myself, although it's just a simple form of cellular automata and I'm not the first to come up with it. The process is to fill the area with cave floors and walls at random then to smooth everything out by turning areas with mostly neighboring walls into walls and areas with mostly neighboring floors into floors. Repeat the smoothing process a couple times and you have an interesting mix of cave walls and floors.

So the builder should be able to randomize the tiles.

private WorldBuilder randomizeTiles() {
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                tiles[x][y] = Math.random() < 0.5 ? Tile.FLOOR : Tile.WALL;
            }
        }
        return this;
    }

And repeatedly smooth them.

private WorldBuilder smooth(int times) {
        Tile[][] tiles2 = new Tile[width][height];
        for (int time = 0; time < times; time++) {

         for (int x = 0; x < width; x++) {
             for (int y = 0; y < height; y++) {
              int floors = 0;
              int rocks = 0;

              for (int ox = -1; ox < 2; ox++) {
                  for (int oy = -1; oy < 2; oy++) {
                   if (x + ox < 0 || x + ox >= width || y + oy < 0
                        || y + oy >= height)
                       continue;

                   if (tiles[x + ox][y + oy] == Tile.FLOOR)
                       floors++;
                   else
                       rocks++;
                  }
              }
              tiles2[x][y] = floors >= rocks ? Tile.FLOOR : Tile.WALL;
             }
         }
         tiles = tiles2;
        }
        return this;
    }

We put the new tile into tiles2 because it's usually a bad idea to update data that you're using as input to next updates. It's hard to explain but if you change the code to not use the tiles2 variable you'll see what I mean.

I don't like all those nested loops. Arrow code like this is usually a bad sign but this is simple enough and only used during world gen so I'll leave it as it is for now. This is also just part of working with multi dimentional arrays in java.

And that's how you can make some caves.

public WorldBuilder makeCaves() {
    return randomizeTiles().smooth(8);
}

So now we can create a World of Tiles to play around in. But in order to play in our new world of cave floors and cave walls, we need to display it. Scrolling is easy to implement so we'll add that now. All this talk of playing reminds me of our PlayScreen class, which makes since because it's responsible for displaying the world we're playing in and reacting to player input.

If we want the PlayScreen class to display a world then we need to make some changes to it. We need to track the world we're looking at, what part we're looking at, and how much of the screen is used for displaying the world. Here's the variables and constructor to add to the PlayScreen:

private World world;
    private int centerX;
    private int centerY;
    private int screenWidth;
    private int screenHeight;

    public PlayScreen(){
        screenWidth = 80;
        screenHeight = 21;
        createWorld();
    }

The createWorld method does exactly that, create's a world. I have a feeling this is going to expand as we make the world more interesting so putting it in a separate method will reduce how tangled it get's with other code and make changes easier later on.

private void createWorld(){
        world = new WorldBuilder(90, 31)
              .makeCaves()
              .build();
    }

We need a method to tell us how far along the X axis we should scroll. This makes sure we never try to scroll too far to the left or right.

public int getScrollX() {
    return Math.max(0, Math.min(centerX - screenWidth / 2, world.width() - screenWidth));
}

And we need a method to tell us how far along the Y axis we should scroll. This makes sure we never try to scroll too far to the top or bottom.

public int getScrollY() {
    return Math.max(0, Math.min(centerY - screenHeight / 2, world.height() - screenHeight));
}

We need a method to display some tiles. This takes a left and top to know which section of the world it should display.

private void displayTiles(AsciiPanel terminal, int left, int top) {
    for (int x = 0; x < screenWidth; x++){
        for (int y = 0; y < screenHeight; y++){
            int wx = x + left;
            int wy = y + top;

            terminal.write(world.glyph(wx, wy), x, y, world.color(wx, wy));
        }
    }
}

Now that we have a world to look at, we need to update the displayOutput method to show the section we're looking at on part of the screen - the rest of the screen is for user stats, messages, etc.

int left = getScrollX();
        int top = getScrollY();
   
        displayTiles(terminal, left, top);

Might as well show where were actually looking while we are here.

terminal.write('X', centerX - left, centerY - top);

We also need a new method to actually scroll. It should make sure we're never trying to scroll out of bounds.

private void scrollBy(int mx, int my){
        centerX = Math.max(0, Math.min(centerX + mx, world.width() - 1));
        centerY = Math.max(0, Math.min(centerY + my, world.height() - 1));
    }

Lastly, we need to add cases the the respondToUserInput code se we scroll based on user input.

case KeyEvent.VK_LEFT:
        case KeyEvent.VK_H: scrollBy(-1, 0); break;
        case KeyEvent.VK_RIGHT:
        case KeyEvent.VK_L: scrollBy( 1, 0); break;
        case KeyEvent.VK_UP:
        case KeyEvent.VK_K: scrollBy( 0,-1); break;
        case KeyEvent.VK_DOWN:
        case KeyEvent.VK_J: scrollBy( 0, 1); break;
        case KeyEvent.VK_Y: scrollBy(-1,-1); break;
        case KeyEvent.VK_U: scrollBy( 1,-1); break;
        case KeyEvent.VK_B: scrollBy(-1, 1); break;
        case KeyEvent.VK_N: scrollBy( 1, 1); break;

And now you have some random caves that you can look around in.


That seems like a fair bit of work. On the other hand, we did create a way to build thousands of worlds (90 tiles by 32 tiles with 2 tiles types = 5,760 2^2880 possible worlds) and a way to look around in them so hopefully it's worth it. Not only that, but most of this was from adding new code and not modifying old code; that's always a good sign. You should also try adding new tile types, or tweaking the cave algorithm, changing the world size, importing a hard coded level, or even implementing other world generation algorithms.

download the code

Friday, August 19, 2011

roguelike tutorial 02: input, output, modes, and screens

Nearly all games follow the same basic main loop:

while the game isn't over:
    show stuff to the user
    get user input
    respond to user input

Roguelikes are no different. But showing stuff to the user, getting user input, and responding to the input doesn't always mean the same thing. Usually we're showing the world and waiting for a player's command but sometimes we're showing a list of spells and waiting for the user to tell us which one to cast or maybe we're showing the player how he died and asking if he wants to play again. Said another way, sometimes we're in "play" mode, sometimes in "select spell" mode, and sometimes "you lost" mode. Each mode has a different way of handling input and output, and I've found that having a different class for each mode with it's own input and output logic is a good way of handling that — much better than having a big mess of if statements and mode-related variables.

Each mode will be represented by a different screen. Each screen displays output on our AsciiPanel and responds to user input; this abstraction can be represented as a simple Screen interface.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public interface Screen {
    public void displayOutput(AsciiPanel terminal);

    public Screen respondToUserInput(KeyEvent key);
}

The displayOutput method takes an AsciiPanel to display itself on and the respondToUserInput takes the KeyEvent and can return the new screen. This way pressing a key can result in looking at a different screen.

The first screen players will see is the StartScreen. This is just a screen that displays some info and sets us in "play" mode when the user hits enter.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public class StartScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.write("rl tutorial", 1, 1);
        terminal.writeCenter("-- press [enter] to start --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
    }
}

The PlayScreen class will be responsible for showing the dungeon and all it's inhabitants and loot — but since we don't have that yet we'll just tell the player how much fun he's having. It will also respond to user input by moving the player and either setting us in "win" mode if we won the game, or "lose" mode if we lost.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public class PlayScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.write("You are having fun.", 1, 1);
        terminal.writeCenter("-- press [escape] to lose or [enter] to win --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        switch (key.getKeyCode()){
        case KeyEvent.VK_ESCAPE: return new LoseScreen();
        case KeyEvent.VK_ENTER: return new WinScreen();
        }
    
        return this;
    }
}

Yes, using escape and enter to lose and win is pretty lame, but we know it's temporary and we can swap it out for real stuff later.

The WinScreen will eventually display how awesome our brave hero is and ask if they'd like to play again. But not yet.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public class WinScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.write("You won.", 1, 1);
        terminal.writeCenter("-- press [enter] to restart --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
    }
}

The LoseScreen will eventually display how lame our foolish hero was and ask if they'd like to play again. But not yet.

package rltut.screens;

import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;

public class LoseScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.write("You lost.", 1, 1);
        terminal.writeCenter("-- press [enter] to restart --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
    }
}

That's it for the screens. Each one is only a dozen or so lines and does only a few simple things. That's a really good sign: small classes with few related responsibilities are good. Very good.

The ApplicationMain class needs to be updated though. It now has to display the current screen when the window repaints and pass user input to the current screen. It's generally best to avoid changing current code but this is only delegating input and output to other things, exactly what ApplicationMain is for. If we were changing it so it handles game logic then I'd be worried, but this is ok.

package rltut;

import javax.swing.JFrame;
import asciiPanel.AsciiPanel;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import rltut.screens.Screen;
import rltut.screens.StartScreen;

public class ApplicationMain extends JFrame implements KeyListener {
    private static final long serialVersionUID = 1060623638149583738L;

    private AsciiPanel terminal;
    private Screen screen;

    public ApplicationMain(){
        super();
        terminal = new AsciiPanel();
        add(terminal);
        pack();
        screen = new StartScreen();
        addKeyListener(this);
        repaint();
    }

    public void repaint(){
        terminal.clear();
        screen.displayOutput(terminal);
        super.repaint();
    }

    public void keyPressed(KeyEvent e) {
        screen = screen.respondToUserInput(e);
        repaint();
    }

    public void keyReleased(KeyEvent e) { }

    public void keyTyped(KeyEvent e) { }

    public static void main(String[] args) {
        ApplicationMain app = new ApplicationMain();
        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.setVisible(true);
    }
}

Don't forget to update the AppletMain class to handle input and delegate everything to it's current screen.

I'm sure you want to have an @ running around quaffing potions and saying dragons but what we've done so far is very good. Not only are we ready for inventory screens, targeting screens, help screens, and any other screen you can think of, but we've already begin thinking about how user input and output is different than actual gameplay and should be in it's own separate code. Let me repeat, bold, and italicize that: It's good when input, output, and gameplay logic are separate. Once you start mixing them, everything goes downhill. In fact, all we really need to do now is implement the guts of PlayScreen. We'll start that next.

download the code

Tuesday, August 16, 2011

roguelike tutorial 01: Java, Eclipse, AsciiPanel, application, applet

This tutorial will be written in Java since I'm familiar with it and it's a decent enough language, has many tools and libraries, a large and helpful community, and runs on Mac, Windows, linux, and web browsers. It's assumed you know at least the basics but I'll explain some of it as I go along. If you have a preferred IDE then go ahead and use that; otherwise, download the latest version of Eclipse since it's powerful and used by may developers, including me. The one other thing this tutorial uses is AsciiPanel (sourcejar), a side project I started to help display the old-school ascii graphics so common to roguelikes. Even if you want to support graphics, starting with ascii will let us get started quickly.

So let's get started. Download and start Eclipse (or whatever IDE you're most familiar with) and start up a new project. I'm calling mine "rltut". Download the AsciiPanel jar file and add that to your project.

We'll start with something very simple: just a window with some text on it.

package rltut;

import javax.swing.JFrame;
import asciiPanel.AsciiPanel;

public class ApplicationMain extends JFrame {
    private static final long serialVersionUID = 1060623638149583738L;

    private AsciiPanel terminal;

    public ApplicationMain(){
        super();
        terminal = new AsciiPanel();
        terminal.write("rl tutorial", 1, 1);
        add(terminal);
        pack();
    }

    public static void main(String[] args) {
        ApplicationMain app = new ApplicationMain();
        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.setVisible(true);
    }
}

If you're using Eclipse, your project
should look something like this
A humble beginning, but it means that we have all our tools, libraries, and project settings wired up correctly, which can be one of the most frustrating parts. We'll need a few minor changes but the ApplicationMain class is going to stay simple, it's only responsibility is to create a window and delegate input and output to other things.

The serialVersionUID is suggested by Eclipse and helps to prevent show-stopping failures when serializing different versions of our class. We won't be doing that in this tutorial but it's almost always a good idea to take care of compiler and IDE warning as soon as possible; It will save much trouble down the line.

The ApplicationMain constructor has all the set up code. So far that's just creating an AsciiPanel to display some text and making sure the window is the correct size. The AsciiPanel defaults to 80 by 24 characters but you can specify a different size in it's constructor - go ahead and try it. Play around with the write method while you're at it.

The main method just creates an instance of our window and show's it, making sure that the application exits when the window is closed. Simple as can be.

For extra awesomeness you can make your roguelike run from the users browser as an applet. Just add a file like this to your project:

package rltut;

import java.applet.Applet;
import asciiPanel.AsciiPanel;

public class AppletMain extends Applet {
    private static final long serialVersionUID = 2560255315130084198L;

    private AsciiPanel terminal;

    public AppletMain(){
        super();
        terminal = new AsciiPanel();
        terminal.write("rl tutorial", 1, 1);
        add(terminal);
    }

    public void init(){
        super.init();
        this.setSize(terminal.getWidth() + 20, terminal.getHeight() + 20);
    }

    public void repaint(){
        super.repaint();
        terminal.repaint();
    }
}

It's a good start. You don't have much but anyone can play it since it runs on any modern computer either from the user's browser or downloaded and run from the user's machine.

download the code

Saturday, August 13, 2011

roguelike tutorial: the what and why

Let's make a roguelike!

I've made a few roguelikes — mostly for myself — and I think they're a fun mix of computer science algorithms, code design decisions, and game design decisions. They are also fun to make and a great way to try new programing ideas and new game ideas.

I've created a 20 part series that I'm going to post here where I stumble my way through creating a roguelike that has most of the features any mainstream roguelike has. I consider myself a decent programmer but I'll try some things that may or may not work out and, for the sake of explanation via blog posts, cut some corners and leave out a few details. Because of that, don't take this as an example of the ideal rogulike or ideal code but if you stumble through it with me and add your own ideas, you'll have a roguelike too and, more importantly, may be better armed to make your own roguelike or contribute to someone else's roguelike.

Here's the rundown of what's to come:

Part 01: Java, Eclipse, AsciiPanel, application, applet
Part 02: input, output, modes and screens
Part 03: scrolling through random caves
Part 04: the player
Part 05: stationary monsters
Part 06: hitpoints, combat, and messages
Part 07: z levels and deeper caves
Part 08: vision, line of sight, and field of view
Part 09: wandering monsters
Part 10: items, inventory, inventory screens
Part 11: hunger and food
Part 12: weapons and armor
Part 13: aggressive monsters
Part 14: experience and leveling up
Part 15: help, examine, and look screens
Part 16: throwing and ranged weapons
Part 17: smarter monsters
Part 18: potions and effects
Part 19: mana, spells, and magic books
Part 20: item appearance and identification

Some experience with Java and object oriented programming is expected but I can answer any questions that you may have. Not only am I including the code for each part, but I've got a final version, with a few additional tweaks, running as an applet on my blog.

Nameless roguelike for tutorial

Here's a little roguelike I made over a few days. It was mostly for my own amusement and to try a few different programming things so it doesn't have anything new or great. I also created a tutorial about how I wrote it. This applet can be downloaded and run from the desktop too.




Standard story: descend the Caves Of Slight Danger, find the lost Teddy Bear, and return to the surface to win. Use what you find to avoid dying. Press [?] for help.

main jar file: https://sites.google.com/site/trystansprojects/rltutfinal.jar
display jar file: https://sites.google.com/site/trystansprojects/asciiPanel.jar

[edit]Some people were having problems with it not displaying correctly. I think I've fixed it but let me know if there are problems.[/edit]

Saturday, August 6, 2011

Living with observational tests

I'd say that at least half of the unit tests I look at end up having a ton of extra work and extra assertions. It's always really frustrated me but I think some developers just find this style of tests to be more helpful. I think some people view tests as a central claim with proof and others view it as a record of observations.

I view unit tests like a math proof or logical argument. I have a claim (also known as an assertion) and I have the simplest proof of that claim I can think of. If my claim is that new bank accounts start with a zero balance then my test will mention the claim in the title and assertion. Maybe something like this:

[Test]
public void NewBankAccountShouldStartWithAZeroBalance()
{
    BankAccount newBankAccount = new BankAccount();
    Assert.AreEqual(0, newBankAccount.Balance, "A new bank account should start with a zero balance.");
}

My recent insight is that others seem to view tests more like a record of observations. Imagine walking through a park and writing what you see and hear along the way. "There's a dog. The dog is barking. There's a tree. There's a guy in a suit. The dog is still barking. There's a jogger." etc. Many of the unit test I see are like this, a walk through some scenario or object lifetime with observations made along the way:

[Test]
public void BankAccountTest()
{
    BankAccount account = new BankAccount();
    Assert.IsTrue(account != null, "The account is not null");
    Assert.IsTrue(account.Balance == 0, "The balance is zero."); 
    
    account.DoSomething("ABC", 123);
    Assert.IsTrue(account.Things.Count == 3, "The account has three things."); 
    Assert.IsTrue(account.Balance == 0, "The balance is zero."); 

    // 60 more lines of stuff somewhat related to bank accounts....
}

My frustrations came from realizing that these tests were written differently than what I prefer but not realizing that it's because there's a completely different way of looking at what a test is. The assertion messages are worded differently, the test is named differently, the assertions are different, the only similarity is that both styles are run from NUnit. These aren't unit tests in the strict meaning, they aren't integration tests, and they aren't really system tests; they're what I think of as observational tests — just a bunch of observations. I couldn't figure out what the point of this kind of test was because there is no point to the test. The assertions seemed irrelevant or superfluous because the assertions aren't used as proof of a central claim, they're just things that the original developer noticed or had on his mind when writing the test. They don't say what something should be or why because they're just observations of how things are — there is no expectation or reasoning behind it — just a bunch of observations.

I'm still not sure what to do with failing assertions on these observational tests. I don't want to delete the entire test because they provide some value to someone (well, actually yes I do want to delete them but I won't). These observational tests often fail but I've never found a bug in the code from these; it's always been a bad test or a valid change in the code that broke the now invalid assertion. There's no point in trying to change the developers who like these tests since not only do they prefer these observational tests but I've already tried and failed. I think that if it doesn't look like an obvious bug then I should just update the assertion or delete it. That's what I've always ended up doing anyway but now I won't waste more than a couple minutes on it since it's not worth the frustration.

Friday, August 5, 2011

A unit test case study

I've been trying to pay more attention to my process for fixing broken unit tests and I'm trying to improve my writing skills, especially when it comes to descriptive narration, so I'm going to fix a test and write down my thoughts as I go. I'll change the business object names to Widgets and Things, but everything else is exactly what happened as it happened. Here goes.



I ran some of our unit tests and the first one to fail has this error message:

XXX.Test.YYY.Widgets.WidgetsTest:
  Two things should be available.
  Expected: True
  But was:  False
Uh, ok. No indication of which two things were expected or which two things were found. Not a very useful error message. The test name doesn't help me much since the Widgets domain encompasses so much. The Widgets code does use Things though so that part makes sense.

I'm guessing the requirements and code changed so instead of two Things available there are now more. This is what happens with at least 95% of the broken tests I look at. I've gotten all I can from the test name and assertion message so it's time to take a look at the test itself.

public void WidgetsTest()
{
    // 44 lines of code
    Assert.IsTrue(thingList.Count == 2, "Two things should be available.");
    // 17 more lines of code
}
Ugh. I'm not even going to try to wrap my head around this yet. Some people are really good at reading through long methods where state winds in and out of methods but I'm not. I'll start with a small change and use AreEqual instead of IsTrue so the failure message indicates what was expected and what was found.
public void WidgetsTest()
{
    // 44 lines of code
    Assert.AreEqual(2, thingList.Count, "Two things should be available.");
    // 17 more lines of code
}
At least now I can see how many were available. It would be better if I know which two Things were expected so I could assert that only ThingA and ThingB are available. Even better than that would be knowing why so I could add that to the message, something like "ThingA and ThingB should be available if Widget.IsAwesome()". That way someone could see it and say "yup, there's a bug" or "no that's not true anymore". At least that would work better for me but I remind myself that I can't really fault others for writing tests in the style they prefer to work with.

Now I wait nearly five minutes while the tests rebuild then wait about a minute for the test to fail again. New error message:

XXX.Test.YYY.Widgets.WidgetsTest:
  Two things should be available.
  Expected: 2
  But was:  3
That's what I expected. I'm pretty sure no one updated the test when they changed the code. Had it said 0 or 500 or something interesting like that then it might be a bug but I'd say there's now a 99% chance it's just an invalid assertion. There is a reference in the test to an issue number so I look that up....

WTF? This issue has absolutely nothing to do with with Things or Widgets! Maybe it's a typo? Maybe it's a bad April Fools joke? Maybe the test was copied and pasted and this reference was left in? I've noticed that's common; I guess people copy the entire thing and leave in a bunch of irrelevant code and assertions that are no longer needed. I search the tests for the original assertion and see that there's one other test with the same line of code. Not only is almost the entire test is the same as the one that's failing but this one is also older so it's almost certainly a copy-and-paste job. This original test does have a comment with an issue number but when I look that up I find the issue has nothing to do with Widgets or Things either. Maybe I can check source control to see when these tests were made and who made them?

The original developer of both tests is long gone so I can't ask him what this is really about. Another developer who's focus is a different domain has made changes to the failing test eight months ago. He probably won't remember but I'll ask him later if I run into a dead end.

I try looking at our continuous integration logs to see when the test started failing but no luck: our logs only go back a couple months and this has been broken the whole time. *sigh* We've always got broken tests but this one has actually been broken since the beginning of recorded history. Looks like I'll have to dive into the test code and see what it's doing.

The test starts by dropping the database then reloading a backup (I've been here 5 or so years and brought this up before and I still don't know why so many of our tests do this), creates a handful of objects while doing a ton of work with them, reading and writing to the file system and database through it all. It even clears some caches along the way; I've always wondered if so many of other peoples tests do this because of superstition or to make sure we do as much work as possible. There are several comments but they just say the same thing as the code following it with no indication of why any of it is being done. This test seems to be running through some very specific scenario involving Widgets from beginning to end. There's a lot of data and I'm not sure what's relevant and what just happened to be that way. There are 14 scattered assertions, not including the assertions buried within other methods, on seemingly random objects and values. A lot of this work is somewhat related to Widgets but this is the only line related to Things. In fact, only one of the assertions is directly related to Widgets.

Time to recap: I've got a failing assertion in a large test filled with seemingly arbitrary assertions and a lot of work. The test name says nothing about what is expected or why. This failing assertion says nothing about what is expected or why. The issue it refers to is completely unrelated to Things or Widgets and the original developer has moved on to another job. I have no idea when it started failing or why and what it's asserting on doesn't even seem to be relevant to the other work happening in the test. This is all quite normal for our tests. Luckily it's the end of the day so I send an email to the other developer who worked on this eight months ago and leave this until tomorrow.

Here's the email:
Unit test XXX.Test.YYY.Widgets.WidgetsTest has a failing assertion on line 2199. The message is "Two Things should be available.". It looks like this test is from you and Mr Doesn't Work Here Anymore. Are there two specific Things or are any two Things acceptable? Why are those two Things expected and why are three Things a problem?

Hopefully he has some answers.



When I get to work the next day there's no response from the developer I emailed. I move on to some other tasks until I can ask him or someone else.

Eventually another developer is available to help me with this one and we walk through almost the same steps that I did. It turns out that if you look at the beginning of the test a certain way and you look at the issue a certain way, then the test looks like it's related to something implied by part of the issue. The weird part is the test has only one assertion related to that and that assertion is the exact opposite of what the issue says.

After about an hour of rereading it and commenting out chunks of the test to see what happens we figure that the last half of the test is completely unnecessary and we delete it — including the failing assertion. Now the test passes and I can move on to the next test.