I'm writing a simple text-based RPG in Java. I think it's a good exercise to practice OO and think about how objects should best interact. I'd be interested in hearing any thoughts!
The Game
class contains a single game:
public final class Game {
private final Player player = Player.newInstance();
public void play() throws IOException {
System.out.println("You are " + player + " " + player.getDescription());
Dungeon.newInstance().startQuest(player);
}
public static void main(String[] args) throws IOException {
Game game = new Game();
game.play();
}
}
Here's a simple Dungeon
class, a collection of Room
s laid out in the map. The player moves from room to room, encountering and battling monsters.
public final class Dungeon {
private final Map<Integer, Map<Integer, Room>> map = new HashMap<Integer, Map<Integer, Room>>();
private Room currentRoom;
private int currentX = 0;
private int currentY = 0;
private Dungeon() {
}
private void putRoom(int x, int y, Room room) {
if (!map.containsKey(x)) {
map.put(x, new HashMap<Integer, Room>());
}
map.get(x).put(y, room);
}
private Room getRoom(int x, int y) {
return map.get(x).get(y);
}
private boolean roomExists(int x, int y) {
if (!map.containsKey(x)) {
return false;
}
return map.get(x).containsKey(y);
}
private boolean isComplete() {
return currentRoom.isBossRoom() && currentRoom.isComplete();
}
public void movePlayer(Player player) throws IOException {
boolean northPossible = roomExists(currentX, currentY + 1);
boolean southPossible = roomExists(currentX, currentY - 1);
boolean eastPossible = roomExists(currentX + 1, currentY);
boolean westPossible = roomExists(currentX - 1, currentY);
System.out.print("Where would you like to go :");
if (northPossible) {
System.out.print(" North (n)");
}
if (eastPossible) {
System.out.print(" East (e)");
}
if (southPossible) {
System.out.print(" South (s)");
}
if (westPossible) {
System.out.print(" West (w)");
}
System.out.print(" ? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String direction = in.readLine();
if (direction.equals("n") && northPossible) {
currentY++;
} else if (direction.equals("s") && southPossible) {
currentY--;
} else if (direction.equals("e") && eastPossible) {
currentX++;
} else if (direction.equals("w") && westPossible) {
currentX--;
}
currentRoom = getRoom(currentX, currentY);
currentRoom.enter(player);
}
public void startQuest(Player player) throws IOException {
while (player.isAlive() && !isComplete()) {
movePlayer(player);
}
if (player.isAlive()) {
System.out.println(Art.CROWN);
} else {
System.out.println(Art.REAPER);
}
}
public static Dungeon newInstance() {
Dungeon dungeon = new Dungeon();
dungeon.putRoom(0, 0, Room.newRegularInstance());
dungeon.putRoom(-1, 1, Room.newRegularInstance());
dungeon.putRoom(0, 1, Room.newRegularInstance());
dungeon.putRoom(1, 1, Room.newRegularInstance());
dungeon.putRoom(-1, 2, Room.newRegularInstance());
dungeon.putRoom(1, 2, Room.newRegularInstance());
dungeon.putRoom(-1, 3, Room.newRegularInstance());
dungeon.putRoom(0, 3, Room.newRegularInstance());
dungeon.putRoom(1, 3, Room.newRegularInstance());
dungeon.putRoom(0, 4, Room.newBossInstance());
dungeon.currentRoom = dungeon.getRoom(0, 0);
return dungeon;
}
}
Here's the Monster
class:
public final class Monster {
private final String name;
private final String description;
private int hitPoints;
private final int minDamage;
private final int maxDamage;
private final static Random random = new Random();
private final static Set<Integer> monstersSeen = new HashSet<Integer>();
private final static int NUM_MONSTERS = 3;
public static Monster newRandomInstance() {
if (monstersSeen.size() == NUM_MONSTERS) {
monstersSeen.clear();
}
int i;
do {
i = random.nextInt(NUM_MONSTERS);
} while (monstersSeen.contains(i));
monstersSeen.add(i);
if (i == 0) {
return new Monster("Harpy", Art.HARPY, 40, 8, 12);
} else if (i == 1) {
return new Monster("Gargoyle", Art.GARGOYLE, 26, 4, 6);
} else {
return new Monster("Hobgoblin", Art.HOBGOBLIN, 18, 1, 2);
}
}
public static Monster newBossInstance() {
return new Monster("Dragon", Art.DRAGON, 60, 10, 20);
}
private Monster(String name, String description, int hitPoints, int minDamage, int maxDamage) {
this.name = name;
this.description = description;
this.minDamage = minDamage;
this.maxDamage = maxDamage;
this.hitPoints = hitPoints;
}
@Override
public String toString() {
return name;
}
public String getDescription() {
return description;
}
public String getStatus() {
return "Monster HP: " + hitPoints;
}
public int attack() {
return random.nextInt(maxDamage - minDamage + 1) + minDamage;
}
public void defend(Player player) {
int attackStrength = player.attack();
hitPoints = (hitPoints > attackStrength) ? hitPoints - attackStrength : 0;
System.out.printf(" %s hits %s for %d HP of damage (%s)\n", player, name, attackStrength,
getStatus());
if (hitPoints == 0) {
System.out.println(" " + player + " transforms the skull of " + name
+ " into a red pancake with his stone hammer");
}
}
public boolean isAlive() {
return hitPoints > 0;
}
}
Battle
class:
public final class Battle {
public Battle(Player player, Monster monster) throws IOException {
System.out.println("You encounter " + monster + ": " + monster.getDescription() + "\n");
System.out.println("Battle with " + monster + " starts (" + player.getStatus() + " / "
+ monster.getStatus() + ")");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (player.isAlive() && monster.isAlive()) {
System.out.print("Attack (a) or heal (h)? ");
String action = in.readLine();
if (action.equals("h")) {
player.heal();
} else {
monster.defend(player);
}
if (monster.isAlive()) {
player.defend(monster);
}
}
}
}
Room
class:
public final class Room {
private final String description;
private final Monster monster;
private final Boolean isBossRoom;
private final static Random random = new Random();
private final static Set<Integer> roomsSeen = new HashSet<Integer>();
private final static int NUM_ROOMS = 7;
private Room(String description, Monster monster, Boolean isBossRoom) {
this.description = description;
this.monster = monster;
this.isBossRoom = isBossRoom;
}
public static Room newRegularInstance() {
if (roomsSeen.size() == NUM_ROOMS) {
roomsSeen.clear();
}
int i;
do {
i = random.nextInt(NUM_ROOMS);
} while (roomsSeen.contains(i));
roomsSeen.add(i);
String roomDescription = null;
if (i == 0) {
roomDescription = "a fetid, dank room teeming with foul beasts";
} else if (i == 1) {
roomDescription = "an endless mountain range where eagles soar looking for prey";
} else if (i == 2) {
roomDescription = "a murky swamp with a foul smelling odour";
} else if (i == 3) {
roomDescription = "a volcano with rivers of lava at all sides";
} else if (i == 4) {
roomDescription =
"a thick forest where strange voices call out from the trees high above";
} else if (i == 5) {
roomDescription =
"an old abandoned sailing ship, littered with the remains of some unlucky sailors";
} else if (i == 6) {
roomDescription = "a cafe filled with hipster baristas who refuse to use encapsulation";
} else {
}
return new Room(roomDescription, Monster.newRandomInstance(), false);
}
public static Room newBossInstance() {
return new Room("a huge cavern thick with the smell of sulfur", Monster.newBossInstance(),
true);
}
public boolean isBossRoom() {
return isBossRoom;
}
public boolean isComplete() {
return !monster.isAlive();
}
@Override
public String toString() {
return description;
}
public void enter(Player player) throws IOException {
System.out.println("You are in " + description);
if (monster.isAlive()) {
new Battle(player, monster);
}
}
}
And the Player
class:
public final class Player {
private final String name;
private final String description;
private final int maxHitPoints;
private int hitPoints;
private int numPotions;
private final int minDamage;
private final int maxDamage;
private final Random random = new Random();
private Player(String name, String description, int maxHitPoints, int minDamage, int maxDamage,
int numPotions) {
this.name = name;
this.description = description;
this.maxHitPoints = maxHitPoints;
this.minDamage = minDamage;
this.maxDamage = maxDamage;
this.numPotions = numPotions;
this.hitPoints = maxHitPoints;
}
public int attack() {
return random.nextInt(maxDamage - minDamage + 1) + minDamage;
}
public void defend(Monster monster) {
int attackStrength = monster.attack();
hitPoints = (hitPoints > attackStrength) ? hitPoints - attackStrength : 0;
System.out.printf(" " + name + " is hit for %d HP of damage (%s)\n", attackStrength,
getStatus());
if (hitPoints == 0) {
System.out.println(" " + name + " has been defeated");
}
}
public void heal() {
if (numPotions > 0) {
hitPoints = Math.min(maxHitPoints, hitPoints + 20);
System.out.printf(" %s drinks healing potion (%s, %d potions left)\n", name,
getStatus(), --numPotions);
} else {
System.out.println(" You've exhausted your potion supply!");
}
}
public boolean isAlive() {
return hitPoints > 0;
}
public String getStatus() {
return "Player HP: " + hitPoints;
}
@Override
public String toString() {
return name;
}
public String getDescription() {
return description;
}
public static Player newInstance() {
return new Player("Mighty Thor",
"a musclebound hulk intent on crushing all evil in his way", 40, 6, 20, 10);
}
}
System.out.println()
directly - in fact, your back-end classes should have absolutely no clue that it's a text based system. Interestingly, doing so makes a change to a graphical interface much easier, later. \$\endgroup\$