Skip to content
wannacode

RPG 3: the whole game

Problem

From one fight to a game

The heroes and the monsters already fight. But a game is more than one fight: you create a hero, line up the monsters he will fight, and he goes through them one by one. Between the fights he rests or heals with magic.

If all of this is in Main, that is where the hero will be, along with an array of monsters, the number of the next one, counters and checks. And who will stop the player from starting a fight without a hero, or asking a barbarian for magic?

The state of the game and its rules have to be in one place: in a class Game.

The idea

The Game class

The hero and the monsters are the state of the game. The methods of Game are the rules: what the player can do and what happens after that. The first one is creating a hero:

private Character hero;
private Monster[] monsters = new Monster[5];
private int count;
private int next;
private Random random;
public Game(Random random) {
this.random = random;
}
public void createHero(String name, String race) {
if (race.equals("paladin")) {
hero = new Paladin(name);
} else if (race.equals("barbarian")) {
hero = new Barbarian(name);
} else if (race.equals("witch")) {
hero = new Witch(name);
} else {
throw new GameException("No such race: " + race);
}
}
The random number generator comes from outside, through the constructor. In the real game it will be new Random(), and in the examples it has a seed, so the output is always the same.

An unknown race is no reason to create some random hero. The method throws GameException: our own exception for every move the rules do not allow. It gets its own card further on.

The idea

A lineup of monsters

The monsters wait in a queue: the one added first is the first fight. Here the queue is an array and two indexes:

public void addMonster(Monster monster) {
if (count == monsters.length) {
throw new GameException("The lineup is full: at most " + monsters.length + " monsters");
}
monsters[count] = monster;
count++;
}
public Monster createRandomMonster() {
int kind = random.nextInt(3);
if (kind == 0) {
return new FallenShaman();
}
if (kind == 1) {
return new SkeletonKing();
}
return new Butcher();
}
public boolean hasMonsters() {
return next < count;
}
  • count is how many monsters have been added. The new one goes to monsters[count].
  • next is which one fights next. After every fight it goes up by 1, and when it reaches count, the lineup is over.
The array has 5 places. The sixth monster does not get in: AddMonster throws GameException before it writes outside the array.

CreateRandomMonster is the bonus from the old exercise: a monster picked at random.

More precisely, if you are curious
  • The old exercise kept the monsters in a real queue, Queue<Monster>. A queue remembers by itself which one is first, and it never fills up. The list List<T> comes in the Web and desktop module, and Queue<T> is from the same library and is used the same way: Enqueue adds at the back, Dequeue takes from the front.
  • The monsters before next stay in the array. They do no harm: no one reads them anymore.

The idea

Levels and experience

After every win the hero gets experience equal to the monster's health. On level 1 it takes 100 experience to reach level 2, on level 2 it takes 200 to reach level 3, and so on. A new level gives 10 more health, 3 more power and full health:

public boolean gainExperience(int exp) {
experience += exp;
boolean up = false;
while (experience >= level * 100) {
experience -= level * 100;
levelUp();
up = true;
}
return up;
}
public void levelUp() {
level++;
maxHealth += 10;
power += 3;
health = maxHealth;
}

The methods are in Character. Experience above the threshold stays for the next level. The while loop is for the case where one win is enough for two levels.

The fields MaxHealth, Level and Experience are new in Character. With MaxHealth, healing no longer raises health above the maximum.

Try it

What level is the barbarian on?

Grok starts on level 1 with power 20. He wins against the shaman (60 experience) and against the king (90 experience).

What will the program print? Level, experience, power.

public bool GainExperience(int exp)
{
Experience += exp;
bool up = false;
while (Experience >= Level * 100)
{
Experience -= Level * 100;
LevelUp();
up = true;
}
return up;
}
public void LevelUp()
{
Level++;
MaxHealth += 10;
Power += 3;
Health = MaxHealth;
}
Character grok = new Barbarian("Grok");
grok.GainExperience(60);
grok.GainExperience(90);
Console.WriteLine(grok.Level + " " + grok.Experience + " " + grok.Power);
Answer

See it

The battle and the rest

StartBattle first checks whether the fight is possible. Then it takes the next monster and runs the fight to the death from the second part. It returns whether the hero survived:

