Friday, October 7, 2011

roguelike tutorial 15: help, examine, and look screens

We've got the basics of a decent game so far. Let's take some time out to add some more screens.

We'll start with the easiest, a HelpScreen.

package rltut.screens;

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

public class HelpScreen implements Screen {

    public void displayOutput(AsciiPanel terminal) {
        terminal.clear();
        terminal.writeCenter("roguelike help", 1);
        terminal.write("Descend the Caves Of Slight Danger, find the lost Teddy Bear, and return to", 1, 3);
        terminal.write("the surface to win. Use what you find to avoid dying.", 1, 4);
    
        int y = 6;
        terminal.write("[g] or [,] to pick up", 2, y++);
        terminal.write("[d] to drop", 2, y++);
        terminal.write("[e] to eat", 2, y++);
        terminal.write("[w] to wear or wield", 2, y++);
        terminal.write("[?] for help", 2, y++);
        terminal.write("[x] to examine your items", 2, y++);
        terminal.write("[;] to look around", 2, y++);
    
        terminal.writeCenter("-- press any key to continue --", 22);
    }

    public Screen respondToUserInput(KeyEvent key) {
        return null;
    }
}

That's sufficient but kind of lame. I'm sure you could come up with a better story and maybe say something about what all the symbols are. Don't forget to add this new screen to the respondToUserInput method in the PlayScreen class.


Now lets make a screen that tells us details about what's in our inventory. I'll call it the ExamineScreen and map it to the x key in the PlayScreen.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class ExamineScreen extends InventoryBasedScreen {

    public ExamineScreen(Creature player) {
        super(player);
    }

    protected String getVerb() {
        return "examine";
    }

    protected boolean isAcceptable(Item item) {
        return true;
    }

    protected Screen use(Item item) {
        String article = "aeiou".contains(item.name().subSequence(0, 1)) ? "an " : "a ";
        player.notify("It's " + article + item.name() + "." + item.details());
        return null;
    }
}

And add a details method to the Item class. You can do whatever you like but here's what I came up with:
public String details() {
    String details = "";

    if (attackValue != 0)
        details += "     attack:" + attackValue;

    if (defenseValue != 0)
        details += "     defense:" + defenseValue;

    if (foodValue != 0)
        details += "     food:" + foodValue;
    
    return details;
}

You could also display some extra description that gets passed into the item constructor.


Let's start our screen to let us look around. We'll let the user pick a tile and then tell them what it is. If you think about it, this isn't the only time the user will pick a tile through. Throwing, firing bows, and aiming spells all involve picking a tile. Since the InventoryBasedScreen has payed off so well, I think we should create a TargetBasedScreen. Let's get to it.

package rltut.screens;

import java.awt.event.KeyEvent;
import rltut.Creature;
import rltut.Line;
import rltut.Point;
import asciiPanel.AsciiPanel;

public abstract class TargetBasedScreen implements Screen {

    protected Creature player;
    protected String caption;
    private int sx;
    private int sy;
    private int x;
    private int y;

    public TargetBasedScreen(Creature player, String caption, int sx, int sy){
        this.player = player;
        this.caption = caption;
        this.sx = sx;
        this.sy = sy;
    }
}

We'll keep track of the player, a caption representing what we're targeting, the screen coordinates where the player is looking from, and the s and y offset of where we're targeting. The player and caption are protected so our subclasses can use them. Don't worry, it will make sense.

When it's time to display the output, we need to draw a line from the player to the target. I chose a line of magenta *s, but that's up to you. We also need to display the caption to the user.
public void displayOutput(AsciiPanel terminal) {
    for (Point p : new Line(sx, sy, sx + x, sy + y)){
        if (p.x < 0 || p.x >= 80 || p.y < 0 || p.y >= 24)
            continue;
        
        terminal.write('*', p.x, p.y, AsciiPanel.brightMagenta);
    }
    
    terminal.clear(' ', 0, 23, 80, 1);
    terminal.write(caption, 0, 23);
}

The user can change what's being targeted with the movement keys, select a target with Enter, or cancel with Escape. If the user tries to target something it can't, like firing out of range, then we go back to where we were targeting before.

public Screen respondToUserInput(KeyEvent key) {
        int px = x;
        int py = y;

        switch (key.getKeyCode()){
        case KeyEvent.VK_LEFT:
        case KeyEvent.VK_H: x--; break;
        case KeyEvent.VK_RIGHT:
        case KeyEvent.VK_L: x++; break;
        case KeyEvent.VK_UP:
        case KeyEvent.VK_J: y--; break;
        case KeyEvent.VK_DOWN:
        case KeyEvent.VK_K: y++; break;
        case KeyEvent.VK_Y: x--; y--; break;
        case KeyEvent.VK_U: x++; y--; break;
        case KeyEvent.VK_B: x--; y++; break;
        case KeyEvent.VK_N: x++; y++; break;
        case KeyEvent.VK_ENTER: selectWorldCoordinate(player.x + x, player.y + y, sx + x, sy + y); return null;
        case KeyEvent.VK_ESCAPE: return null;
        }
    
        if (!isAcceptable(player.x + x, player.y + y)){
            x = px;
            y = py;
        }
    
        enterWorldCoordinate(player.x + x, player.y + y, sx + x, sy + y);
    
        return this;
    }

We'll provide a simple method to determine if a tile is an acceptable target. Subclasses can override this if they want something more specific.

public boolean isAcceptable(int x, int y) {
        return true;
    }

After each time the target moves, we let subclasses do whatever they want, usually this will be to update the caption or do nothing.
public void enterWorldCoordinate(int x, int y, int screenX, int screenY) {
    }

And we do the same once the user has selected a specific location.

public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
    }

This should provide a good base for any kind of targeting action.


The simplest targeting action is looking at surroundings; a LookScreen.

package rltut.screens;

import rltut.Creature;
import rltut.Item;
import rltut.Tile;

public class LookScreen extends TargetBasedScreen {

    public LookScreen(Creature player, String caption, int sx, int sy) {
        super(player, caption, sx, sy);
    }

    public void enterWorldCoordinate(int x, int y, int screenX, int screenY) {
        Creature creature = player.creature(x, y, player.z);
        if (creature != null){
            caption = creature.glyph() + " "     + creature.name() + creature.details();
            return;
        }
    
        Item item = player.item(x, y, player.z);
        if (item != null){
            caption = item.glyph() + " "     + item.name() + item.details();
            return;
        }
    
        Tile tile = player.tile(x, y, player.z);
        caption = tile.glyph() + " " + tile.details();
    }
}

This will display details to the user about whatever they are targeting. If you use this code then you'll need to create a few methods to get details about creatures and tiles. Don't forget to map it to the ';' key, or whatever key you want, in the PlayScreen.


