Monday, October 31, 2011

Randomized metroidvania 05: map specials

I was going to create the player and rooms to move around in but I decided to add a little more to the map. The very crowded map now shows bonus items in dead ends and secret rooms. You can also mouseover a room to see details about it's region, name, and contents.




There's a limited amount of keys and locks so if you add enough regions then you'll see "key #33" or something like that.

Here are some things I added:
    for each unlocked dead end, there's a chance of adding a random lock and bonus
    when placing a room, there's a chance of adding a secret room
    when placing a room, there's a chance of a name and or some item

I'm not sure what theme or items I want in the final game, the ones here are just examples. Once I get the mechanics figured out (like how to create an interesting map) then I can figure out what theme I want to have.

I think I've crammed all I can into the map for now; time to start on actual gameplay.

Sunday, October 30, 2011

Randomized metroidvania bugs

My last post had a bug where new regions may not connect to a neighboring room. I thought about fixing it before posting but decided to post it anyway. Now I hate buggy software as much as anyone, and this bug makes the map unsolveable since there can be rooms you can't get to, so why did I post it anyway?

  1. This is a work in progress and sometimes - most of the time - those are messy. Sometimes it's good to see how things are made, bugs and all.
  2. I already fixed it when I changed that part of the code for my next iteration.
So what caused the bug in the first place? I changed the code that connects a room when it is placed. I made it keep placing connections until the new room is connected to another room in the same region unless it was the first room in that region. Otherwise a room could be placed and it would connect to a neighboring room from a different region and not connect to a room in it's own region. If that was the room that had the key and it was locked with that corresponding lock, then there was no way to get the key. In fixing that bug I introduced a new one.

Randomized metroidvania 04: map keys and locks

It took a few hours and I had to rewrite the code for doors, but I've got keys and locks working. A major part of any metroidvania is exploring new areas and finding some kind of impassable barrier -  a wide pit, a wall of metal blocks, a force field, etc. It's only later when you find some new ability or item - double jump, rocket launcher, field disabler, etc - that you can go past the barrier to a new region. You can abstract this idea of obstacles and items as "locks" and matching "keys". In one game the "red locks" and "red key" might represent actual locked doors and a key, in another game they might be security cameras and a disguise, and in another game they might be tall walls and the ability to climb walls.

You can see the locks and keys in my latest update. The icons are ugly and hard to distinguish, but it's just for example and will be redone many times before the project is over. Consider this the "alpha version" artwork.