public boolean startBattle() {
if (hero == null) {
throw new GameException("Create a hero first");
}
if (!hasMonsters()) {
throw new GameException("No monsters in the lineup");
}
Monster monster = monsters[next];
next++;
int rounds = 0;
while (hero.isAlive() && monster.isAlive()) {
rounds++;
hero.attack(monster);
if (monster.isAlive()) {
monster.attack(hero);
}
}
if (!hero.isAlive()) {
System.out.println(hero.getName() + " falls in round " + rounds + ". Winner: " + monster.getName());
return false;
}
System.out.println(monster.getName() + " is defeated in " + rounds + " rounds. " + hero.getName() + ": health " + hero.getHealth() + "/" + hero.getMaxHealth());
if (hero.gainExperience(monster.getReward())) {
System.out.println(hero.getName() + " reaches level " + hero.getLevel() + ": health " + hero.getMaxHealth() + ", power " + hero.getPower());
}
return true;
}

Recover is the choice after the fight. Only a hero who implements IHealable can use magic. The rest is 5 turns of 10 health:

public void recover(String way) {
if (way.equals("rest")) {
for (int turn = 0; turn < 5; turn++) {
hero.restore(10);
}
System.out.println(hero.getName() + " rests for 5 turns: health " + hero.getHealth() + "/" + hero.getMaxHealth());
} else if (way.equals("magic")) {
if (!(hero instanceof IHealable)) {
throw new GameException(hero.getName() + " does not know healing magic");
}
System.out.println(hero.useAbility());
} else {
throw new GameException("Unknown choice '" + way + "': rest or magic?");
}
}

In the old exercise the hero waited 5 seconds. Here the rest is in turns, because in a turn-based game time is measured in turns, and the check of the game should not have to wait.

See it

The whole game

Grok the barbarian against the shaman and two random monsters. After every fight he rests. If he falls, the game is over:

Game game = new Game(new Random(12));
game.createHero("Grok", "barbarian");
game.addMonster(new FallenShaman());
game.addMonster(game.createRandomMonster());
game.addMonster(game.createRandomMonster());
while (game.hasMonsters()) {
if (!game.startBattle()) {
System.out.println("Game over");
break;
}
game.recover("rest");
}
Output
Fallen Shaman is defeated in 3 rounds. Grok: health 90/110
Grok rests for 5 turns: health 110/110
Fallen Shaman is defeated in 3 rounds. Grok: health 90/110
Grok reaches level 2: health 120, power 23
Grok rests for 5 turns: health 120/120
Butcher is defeated in 6 rounds. Grok: health 70/120
Grok rests for 5 turns: health 120/120
Compare the output in the two tabs. The seed is 12 in both languages, but the generators are different, and so are the random monsters. In Java Grok meets a second shaman and the Butcher, and in C# the Butcher and the Skeleton King.

Game game = new Game(new Random(12));
game.CreateHero("Grok", "barbarian");
game.AddMonster(new FallenShaman());
game.AddMonster(game.CreateRandomMonster());
game.AddMonster(game.CreateRandomMonster());
while (game.HasMonsters())
{
if (!game.StartBattle())
{
Console.WriteLine("Game over");
break;
}
game.Recover("rest");
}
Step by step, in words
  1. The game gets a generator with seed 12. The hero is Grok, and the array has 5 places, still empty.
  2. Each AddMonster() writes to monsters[count] and raises count. The second and the third are random: Butcher and Skeleton King.
  3. next is 0: out comes Fallen Shaman. It is defeated in 3 rounds, and Grok has 90/110 left.
  4. next becomes 1. The rest is 5 turns of 10 health: 110/110.
  5. next is 1: out comes Butcher. It is defeated in 7 rounds, and Grok has 48/110 left. The experience is enough for level 2: health 120, power 23.
  6. next becomes 2. The rest is 5 turns of 10 health: 120/120.
  7. next is 2: out comes Skeleton King. It is defeated in 5 rounds, and Grok has 68/120 left.
  8. next becomes 3. The rest is 5 turns of 10 health: 118/120.
  9. next is 3, the same as count: HasMonsters() returns false, and the loop ends.

The idea

A wrong move: GameException

The player can ask for a fight without a hero, a race that does not exist, or magic from a barbarian. That is not a bug in the program. It is a move the rules do not allow. The methods of Game throw their own exception for it:

class GameException extends RuntimeException {
public GameException(String message) {
super(message);
}
}

The game catches only that one, tells the player why the move is not allowed and carries on with the next move:

Game game = new Game(new Random(12));
String[] commands = { "fight", "hero Grok dragon", "hero Grok barbarian", "magic", "sleep" };
for (String command : commands) {
String[] words = command.split(" ");
try {
if (words[0].equals("fight")) {
game.startBattle();
} else if (words[0].equals("hero")) {
game.createHero(words[1], words[2]);
System.out.println("The hero is " + words[1]);
} else {
game.recover(words[0]);
}
} catch (GameException e) {
System.out.println("Not allowed: " + e.getMessage());
}
}
Output
Not allowed: Create a hero first
Not allowed: No such race: dragon
The hero is Grok
Not allowed: Grok does not know healing magic
Not allowed: Unknown choice 'sleep': rest or magic?

