Friday, September 30, 2011

roguelike tutorial 13: aggressive monsters

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

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

package rltut;



import java.util.ArrayList;

import java.util.Collections;

import java.util.HashMap;



public class PathFinder {

private ArrayList<Point> open;

private ArrayList<Point> closed;

private HashMap<Point, Point> parents;

private HashMap<Point,Integer> totalCost;



public PathFinder() {

this.open = new ArrayList<Point>();

this.closed = new ArrayList<Point>();

this.parents = new HashMap<Point, Point>();

this.totalCost = new HashMap<Point, Integer>();

}



private int heuristicCost(Point from, Point to) {

return Math.max(Math.abs(from.x - to.x), Math.abs(from.y - to.y));

}



private int costToGetTo(Point from) {

return parents.get(from) == null ? 0 : (1 + costToGetTo(parents.get(from)));

}



private int totalCost(Point from, Point to) {

if (totalCost.containsKey(from))

return totalCost.get(from);



int cost = costToGetTo(from) + heuristicCost(from, to);

totalCost.put(from, cost);

return cost;

}



private void reParent(Point child, Point parent){

parents.put(child, parent);

totalCost.remove(child);

}



public ArrayList<Point> findPath(Creature creature, Point start, Point end, int maxTries) {

open.clear();

closed.clear();

parents.clear();

totalCost.clear();



open.add(start);



for (int tries = 0; tries < maxTries && open.size() > 0; tries++){

Point closest = getClosestPoint(end);



open.remove(closest);

closed.add(closest);



if (closest.equals(end))

return createPath(start, closest);

else

checkNeighbors(creature, end, closest);

}

return null;

}



private Point getClosestPoint(Point end) {

Point closest = open.get(0);

for (Point other : open){

if (totalCost(other, end) < totalCost(closest, end))

closest = other;

}

return closest;

}



private void checkNeighbors(Creature creature, Point end, Point closest) {

for (Point neighbor : closest.neighbors8()) {

if (closed.contains(neighbor)

|| !creature.canEnter(neighbor.x, neighbor.y, creature.z)

&& !neighbor.equals(end))

continue;



if (open.contains(neighbor))

reParentNeighborIfNecessary(closest, neighbor);

else

reParentNeighbor(closest, neighbor);

}

}



private void reParentNeighbor(Point closest, Point neighbor) {

reParent(neighbor, closest);

open.add(neighbor);

}



private void reParentNeighborIfNecessary(Point closest, Point neighbor) {

Point originalParent = parents.get(neighbor);

double currentCost = costToGetTo(neighbor);

reParent(neighbor, closest);

double reparentCost = costToGetTo(neighbor);



if (reparentCost < currentCost)

open.remove(neighbor);

else

reParent(neighbor, originalParent);

}



private ArrayList<Point> createPath(Point start, Point end) {

ArrayList<Point> path = new ArrayList<Point>();



while (!end.equals(start)) {

path.add(end);

end = parents.get(end);

}



Collections.reverse(path);

return path;

}

}


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

package rltut;



import java.util.List;



public class Path {



private static PathFinder pf = new PathFinder();



private List<Point> points;

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



public Path(Creature creature, int x, int y){

points = pf.findPath(creature,

new Point(creature.x, creature.y, creature.z),

new Point(x, y, creature.z),

300);

}

}


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


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

package rltut;



import java.util.List;



public class ZombieAi extends CreatureAi {

private Creature player;



public ZombieAi(Creature creature, Creature player) {

super(creature);

this.player = player;

}

}


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

public void onUpdate(){

if (Math.random() < 0.2)

return;



if (creature.canSee(player.x, player.y, player.z))

hunt(player);

else

wander();

}


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

The hunt method finds a path to the target and moves to it.
public void hunt(Creature target){

List<Point> points = new Path(creature, target.x, target.y).points();



int mx = points.get(0).x - creature.x;

int my = points.get(0).y - creature.y;



creature.moveBy(mx, my, 0);

}


Now we can add zombies to our factory. Since the Ai needs a reference to the player, we have to pass that in.
public Creature newZombie(int depth, Creature player){

Creature zombie = new Creature(world, 'z', AsciiPanel.white, "zombie", 50, 10, 10);

world.addAtEmptyLocation(zombie, depth);

new ZombieAi(zombie, player);

return zombie;

}


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

for (int i = 0; i < z + 3; i++){

factory.newZombie(z, player);

}


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

download the code

Tuesday, September 27, 2011

roguelike tutorial 12: weapons and armor

Time for some weapons and armor.

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

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

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


And create some new items in our factory class.

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

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

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

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

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

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

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

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

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

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


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

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

private Item armor;
public Item armor() { return armor; }
public void unequip(Item item){
if (item == null)
return;

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

if (item.attackValue() >= item.defenseValue()){
unequip(weapon);
doAction("wield a " + item.name());
weapon = item;
} else {
unequip(armor);
doAction("put on a " + item.name());
armor = item;
}
}

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