[I just noticed that sometimes the first room in a region doesn't connect to the other rooms. A bug I just fixed when I redid that code for my latest work.]

when visiting a region for locks and keys:
    place an unused key
    put the matching lock on one door to each unvisited adjacent region
    any unlocked doors to adjacent regions are added to a "to do" list
    visit each unvisited adjacent region

after visiting all regions for locks and keys:
    for each unlocked door on the "to do" list, put a random used lock

This ensures that each region has a key that you need to proceed to the neighboring regions. Although; since each region can connect with its neighboring regions multiple times, it's theoretically possible that getting the first key would be enough to explore all regions.

Saturday, October 29, 2011

Randomized metroidvania 03: map regions

Different regions! It was easier than I thought it would be - of course it has been in the back of my head for a while so I didn't jump in with my first thought.




to start a new region:
    create a list of all frontiers for all rooms
    randomly pick one to start the new region
    clear all other frontiers
    place a room as normal

The current region determines the room color and a randomly chosen maximum height and width and can easily be used to control more things like the artwork, items, bad guys, secrets, and other things. Like placing doors, this algorithm is guaranteed to connect with existing regions and could even connect multiple other regions.

Randomized metroidvania 02: map connections

The rooms are now connected! I used a very simple method that's guaranteed to connect and will even create loops to join different "branches" that happen to be near by.




to connect a room:
    after placing a room
    get a list of all adjacent cells that are in a room
    randomly pick some cells to create doors

This can create multiple opening between two rooms. That's not a problem yet but it could be tweaked later on. This should be enough to create the gameplay portion but I'd like to work on defining different regions at the world-gen level before I move on to creating a player to run around.

Friday, October 28, 2011

Randomized metroidvania 01: map rooms

So I've decided to make a randomized metroidvania game; sort of a cross between a roguelike and one of the greatest games ever, Castlevania: Symphony Of The Night. Since I want this to be a flash game, I'll use Eclipse and Flixel.

I spent a few hours getting Eclipse to work with Actionscript and playing with Flixel a bit. For my game I decided to start with generating a random world map.




Here's the basic algorithm, which took less time than setting up Eclipse and Actionscript:

to start:
    add a 1x1 "frontier" space
    add a room

to add a room:
    randomly pick a frontier space
    start a room there
    randomly grow it for a while
    remove frontiers under it
    add its empty neighboring cells to the list of frontiers

roguelike tutorial retrospective

Time for a retrospective! I think retrospectives are probably the most important thing a developer could do since reflecting on what you've done is the difference between someone with 10 years of experience and someone with 1 year of experience repeated 10 times. There are as many different styles of retrospectives as there are teams who do them but I've found that these few questions are usually good enough for me.

What does this retrospective cover? For this roguelike tutorial I tried a few new and different things; the most obvious one being that this was a series of blog posts. This is also the first time I had something of a magic system. There were also a few tweaks to how I normally code things. The roguelike itself is not meant to be a complete and fun game - it's just a basic example of a few different things that may help some roguelike developers - so I'm not going to review the game as a game.

What worked well? As far as code goes, the Screen interface worked really well. Having goblins pickup and use items is neat, as is learning the identity of things by watching others use them. As far as a tutorial goes, I think it can be useful for some people. Even though like there are, at most, a handful of people who read each post, I did get more feedback than I expected, including a reference to behavior trees - something I didn't know about. I'm also pleased that my posts were a mix of code, references to other sources, and some of my own thoughts about why I'm doing what I'm doing. It's not as authoritative as saying "THIS IS THE ONE TRUE AND RIGHT WAY TO DO THIS", and some of it was probably a bad idea, but I've always found that the most instructive guides show the uncertainties and mistakes we run into and how to deal with them.

What needs to work better? One thing I've learned about building something via blog posts is that refactoring is very difficult to show; it's easy to show new code to add, especially if you have small classes and methods, but I don't know of any easy way to show removing, rewriting, and refactoring. This means that the final code, although mostly small pieces, could use a lot of cleanup work and this isn't representative of my best code and may even be a bad example. Oops. Using no globals was a laudable goal but lead to some things I'm not to happy about, like the path finder using a collection of points rather than a fixed size 2D array since I didn't have an easy way to tell the path finder what the map size is. There are a few minor annoyances with the code, like the creature class becoming a behemoth, but most of that is from adding features without refactoring. The spell-related code is quite ugly and clunky too - mostly because I'm not familiar with writing magic systems and couldn't refractor so it was just charging ahead with little idea what I was doing.

What should be done differently next time? I still think a series of blog posts can make a good tutorial but it needs something that makes it easier to show refactoring - perhaps putting everything on github and discussing the changes each time. That would be a slight improvement but still not quite good enough. I tried to be very structured to make sure I followed through and didn't lose interest halfway through but it would have been better to break it into more than 20 parts. I've used Ninject to make it easier to wire things together and something like that would work well for this kind of project; I thought about it but didn't want to add another post about how to use a dependency injection framework.

In summary
  • Use something to counter the limits to show refactoring on a series of blog posts.
  • Don't limit yourself to a fixed number of posts.
  • The Screen interface was the best thing I did: keep doing that.
  • The AsciiPanel was useful but it needs work to avoid flickering and to support animations.
  • Each post should have code, narration, references, and details about the author's thought process.
  • I liked getting comments from readers even more than I thought I would.
Any feedback from you, readers? What did you like? What did you not like? What could be done differently?

Tuesday, October 25, 2011

roguelike tutorial 20: item appearance and identification

One of the things I enjoy the most about roguelikes is identifying items. Usually not by scroll of identify or funky unicorn tricks, but by boldly quaffing and reading — yeah, I tend to die a lot. Identification is something I haven't seen in other games and something that I think any good roguelike needs. The basic idea is that if we've identified an item, or given it a name of our own, then we see the name, otherwise we see it's appearance.

So let's start by adding an appearance to the Item class.

private String appearance;
public String appearance() { 
    if (appearance == null)
        return name;

    return appearance;
}

Then have it passed in to the constructor.

Since the appearance has to be passed in, our code is broken in all the places that create items - this is a good thing because now we can see what areas we need to change. If appearance is null then the name is used since most things look like what they are; e.g. a rock looks like a rock and a sword looks like a sword. Because of this we can pass a null appearance to all items except the potions. Before we get to the potions we need to set up some colors and text to use in our factory.

private Map<String, Color> potionColors;
    private List<String> potionAppearances;
 
    public StuffFactory(World world){
        this.world = world;
  
        setUpPotionAppearances();
    }
 
    private void setUpPotionAppearances(){
        potionColors = new HashMap<String, Color>();
        potionColors.put("red potion", AsciiPanel.brightRed);
        potionColors.put("yellow potion", AsciiPanel.brightYellow);
        potionColors.put("green potion", AsciiPanel.brightGreen);
        potionColors.put("cyan potion", AsciiPanel.brightCyan);
        potionColors.put("blue potion", AsciiPanel.brightBlue);
        potionColors.put("magenta potion", AsciiPanel.brightMagenta);
        potionColors.put("dark potion", AsciiPanel.brightBlack);
        potionColors.put("grey potion", AsciiPanel.white);
        potionColors.put("light potion", AsciiPanel.brightWhite);

        potionAppearances = new ArrayList<String>(potionColors.keySet());
        Collections.shuffle(potionAppearances);
    }

This creates some text ("red potion", "green potion") and some corresponding colors (red, green), and shuffles the text so each time you create a new factory they will be in a different order. Since we create a new factory once per game, each game will have different text and colors.

Now to set the potion color, just use the new data when creating potions.
String appearance = potionAppearances.get(0);
  Item item = new Item('!', potionColors.get(appearance), "health potion", appearance);
...and...
String appearance = potionAppearances.get(1);
  Item item = new Item('!', potionColors.get(appearance), "mana potion", appearance);
...etc.

Not the best way but it works well enough and is easy to show.

If you play the game the potions should be random colors now.



Our CreatureAi class needs to record what items have been identified and what the names of identified or renamed items are. We could have this tracked by the items themselves or some global variables, but this way creatures can identify and converse about item appearances too. By modeling it as close to reality as possible, i.e. where creature's have their own mind and names for items, interesting possibilities and emergent behavior are more likely.

private Map<String, String> itemNames;

    public String getName(Item item){
        String name = itemNames.get(item.name());
        return name == null ? item.appearance() : name;
    }
 
    public void setName(Item item, String name){
        itemNames.put(item.name(), name);
    }

And we need this to be available to the creatures.

public String nameOf(Item item){
    return ai.getName(item);
}
 
public void learnName(Item item){
    notify("The " + item.appearance() + " is a " + item.name() + "!");
    ai.setName(item, item.name());
}

Wherever we use an item's name we need to use nameOf instead. The easiest way I can think of doing this is to make the name method private and see where the code breaks. Those places need to use the creature's nameOf method instead. The only places we need the real name is in the PlayScreen where we check to see if the player has the Teddy Bear, or whatever the victory object is, and in the CreatureAi getName and setName methods. Once we make all the other changes we can make the name method public and everything should compile again.

Run it and you should see that potions are now listed by color instead of the real name. I got an error when starting but that was fixed by setting up the GoblinAi before the Goblin is given a weapon and armor.


Now we need to let the player identify things when used. In the StuffFactory, for each quaffEffect make the creature learn the name of the potion if any effect happens. Here's an example:

public Item newPotionOfHealth(int depth){
    String appearance = potionAppearances.get(0);
    final Item item = new Item('!', potionColors.get(appearance), "health potion", appearance);
    item.setQuaffEffect(new Effect(1){
        public void start(Creature creature){
           if (creature.hp() == creature.maxHp())
               return;
    
           creature.modifyHp(15);
           creature.doAction("look healthier");
           creature.learnName(item);
       }
    });
  
    world.addAtEmptyLocation(item, depth);
    return item;
}

The Creature's throwAttack should also let the thrower learn the name of anything that has a quaffEffect. Now when you play you can learn the identity of potions by quaffing or throwing them.



It's still possible to learn the name of an Effect that you shouldn't learn. If you kill a bat with a yellow potion you shouldn't be able to see the effect it had or identify what the potion is since the bat died from being hit by a bottle and there wasn't anything left for the effect to apply to. Let's create a version of the doAction method that handles this logic. This version of doAction will take a message and an item. Anyone who can see the creature should see the message and learn the identity of the item, unless the creature is dead, then nothing happens. We can reuse some of the code in the current doAction.

public void doAction(String message, Object ... params){
    for (Creature other : getCreaturesWhoSeeMe()){
        if (other == this){
            other.notify("You " + message + ".", params);
        } else {
            other.notify(String.format("The %s %s.", name, makeSecondPerson(message)), params);
        }
    }
}
 
public void doAction(Item item, String message, Object ... params){
    if (hp < 1)
        return;
  
    for (Creature other : getCreaturesWhoSeeMe()){
        if (other == this){
            other.notify("You " + message + ".", params);
        } else {
            other.notify(String.format("The %s %s.", name, makeSecondPerson(message)), params);
        }
        other.learnName(item);
    }
}
 
private List<Creature> getCreaturesWhoSeeMe(){
    List<Creature> others = new ArrayList<Creature>();
    int r = 9;
    for (int ox = -r; ox < r+1; ox++){
        for (int oy = -r; oy < r+1; oy++){
            if (ox*ox + oy*oy > r*r)
                continue;
    
            Creature other = world.creature(x+ox, y+oy, z);
    
            if (other == null)
                continue;
    
            others.add(other);
        }
    }
    return others;
}

A possibility:
The goblin quaffs a cyan potion.
The goblin looks stronger.
The cyan potion is a strength potion!

If the potions' quaffEffect use this new doAction then things will work a little better and we can remove the learnName from throwAttack since everyone who see's the creature will identify the potion based on the effect. This also means that if your creatures quaff potions everyone who watches can figure out what it is.


And that's one way of doing item identification. We had to make a lot of little changes all over the place, like using the creature's nameOf instead of the item's name method, but it wasn't difficult or error-prone. Each creature has it's own idea of what things are named so you could even let intelligent creatures tell each other the names. Maybe each goblin should quaff one unidentified potion during it's life and then discuss with others.

public void discussItemName(Item item, Creature other){
    creature.doAction(item, "say \"%ss are %ss\"", item.appearance(), item.name());
    other.learnName(item);
}

Or all goblins could share the same itemNames map — although that's kind of cheating.

You could also let the player give names to things. A RenameItemScreen that let's you rename a specific item or all items with a specific appearance. That way if the player deduces that red potions are healing potions then they can rename all red potions to healing potions or if they like their individual sword they can rename it Excalibur.


One last addition: add a "cause of death" string to the creature class and pass one in to modifyHp.

private String causeOfDeath;
 public String causeOfDeath() { return causeOfDeath; }
 
 public void modifyHp(int amount, String causeOfDeath) { 
     hp += amount;
     this.causeOfDeath = causeOfDeath;
  
     if (hp > maxHp) {
         hp = maxHp;
     } else if (hp < 1) {
         doAction("die");
         leaveCorpse();
         world.remove(this);
     }
 }
Then update the places that modify hp. Here's an example:
public void modifyFood(int amount) { 
    food += amount;
  
    if (food > maxFood) {
        maxFood = (maxFood + food) / 2;
        food = maxFood;
        notify("You can't belive your stomach can hold that much!");
        modifyHp(-1, "Killed by overeating.");
    } else if (food < 1 && isPlayer()) {
        modifyHp(-1000, "Starved to death.");
    }
 }
Then pass the player to the WinScreen and LoseScreen and you can tell the user details about how they died, what they were carrying, conduct, etc. download the code

Friday, October 21, 2011

roguelike tutorial 19: mana, spells, and magic books

Time to add some magic to our game. This will take a lot of code and will take a long time to balance. To keep it simple, we'll just use mana and spellbooks. Scrolls are easy to add since they're basically potions that you read instead of quaff so I won't add scrolls - but I'm sure you can figure out how to do that by now or at least you will be able to after this tutorial.



We're going to have spells and each will have it's own effect. But when we add that effect to a creature, we want each creature to get it's own effect, otherwise weird things will happen because the spell will have an effect being applied to many creatures and the shared state (like duration) will be wonky. Instead, a spell will have an effect and when applying it to something we can create a copy and apply the copy. That way each time you cast a spell the effect will have it's own state. Add a copy constructor to the Effect class like this:

public Effect(Effect other){
    this.duration = other.duration; 
}


The first new class we'll need is a Spell class to tie together a spell name, cost, and effect.

package rltut;

public class Spell {

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

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

    private Effect effect;
    public Effect effect() { return new Effect(effect); }

    public Spell(String name, int manaCost, Effect effect){
        this.name = name;
        this.manaCost = manaCost;
        this.effect = effect;
    }
}
See how we return a copy of the effect instead of the original?


Let's add the mana related stuff to the Creature class.

private int maxMana;
    public int maxMana() { return maxMana; }
    
    private int mana;
    public int mana() { return mana; }
    public void modifyMana(int amount) { mana = Math.max(0, Math.min(mana+amount, maxMana)); 
    
    private int regenManaCooldown;
    private int regenManaPer1000;
    public void modifyRegenManaPer1000(int amount) { regenManaPer1000 += amount; }

    private void regenerateMana(){
        regenManaCooldown -= regenManaPer1000;
        if (regenManaCooldown < 0){
            if (mana < maxMana) {
                modifyMana(1);
                modifyFood(-1);
            }
            regenManaCooldown += 1000;
        }
    }

Don't forget to initialize regenManaPer1000 in the constructor and to call regenerateMana during the update method. We also have something else we can gain during a level-up.
public void gainMaxMana() {
        maxMana += 5;
        mana += 5;
        doAction("look more magical");
    }

    public void gainRegenMana(){
        regenManaPer1000 += 5;
        doAction("look a little less tired");
    }


And add the new options to the LevelUpController.
new LevelUpOption("Increased mana"){
            public void invoke(Creature creature) { creature.gainMaxMana(); }
        },new LevelUpOption("Increased mana regeneration"){
            public void invoke(Creature creature) { creature.gainRegenMana(); }
        }


It would also be nice to add our new stats to the displayOutput method of the PlayScreen class.
String stats = String.format(" %3d/%3d hp  %d/%d mana  %8s", 
    player.hp(), player.maxHp(), player.mana(), player.maxMana(), hunger());  


Adding spell books could be done many different ways. I think we should stick with the Item class and extend it with what we need.
private List<Spell> writtenSpells;
    public List<Spell> writtenSpells() { return writtenSpells; }

    public void addWrittenSpell(String name, int manaCost, Effect effect){
        writtenSpells.add(new Spell(name, manaCost, effect));
    }

Don't forget to initialize writtenSpells in the Item constructor. This should allow us to create scrolls, spell books, notes, or even engraved items. Very simple and flexible. If we create an effect that just displays a note to the user then we could even let the player add his own non-magical engravings to items.

Let's add some spell books. I'm going to create two simple ones but you should add more spells and more books. These are just examples of what can be done. The first is a healer type book:
public Item newWhiteMagesSpellbook(int depth) {
        Item item = new Item('+', AsciiPanel.brightWhite, "white mage's spellbook");
        item.addWrittenSpell("minor heal", 4, new Effect(1){
            public void start(Creature creature){
                if (creature.hp() == creature.maxHp())
                    return;
                
                creature.modifyHp(20);
                creature.doAction("look healthier");
            }
        });
        
        item.addWrittenSpell("major heal", 8, new Effect(1){
            public void start(Creature creature){
                if (creature.hp() == creature.maxHp())
                    return;
                
                creature.modifyHp(50);
                creature.doAction("look healthier");
            }
        });
        
        item.addWrittenSpell("slow heal", 12, new Effect(50){
            public void update(Creature creature){
                super.update(creature);
                creature.modifyHp(2);
            }
        });

        item.addWrittenSpell("inner strength", 16, new Effect(50){
            public void start(Creature creature){
                creature.modifyAttackValue(2);
                creature.modifyDefenseValue(2);
                creature.modifyVisionRadius(1);
                creature.modifyRegenHpPer1000(10);
                creature.modifyRegenManaPer1000(-10);
                creature.doAction("seem to glow with inner strength");
            }
            public void update(Creature creature){
                super.update(creature);
                if (Math.random() < 0.25)
                    creature.modifyHp(1);
            }
            public void end(Creature creature){
                creature.modifyAttackValue(-2);
                creature.modifyDefenseValue(-2);
                creature.modifyVisionRadius(-1);
                creature.modifyRegenHpPer1000(-10);
                creature.modifyRegenManaPer1000(10);
            }
        });
        
        world.addAtEmptyLocation(item, depth);
        return item;
    }

And the second has a hodgepodge of spells that do weird things, mostly to show what can be done within the effects:
public Item newBlueMagesSpellbook(int depth) {
        Item item = new Item('+', AsciiPanel.brightBlue, "blue mage's spellbook");

        item.addWrittenSpell("blood to mana", 1, new Effect(1){
            public void start(Creature creature){
                int amount = Math.min(creature.hp() - 1, creature.maxMana() - creature.mana());
                creature.modifyHp(-amount);
                creature.modifyMana(amount);
            }
        });
        
        item.addWrittenSpell("blink", 6, new Effect(1){
            public void start(Creature creature){
                creature.doAction("fade out");
                
                int mx = 0;
                int my = 0;
                
                do
                {
                    mx = (int)(Math.random() * 11) - 5;
                    my = (int)(Math.random() * 11) - 5;
                }
                while (!creature.canEnter(creature.x+mx, creature.y+my, creature.z)
                        && creature.canSee(creature.x+mx, creature.y+my, creature.z));
                
                creature.moveBy(mx, my, 0);
                
                creature.doAction("fade in");
            }
        });
        
        item.addWrittenSpell("summon bats", 11, new Effect(1){
            public void start(Creature creature){
                for (int ox = -1; ox < 2; ox++){
                    for (int oy = -1; oy < 2; oy++){
                        int nx = creature.x + ox;
                        int ny = creature.y + oy;
                        if (ox == 0 && oy == 0 
                                || creature.creature(nx, ny, creature.z) != null)
                            continue;
                        
                        Creature bat = newBat(0);
                        
                        if (!bat.canEnter(nx, ny, creature.z)){
                            world.remove(bat);
                            continue;
                        }
                        
                        bat.x = nx;
                        bat.y = ny;
                        bat.z = creature.z;
                        
                        creature.summon(bat);
                    }
                }
            }
        });
        
        item.addWrittenSpell("detect creatures", 16, new Effect(75){
            public void start(Creature creature){
                creature.doAction("look far off into the distance");
                creature.modifyDetectCreatures(1);
            }
            public void end(Creature creature){
                creature.modifyDetectCreatures(-1);
            }
        });
        world.addAtEmptyLocation(item, depth);
        return item;
    }

I had to add a couple methods to the creature class to support these.
public void summon(Creature other) {
        world.add(other);
    }
    
    private int detectCreatures;
    public void modifyDetectCreatures(int amount) { detectCreatures += amount; }
    
    public void castSpell(Spell spell, int x2, int y2) {
        Creature other = creature(x2, y2, z);
        
        if (spell.manaCost() > mana){
            doAction("point and mumble but nothing happens");
            return;
        } else if (other == null) {
            doAction("point and mumble at nothing");
            return;
        }
        
        other.addEffect(spell.effect());
        modifyMana(-spell.manaCost());
    }

And update one method:
public boolean canSee(int wx, int wy, int wz){
        return (detectCreatures > 0 && world.creature(wx, wy, wz) != null
                || ai.canSee(wx, wy, wz));
    }

You should also add a potion to restore mana.


Now we just need a ReadScreen to select an item to read, a ReadSpell screen to select a spell from the item, and a CastSpell screen to select a target for the spell. You should be able to figure those out but let's do them in the opposite order:
package rltut.screens;

import rltut.Creature;
import rltut.Spell;

public class CastSpellScreen extends TargetBasedScreen {
    private Spell spell;
    
    public CastSpellScreen(Creature player, String caption, int sx, int sy, Spell spell) {
        super(player, caption, sx, sy);
        this.spell = spell;
    }
    
    public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
        player.castSpell(spell, x, y);
    }
}


The ReadSpellScreen is very similar to the InventoryBasedScreen.
package rltut.screens;

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

import rltut.Creature;
import rltut.Item;
import rltut.Spell;

import asciiPanel.AsciiPanel;

public class ReadSpellScreen implements Screen {

    protected Creature player;
    private String letters;
    private Item item;
    private int sx;
    private int sy;
    
    public ReadSpellScreen(Creature player, int sx, int sy, Item item){
        this.player = player;
        this.letters = "abcdefghijklmnopqrstuvwxyz";
        this.item = item;
        this.sx = sx;
        this.sy = sy;
    }
    
    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 read?", 2, 23);
        
        terminal.repaint();
    }
    
    private ArrayList<String> getList() {
        ArrayList<String> lines = new ArrayList<String>();
        
        for (int i = 0; i < item.writtenSpells().size(); i++){
            Spell spell = item.writtenSpells().get(i);
            
            String line = letters.charAt(i) + " - " + spell.name() + " (" + spell.manaCost() + " mana)";
            
            lines.add(line);
        }
        return lines;
    }

    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) {
            return use(item.writtenSpells().get(letters.indexOf(c)));
        } else if (key.getKeyCode() == KeyEvent.VK_ESCAPE) {
            return null;
        } else {
            return this;
        }
    }

    protected Screen use(Spell spell){
        return new CastSpellScreen(player, "", sx, sy, spell);
    }
}