Add a details method to the Creature class.
public String details() {
        return String.format("     level:%d     attack:%d     defense:%d     hp:%d", level, attackValue(), defenseValue(), hp);
    }

And add details to the Tile class.

FLOOR((char)250, AsciiPanel.yellow, "A dirt and rock cave floor."),
    WALL((char)177, AsciiPanel.yellow, "A dirt and rock cave wall."),
    BOUNDS('x', AsciiPanel.brightBlack, "Beyond the edge of the world."),
    STAIRS_DOWN('>', AsciiPanel.white, "A stone staircase that goes down."),
    STAIRS_UP('<', AsciiPanel.white, "A stone staircase that goes up."),
    UNKNOWN(' ', AsciiPanel.white, "(unknown)");

    private String details;
    public String details(){ return details; }

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


You may have noticed a problem with the LookScreen in that you can get details about things the player can't see. Let's fix it by making some small changes to the Creature class. Basically, only return what the creature can see or remember.

public Tile realTile(int wx, int wy, int wz) {
        return world.tile(wx, wy, wz);
    }

public Tile tile(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.tile(wx, wy, wz);
        else
            return ai.rememberedTile(wx, wy, wz);
    }

public Creature creature(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.creature(wx, wy, wz);
        else
            return null;
    }

public Item item(int wx, int wy, int wz) {
        if (canSee(wx, wy, wz))
            return world.item(wx, wy, wz);
        else
            return null;
    }

The CreatureAi canSee method needs to use the new realTile method to avoid getting caught in an infinite recursion loop.

Here's a good-enough-for-now implementation of the CreatureAi rememberedTile method since they don't actually have a memory:
public Tile rememberedTile(int wx, int wy, int wz) {
        return Tile.UNKNOWN;
    }

And the PlayerAi override:

public Tile rememberedTile(int wx, int wy, int wz) {
        return fov.tile(wx, wy, wz);
    }

Now we've got a help screen, a way to see details about what's in our inventory, and our surroundings. Nothing very glamorous or game changing this time but it's all very helpful for the user. The TargetBasedScreen should also make future screens easier.

download the code

Tuesday, October 4, 2011

roguelike tutorial 14: experience and leveling up