public void eat(Item item){
if (item.foodValue() < 0)
notify("Gross!");

modifyFood(item.foodValue());
inventory.remove(item);
unequip(item);
}

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

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

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


And now create an EquipScreen.
package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EquipScreen extends InventoryBasedScreen {

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

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

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

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

Wasn't that easy?

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

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


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

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


Wouldn't it also be nice if the inventory screens told us what we have equipped so we don't eat the armor we're wearing or try to wear something we're already wearing? Here's one possible update to the InventoryBasedScreen:
String line = letters.charAt(i) + " - " + item.glyph() + " " + item.name();

if(item == player.weapon() || item == player.armor())
line += " (equipped)";

lines.add(line);
Maybe the EquipScreen shouldn't let us equip what we're already using. Or maybe wearing or wielding what's already equipped should un-wear or un-weild it? That way the 'w' key can equip or unequip. It's your game so it's up to you. Implementing those is left as an exercise.

download the code

Friday, September 23, 2011

roguelike tutorial 11: hunger and food

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

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

And update our creature to leave corpses.

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

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

Update creatures to also have hunger.

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

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

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

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

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

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

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

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

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

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

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

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

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class EatScreen extends InventoryBasedScreen {

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

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

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

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

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


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

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

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

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


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

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

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

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

download the code

Tuesday, September 20, 2011

roguelike tutorial 10: items, inventory, inventory screens

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

package rltut;

import java.awt.Color;

public class Item {

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

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

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

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

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

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

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

And now we can add our first item.

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

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

Our world needs one item per tile.

private Item[][][] items;

This should get initialized in the constructor.

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

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

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

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

public void addAtEmptyLocation(Item item, int depth) {
int x;
int y;

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

items[x][y][depth] = item;
}

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

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

if (item(x,y,z) != null)
return item(x,y,z).glyph();

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

if (item(x,y,z) != null)
return item(x,y,z).color();

return tile(x, y, z).color();
}

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

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

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


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

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

package rltut;

public class Inventory {

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

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

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

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

And a way to remove an item from our inventory.

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

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

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

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

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

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

this.inventory = new Inventory(20);

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

public void pickup(){
Item item = world.item(x, y, z);

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

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


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

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

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

public void addAtEmptySpace(Item item, int x, int y, int z){
if (item == null)
return;

List<Point> points = new ArrayList<Point>();
List<Point> checked = new ArrayList<Point>();

points.add(new Point(x, y, z));

while (!points.isEmpty()){
Point p = points.remove(0);
checked.add(p);

if (!tile(p.x, p.y, p.z).isGround())
continue;

if (items[p.x][p.y][p.z] == null){
items[p.x][p.y][p.z] = item;
Creature c = this.creature(p.x, p.y, p.z);
if (c != null)
c.notify("A %s lands between your feet.", item.name());
return;
} else {
List<Point> neighbors = p.neighbors8();
neighbors.removeAll(checked);
points.addAll(neighbors);
}
}
}

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

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


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

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

Try it out.


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

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

Let's start with a basic InventoryBasedScreen:

package rltut.screens;

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

public abstract class InventoryBasedScreen implements Screen {

protected Creature player;
private String letters;

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

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

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

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

public void displayOutput(AsciiPanel terminal) {
ArrayList<String> lines = getList();

int y = 23 - lines.size();
int x = 4;

if (lines.size() > 0)
terminal.clear(' ', x, y, 20, lines.size());

for (String line : lines){
terminal.write(line, x, y++);
}

terminal.clear(' ', 0, 23, 80, 1);
terminal.write("What would you like to " + getVerb() + "?", 2, 23);

terminal.repaint();
}

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

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

private ArrayList<String> getList() {
ArrayList<String> lines = new ArrayList<String>();
Item[] inventory = player.inventory().getItems();

for (int i = 0; i < inventory.length; i++){
Item item = inventory[i];

if (item == null || !isAcceptable(item))
continue;

String line = letters.charAt(i) + " - " + item.glyph() + " " + item.name();

lines.add(line);
}
return lines;
}

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

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

Item[] items = player.inventory().getItems();

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

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

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

package rltut.screens;

import rltut.Creature;
import rltut.Item;

public class DropScreen extends InventoryBasedScreen {

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

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

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

Since anything can be dropped, all items are acceptable.

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

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

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

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

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

private Screen subscreen;

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

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

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

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

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

if (subscreen == null)
world.update();

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

return this;
}

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


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

Add our new item to the SuffFactory:

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

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

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

And update the WorldBuilder to include some exist stairs.

private WorldBuilder addExitStairs() {
int x = -1;
int y = -1;

do {
x = (int)(Math.random() * width);
y = (int)(Math.random() * height);
}
while (tiles[x][y][0] != Tile.FLOOR);

tiles[x][y][0] = Tile.STAIRS_UP;
return this;
}

And make that part of creating caves.

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


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

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

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

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

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

download the code

Friday, September 16, 2011

roguelike tutorial 09: wandering monsters

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

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

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

if (tile.isGround()){

creature.x = x;

creature.y = y;

creature.z = z;

} else {

creature.doAction("bump into a wall");

}

}


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

public void wander(){

int mx = (int)(Math.random() * 3) - 1;

int my = (int)(Math.random() * 3) - 1;

creature.moveBy(mx, my, 0);

}


Now the ai for our bats.

package rltut;



public class BatAi extends CreatureAi {



public BatAi(Creature creature) {

super(creature);

}



public void onUpdate(){

wander();

wander();

}

}


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

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

public Creature newBat(int depth){

Creature bat = new Creature(world, 'b', AsciiPanel.yellow, 15, 5, 0);

world.addAtEmptyLocation(bat, depth);

new BatAi(bat);

return bat;

}


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

for (int i = 0; i < 20; i++){

creatureFactory.newBat(z);

}


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

if (player.hp() < 1)

return new LoseScreen();


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


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

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

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

return;


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

public Creature creature(int wx, int wy, int wz) {

return world.creature(wx, wy, wz);

}


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

public void wander(){

int mx = (int)(Math.random() * 3) - 1;

int my = (int)(Math.random() * 3) - 1;



Creature other = creature.creature(creature.x + mx, creature.y + my, creature.z);



if (other != null && other.glyph() == creature.glyph())

return;

else

creature.moveBy(mx, my, 0);

}


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

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



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

private String name;

public String name() { return name; }


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

Much better.

download the code

Tuesday, September 13, 2011

roguelike tutorial 08: vision, line of sight, and field of view

It doesn't feel like we're exploring much since we see the whole level from the beginning. Ideally we can only see our immediate surroundings and remember what we've already seen. I think we can do that in one session.

The first thing we need is a way to determine if something is in our line of sight. To do this we get all the points in between us and what we want to look at and see if any of them block our vision. For this, we can create a new Line class that uses Bresenham's line algorithm to find all the points along the line.

package rltut;



import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;



public class Line {

private List<Point> points;

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



public Line(int x0, int y0, int x1, int y1) {

points = new ArrayList<Point>();



int dx = Math.abs(x1-x0);

int dy = Math.abs(y1-y0);



int sx = x0 < x1 ? 1 : -1;

int sy = y0 < y1 ? 1 : -1;

int err = dx-dy;



while (true){

points.add(new Point(x0, y0, 0));



if (x0==x1 && y0==y1)

break;



int e2 = err * 2;

if (e2 > -dx) {

err -= dy;

x0 += sx;

}

if (e2 < dx){

err += dx;

y0 += sy;

}

}

}

}


If you look this all the work is done in the constructor - that's a bad sign. So says Misko Hevery of Google fame, Martian Feathers of Working Effectively With Legacy Code, and anyone who's had to deal with this before. On the other hand, it doesn't do that much work; it just creates a list of points. The points are value objects and the line itself could be a value object. I'm certainly no fan of constructors that initialize their collaborators but this seems like a special case. Since it's just a personal project and no one's life or money are on the line, I'll try it and see if it becomes a problem.

To make things a tiny bit more convenient to loop through the points in a line, we can make the class implement Iterable<Point>. All we have to do is declare that the Line implements Iterable<Point> and add the following method:

public Iterator<Point> iterator() {

return points.iterator();

}


I have a feeling that we will use this Line class a lot as we add more features.


Since creatures are the ones who are doing the seeing, it makes since to give creatures a new stat to say how far they can see and a couple methods for looking at the world.

private int visionRadius;

public int visionRadius() { return visionRadius; }



public boolean canSee(int wx, int wy, int wz){

return ai.canSee(wx, wy, wz);

}



public Tile tile(int wx, int wy, int wz) {

return world.tile(wx, wy, wz);

}


I set the vision radius to 9 in the creature's constructor but you should use whatever value you prefer or even have it passed in from the creatureFactory. Since we delegate the task to seeing to the CreatureAi, that's where the work is done and what we'll add to next.

public boolean canSee(int wx, int wy, int wz) {

if (creature.z != wz)

return false;



if ((creature.x-wx)*(creature.x-wx) + (creature.y-wy)*(creature.y-wy) > creature.visionRadius()*creature.visionRadius())

return false;



for (Point p : new Line(creature.x, creature.y, wx, wy)){

if (creature.tile(p.x, p.y, wz).isGround() || p.x == wx && p.y == wy)

continue;



return false;

}



return true;

}


Now that our player can see his immediate surroundings, we should update the PlayScreen to only show the monsters and tiles that can be seen. Tiles outside the line of sight are shown in dark grey.

private void displayTiles(AsciiPanel terminal, int left, int top) {

for (int x = 0; x < screenWidth; x++){

for (int y = 0; y < screenHeight; y++){

int wx = x + left;

int wy = y + top;



if (player.canSee(wx, wy, player.z)){

Creature creature = world.creature(wx, wy, player.z);

if (creature != null)

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

else

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

} else {

terminal.write(world.glyph(wx, wy, player.z), x, y, Color.darkGray);

}

}

}

}


Try it. You should see tiles outside of the player's range in a dark grey color.


We need a place to store what tiles the user has seen but who tracks what's been seen? It's part of the GUI so maybe the PlayScreen? But it's heavely based on the map so maybe the World class should flag tiles that have been seen — many games do this so maybe we should too? But the Creature is the one seeing so maybe it should. But we want the player to record this, the other monsters don't need to so maybe the CreatureAi should track what the creature has seen? That last one seems right to me; the PlayerAi should track what the player has seen and the PlayScreen should use that info to determines what get's displayed. This also means that we can store what tile the player saw at each location so what the player remembers may be different than what is in the real world. So if there's a cave in, or tunnels are dug, or some other change in the world then the player will remember what was last seen and be quite surprised when returning to that area. Neat posiblities.

We need a new tile to indicate a place that has not been seen. This is similar to the out of bounds tile we have since it's not really part of the world but it makes things much easier.
UNKNOWN(' ', AsciiPanel.white)


There are several different ways of determining what is in the player's field of view this but the simplest, and therefore what I prefer, is called raycasting. It's exactly what we're already doing: draw a line from the viewer to the tile in question to see if anything is blocking the vision. Raycasting is probably the slowest way, but it's quick enough and I think has the best overall look. Other methods perform differently when columns and doorways are involved.

Let's create a new class for our field of view. We can slightly extend the common definition of ours to not only determine what is in view but to remember what has already been seen too. What's visible now and what was seen earlier are technically two different things and possibly should be implemented by two different classes, but they're close enough and we can change it later if necessary.
package rltut;



public class FieldOfView {

private World world;

private int depth;



private boolean[][] visible;

public boolean isVisible(int x, int y, int z){

return z == depth && x >= 0 && y >= 0 && x < visible.length && y < visible[0].length && visible[x][y];

}



private Tile[][][] tiles;

public Tile tile(int x, int y, int z){

return tiles[x][y][z];

}



public FieldOfView(World world){

this.world = world;

this.visible = new boolean[world.width()][world.height()];

this.tiles = new Tile[world.width()][world.height()][world.depth()];



for (int x = 0; x < world.width(); x++){

for (int y = 0; y < world.height(); y++){

for (int z = 0; z < world.depth(); z++){

tiles[x][y][z] = Tile.UNKNOWN;

}

}

}

}

}

That seems like a good interface. We can ask if a tile is visible and we can ask what tile was last seen somewhere. We just need to add the method to update what's visible and has been seen.

public void update(int wx, int wy, int wz, int r){

depth = wz;

visible = new boolean[world.width()][world.height()];



for (int x = -r; x < r; x++){

for (int y = -r; y < r; y++){

if (x*x + y*y > r*r)

continue;



if (wx + x < 0 || wx + x >= world.width()

|| wy + y < 0 || wy + y >= world.height())

continue;



for (Point p : new Line(wx, wy, wx + x, wy + y)){

Tile tile = world.tile(p.x, p.y, wz);

visible[p.x][p.y] = true;

tiles[p.x][p.y][wz] = tile;



if (!tile.isGround())

break;

}

}

}

}



Only the player is going to use this advanced field of view, all other creatures can use the default line of sight code. Add a FieldOfView variable to the PlayerAi and override the canSee method.
public boolean canSee(int wx, int wy, int wz) {

return fov.isVisible(wx, wy, wz);

}


Since the FieldOfView requires a world to be passed in the constructor and we don't want the ai's to know about the world, we can build the FieldOfView elseware and rely on constructor injection to give it to the PlayerAi. This means it will have to be passed into the CreatureFactory from the PlayScreen too.

The PlayScreen should construct a new field of view once the world has been made and pass it to the CreatureFactory. Since the PlayScreen is also responsible for displaying the world to the user, we should keep a reference to the field of view. Then we can update it before displaying the world and rely on it for tiles outside of the player's view. After that, we just need to modify the displayTiles method.
private void displayTiles(AsciiPanel terminal, int left, int top) {

fov.update(player.x, player.y, player.z, player.visionRadius());



for (int x = 0; x < screenWidth; x++){

for (int y = 0; y < screenHeight; y++){

int wx = x + left;

int wy = y + top;



if (player.canSee(wx, wy, player.z)){

Creature creature = world.creature(wx, wy, player.z);

if (creature != null)

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

else

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

else

terminal.write(fov.tile(wx, wy, player.z).glyph(), x, y, Color.darkGray);

}

}

}

}

And there you go, line of sight and field of view. These caves are starting to feel like real caves. If only they had some more monsters....



I just remembered something I wanted to do with the world once we added creatures. Replace the glyph and color methods with these:
public char glyph(int x, int y, int z){

Creature creature = creature(x, y, z);

return creature != null ? creature.glyph() : tile(x, y, z).glyph();

}

public Color color(int x, int y, int z){

Creature creature = creature(x, y, z);

return creature != null ? creature.color() : tile(x, y, z).color();

}

Since the world takes care of that for us, the PlayScreen becomes simpler.
private void displayTiles(AsciiPanel terminal, int left, int top) {

fov.update(player.x, player.y, player.z, player.visionRadius());



for (int x = 0; x < screenWidth; x++){

for (int y = 0; y < screenHeight; y++){

int wx = x + left;

int wy = y + top;



if (player.canSee(wx, wy, player.z))

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

else

terminal.write(fov.tile(wx, wy, player.z).glyph(), x, y, Color.darkGray);

}

}

}




Instead of having doAction notify everyone nearby, it should only notify them if they can see the one doing the action.

public void doAction(String message, Object ... params){

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;



if (other == this)

other.notify("You " + message + ".", params);

else if (other.canSee(x, y, z))

other.notify(String.format("The %s %s.", name, makeSecondPerson(message)), params);

}

}

}


Now you have to actually see something happen in order to be notified about it.

download the code

Saturday, September 10, 2011

Làm đẹp tiện ích Popular Posts (hiệu ứng tooltip)

Tiện ích Popular posts là 1 tiện ích được cung cấp bởi Blogger, nó hiển thị các bài viết được xem nhiều nhất trong blog với các khoảng thời gian như : 1 tuần, 1 tháng, hay mọi lúc. Và nó cũng có 3 chế độ hiển thị : chỉ hiển thị tiêu đề, hiện thị tiêu đề và ảnh thumbnail, và thứ 3 là kiểu hiện thị đầy đủ (gồm : tiêu đề, ảnh thumbnail và trích dẫn).
Lâu rồi mới lại "nổi hứng", nên bữa nay rảnh ngồi chế lại cái tiện ích Popular posts cho nó đẹp chút. Như các bạn đã biết, tiện ích này có 3 chế độ hiển thị khác nhau. Và trong 3 chế độ này đa phần mọi người thường dùng cách hiển thị ảnh thumnnail và tiêu đề vì cách này nhìn tương đối nhất. Với cách hiển thị đầy đủ (gồm : tiêu đề, ảnh thumbnail và trích dẫn), thì cách này thường chiếm nhiều không gian của blog, do đó mà ít người dùng. Hôm nay mình sẽ áp dụng hiệu ứng tooltips cho tiện ích này, để có thể hiển thị đầy đủ nhất nôi dung của tiện ích này mà không tốn nhiều không gian của blog.


Với áp dụng này, phần trích dẫn của tiện ích sẽ hiển thị khi ta rê chuột vào tiêu đề của bài viết. Việc làm này cũng ko ảnh hưởng nhiều đến việc load tiện ích, do tooltips cũng khá nhẹ, vì thế các bạn có thể an tâm về tốc độ load.

Ở đây mình sẽ hướng dẫn 2 cách hiển thị với tooltips.

Cách 1 : Hiển thị tiêu đề và ảnh thumnail, phần trích dẫn sẽ hiển thị khi rê chuột vào tiêu đề.
Cách 2 : Chỉ hiển thị tiêu đề, khi rê chuột sẽ hiển thị ảnh thumbnail và trích dẫn.

Ảnh minh họa :
Cách 1

Cách 2

Sau đây là hướng dẫn:
1. Đầu tiên để thực hiện được thủ thuật này, blog bạn phải có tiện ích popular posts.
- Các bạn thực hiện các bước chuẩn bị như hình bên dưới.

2. Chèn code javascript và CSS cho tooltips:
- Các bạn vào phần chỉnh sửa HTML và chèn đoạn code này vào trước thẻ </head>
<style type='text/css'>
/* CSS ToolTip */
#dhtmltooltip{
border-right: #555 1px solid;
padding-right: 4px;
border-top: #555 1px solid;
padding-left: 4px;
font-size: 10pt;
z-index: 100;
filter: alpha(opacity=90);
-moz-opacity: .90;
-khtml-opacity: .90;
opacity: .90;
left: -300px;
visibility: hidden;
padding-bottom: 4px;
border-left: #555 1px solid;
padding-top: 4px;
border-bottom: #555 1px solid;
font-family: Arial;
position: absolute;
background: #ffffe0;
width: 250px;
}
#dhtmlpointer{
z-index: 101;
left: -300px;
visibility: hidden;
position: absolute;
}
/* END CSS ToolTip */
</style>
<script language='javascript' src='http://data.fandung.com/js/tooltip.js'/>