And the ReadScreen is simple enough.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class ReadScreen extends InventoryBasedScreen {

    private int sx;
    private int sy;
    
    public ReadScreen(Creature player, int sx, int sy) {
        super(player);
        this.sx = sx;
        this.sy = sy;
    }

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

    protected boolean isAcceptable(Item item) {
        return !item.writtenSpells().isEmpty();
    }

    protected Screen use(Item item) {
        return new ReadSpellScreen(player, sx, sy, item);
    }
}


Update the PlayScreen to add spell books, map the r key to the new ReadScreen, and update the HelpScreen.



I know this is already a very long tutorial, but there's one little thing that bugs me. The creature class has some special methods that are only called when gaining a level, the gain* methods. They're only called from one place and they're small enough that we can just inline them. Eclipse, and most IDEs, can do this for you, just select the method name, right click, Refactor, Inline.



(days later....) I just realized a different, and almost certainly better, way of handling effects for each spell. Instead of each spell having a reference to an Effect and using the Effect's copy constructor, the spell class should act as an Effect factory and have an abstract newEffect method. Each individual spell would subclass Spell and implement newEffect. I'll have to do that with my next roguelike.

download the code

Tuesday, October 18, 2011

roguelike tutorial 18: potions and effects

Let's add some potions now. When the player, or smart monster, quaffs a potion it's effect will be applied to the creature. What can we say about effects? Most effects will only last for a certain duration. Some will apply every turn or every few turns (like poison or a slow heal). Others will have a change that lasts for the duration (like confuse or resist cold). This can be done with a start method that that applies the change when first quaffed, an end method that unapplies the change when the duration has run out, and an update method that is called every turn in between. I think this will cover the basics so let's get started on them. We'll create a base Effect class and subclass it with specific effects.