It's great that we've got some caves, equipment, and monsters but what about character development? Levels, feats, attributes, classes, and skill trees can be a complex subject worth studying but, and maybe you saw this coming, we'll do something simple to start with. For now, when a creature is killed, it's killer gains xp, when enough xp is gained, a new level is gained and some benefit is chosen. One thing I'd like to have is other creatures can gain levels too. Why? Just because I haven't seen it done before (at least not that I've noticed). It also means that positioning weak monsters between you and the big bad guy may just make the big bad guy gain levels. Maybe you should start rethinking the use of meat shields....

Our creatures need xp, a level, and a way to gain xp and levels. When the xp passes the next level threshold, the level should be incremented, the ai should get notified, and the creature should heal a bit.

private int xp;
  public int xp() { return xp; }
  public void modifyXp(int amount) {
      xp += amount;

      notify("You %s %d xp.", amount < 0 ? "lose" : "gain", amount);

      while (xp > (int)(Math.pow(level, 1.5) * 20)) {
          level++;
          doAction("advance to level %d", level);
          ai.onGainLevel();
          modifyHp(level * 2);
      }
  }

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

The starting level is initialized to 1 in the constructor. I'm using a formula to determine how much experience is needed for the next level but you can use a lookup table or some other formula. Many interesting things have been said about leveling and power curves so read up, try different things, and do what works for you.

We need to update the attack method to grant experience when a creature is killed. Add the following to the end of the attack method:

if (other.hp < 1)
      gainXp(other);

Now we can grant experience based on some experience value the creature has, or on it's level, or on the killers level, or by some combination. I'm using a simple formula for now.

public void gainXp(Creature other){
    int amount = other.maxHp
      + other.attackValue()
      + other.defenseValue()
      - level * 2;

    if (amount > 0)
      modifyXp(amount);
  }

This ensures that tougher creatures are worth more and by subtracting the killer's level, easy creatures will soon be worth nothing. It's not perfect but it's simple to explain, understand, and code.

If you play it now (after creating an empty onGainLevel in CreatureAi) you should see notices about gaining levels. That's a good sign.


For this simple roguelike, when something gains a level it get's some stat bonus; increased hp, increased attack, etc. The player will be shown a list to chose from but other creatures will get one at random.

Let's create a class to represent a level up option's name and actual effect.

package rltut;

public abstract class LevelUpOption {
  private String name;
  public String name() { return name; }

  public LevelUpOption(String name){
    this.name = name;
  }

  public abstract void invoke(Creature creature);
}

We need something to track all the possible options and enforce some of our level-up logic. We'll call it a LevelUpController — even though classes with Manager or Controller in the name are usually vague and messy and not the best way to do things.

package rltut;

import java.util.ArrayList;
import java.util.List;

public class LevelUpController {

  private static LevelUpOption[] options = new LevelUpOption[]{
    new LevelUpOption("Increased hit points"){
      public void invoke(Creature creature) { creature.gainMaxHp(); }
    },
    new LevelUpOption("Increased attack value"){
      public void invoke(Creature creature) { creature.gainAttackValue(); }
    },
    new LevelUpOption("Increased defense value"){
      public void invoke(Creature creature) { creature.gainDefenseValue(); }
    },
    new LevelUpOption("Increased vision"){
      public void invoke(Creature creature) { creature.gainVision(); }
    }
  };
}
I created a few simple options based on the stats we already have but I'm sure you can come up with more. These are anonymous classes - if you're not familiar with them you should check them out. Anonymous classes can make some things very clear and succinct - other things are best left to regular classes.

This LevelUpController should be able to select one option at random and apply it to a given creature.
public void autoLevelUp(Creature creature){
    options[(int)(Math.random() * options.length)].invoke(creature);
  }

Now the CreatureAi can call this to automatically gain some benefit when a creature gains a level.
public void onGainLevel() {
    new LevelUpController().autoLevelUp(creature);
  }

Of course the Creature class needs to support these new options too. Here's what I came up with:
public void gainMaxHp() {
    maxHp += 10;
    hp += 10;
    doAction("look healthier");
  }

  public void gainAttackValue() {
    attackValue += 2;
    doAction("look stronger");
  }

  public void gainDefenseValue() {
    defenseValue += 2;
    doAction("look tougher");
  }

  public void gainVision() {
    visionRadius += 1;
    doAction("look more aware");
  }

If you try it now you should see that when you gain a level you look tougher or stronger etc. You may even notice something else looking stronger or more aware. That's why I think it's so cool to have a method like doAction; you can, if you're lucky, see the rare and subtle events like these.

Try it.


What about when the player gains a level? Shouldn't we show a list of options for the user to choose from? Let's start by making sure the player doesn't automatically get free bonuses.

Override the onGainLevel method in the PlayerAi class.
public void onGainLevel(){
  }

Now we update the PlayScreen's respondToUserInput method. When we first enter the method, before the user does anything, we need to record the player's level.
int level = player.level();
After responding to the player's input, we need to see if that resulted in a level up. If so, we jump into a LevelUpScreen and tell it how many bonuses the player get's to pick.
if (player.level() > level)
      subscreen = new LevelUpScreen(player, player.level() - level);

Now all we need is a LevelUpScreen that uses a LevelUpController to show what can be picked and applies that choice.
package rltut.screens;

import java.awt.event.KeyEvent;
import java.util.List;

import rltut.Creature;
import rltut.LevelUpController;
import asciiPanel.AsciiPanel;

public class LevelUpScreen implements Screen {
  private LevelUpController controller;
  private Creature player;
  private int picks;

  public LevelUpScreen(Creature player, int picks){
    this.controller = new LevelUpController();
    this.player = player;
    this.picks = picks;
  }

  @Override
  public void displayOutput(AsciiPanel terminal) {
    List<String> options = controller.getLevelUpOptions();

    int y = 5;
    terminal.clear(' ', 5, y, 30, options.size() + 2);
    terminal.write("     Choose a level up bonus       ", 5, y++);
    terminal.write("------------------------------", 5, y++);

    for (int i = 0; i < options.size(); i++){
      terminal.write(String.format("[%d] %s", i+1, options.get(i)), 5, y++);
    }
  }

  @Override
  public Screen respondToUserInput(KeyEvent key) {
    List<String> options = controller.getLevelUpOptions();
    String chars = "";

    for (int i = 0; i < options.size(); i++){
      chars = chars + Integer.toString(i+1);
    }

    int i = chars.indexOf(key.getKeyChar());

    if (i < 0)
      return this;

    controller.getLevelUpOption(options.get(i)).invoke(player);

    if (--picks < 1)
      return null;
    else
      return this;
  }
}

And there you go; a simple leveling system that lets the player decide how they want to progress their character. You could add different bonuses like special moves, critical hits, extra xp per kill, or special abilities. You could even make it so the player could chose a new item after leveling up.

download the code

Friday, September 30, 2011

roguelike tutorial 13: aggressive monsters

Now that we have all these cool weapons and armor and food, the bat's and fungi aren't as troublesome as they used to be. We need something that charges straight for us, something that peruses us relentlessly, a simple minded foe that we don't want to run into. For that we need a way to find a path to the player.

The monsters are only going to pathfind to the player if they see him so we could do the simpleist thing and move east if the player is east, north if the player is north, etc. That would almost always work well enough but let's go ahead and add real path finding. Entire tutorials are written about path finding but for this we can use the following code that implements the A Star algorithm and is specialized for our creatures:

package rltut;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class PathFinder {
       private ArrayList<Point> open;
       private ArrayList<Point> closed;
       private HashMap<Point, Point> parents;
       private HashMap<Point,Integer> totalCost;
     
       public PathFinder() {
             this.open = new ArrayList<Point>();
             this.closed = new ArrayList<Point>();
             this.parents = new HashMap<Point, Point>();
             this.totalCost = new HashMap<Point, Integer>();
       }
     
       private int heuristicCost(Point from, Point to) {
             return Math.max(Math.abs(from.x - to.x), Math.abs(from.y - to.y));
       }

       private int costToGetTo(Point from) {
             return parents.get(from) == null ? 0 : (1 + costToGetTo(parents.get(from)));
       }
     
       private int totalCost(Point from, Point to) {
             if (totalCost.containsKey(from))
                 return totalCost.get(from);
           
             int cost = costToGetTo(from) + heuristicCost(from, to);
             totalCost.put(from, cost);
             return cost;
       }

       private void reParent(Point child, Point parent){
             parents.put(child, parent);
             totalCost.remove(child);
       }

       public ArrayList<Point> findPath(Creature creature, Point start, Point end, int maxTries) {
             open.clear();
             closed.clear();
             parents.clear();
             totalCost.clear();
       
             open.add(start);
           
             for (int tries = 0; tries < maxTries && open.size() > 0; tries++){
                   Point closest = getClosestPoint(end);
                 
                   open.remove(closest);
                   closed.add(closest);

                   if (closest.equals(end))
                         return createPath(start, closest);
                   else
                         checkNeighbors(creature, end, closest);
             }
             return null;
       }

        private Point getClosestPoint(Point end) {
            Point closest = open.get(0);
            for (Point other : open){
                if (totalCost(other, end) < totalCost(closest, end))
                    closest = other;
            }
            return closest;
        }

        private void checkNeighbors(Creature creature, Point end, Point closest) {
            for (Point neighbor : closest.neighbors8()) {
                if (closed.contains(neighbor)
                 || !creature.canEnter(neighbor.x, neighbor.y, creature.z)
                 && !neighbor.equals(end))
                     continue;
    
                if (open.contains(neighbor))
                    reParentNeighborIfNecessary(closest, neighbor);
                else
                    reParentNeighbor(closest, neighbor);
            }
        }

        private void reParentNeighbor(Point closest, Point neighbor) {
            reParent(neighbor, closest);
            open.add(neighbor);
        }

        private void reParentNeighborIfNecessary(Point closest, Point neighbor) {
            Point originalParent = parents.get(neighbor);
            double currentCost = costToGetTo(neighbor);
            reParent(neighbor, closest);
            double reparentCost = costToGetTo(neighbor);
  
            if (reparentCost < currentCost)
                open.remove(neighbor);
            else
                reParent(neighbor, originalParent);
        }

        private ArrayList<Point> createPath(Point start, Point end) {
            ArrayList<Point> path = new ArrayList<Point>();

            while (!end.equals(start)) {
                path.add(end);
                end = parents.get(end);
            }

            Collections.reverse(path);
            return path;
        }
    }

So far I've liked having Points and Lines where all the work is done in the constructor and would like to extend this idea to Paths. So let's create a Path class that hides the details from us.

package rltut;

import java.util.List;

public class Path {

  private static PathFinder pf = new PathFinder();

  private List<Point> points;
  public List<Point> points() { return points; }

  public Path(Creature creature, int x, int y){
      points = pf.findPath(creature, 
                           new Point(creature.x, creature.y, creature.z), 
                           new Point(x, y, creature.z), 
                           300);
  }
}

If having our Line path do all that work in the constructor was questionable then this is far more questionable. I may end up regretting this and making sure future employers never see this but for now I'll try it and we'll see if it becomes a problem.


Like with our other creatures we need a CreatureAi. I'll take the easy and uncreative way out and pick Zombies for our new monster. The ZombieAi will be a bit different than the others since it needs a reference to the player so it knows who to look for.

package rltut;

import java.util.List;

public class ZombieAi extends CreatureAi {
  private Creature player;

  public ZombieAi(Creature creature, Creature player) {
    super(creature);
    this.player = player;
  }
}

During the zombie's turn it will move to the player if it can see him, otherwise it will wander around. Since zombies are a little slow, I gave them a chance of doing nothing during their turn for just a little bit of interest.

public void onUpdate(){
      if (Math.random() < 0.2)
          return;
  
      if (creature.canSee(player.x, player.y, player.z))
          hunt(player);
      else
          wander();
  }

Creating a new path each turn may not be the best idea but we'll only have a few zombies and rogulikes are turn based so it shouldn't be too much of a problem. If it does be come a performance problem we can fix it.

The hunt method finds a path to the target and moves to it.
public void hunt(Creature target){
      List<Point> points = new Path(creature, target.x, target.y).points();
  
      int mx = points.get(0).x - creature.x;
      int my = points.get(0).y - creature.y;
  
      creature.moveBy(mx, my, 0);
  }

Now we can add zombies to our factory. Since the Ai needs a reference to the player, we have to pass that in.
public Creature newZombie(int depth, Creature player){
      Creature zombie = new Creature(world, 'z', AsciiPanel.white, "zombie", 50, 10, 10);
      world.addAtEmptyLocation(zombie, depth);
      new ZombieAi(zombie, player);
      return zombie;
  }

To add zombies to our world we need to update createCreatures in the PlayScreen.

for (int i = 0; i < z + 3; i++){
         factory.newZombie(z, player);
     }

Adding pathfinding to a game is a big deal. The PathFinder we're using for now is good enough but has some major inefficiencies. I'm using a HashMap of points rather than an array so we don't have to worry about the world size or anything like that. This will take up less memory and handle aarbitrarily large maps but it will be much much slower.

download the code

Tuesday, September 27, 2011

roguelike tutorial 12: weapons and armor

Time for some weapons and armor.

Since we have a very simple Attack value and Defense value for creatures, let's use that for our weapons and armor. Go ahead and add that to the Item class.

private int attackValue;
  public int attackValue() { return attackValue; }
  public void modifyAttackValue(int amount) { attackValue += amount; }

  private int defenseValue;
  public int defenseValue() { return defenseValue; }
  public void modifyDefenseValue(int amount) { defenseValue += amount; }


And create some new items in our factory class.

public Item newDagger(int depth){
    Item item = new Item(')', AsciiPanel.white, "dagger");
    item.modifyAttackValue(5);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item newSword(int depth){
    Item item = new Item(')', AsciiPanel.brightWhite, "sword");
    item.modifyAttackValue(10);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item newStaff(int depth){
    Item item = new Item(')', AsciiPanel.yellow, "staff");
    item.modifyAttackValue(5);
    item.modifyDefenseValue(3);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item newLightArmor(int depth){
    Item item = new Item('[', AsciiPanel.green, "tunic");
    item.modifyDefenseValue(2);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item newMediumArmor(int depth){
    Item item = new Item('[', AsciiPanel.white, "chainmail");
    item.modifyDefenseValue(4);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item newHeavyArmor(int depth){
    Item item = new Item('[', AsciiPanel.brightWhite, "platemail");
    item.modifyDefenseValue(6);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

  public Item randomWeapon(int depth){
    switch ((int)(Math.random() * 3)){
    case 0: return newDagger(depth);
    case 1: return newSword(depth);
    default: return newStaff(depth);
    }
  }

  public Item randomArmor(int depth){
    switch ((int)(Math.random() * 3)){
    case 0: return newLightArmor(depth);
    case 1: return newMediumArmor(depth);
    default: return newHeavyArmor(depth);
    }
  }

Don't forget to add them to the newly created game in the PlayScreen createItems method.

If you play you should be able to see them and carry them around.


If we want to use them then we need to add some methods to the creature class to equip and unequip weapons and armor. For now, creatures can wield one weapon and wear one pice of armor at a time. If you want separate armor slots for helmet, rings, shoes, etc, you can do that too. I'm also going to use the same methods to deal with armor or weapons.

private Item weapon;
  public Item weapon() { return weapon; }

  private Item armor;
  public Item armor() { return armor; }
public void unequip(Item item){
      if (item == null)
         return;
  
      if (item == armor){
          doAction("remove a " + item.name());
          armor = null;
      } else if (item == weapon) {
          doAction("put away a " + item.name());
          weapon = null;
      }
  }
public void equip(Item item){
      if (item.attackValue() == 0 && item.defenseValue() == 0)
          return;
  
      if (item.attackValue() >= item.defenseValue()){
          unequip(weapon);
          doAction("wield a " + item.name());
          weapon = item;
      } else {
          unequip(armor);
          doAction("put on a " + item.name());
          armor = item;
      }
  }

And make sure that we unequip anything we eat or drop.

public void eat(Item item){
      if (item.foodValue() < 0)
         notify("Gross!");
  
      modifyFood(item.foodValue());
      inventory.remove(item);
      unequip(item);
  }

public void drop(Item item){
    if (world.addAtEmptySpace(item, x, y, z)){
        doAction("drop a " + item.name());
        inventory.remove(item);
        unequip(item);
    } else {
        notify("There's nowhere to drop the %s.", item.name());
    }
}

The easiest way to use our new equipment when calculating our overall attack and defense values is just to add them to the creature's getters.
public int attackValue() {
    return attackValue
     + (weapon == null ? 0 : weapon.attackValue())
     + (armor == null ? 0 : armor.attackValue());
  }

  public int defenseValue() {
    return defenseValue
     + (weapon == null ? 0 : weapon.defenseValue())
     + (armor == null ? 0 : armor.defenseValue());
  }


And now create an EquipScreen.
package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EquipScreen extends InventoryBasedScreen {

  public EquipScreen(Creature player) {
    super(player);
  }

  protected String getVerb() {
    return "wear or wield";
  }

  protected boolean isAcceptable(Item item) {
    return item.attackValue() > 0 || item.defenseValue() > 0;
  }

  protected Screen use(Item item) {
    player.equip(item);
    return null;
  }
}

Wasn't that easy?

All that's left is making the 'w' wear or wield items in the PlayScreen. I prefer having one key for both rather than one for armor and another for weapons. If you'd rather have different keys then you can do that.
case KeyEvent.VK_W: subscreen = new EquipScreen(player); break;

And now you can find and use weapons and armor. Play around with different attackValues, defenseValues, and hit points. You can have 3 or 4 weapons or 300 weapons. Try changing how abundant weapons and armor are or maybe have some more common than others.


One advantage of having all our items be the same class but have different values is that an item can be more than one thing, e.g. you could make an edible weapon and the player would be able to eat or wield it with no extra code or you could have have a weapon that increases attack and defense.
public Item newEdibleWeapon(int depth){
    Item item = new Item(')', AsciiPanel.yellow, "baguette");
    item.modifyAttackValue(3);
    item.modifyFoodValue(50);
    world.addAtEmptyLocation(item, depth);
    return item;
  }

You can't do that with Weapon, Food, and Armor subclasses.


Wouldn't it also be nice if the inventory screens told us what we have equipped so we don't eat the armor we're wearing or try to wear something we're already wearing? Here's one possible update to the InventoryBasedScreen:
String line = letters.charAt(i) + " - " + item.glyph() + " " + item.name();
    
  if(item == player.weapon() || item == player.armor())
      line += " (equipped)";
    
  lines.add(line);
Maybe the EquipScreen shouldn't let us equip what we're already using. Or maybe wearing or wielding what's already equipped should un-wear or un-weild it? That way the 'w' key can equip or unequip. It's your game so it's up to you. Implementing those is left as an exercise.

download the code

Friday, September 23, 2011

roguelike tutorial 11: hunger and food

Now that we've got monsters to kill and the ability to pick up and use things, how about we add some corpses and the ability to eat them?

We first need to update our Item class to support some nutritional value.
private int foodValue;
public int foodValue() { return foodValue; }
public void modifyFoodValue(int amount) { foodValue += amount; }

And update our creature to leave corpses.

public void modifyHp(int amount) {
    hp += amount;
    if (hp < 1) {
        doAction("die");
        leaveCorpse();
        world.remove(this);
    }
}

private void leaveCorpse(){
    Item corpse = new Item('%', color, name + " corpse");
    corpse.modifyFoodValue(maxHp * 3);
    world.addAtEmptySpace(corpse, x, y, z);
}

Update creatures to also have hunger.

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

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

public void modifyFood(int amount) {
    food += amount;
    if (food > maxFood) {
        food = maxFood;
    } else if (food < 1 && glyph == '@') {
        modifyHp(-1000);
    }
}

Do you see the terrible hack there? We only want the player to be able to die of starvation since it would be boring if every monster dropped dead of starvation and if they need to eat they'd have to go around killing each other. We could have an entire ecosystem of bats farming fungus, that would introduce some neat gameplay options, but that's quite a bit more complicated than I'd like to do right now. Anyway dying only if you look like a @ is still an ugly hack — a hack so ugly our children's children will feel the shame. Let's fix it right now:

public void modifyFood(int amount) {
    food += amount;
    if (food > maxFood) {
        food = maxFood;
    } else if (food < 1 && isPlayer()) {
        modifyHp(-1000);
    }
}

public boolean isPlayer(){
    return glyph == '@';
}

The hack is still there but it's isolated for now. Later if we have other creatures with an @ glyph or if the player can assume other forms, we can update this one isolated place. One thing I've learned from real life software is that although ugly hacks are inevitable, you can always isolate them so the callers don't need to deal with it.

But enough preaching, our Creatures also need a method to eat.

public void eat(Item item){
    modifyFood(item.foodValue());
    inventory.remove(item);
}

Don't forget that creatures should start with decent belly full. Add this to the creature constructor:

this.maxFood = 1000;
this.food = maxFood / 3 * 2;

Now add an EatScreen so we can eat something in our inventory.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EatScreen extends InventoryBasedScreen {

    public EatScreen(Creature player) {
        super(player);
    }

    protected String getVerb() {
        return "eat";
    }

    protected boolean isAcceptable(Item item) {
        return item.foodValue() != 0;
    }

    protected Screen use(Item item) {
        player.eat(item);
        return null;
    }
}

Wow, that was easy. InventoryBasedScreen is paying off already.


For the PlayScreen we need to map the 'e' key to the EatScreen.
case KeyEvent.VK_E: subscreen = new EatScreen(player); break;
We should also let the player know how hungry he is. Change the stats in displayOutput to this:
String stats = String.format(" %3d/%3d hp %8s", player.hp(), player.maxHp(), hunger());
And add a helper method. You can use whatever text and amounts you want.
private String hunger(){
    if (player.food() < player.maxFood() * 0.1)
        return "Starving";
    else if (player.food() < player.maxFood() * 0.2)
        return "Hungry";
    else if (player.food() > player.maxFood() * 0.9)
        return "Stuffed";
    else if (player.food() > player.maxFood() * 0.8)
        return "Full";
    else
        return "";
}

Of course none of this will do anything if we don't use up the food we've eaten. Go ahead and add a call to modifyFood in the relevant creature methods. Here's a couple examples:
public void dig(int wx, int wy, int wz) {
    modifyFood(-10);
    world.dig(wx, wy, wz);
    doAction("dig");
}
public void update(){
    modifyFood(-1);
    ai.onUpdate();
}

Go ahead and use the food values you want. You should play around with it for a while to decide what feels right. Maybe you want starvation to be a serious problem and hunting bats is the only way to stay alive or maybe you want starvation to hardly ever happen. Maybe heroes start with an inventory full of supplies or maybe they start with an empty and growling belly — as the designer it's up to you.

While looking at the modifyFood method I noticed we don't prevent hp from going higher than maxHp. Even though we don't have a way to do that yet you should add a check for that.


If we eat more than the maxFood shouldn't our stomach stretch and increase our maxFood? Or maybe the user should explode from overeating? Here's my implementation:
public void modifyFood(int amount) {
    food += amount;

    if (food > maxFood) {
        maxFood = maxFood + food / 2;
        food = maxFood;
        notify("You can't believe your stomach can hold that much!");
        modifyHp(-1);
    } else if (food < 1 && isPlayer()) {
        modifyHp(-1000);
    }
}

It's a subtle effect but it gives the player a decision to make when full and carrying a lot of food and under the right circumstances overeating may become a useful strategy.

Now go ahead and add some food to your world. Bread, meat, apples, whatever.

download the code

Tuesday, September 20, 2011

roguelike tutorial 10: items, inventory, inventory screens

Before we add potions, spellbooks, treasures, armor, weapons, food, and other roguelike goodies we need to think about inventory and start small. We'll need a new class for items, we'll need to update the world to track, add, and remove items, the creature class will need to be updated to pickup, use, and drop items, and the PlayScreen needs to be updated to display items and accept keystrokes to let the player actually pickup or drop items. Let's start with a new Item class.

package rltut;

import java.awt.Color;

public class Item {

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

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

    private String name;
    public String name() { return name; }

    public Item(char glyph, Color color, String name){
        this.glyph = glyph;
        this.color = color;
        this.name = name;
    }
}

Pretty simple so far. I didn't give it an x, y, or z coordinate because items don't need to know where they are; it kind of makes since to have them when laying on the ground but what about when they're in a container or being carried around by a creature?
I guess you could also make a Location interface. Then Point, Creature, and Item could implement it. That way an item's location could be a point in the world, a creature that's carrying it (or it's point in the world), or a container it's in. That would also be useful because an item would have a reference to wherever it is and whoever is carrying it. I'll have to try that on my next roguelike.
I guess you could have the owner update their location and add another flag indicating if the item is on the floor, in a container, or being carried. Sounds cumbersome and unnecessary; best to do without for now.

I'm happy with having a CreatureFactory to handle the details of creating a new creature so let's do the same for items. We could create an ItemFactory but I'd like to try something different: add items to the CreatureFactory. I haven't tried this before so I'm not sure if it's better to keep the two separate or not. I guess I'm going to find out.

The first step is the most powerful refactoring of all, Rename. We'll rename the CreatureFactory to something more general. I'm going to just call it StuffFactory. That's an atrociously non-descriptive name but I can rename it when I think of something better — of course temporary things often stay that way so this will probably remain a StuffFactory for a while.

And now we can add our first item.

public Item newRock(int depth){
        Item rock = new Item(',', AsciiPanel.yellow, "rock");
        world.addAtEmptyLocation(rock, depth);
        return rock;
    }

Now that we have an item to put in the world, we need to extend the world class to handle that. Instead of a list of all items I'm going to try something different — I'm only going to allow one item per tile. Good idea or bad, let's go ahead with that for now.

Our world needs one item per tile.

private Item[][][] items;

This should get initialized in the constructor.

this.items = new Item[width][height][depth];

We need a way to determine what item is in a location.

public Item item(int x, int y, int z){
    return items[x][y][z];
}

And a way to add an item to a random spot similar to how we add creatures.

public void addAtEmptyLocation(Item item, int depth) {
    int x;
    int y;
    
    do {
        x = (int)(Math.random() * width);
        y = (int)(Math.random() * height);
    }
    while (!tile(x,y,depth).isGround() || item(x,y,depth) != null);
    
    items[x][y][depth] = item;
}

And lastly, we need to update our methods that are used for displaying the world to also display items.

public char glyph(int x, int y, int z){
    Creature creature = creature(x, y, z);
    if (creature != null)
        return creature.glyph();
    
    if (item(x,y,z) != null)
        return item(x,y,z).glyph();
    
    return tile(x, y, z).glyph();
}
public Color color(int x, int y, int z){
    Creature creature = creature(x, y, z);
    if (creature != null)
        return creature.color();
    
    if (item(x,y,z) != null)
        return item(x,y,z).color();
    
    return tile(x, y, z).color();
}

And the only change to the PlayScreen is to add our new rocks.

private void createItems(StuffFactory factory) {
    for (int z = 0; z < world.depth(); z++){
        for (int i = 0; i < world.width() * world.height() / 20; i++){
            factory.newRock(z);
        }
    }
}

Just call that during setup of a new game and play it.


Now that we've got a world with items in it, we need to be able to pick them up and do stuff with them.

A lot can happen with a creature's inventory so let's create another class for that. Instead of using a list I'm going to use an array so the items index doesn't change when we lose something before it. E.g. if we quaff the potion in our 'd' slot, whatever was in the 'e' slot should remain there and not slide into the 'd' slot. If you want that kind of behavior then you could use a List — it's your choice.

package rltut;

public class Inventory {

    private Item[] items;
    public Item[] getItems() { return items; }
    public Item get(int i) { return items[i]; }

    public Inventory(int max){
        items = new Item[max];
    }
}

We need a method to add an item to the first open slot in our inventory.

public void add(Item item){
    for (int i = 0; i < items.length; i++){
        if (items[i] == null){
             items[i] = item;
             break;
        }
    }
}

And a way to remove an item from our inventory.

public void remove(Item item){
    for (int i = 0; i < items.length; i++){
        if (items[i] == item){
             items[i] = null;
             return;
        }
    }
}

We also need to know if the inventory is full and we can't carry any more.

public boolean isFull(){
    int size = 0;
    for (int i = 0; i < items.length; i++){
        if (items[i] != null)
             size++;
    }
    return size == items.length;
}

Now that we've got something to represent an inventory, we can add one to our Creature class. This means that potentially any creature can have an inventory (Spoiler alert!)

private Inventory inventory;
    public Inventory inventory() { return inventory; }

We need to initialize it in the constructor. I prefer smaller inventories since that means the player can't carry half the world with them; having to chose which two swords to bring with you is more interesting than just carrying them all. I also tend to forget what I've got once it goes beyond a screenfull.

this.inventory = new Inventory(20);

And our creatures need to be able to pickup and drop stuff, moving it from the world to the creatures inventory or back.

public void pickup(){
        Item item = world.item(x, y, z);
    
        if (inventory.isFull() || item == null){
            doAction("grab at the ground");
        } else {
            doAction("pickup a %s", item.name());
            world.remove(x, y, z);
            inventory.add(item);
        }
    }

public void drop(Item item){
        doAction("drop a " + item.name());
        inventory.remove(item);
        world.addAtEmptySpace(item, x, y, z);
    }


Your IDE has probably warned you that the world class doesn't support removing and adding items so let's take care of that. Removing an item is easy:

public void remove(int x, int y, int z) {
    items[x][y][z] = null;
}

Adding an item to a specific place is more complicated since we only allow one item per tile. Because of that, we need to check adjacent tiles for an open space and repeat until we find one or run out of open spaces.

public void addAtEmptySpace(Item item, int x, int y, int z){
    if (item == null)
        return;
    
    List<Point> points = new ArrayList<Point>();
    List<Point> checked = new ArrayList<Point>();
    
    points.add(new Point(x, y, z));
    
    while (!points.isEmpty()){
        Point p = points.remove(0);
        checked.add(p);
        
        if (!tile(p.x, p.y, p.z).isGround())
            continue;
         
        if (items[p.x][p.y][p.z] == null){
            items[p.x][p.y][p.z] = item;
            Creature c = this.creature(p.x, p.y, p.z);
            if (c != null)
                c.notify("A %s lands between your feet.", item.name());
            return;
        } else {
            List<Point> neighbors = p.neighbors8();
            neighbors.removeAll(checked);
            points.addAll(neighbors);
        }
    }
}

A funky side effect of this is that if there are no open spaces then the item won't be added but will no longer be in the creature's inventory - it will vanish from the game. You can either let that happen or somehow let the caller know that it hasn't been added and shouldn't be removed from the inventory. Or you could notify everyone in viewing distance that it has vanished. I'll leave that up to you. If you leave it as it is then there's no indication that the item vanished and that may be interpreted as a bug. If you tell users it happens they probably won't consider it a bug - just part of the game. This could also make a funny scenario: imagine being trapped in a room where the floor is covered in treasure but you can't pick any up since your inventory is full and there's no room to drop your useless rusty sword.

Here's one possibility:
public void drop(Item item){
    if (world.addAtEmptySpace(item, x, y, z)){
         doAction("drop a " + item.name());
         inventory.remove(item);
    } else {
         notify("There's nowhere to drop the %s.", item.name());
    }
}


The final step is to update the PlayScreen's respondToUserInput method so the user can actually pickup things. Some roguelikes use the 'g' key to get things, some use the ',' key, and some use either one.

switch (key.getKeyChar()){
         case 'g':
         case ',': player.pickup(); break;
         case '<': player.moveBy( 0, 0, -1); break;
         case '>': player.moveBy( 0, 0, 1); break;
         }

Try it out.


We can pick up some rocks and the code is there to drop them, but we don't have a way to specify what to drop. Code that isn't being used gives me a bad feeling so let's wire up the GUI to that drop method soon. Ideally the user will press the 'd' key, the GUI will ask what to drop, the user types the letter of the thing to drop, the player drops it, and we go back to the game. Remember all that time we spent creating Screen interface and thinking about cases with different rules for user input and output? Time to make a new Screen.

Actually, if we think about what we want to do with inventory, we can do better. Here's a few scenarios off the top of my head:
press 'd', ask what to drop, the user selects something that can be dropped, drop it
press 'q', ask what to quaff, the user selects something that can be quaffed, quaff it
press 'r', ask what to read, the user selects something that can be read, read it
press 't', ask what to throw, the user selects something that can be thrown, throw it
press 'e', ask what to eat, the user selects something that can be eaten, eat it
Notice a pattern? There's a key that get's pressed, some verb (drop, quaff, read), some check against the items (droppable, quaffable, readable), and some action (drop, quaff, read). The common behavior can be put in one class called InventoryBasedScreen and the specific details can be in subclasses. That way we can have a DropScreen, QuaffScreen, ReadScreen and others that all subclass the InventoryBasedScreen and just provide a few simple details.

Let's start with a basic InventoryBasedScreen:

package rltut.screens;

import java.awt.event.KeyEvent;
import java.util.ArrayList;
import rltut.Creature;
import rltut.Item;
import asciiPanel.AsciiPanel;

public abstract class InventoryBasedScreen implements Screen {

    protected Creature player;
    private String letters;

    protected abstract String getVerb();
    protected abstract boolean isAcceptable(Item item);
    protected abstract Screen use(Item item);

    public InventoryBasedScreen(Creature player){
        this.player = player;
        this.letters = "abcdefghijklmnopqrstuvwxyz";
    }
}

We need the reference to the player because that's the one who's going to do the work of dropping, quaffing, eating, etc. It's protected so that the subclasses can use it. The letters are so we can assign a letter to each inventory slot (If you allow the inventory to be larger then you need to add more characters). Maybe this should be part of the inventory class but I think this is the only place where we will use it so I'll put it here for now. We've also got abstract methods so our subclasses can specify the verb, what items are acceptable for the action, and a method to actually perform the action. Using an item returns a Screen since it may lead to a different screen, e.g. if we're going to throw something then we can transition into some sort of targeting screen.

Since this is a screen it needs to actually display some output. We not only ask what they want to use but go ahead and show a list of acceptable items.

public void displayOutput(AsciiPanel terminal) {
        ArrayList<String> lines = getList();
    
        int y = 23 - lines.size();
        int x = 4;

        if (lines.size() > 0)
            terminal.clear(' ', x, y, 20, lines.size());
    
        for (String line : lines){
            terminal.write(line, x, y++);
        }
    
        terminal.clear(' ', 0, 23, 80, 1);
        terminal.write("What would you like to " + getVerb() + "?", 2, 23);
    
        terminal.repaint();
    }

That should be pretty clear: write the list in the lower left hand corner and ask the user what to do. If you allow a larger inventory then you'll have to show two columns or scroll the list or something.

The getList method will make a list of all the acceptable items and the letter for each corresponding inventory slot.

private ArrayList<String> getList() {
        ArrayList<String> lines = new ArrayList<String>();
        Item[] inventory = player.inventory().getItems();
    
        for (int i = 0; i < inventory.length; i++){
            Item item = inventory[i];
        
            if (item == null || !isAcceptable(item))
                continue;
        
            String line = letters.charAt(i) + " - " + item.glyph() + " " + item.name();
        
            lines.add(line);
        }
        return lines;
    }

Now that we've got some output we need to respond to user input. The user can press escape to go back to playing the game, select a valid character to use, or some invalid key that will do nothing and keep them on the current screen.

public Screen respondToUserInput(KeyEvent key) {
        char c = key.getKeyChar();

        Item[] items = player.inventory().getItems();
    
        if (letters.indexOf(c) > -1
             && items.length > letters.indexOf(c)
             && items[letters.indexOf(c)] != null
             && isAcceptable(items[letters.indexOf(c)]))
            return use(items[letters.indexOf(c)]);
        else if (key.getKeyCode() == KeyEvent.VK_ESCAPE)
            return null;
        else
            return this;
    }

I hope that little mess makes sense. Use it, exit, or ask again.

This doesn't do anything yet, in fact it's an abstract class so it can't do anything until we create a subclass and use that. The first subclass will be a DropScreen.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class DropScreen extends InventoryBasedScreen {

    public DropScreen(Creature player) {
        super(player);
    }
}

We just need to supply the methods that were abstract in the original. We're asking the use what they want to drop so the getVerb should return that.

protected String getVerb() {
        return "drop";
    }

Since anything can be dropped, all items are acceptable.

protected boolean isAcceptable(Item item) {
        return true;
    }

Once the user selects what to drop we tell the player to do the work and return null since we are done with the DropScreen.

protected Screen use(Item item) {
        player.drop(item);
        return null;
    }

Now we can update the PlayScreen to use our fancy new DropScreen. The DropScreen is a little different that the start, play, win, and lose screens since it needs to return to the PlayScreen once it's done. We could pass the current play screen into newly created DropScreen and have it return the PlayScreen when it's done, but I've tried that before and it became kind of messy. This time I'll try something different. We can have the PlayScreen know if we're working with another sub screen and delegate input and output to that screen screen. Once the subscreen is done, it get's set to null and the PlayScreen works as normal.

First the PlayScreen needs to know what the subscreen is. If it's null then everything should work as it did before. There's no need to initialize this since we check for nulls when we use it.

private Screen subscreen;

After we displayOutput the subscreen should get a chance to display. This way the current game world will be a background to whatever the subscreen wants to show.

if (subscreen != null)
    subscreen.displayOutput(terminal);

And any user input needs to be sent to the subscreen if it exists. The subscreen will also tell the PlayScreen what the new subscreen is. We also need to handle the users pressing the 'd' key to drop items from inventory. Lastly, if we should update the world only if we don't have a subscreen.

public Screen respondToUserInput(KeyEvent key) {
     if (subscreen != null) {
         subscreen = subscreen.respondToUserInput(key);
     } else {
         switch (key.getKeyCode()){
         case KeyEvent.VK_ESCAPE: return new LoseScreen();
         case KeyEvent.VK_ENTER: return new WinScreen();
         case KeyEvent.VK_LEFT:
         case KeyEvent.VK_H: player.moveBy(-1, 0, 0); break;
         case KeyEvent.VK_RIGHT:
         case KeyEvent.VK_L: player.moveBy( 1, 0, 0); break;
         case KeyEvent.VK_UP:
         case KeyEvent.VK_K: player.moveBy( 0,-1, 0); break;
         case KeyEvent.VK_DOWN:
         case KeyEvent.VK_J: player.moveBy( 0, 1, 0); break;
         case KeyEvent.VK_Y: player.moveBy(-1,-1, 0); break;
         case KeyEvent.VK_U: player.moveBy( 1,-1, 0); break;
         case KeyEvent.VK_B: player.moveBy(-1, 1, 0); break;
         case KeyEvent.VK_N: player.moveBy( 1, 1, 0); break;
         case KeyEvent.VK_D: subscreen = new DropScreen(player); break;
         }
        
         switch (key.getKeyChar()){
         case 'g':
         case ',': player.pickup(); break;
         case '<': player.moveBy( 0, 0, -1); break;
         case '>': player.moveBy( 0, 0, 1); break;
         }
     }
    
     if (subscreen == null)
         world.update();
    
     if (player.hp() < 1)
         return new LoseScreen();
    
     return this;
 }

That was a lot of little changes to a few different places but if the DropScreen is any indication, the InventoryBasedScreen should be a major win in terms of being able to implement new features with little effort. The PlayScreen is getting a little out of hand now that it creates a new world, displays the world, handles user commands and deals with subscreens. Maybe the part about setting up a new game should be moved somewhere else.


Let's make this a proper roguelike with a special object to retrieve and return to the surface with. This will also give the player a legitimate victory condition other than pressing enter.

Add our new item to the SuffFactory:

public Item newVictoryItem(int depth){
        Item item = new Item('*', AsciiPanel.brightWhite, "teddy bear");
        world.addAtEmptyLocation(item, depth);
        return item;
    }

And make sure it get's created when we start a new game:

private void createItems(StuffFactory factory) {
    for (int z = 0; z < world.depth(); z++){
        for (int i = 0; i < world.width() * world.height() / 20; i++){
            factory.newRock(z);
        }
    }
    factory.newVictoryItem(world.depth() - 1);
}

And update the WorldBuilder to include some exist stairs.

private WorldBuilder addExitStairs() {
        int x = -1;
        int y = -1;
    
        do {
            x = (int)(Math.random() * width);
            y = (int)(Math.random() * height);
        }
        while (tiles[x][y][0] != Tile.FLOOR);
    
        tiles[x][y][0] = Tile.STAIRS_UP;
        return this;
    }

And make that part of creating caves.

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


Our normal stair handling won't work with up stairs on the uppermost layer of the world so let's handle that in the PlayScreen.

switch (key.getKeyChar()){
    case 'g':
    case ',': player.pickup(); break;
    case '<':
        if (userIsTryingToExit())
         return userExits();
        else
         player.moveBy( 0, 0, -1); 
        break;
    case '>': player.moveBy( 0, 0, 1); break;
    }

private boolean userIsTryingToExit(){
    return player.z == 0 && world.tile(player.x, player.y, player.z) == Tile.STAIRS_UP;
}

private Screen userExits(){
    for (Item item : player.inventory().getItems()){
        if (item != null && item.name().equals("teddy bear"))
            return new WinScreen();
    }
    return new LoseScreen();
}

Now you can remove the cases for VK_ESCAPE and VK_ENTER. You can also remove the message about pressing escape or enter. It took half the tutorials but we finally have a victory condition.

download the code

Friday, September 16, 2011

roguelike tutorial 09: wandering monsters

We need some monsters to wander around our caves. How about some fast moving bats?

First let's add some default movement behavior to the CreatureAi.

public void onEnter(int x, int y, int z, Tile tile){
    if (tile.isGround()){
         creature.x = x;
         creature.y = y;
         creature.z = z;
    } else {
         creature.doAction("bump into a wall");
    }
}

Let's also give it a method of moving randomly. This common behavior can be called by any subclass.

public void wander(){
    int mx = (int)(Math.random() * 3) - 1;
    int my = (int)(Math.random() * 3) - 1;
    creature.moveBy(mx, my, 0);
}

Now the ai for our bats.

package rltut;

public class BatAi extends CreatureAi {

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

    public void onUpdate(){
        wander();
        wander();
    }
}

We could set up a system for dealing with different monster speeds but this is simple enough: bats move twice for every one of your moves. Easy to implement, easy to understand.

Now we add bats to our CreatureFactory. I picked some low hp and attack so they could nibble on you a bit but shouldn't be too much of a problem.

public Creature newBat(int depth){
    Creature bat = new Creature(world, 'b', AsciiPanel.yellow, 15, 5, 0);
    world.addAtEmptyLocation(bat, depth);
    new BatAi(bat);
    return bat;
}

And in our PlayScreen we need to update createCreatures. How about 20 bats per level?

for (int i = 0; i < 20; i++){
    creatureFactory.newBat(z);
}

And now that our player is in some danger of being killed, let's add a check after the world get's updated.

if (player.hp() < 1)
    return new LoseScreen();

Try it out. You should see bats being flying about and being batty.


If you run this you'll soon notice that the bats drop dead from attacking each other or even attacking themselves. Suicide bats are funny but they quickly go extinct.

Let's add a check to the first line of the moveBy method in the Creature class. It should bail out early if we're not actually moving. This will take care of creatures killing themselves when all they want to do is stand in one place.

if (mx==0 && my==0 && mz==0)
    return;

Creatures should also be able to see what other creatures are in the world so the CreatureAi can know what's going on.

public Creature creature(int wx, int wy, int wz) {
    return world.creature(wx, wy, wz);
}

We can now improve the CreatureAi wander method to make sure creatures don't fight other's like them.

public void wander(){
    int mx = (int)(Math.random() * 3) - 1;
    int my = (int)(Math.random() * 3) - 1;
    
    Creature other = creature.creature(creature.x + mx, creature.y + my, creature.z);
    
    if (other != null && other.glyph() == creature.glyph())
        return;
    else
        creature.moveBy(mx, my, 0);
}

You could also make it keep trying until mx != 0 && my != 0, that way it would never stand in the same spot. You may want to make sure it doesn't try to move into a wall or make it able to go up or down stairs.

And there you go. You now have some deep and bat-filled caves to explore.



Using the glyph in messages is lame; Creatures should have a name.

private String name;
    public String name() { return name; }

Using constructor injection, update the creatureFactory to pass in the appropriate name. Finally, update any messages that used the creature's glyph to now use the creature's name.

Much better.

download the code