3. Như vậy đã chuẩn bị xong. bây giờ sẽ là hướng dẫn thực hiện thủ thuật.

A. Cách 1 : hiển thị ảnh thumnail và tiêu để, phần trích dẫn sẽ hiển thị khi rê chuột.

- Vào bố cục, vào chỉnh sửa code HTML, nhấp chọn mở rộng mẫu tiện ích.
- Chèn đoạn code CSS của thủ thuật vào trước dòng ]]></b:skin>
/* CSS Most View with ToolTip */
.popular-posts {display:none;}
#fdmostview {
margin:0px;
padding: 0px 5px 5px 5px;
}
#fdmostview ul {margin:0!important;}
#fdmostview li.lipopu {min-height:54px;border-bottom:1px dashed #F7AA4F;padding:5px 0 5px 0;}
#fdmostview li.lipopu img {float:left;border:1px solid #ccc;padding:1px;margin-right:4px;width:50px;}
#fdmostview ul li {list-style:none;}
/* END Most View with ToolTip */

- Tiếp tục tìm đến code của tiện ích Popular Posts. code của nó sẽ trông như bên dưới :
<b:widget id='PopularPosts1' locked='false' title='Most View' type='PopularPosts'>
<b:includable id='main'>
<b:if cond='data:title'><data:title/></b:if>
<div class='widget-content popular-posts'>
<ul>
<b:loop values='data:posts' var='post'>
<li>
...
...
...
</li>
</b:loop>
</ul>
<b:include name='quickedit'/>
</div>

