Thursday, January 21, 2016
roguelike tutorial 00: The table of contents
Tuesday, February 26, 2013
Choosing colors for your Roguelike
If you're making a roguelike you need to decide what colors to use. You could stick with the original 16 colors or you can use hundreds or thousands of colors. I don't know much about colors and color schemes but I have some advice on choosing colors for a roguelike. For my 2012 7DRL, I rule, you rule, we all rule old-school Hyrule, I followed three simple rules about color:
- The hue should match the real world color. That is; red things should be red and green things should be green.
- The saturation and brightness should match how important something is. If it's trying to kill you or it can save you then it should stand out as a bright color. If it's just background - floors, trees, walls - then it should be mostly gray or black so it doesn't get your attention. You can only notice so much at once and you should notice the important things first.
- Subtle variation in color is a good thing. Some variation makes large blocks of trees or water look better but too much variation and shading makes it hard to tell if the color is relevant or not. In iryrwarosh, each tree has three different colors. Water, lava, and sand also had some variation. The variation was subtle enough to break up the monotony and allow things like animated water and lava. Possibly too subtle, but that's just fine with me. Just don't overdo it; even if two trees are a slightly different shade of red or green, you should still know they're the same thing.
Overall I'm very happy with how iryrwarosh looked and I think it's due to following these three guidelines. It did take some time to tweak the colors, but it was really worth it. I plan on following the same guidelines this year.
As a bonus, here's the ActionScript that I use to make a color with a specific hue, saturation, and value.
Saturday, February 23, 2013
A* algoritm in ActionScript
Want to know the shortest path from one point to another? If you're making a roguelike then you probably do. You could use Dijkstra's algorithm, but there's an alternative that finds the result much faster. It's the A*, or A Star, algorithm. It's a lot like Dijkstra's algorithm except instead of checking the neighbors of all the currently visited cells, it just checks the neighbor of the visited cell that's the closest to the target. Of course if you already know which one is closest then you don't need to pathfind. Instead you just estimate which one is closest. Even if your estimates are a bit off, you'll still find the shortest path in much less time.
Here's an ActionScript implementation.
This algorithm needs some explanation since it was made by mathematicians - who generally suck at naming variables. That's why there's a g, h, and f cost. The H cost is the heuristic or estimated cost from a point to the end point. The G cost is the actual cost from the start point to a point. There's no need to estimate that because you only calculate this on points that you've visited. The F cost is the total of the two. By only checking the neighbors of the point with the lowest F cost, you end up - in the best case - only checking the points that are in the shortest path. You can change the getGCost and getHCost methods to cause different behavior. Maybe some points cost more to travel through? Maybe moving diagonal cost more than moving in the cardinal directions? Try it and see what happens.
The checkEnding boolean is because in my game people sometimes can pathfind to walls and obstacles that block movement.
It's mostly used for pathfinding but can also be used for carving rivers, roads, or caves during worldgen. You could also use it to tell where someone is probably going to go - useful for placing traps I suppose. Have you seen any interesting uses for this algorithm?
Tuesday, February 19, 2013
Dijkstra's algorithm in ActionScript
Dijkstra's algorithm is a useful algorithm for roguelike developers to know. It basically calculates the distance from a starting point to all other points. Once you know the distances, you can walk backwards from any point to the start. It's great for worldgen, floodfilling, ai, and pathfinding to the nearest something. Dijkstra's algorithm is also the basis for autoexplore in brogue. If you want to know "where's the nearest" of something, use Dijkstra's algorithm.
Here's an ActionScript implementation.
Note that it uses a grid to track parent nodes as well as if a cell has been visited or not. It does this for performance reasons but it's possible to keep a list or dictionary of visited nodes instead.
This implementation takes the start position, a function that verifies a cell can be entered, and a function that determines if a cell is what you're looking for. It returns an array of points from the start position to the end position or an empty array if no path was found. Using canEnter and check functions means that there's no dependancies on details of your game code - which is a very good thing. Passing in your game details means that if you want this to work with land creatures, water creatures, and amphibians, just pass different versions of canEnter depending on who's searching. And since you pass your own check function, you can search for whatever you want. Injured creatures could search for the nearest healing potion. Fleeing creatures could look for the closest tile that isn't visible from the player. Exploring creatures could look for the nearest tile that they haven't seen before. There's a lot of potential uses for this algorithm.
Saturday, February 16, 2013
Field Of View (FOV) in ActionScript
One of the simplest ways of calculating Field Of View (FOV) in a roguelike is also my favorite. There's several different algorithms but I like to keep it simple. Just check every tile within a view radius of the player. If you know how to calculate Line Of Sight, then it's very easy. If the line to the end point isn't blocked, then the end point is visible. Once you can check line of sight between arbitrary points, then it's easy.
Here's a Flash implementation. Use your mouse to see what's viewable from a location.
It's probably the least efficient way to do it - which is why it takes a second or so to calculate. It is super simple to understand and implement though. I also think it looks the best. Not only can this be used for seeing which tiles are visible from a specific tile but it can also be used for seeing what's affected by an explosion or other effect.
Once you have performance problems you can switch to a more efficient algorithm or rely on caching.
Thursday, February 7, 2013
Line Of Sight (LOS) in ActionScript
One of the simplest algorithms in any roguelike is line of sight (LOS). It's basically drawing a line from one point to another and seeing if anything obscures it. Here's an Actionscript implementation of the Bresenham line algorithm (invented in 1962!).
It's not super obvious what's going on. Basically, figure out which direction the line always goes, and how often it goes in the other axis. Then go.
This is the basis for simple field of view (FOV) algorithms and is a quick and easy way of seeing if a tile, item, monster, or event can be seen by the player or another creature. It can also be used to calculate where a projectile will go, corridor generation, or simple pathfinding in open spaces.
I'm sure there are low-level ways to make this perform faster like bit shifting or breaking each octant into it's own function. It could also return an iterator that calculates points as-needed. This would allow really long lines - or even infinitely long lines - while only calculating what's actually needed.
Friday, October 28, 2011
roguelike tutorial retrospective
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.
Tuesday, October 25, 2011
roguelike tutorial 20: item appearance and identification
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;
}
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
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
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
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
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'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
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
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