package rltut;

public class Effect {
        protected int duration;
        
        public boolean isDone() { return duration < 1; }
        
        public Effect(int duration){
                this.duration = duration;
        }
        
        public void update(Creature creature){
                duration--;
        }
        
        public void start(Creature creature){
                
        }
        
        public void end(Creature creature){
                
        }
}

And add a quaffEffect to Item class.

private Effect quaffEffect;
public Effect quaffEffect() { return quaffEffect; }
public void setQuaffEffect(Effect effect) { this.quaffEffect = effect; }

Now that we've got items that can have effects when quaffed it's time to add some potions to our StuffFactory. I'll start with three simple ones that show three ways of doing things.

Here's a simple one-time potion where the work happens in the start method:
public Item newPotionOfHealth(int depth){
    Item item = new Item('!', AsciiPanel.white, "health potion");
    item.setQuaffEffect(new Effect(1){
        public void start(Creature creature){
            if (creature.hp() == creature.maxHp())
                return;
                                
            creature.modifyHp(15);
            creature.doAction("look healthier");
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

Here is a potion that affects the creature each turn.
public Item newPotionOfPoison(int depth){
    Item item = new Item('!', AsciiPanel.white, "poison potion");
    item.setQuaffEffect(new Effect(20){
        public void start(Creature creature){
            creature.doAction("look sick");
        }
                        
        public void update(Creature creature){
            super.update(creature);
            creature.modifyHp(-1);
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

Here's one that will affect the creature at the start and restore it at the end.
public Item newPotionOfWarrior(int depth){
    Item item = new Item('!', AsciiPanel.white, "warrior's potion");
    item.setQuaffEffect(new Effect(20){
        public void start(Creature creature){
            creature.modifyAttackValue(5);
            creature.modifyDefenseValue(5);
            creature.doAction("look stronger");
        }
        public void end(Creature creature){
            creature.modifyAttackValue(-5);
            creature.modifyDefenseValue(-5);
            creature.doAction("look less strong");
        }
    });
                
    world.addAtEmptyLocation(item, depth);
    return item;
}

And add a randomizer to help us add the new potions.

public Item randomPotion(int depth){
                switch ((int)(Math.random() * 3)){
                case 0: return newPotionOfHealth(depth);
                case 1: return newPotionOfPoison(depth);
                default: return newPotionOfWarrior(depth);
                }
        }

Now let's go back to the creature class and make sure it's calling the right methods at the right times. It will need a list of effects that are currently applied to it.

private List<Effect> effects;
public ListList<Effect> effects(){ return effects; }

Don't forget to initialize it in the creature's constructor.

Add quaff method to Creature class. Since they're so similar, the quaff and eat methods can share some code.
public void quaff(Item item){
            doAction("quaff a " + item.name());
            consume(item);
        }

        public void eat(Item item){
            doAction("eat a " + item.name());
            consume(item);
        }
        
        private void consume(Item item){
            if (item.foodValue() < 0)
                notify("Gross!");
                
            addEffect(item.quaffEffect());
                
            modifyFood(item.foodValue());
            getRidOf(item);
        }
        
        private void addEffect(Effect effect){
            if (effect == null)
                return;
                
            effect.start(this);
            effects.add(effect);
        }

Create a new method to update the effects each turn and remove any that are done. Don't forget to call the end method before removing the effect.

private void updateEffects(){
            List<Effect> done = new ArrayList<Effect>();
                
            for (Effect effect : effects){
                effect.update(this);
                if (effect.isDone()) {
                    effect.end(this);
                    done.add(effect);
                }
            }
                
            effects.removeAll(done);
        }

Call this during the creature's update method. The player can't quaff anything until we create a QuaffScreen to go with our new behavior.

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class QuaffScreen extends InventoryBasedScreen {

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

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

        protected boolean isAcceptable(Item item) {
                return item.quaffEffect() != null;
        }

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

I think I've said this before but that InventoryBasedScreen is really paying off. Now bind that to the 'q' key in the PlayScreen class and update the createItems method, also in the PlayScreen class, to add some potions. Try 3 or 4 per level to start with. Don't forget to update the HelpScreen too. Play around and change the durations or strengths or abundance of potions. Try adding some new ones that change the vision radius, heal over time, stop regeneration, consume extra food, or fill someone's stomach. I'm sure you can think of even more potions and effects. There you go; potions and effects that are simple and flexible. We're going to add a few more effects in the next tutorial when we add magic.



Wouldn't it be cool if throwing a potion at a creature caused the effect to apply to it? Easy-peasy.

private void throwAttack(Item item, Creature other) {
    commonAttack(other, attackValue / 2 + item.thrownAttackValue(), "throw a %s at the %s for %d damage", item.name(), other.name);
    other.addEffect(item.quaffEffect());
}

Then make sure the throw method removes the item if it has a quaffEffect and the target was a creature, otherwise it should add to the world like it already does. Now you can sit back and chuck poison bottles at goblins.

download the code

Friday, October 14, 2011

roguelike tutorial 17: smarter monsters

Now that we've got all these nifty items and abilities, wouldn't it be cool if our cave monsters could use them too? Let's create a new creature capable of using weapons.

We can start with a GoblinAi that's almost exactly the same as the ZombieAi.

package rltut;

import java.util.List;

public class GoblinAi extends CreatureAi {
    private Creature player;

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

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

    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 let's add newGoblin to the StuffFactory. Goblins start with random weapons and armor.
public Creature newGoblin(int depth, Creature player){
        Creature goblin = new Creature(world, 'g', AsciiPanel.brightGreen, "goblin", 66, 15, 5);
        goblin.equip(randomWeapon(depth));
        goblin.equip(randomArmor(depth));
        world.addAtEmptyLocation(goblin, depth);
        new GoblinAi(goblin, player);
        return goblin;
    }

Now update creature's equip method to make sure anything equipped is added to the inventory if it isn't already there.
public void equip(Item item){
        if (!inventory.contains(item)) {
            if (inventory.isFull()) {
                notify("Can't equip %s since you're holding too much stuff.", item.name());
                return;
            } else {
                world.remove(item);
                inventory.add(item);
            }
        }
    
        if (item.attackValue() == 0 && item.rangedAttackValue() == 0 && item.defenseValue() == 0)
            return;
    
        if (item.attackValue() + item.rangedAttackValue() >= item.defenseValue()){
            unequip(weapon);
            doAction("wield a " + item.name());
            weapon = item;
        } else {
            unequip(armor);
            doAction("put on a " + item.name());
            armor = item;
        }
    }

And add the missing method to the World class. This allows us to remove an object even if we don't know where it is.
public void remove(Item item) {
        for (int x = 0; x < width; x++){
            for (int y = 0; y < height; y++){
                for (int z = 0; z < depth; z++){
                if (items[x][y][z] == item) {
                    items[x][y][z] = null;
                    return;
                }
            }
        }
    }
}

We also need to make sure that when a creature dies it drops anything it was holding.
private void leaveCorpse(){
        Item corpse = new Item('%', color, name + " corpse");
        corpse.modifyFoodValue(maxHp);
        world.addAtEmptySpace(corpse, x, y, z);
        for (Item item : inventory.getItems()){
            if (item != null)
                drop(item);
            }
    }


After adding goblins to the addCreatures method of the PlayScreen you should be able to play and fight some rather tough goblins with armor and weapons of their own. Don't forget to tweak each creatures hp, attack, defense, the food values of corpses, and the xp gained or how much xp is needed for each level. You should also change the number of items and creatures per level. I prefer few items on the ground. That way the player almost has to confront goblins to get better loot.


So our goblins can use melee weapons and armor — that's nice — but they will run over and beat you with a bow instead of fire them. How about if they could throw things or fire from a distance? Time to work on the GoblinAi. My goblins will, in order of priority, try to: ranged attack, throw attack, melee attack, pickup stuff, and wander if they can't do anything else. Let's get to it.
public void onUpdate(){
        if (canRangedWeaponAttack(player))
            creature.rangedWeaponAttack(player);
        else if (canThrowAt(player))
            creature.throwItem(getWeaponToThrow(), player.x, player.y, player.z);
        else if (creature.canSee(player.x, player.y, player.z))
            hunt(player);
        else if (canPickup())
            creature.pickup();
        else
            wander();
    }
You should make your goblins do things in whatever order you want; maybe you want them to pick things up first so you could slow them down by putting some rocks or something between you.

The new helper methods are:
private boolean canRangedWeaponAttack(Creature other){
        return creature.weapon() != null
                && creature.weapon().rangedAttackValue() > 0
                && creature.canSee(other.x, other.y, other.z);
    }

    private boolean canThrowAt(Creature other) {
        return creature.canSee(other.x, other.y, other.z)
          && getWeaponToThrow() != null;
    }

    private Item getWeaponToThrow() {
        Item toThrow = null;
    
        for (Item item : creature.inventory().getItems()){
        if (item == null || creature.weapon() == item || creature.armor() == item)
            continue;
        
        if (toThrow == null || item.thrownAttackValue() > toThrow.attackValue())
            toThrow = item;
        }
    
        return toThrow;
    }

    private boolean canPickup() {
        return creature.item(creature.x, creature.y, creature.z) != null
          && !creature.inventory().isFull();
    }

It's a good idea to take these helper methods, and the hunt method, and move them up to the CreatureAi class. You can use the Pull Up refactoring in Eclipse. That way they can be used by any future creatures we add.

After adding intelligent goblins things should be much more difficult. Play test many times and tweak all the values and behavior you can.


Goblins should also see if they are holding better weapon or armor and switch to it if they are.

Here's the helper methods I came up with:
protected boolean canUseBetterEquipment() {
        int currentWeaponRating = creature.weapon() == null ? 0 : creature.weapon().attackValue() + creature.weapon().rangedAttackValue();
        int currentArmorRating = creature.armor() == null ? 0 : creature.armor().defenseValue();
    
        for (Item item : creature.inventory().getItems()){
            if (item == null)
                continue;
        
            boolean isArmor = item.attackValue() + item.rangedAttackValue() < item.defenseValue();
        
            if (item.attackValue() + item.rangedAttackValue() > currentWeaponRating
                || isArmor && item.defenseValue() > currentArmorRating)
                return true;
        }
    
        return false;
    }

protected void useBetterEquipment() {
        int currentWeaponRating = creature.weapon() == null ? 0 : creature.weapon().attackValue() + creature.weapon().rangedAttackValue();
        int currentArmorRating = creature.armor() == null ? 0 : creature.armor().defenseValue();
    
        for (Item item : creature.inventory().getItems()){
            if (item == null)
                continue;
        
            boolean isArmor = item.attackValue() + item.rangedAttackValue() < item.defenseValue();
        
            if (item.attackValue() + item.rangedAttackValue() > currentWeaponRating
                || isArmor && item.defenseValue() > currentArmorRating) {
                creature.equip(item);
            }
        }
    }

And now you have goblins that will chase you when they can, attack from afar when they can, and switch to better equipment they find. That's a monster that's more intelligent than many rogulike denizens. You can make them even more intellegent by having them remember where the player is. If they can't see the player anymore then they go to that location. This way they won't lose interest in you just because you step out of view for one turn.


These goblins are tough — sometimes too tough. It would be easier if we could regenerate some health. Add this to the creature class:
private int regenHpCooldown;
    private int regenHpPer1000;
    public void modifyRegenHpPer1000(int amount) { regenHpPer1000 += amount; }

Set the regenHpPer1000 to something reasonable in the constructor. I use 10. You could also use constructor injection or call modifyRegenHpPer1000 in our factory to make some monsters regenerate very quickly.

Then create a new method to regenerateHealth and call it as part of the update method.

private void regenerateHealth(){
        regenHpCooldown -= regenHpPer1000;
        if (regenHpCooldown < 0){
            modifyHp(1);
            modifyFood(-1);
            regenHpCooldown += 1000;
        }
    }

Since we have a new stat, we can add to our level up options too.

download the code

Tuesday, October 11, 2011

roguelike tutorial 16: throwing and ranged weapons

Now that we've got our TargetBasedScreen let's make some more things with it. We've already got items so let's make a ThrowScreen. If we add ranged weapons then we could create a FireWeaponScreen.

First let's add another value to represent how much damage is done when an item is thrown.
private int thrownAttackValue;
    public int thrownAttackValue() { return thrownAttackValue; }
    public void modifyThrownAttackValue(int amount) { thrownAttackValue += amount; }

We can set the default to 1 in the constructor and update the StuffFactory to give a value to things that are good for throwing like certain weapons or rocks. Don't forget to update the item class's details method.


Now let's add a couple methods to the Creature class. One throws an item to a location and one damages any creature there.
public void throwItem(Item item, int wx, int wy, int wz) {
        Point end = new Point(x, y, 0);
    
        for (Point p : new Line(x, y, wx, wy)){
            if (!realTile(p.x, p.y, z).isGround())
                break;
            end = p;
        }
    
        wx = end.x;
        wy = end.y;
    
        Creature c = creature(wx, wy, wz);
    
        if (c != null)
            throwAttack(item, c);
        else
            doAction("throw a %s", item.name());
    
        unequip(item);
        inventory.remove(item);
        world.addAtEmptySpace(item, wx, wy, wz);
    }

And now actual attacks with thrown weapons. I'll add half the base attack value of the thrower since thrown weapons should generally do less damage than mele weapons.
private void throwAttack(Item item, Creature other) {
        modifyFood(-1);
    
        int amount = Math.max(0, attackValue / 2 + item.thrownAttackValue() - other.defenseValue());
    
        amount = (int)(Math.random() * amount) + 1;
    
        doAction("throw a %s at the %s for %d damage", item.name(), other.name, amount);
    
        other.modifyHp(-amount);
    
        if (other.hp < 1)
            gainXp(other);
    }


The throwing screen will be interesting because it's the first time we have a double screen scenario. We need a subclass of InventoryBasedScreen to select what we want to throw and then a subclass of a TargetBasedScreen to pick what to throw it at.

The ThrowAtScreen is simple and self explanatory. We can throw at anything we can see that isn't blocked by walls.
package rltut.screens;

import rltut.Creature;
import rltut.Item;
import rltut.Line;
import rltut.Point;

public class ThrowAtScreen extends TargetBasedScreen {
    private Item item;

    public ThrowAtScreen(Creature player, int sx, int sy, Item item) {
        super(player, "Throw " + item.name() + " at?", sx, sy);
        this.item = item;
    }

    public boolean isAcceptable(int x, int y) {
        if (!player.canSee(x, y, player.z))
            return false;
    
        for (Point p : new Line(player.x, player.y, x, y)){
            if (!player.realTile(p.x, p.y, player.z).isGround())
                return false;
        }
    
        return true;
    }

    public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
        player.throwItem(item, x, y, player.z);
    }
}

The ThrowScreen is also a simple InventoryBasedScreen except we need to pass some values that aren't needed by the ThrowScreen but are used by the ThrowAtScreen.
package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class ThrowScreen extends InventoryBasedScreen {
    private int sx;
    private int sy;

    public ThrowScreen(Creature player, int sx, int sy) {
        super(player);
        this.sx = sx;
        this.sy = sy;
    }

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

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

    protected Screen use(Item item) {
        return new ThrowAtScreen(player, sx, sy, item);
    }
}

Just add throwing to the HelpScreen and the respondToUserInput method of the PlayScreen and then try it.
case KeyEvent.VK_T: subscreen = new ThrowScreen(player,
        player.x - getScrollX(),
        player.y - getScrollY()); break;

Now you have a use for those rocks that are scattered around.


It's great that we can throw but how about some ranged weapons? Let's update the Item class.
private int rangedAttackValue;
    public int rangedAttackValue() { return rangedAttackValue; }
    public void modifyRangedAttackValue(int amount) { rangedAttackValue += amount; }

Anything that has a non zero rangedAttackValue is a ranged weapon. This way a weapon can have separate attack values for melee, thrown, and ranged combat. We'll keep it simple and say that ranged weapons can hit anything we can see that isn't blocked by terrain.

Add new ranged weapons to the StuffFactory. Bows are good at ranged combat but not very good at hitting things up close.
public Item newBow(int depth){
        Item item = new Item(')', AsciiPanel.yellow, "bow");
        item.modifyAttackValue(1);
        item.modifyRangedAttackValue(5);
        world.addAtEmptyLocation(item, depth);
        return item;
    }
Make sure new weapons gan be created when asking for a randomWeapon.
public Item randomWeapon(int depth){
        switch ((int)(Math.random() * 3)){
        case 0: return newDagger(depth);
        case 1: return newSword(depth);
        case 2: return newBow(depth);
        default: return newStaff(depth);
        }
    }

Creatures need a way to attack with ranged weapons. I'll add half the attack value like with thrown items.
public void rangedWeaponAttack(Creature other){
        modifyFood(-1);
    
        int amount = Math.max(0, attackValue / 2 + weapon.rangedAttackValue() - other.defenseValue());
    
        amount = (int)(Math.random() * amount) + 1;
    
        doAction("fire a %s at the %s for %d damage", weapon.name(), other.name, amount);
    
        other.modifyHp(-amount);
    
        if (other.hp < 1)
            gainXp(other);
    }

The FireWeaponScreen is similar to the ThrowAtScreen. We can fire our weapon at anything we can see that isn't blocked by walls.
package rltut.screens;

import rltut.Creature;
import rltut.Line;
import rltut.Point;

public class FireWeaponScreen extends TargetBasedScreen {

    public FireWeaponScreen(Creature player, int sx, int sy) {
        super(player, "Fire " + player.weapon().name() + " at?", sx, sy);
    }

    public boolean isAcceptable(int x, int y) {
        if (!player.canSee(x, y, player.z))
            return false;
    
        for (Point p : new Line(player.x, player.y, x, y)){
            if (!player.realTile(p.x, p.y, player.z).isGround())
                return false;
        }
    
        return true;
    }

    public void selectWorldCoordinate(int x, int y, int screenX, int screenY){
        Creature other = player.creature(x, y, player.z);
    
        if (other == null)
            player.notify("There's no one there to fire at.");
        else
            player.rangedWeaponAttack(other);
    }
}

Just add firing to the HelpScreen and the respondToUserInput method of the PlayScreen and then try it.
case KeyEvent.VK_F:
        if (player.weapon() == null || player.weapon().rangedAttackValue() == 0)
         player.notify("You don't have a ranged weapon equiped.");
        else
         subscreen = new FireWeaponScreen(player,
             player.x - getScrollX(),
             player.y - getScrollY()); break;


Creatures sometimes use up an item and it no longer exists. Other times they no longer have the item but it still exists in the world. Either way, we need to make sure they no longer have it equipped and that they no longer have it in their inventory. One way to do this is to create two helper methods and call these when possible.
private void getRidOf(Item item){
        inventory.remove(item);
        unequip(item);
    }

    private void putAt(Item item, int wx, int wy, int wz){
        inventory.remove(item);
        unequip(item);
        world.addAtEmptySpace(item, wx, wy, wz);
    }


The attack weapons all have similar bodies so we could use an Extract Method refactoring to move the commonalities to a separate method and use Extract Parameter or Parameterize Method to pass in the differences.

public void meleeAttack(Creature other){
        commonAttack(other, attackValue(), "attack the %s for %d damage", other.name);
    }

    private void throwAttack(Item item, Creature other) {
        commonAttack(other, attackValue / 2 + item.thrownAttackValue(), "throw a %s at the %s for %d damage", item.name(), other.name);
    }

    public void rangedWeaponAttack(Creature other){
        commonAttack(other, attackValue / 2 + weapon.rangedAttackValue(), "fire a %s at the %s for %d damage", weapon.name(), other.name);
    }

    private void commonAttack(Creature other, int attack, String action, Object ... params) {
        modifyFood(-2);
    
        int amount = Math.max(0, attack - other.defenseValue());
    
        amount = (int)(Math.random() * amount) + 1;
    
        Object[] params2 = new Object[params.length+1];
        for (int i = 0; i < params.length; i++){
         params2[i] = params[i];
        }
        params2[params2.length - 1] = amount;
    
        doAction(action, params2);
    
        other.modifyHp(-amount);
    
        if (other.hp < 1)
            gainXp(other);
    }

This particular implementation has the hidden side effect that the message that is passed in must end in a damage indicator since the commonAttack method will add that. It's not good to do things like this too often, but sometimes that happens.

download the code

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