Why your own exception and not the built-in ArgumentException? Because catch (GameException e) catches only the refusals by the rules. If the game has a bug and throws NullReferenceException, it will not hide as "Not allowed". It will stop the program, and you will see it.

Try it

Find the bug

The sixth monster should get "The lineup is full". Instead, the program stops with IndexOutOfRangeException. Which line is the bug on?

Pick the line with the bug.

Try it

What is missing?

Which class does GameException inherit from?

class GameException : 
{
    public GameException(string message) : base(message)
    {
    }
}

Exercise

Play and extend

Solve these in your editor. Start from the classes in the lesson.

1. A menu. The commands come from input, one per line, until "quit":

  • "hero Name race" creates the hero;
  • "monster shaman" (or "king", "butcher") adds a ready-made monster, and "monster Name health power" adds your own: a class Beast of the race "beast";
  • "random" adds a random monster;
  • "fight" starts the next fight, and "rest" and "magic" are the choice after it.

Every refusal is a GameException that the menu catches, saying "Not allowed: …". The seed is 12.

Input
hero Morena elf
hero Morena witch
monster Wolf 50 9
monster dragon
monster shaman
random
fight
magic
fight
rest
fight
quit
Output
Not allowed: No such race: elf
The hero is Morena
In the lineup: Wolf
Not allowed: No such monster: dragon
In the lineup: Fallen Shaman
In the lineup: Butcher
Wolf is defeated in 4 rounds. Morena: health 68/95
Morena calls on the elements: health 83/95
Fallen Shaman is defeated in 4 rounds. Morena: health 47/95
Morena reaches level 2: health 105, power 19
Morena rests for 5 turns: health 105/105
Butcher is defeated in 7 rounds. Morena: health 30/105

2. Progress. Add a method ShowProgress() to Game that shows the hero's level and how much experience he has out of how much he needs for the next level. Ardan the paladin goes through two shamans and the Skeleton King. After every fight he shows his progress and heals with magic.

Output
Fallen Shaman is defeated in 5 rounds. Ardan: health 72/120
Level 1, experience 60/100
Ardan heals: health 92/120
Fallen Shaman is defeated in 5 rounds. Ardan: health 44/120
Ardan reaches level 2: health 130, power 17
Level 2, experience 20/200
Ardan heals: health 130/130
Skeleton King is defeated in 8 rounds. Ardan: health 18/130
Level 2, experience 110/200
Ardan heals: health 38/130

3. A boss. A class Diablo, a subclass of Monster: 160 health, 14 power. Every third hit of his is double. A method CanChallenge(Character hero) says whether the hero is at least on level 2. If not, StartBattle throws GameException, and Diablo stays in the lineup. Run him once against Grok on level 1, then in a new game after the shaman and the king.

Output
Not allowed: Diablo fights only heroes of level 2 and up
Fallen Shaman is defeated in 3 rounds. Grok: health 90/110
Grok rests for 5 turns: health 110/110
Skeleton King is defeated in 6 rounds. Grok: health 45/110
Grok reaches level 2: health 120, power 23
Grok rests for 5 turns: health 120/120
Diablo is defeated in 7 rounds. Grok: health 24/120
Grok reaches level 3: health 130, power 26
Grok rests for 5 turns: health 130/130

Keep going on your own

The old exercise suggested more ideas. Some of them need a tool that comes later:

  • Skills with levels. A class Skill with a name and a level, UseSkill(Character target) and UpgradeSkill(). You can do this now, with an array of skills in the hero.
  • Inventory. A class Item with a name and an effect, ApplyEffect(Character target), and an Inventory where items are added and removed. With an array it is clumsy. The list List<T> comes in the Web and desktop module.
  • Bosses with requirements. A boss that wants certain items and skills before it accepts the fight, and drops loot after a win. It needs the inventory above.
  • Screens. Menu, map, inventory: each screen is an object with Show(), Hide() and Update(), and the open screens are on a stack, so that "back" works. In C# the stack is Stack<T>, from the same library as List<T> from the Web and desktop module.
  • Save and load. The hero, the level and the best score are written to a file and read on the next run. This course has no lesson on files. The shortest way is File.WriteAllText and File.ReadAllText from System.IO.

Test

Check yourself

1. The lineup is an array of 5 places, count is 3, next is 1. What does that mean?
2. The hero is on level 1 with 80 experience and wins 130 experience. What happens?
3. Why does the game catch only GameException and not every exception?
4. Why is the rest 5 turns and not 5 seconds of waiting?
5. What does catch (GameException e) catch?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·