</b:includable>
</b:widget>
- và các bạn thêm code của thủ thuật vào đoạn code vừa tìm được như đoạn code highlight bên dưới :
<b:widget id='PopularPosts1' locked='false' title='Most View' type='PopularPosts'>
<b:includable id='main'>
<b:if cond='data:title'><data:title/></b:if>
<div class='widget-content popular-posts'>

<script type='text/javascript'>
var p=0;
var sumpopu = new Array();
var titlepopu = new Array();
var urlpopu = new Array();
var thumbpopu = new Array();
</script>

<ul>
<b:loop values='data:posts' var='post'>

<b:if cond='data:post.thumbnail'>
<script type='text/javascript'>
p++;
sumpopu[p]='<data:post.snippet/>';
titlepopu[p]='<data:post.title/>';
urlpopu[p]='<data:post.href/>';
thumbpopu[p]='<data:post.thumbnail/>';
</script>
<b:else/>
<script type='text/javascript'>
p++;
sumpopu[p]='<data:post.snippet/>';
titlepopu[p]='<data:post.title/>';
urlpopu[p]='<data:post.href/>';
thumbpopu[p]='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitQtThStDXX62F8q7ld-G5UPwDCAsHJV0Z5BvlUkvK4PQjDz3t1b4IkGQfd3zCiZLvhHJJZK4ZIl6SgbvvanJc8mbI5_LM1JMajW21qMf594-NXEvLokqNgBXZl_o6L72NBse70-p7LFZq/';
</script>
</b:if>

<li>
...
...
...
</li>
</b:loop>
</ul>
<b:include name='quickedit'/>
</div>


<div id='fdmostview'>
<ul>
<script type='text/javascript'>
//<![CDATA[
for (k=1;k<p+1;k++) {
document.write("<li class='lipopu'><img src='"+thumbpopu[k]+"' /><a href='"+urlpopu[k]+"' onmouseout='hidetip();' onmouseover='showtip('"+sumpopu[k]+"')'>"+titlepopu[k]+"</a></li>");
}
//]]>
</script>
</ul>
</div>

</b:includable>
</b:widget>

- Cuối cùng save template lại.

B. Cách 2 : chỉ hiện thị tiêu đề bài viết, ảnh thumbnail và trích dẫn sẽ hiện thị khi rê chuột vào tiêu đề.

- Vào bố cục, vào chỉnh sửa code HTML, nhấp chọn mở rộng mẫu tiện ích.
- Chèn đoạn code CSS của thủ thuật vào trước dòng ]]></b:skin>
/* CSS Most View with ToolTip */
.popular-posts {display:none;}
#fdmostview {
margin:0px;
padding: 0px 5px 5px 5px;
}
#fdmostview ul {margin:0!important;}
#fdmostview li.lipopu2 {border-bottom:1px dashed #F7AA4F;padding:5px 0 5px 0;}
#fdmostview ul li {list-style:none;}
img.imgpopu2 {float:left;border:1px solid #ccc;padding:1px;margin-right:4px;width:60px;}
/* END Most View with ToolTip */

- tương tự như cách 1, thêm đoạn code highlight bên dưới :
<b:widget id='PopularPosts1' locked='false' title='Most View' type='PopularPosts'>
<b:includable id='main'>
<b:if cond='data:title'><data:title/></b:if>
<div class='widget-content popular-posts'>

<script type='text/javascript'>
var p=0;
var sumpopu2 = new Array();
var titlepopu = new Array();
var urlpopu = new Array();
</script>

<ul>
<b:loop values='data:posts' var='post'>

<b:if cond='data:post.thumbnail'>
<script type='text/javascript'>
p++;
sumpopu2[p]='&lt;img class="imgpopu2" src="<data:post.thumbnail/>" /&gt;<data:post.snippet/>';
titlepopu[p]='<data:post.title/>';
urlpopu[p]='<data:post.href/>';
</script>
<b:else/>
<script type='text/javascript'>
p++;
sumpopu2[p]='&lt;img class="imgpopu2" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEitQtThStDXX62F8q7ld-G5UPwDCAsHJV0Z5BvlUkvK4PQjDz3t1b4IkGQfd3zCiZLvhHJJZK4ZIl6SgbvvanJc8mbI5_LM1JMajW21qMf594-NXEvLokqNgBXZl_o6L72NBse70-p7LFZq/" /&gt;<data:post.snippet/>';
titlepopu[p]='<data:post.title/>';
urlpopu[p]='<data:post.href/>';
</script>
</b:if>

<li>
...
...
...
</li>
</b:loop>
</ul>
<b:include name='quickedit'/>
</div>


<div id='fdmostview'>
<ul>
<script type='text/javascript'>
//<![CDATA[
for (k=1;k<p+1;k++) {
document.write("<li class='lipopu2'><img src='http://data.fandung.com/img/icon_popular.png' /><a href='"+urlpopu[k]+"' onmouseout='hidetip();' onmouseover='showtip('"+sumpopu2[k]+"')'>"+titlepopu[k]+"</a></li>");
}
//]]>
</script>
</ul>
</div>

</b:includable>
</b:widget>

- Save template lại.

Friday, September 9, 2011

roguelike tutorial 07: z levels and deeper caves

Our simple little roguelike is so flat and two dimensional. Let's change that. I'll try to write use code thats easy to read and understand, even if it's woefully inefficient. It's a good thing world gen only happens once.

Most roguelikes have a 2d level and just simulate the current one. When you leave the current level either a new one is created or an old one is loaded. If loading an old level then the program usually simulates the passage of time and adds some items or creatures and makes it look like things happened even though you weren't there. I find it easier just to create the entire world at once and simulate everything. This also makes world gen easier since you can make decisions based on the entire world, not just the thin slices that already exist. Our world is small enough for now that the extra time to create it and memory it takes up it shouldn't be a problem.

In order to build better caves we're going to have to work with coordinates a lot. We should make a class to represent a point in space.

package rltut;



import java.util.ArrayList;

import java.util.Collections;

import java.util.List;



public class Point {

public int x;

public int y;

public int z;



public Point(int x, int y, int z){

this.x = x;

this.y = y;

this.z = z;

}

}

Two points that represent the same location should be treated as equal. These are known as value objects as opposed to reference objects. We can tell Java that by overriding the hashCode and equals methods. Here's what Eclipse generated:
@Override

public int hashCode() {

final int prime = 31;

int result = 1;

result = prime * result + x;

result = prime * result + y;

result = prime * result + z;

return result;

}



@Override

public boolean equals(Object obj) {

if (this == obj)

return true;

if (obj == null)

return false;

if (!(obj instanceof Point))

return false;

Point other = (Point) obj;

if (x != other.x)

return false;

if (y != other.y)

return false;

if (z != other.z)

return false;

return true;

}


We're also going to spend a lot of time working with points that are adjacent to something. This will be much easier if we can just ask a point for a list of it's eight neighbors.

public List<point> neighbors8(){

List<point> points = new ArrayList<point>();



for (int ox = -1; ox < 2; ox++){

for (int oy = -1; oy < 2; oy++){

if (ox == 0 && oy == 0)

continue;



points.add(new Point(x+ox, y+oy, z));

}

}



Collections.shuffle(points);

return points;

}


We shuffle the list before returning it so we don't introduce bias. Otherwise the upper left neighbor would always be checked first and the lower right would be last which may lead to some odd things. Now that we have a Point class, let's use it to make some better caves. First we need to add two new tile types, stairs up and stairs down.

STAIRS_DOWN('>', AsciiPanel.white),

STAIRS_UP('<', AsciiPanel.white);


Then, in the WorldBuilder class, we create a region map. Each location has a number that identifies what region of contiguous open space it belongs to; i.e. if two locations have the same region number, then you can walk from one to the other without digging through walls.

private WorldBuilder createRegions(){

regions = new int[width][height][depth];



for (int z = 0; z < depth; z++){

for (int x = 0; x < width; x++){

for (int y = 0; y < height; y++){

if (tiles[x][y][z] != Tile.WALL && regions[x][y][z] == 0){

int size = fillRegion(nextRegion++, x, y, z);



if (size < 25)

removeRegion(nextRegion - 1, z);

}

}

}

}

return this;

}


This will look at every space in the world. If it is not a wall and it does not have a region assigned then that empty space, and all empty spaces it's connected to, will be given a new region number. If the region is to small it gets removed. When this method is done, all open tiles will have a region assigned to it and we can use the regions array to see if two tiles are part of the same open space.

The removeRegion method does what it sounds like. It just zero's out the region number and fills in the cave so it's solid wall. I prefer caves where the smaller areas have been filled in but this step isn't necessary.

private void removeRegion(int region, int z){

for (int x = 0; x < width; x++){

for (int y = 0; y < height; y++){

if (regions[x][y][z] == region){

regions[x][y][z] = 0;

tiles[x][y][z] = Tile.WALL;

}

}

}

}


The fillRegion method does a flood-fill starting with an open tile. It, and any open tile it's connected to, gets assigned the same region number. This is repeated until there are no unassigned empty neighboring tiles.

private int fillRegion(int region, int x, int y, int z) {

int size = 1;

ArrayList<Point> open = new ArrayList<Point>();

open.add(new Point(x,y,z));

regions[x][y][z] = region;



while (!open.isEmpty()){

Point p = open.remove(0);



for (Point neighbor : p.neighbors8()){

if (regions[neighbor.x][neighbor.y][neighbor.z] > 0

|| tiles[neighbor.x][neighbor.y][neighbor.z] == Tile.WALL)

continue;



size++;

regions[neighbor.x][neighbor.y][neighbor.z] = region;

open.add(neighbor);

}

}

return size;

}


To connect all the regions with stairs we just start at the top and connect them one layer at a time.

public WorldBuilder connectRegions(){

for (int z = 0; z < depth-1; z++){

connectRegionsDown(z);

}

return this;

}


To connect two adjacent layers we look at each region that sits above another region. If they haven't been connected then we connect them.
private void connectRegionsDown(int z){

List<String> connected = new ArrayList<String>();



for (int x = 0; x < width; x++){

for (int y = 0; y < height; y++){

String region = regions[x][y][z] + "," + regions[x][y][z+1];

if (tiles[x][y][z] == Tile.FLOOR

&& tiles[x][y][z+1] == Tile.FLOOR

&& !connected.contains(region)){

connected.add(region);

connectRegionsDown(z, regions[x][y][z], regions[x][y][z+1]);

}

}

}

}


The region variable is just a way to uniquely combine two numbers into one. This way we just need a list of the region strings instead of some list of pairs or something. If java had tuples then we could use that instead of this way.

To connect two regions, we get a list of all the locations where one is directly above the other. Then, based on how much area overlaps, we connect them with stairs going up and stairs going down.

private void connectRegionsDown(int z, int r1, int r2){

List<Point> candidates = findRegionOverlaps(z, r1, r2);



int stairs = 0;

do{

Point p = candidates.remove(0);

tiles[p.x][p.y][z] = Tile.STAIRS_DOWN;

tiles[p.x][p.y][z+1] = Tile.STAIRS_UP;

stairs++;

}

while (candidates.size() / stairs > 250);

}


Finding which locations of two regions overlap is pretty straight forward.

public List<Point> findRegionOverlaps(int z, int r1, int r2) {

ArrayList<Point> candidates = new ArrayList<Point>();



for (int x = 0; x < width; x++){

for (int y = 0; y < height; y++){

if (tiles[x][y][z] == Tile.FLOOR

&& tiles[x][y][z+1] == Tile.FLOOR

&& regions[x][y][z] == r1

&& regions[x][y][z+1] == r2){

candidates.add(new Point(x,y,z));

}

}

}



Collections.shuffle(candidates);

return candidates;

}


After all that, the method for making caves needs to be amended.

public WorldBuilder makeCaves() {

return randomizeTiles()

.smooth(8)

.createRegions()

.connectRegions();

}


The world class obviously needs to be changed. Not only does a third layer need to be added to the tiles array, but most methods need to accept a new z parameter. I'm suer you can find all the changes that need to be made. Here's an example of an updated method:
public void addAtEmptyLocation(Creature creature, int z){

int x;

int y;



do {

x = (int)(Math.random() * width);

y = (int)(Math.random() * height);

}

while (!tile(x,y,z).isGround() || creature(x,y,z) != null);



creature.x = x;

creature.y = y;

creature.z = z;

creatures.add(creature);

}


The Creature class also needs a new z coordinate and many of it's methods need to be updated too. I'll leave that to you as well. Here's my new version of the moveBy method.
public void moveBy(int mx, int my, int mz){

Tile tile = world.tile(x+mx, y+my, z+mz);



if (mz == -1){

if (tile == Tile.STAIRS_DOWN) {

doAction("walk up the stairs to level %d", z+mz+1);

} else {

doAction("try to go up but are stopped by the cave ceiling");

return;

}

} else if (mz == 1){

if (tile == Tile.STAIRS_UP) {

doAction("walk down the stairs to level %d", z+mz+1);

} else {

doAction("try to go down but are stopped by the cave floor");

return;

}

}



Creature other = world.creature(x+mx, y+my, z+mz);



if (other == null)

ai.onEnter(x+mx, y+my, z+mz, tile);

else

attack(other);

}


The CreatureAi classes, PlayScreen class, and a few others need to be updated. Your IDE should tell you which ones are broken by the new thrid dimension. You should be able to fix all of these on your own. One thing is that the greater than and less than keys used to move up and down stairs don't work with the keyCode() method so that needs to be handled differently. Here's how I did it:

switch (key.getKeyChar()){

case '<': player.moveBy( 0, 0, -1); break;

case '>': player.moveBy( 0, 0, 1); break;

}



This one was a lot of work. The algorithm for creating 3d caves that are connected takes a while to explain even though each step isn't too difficult on it's own. The real bummer was having to update all the places where we were using coordinates. I suppose that if we were using a Point class from the beginning then adding a z coordinate to the Point would have required far fewer changes. Or we could have stuck to a list of 2d arrays and just tracked the current one instead of simulating a 3d world all at once. Oh well. Maybe you wont make the same mistake with the next roguelike you make.

download the code

Popular Posts