Today we will be creating a traditional RPG (role-playing game) battle system featuring Mages, Warriors, Dragons, and Skeletons!
You may run the entire test suite by executing: make test
However it is highly suggested to run individual tests one at a time, write code to make the test pass, and then proceed to the next test, repeating the process.
You may run individual tests by executing:
PYTHONPATH=. py.test tests/[test file] -k [name of test]
So for example to run test_base_creation
in test_heroes.py
you would run:
PYTHONPATH=. py.test tests/test_heroes.py -k test_base_creation
The -k
option indicates a keyword search. You could also use it to run all the creation tests in the test_heroes.py
file by running:
PYTHONPATH=. py.test tests/test_heroes.py -k creation
Finally, you may also run specifically one test using the followind format:
PYTHONPATH=. py.test tests/[test file]::[test case]::[name of test function]
To run only the test_base_creation
from BaseHeroTestCase
you would run:
PYTHONPATH=. py.test tests/test_heroes.py::BaseHeroTestCase::test_base_creation
getattr
and setattr
are useful tools for dynamically reading and setting attributes.
getattr
is defined as such: getattr(object, name[, default])
and allows you to retrieve an attribute from object
named name
, and optionally set a default
value if the attribute doesn't exist. It is important to note that name
is a string, this is what makes getattr
so useful as you can manipulate the string however you want before giving it to getattr
Very basic example, say we had a Person class with the attributes name
, phone
, address
, email
:
person = Person(name='John', email='example@example.com')
for attr in ('name', 'phone', 'address', 'email'):
print("{name}: {value}".format(name=attr, value=getattr(person, attr, 'N/A')))
The above would print the following:
name: John
phone: N/A
address: N/A
email: example@example.com
setattr
is the counterpart to getattr
and is defined as: `getattr(object, name, value)
If we wanted to define the People class from above so that we could take any keyword arguments when intialising we could write it like this:
class People(object):
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
It's important to note how this relates to inheritance. You can use getattr to read values in your base class that may or may not have been set in your child classes.
We will be breaking the project down into three primary parts: Heroes, Monsters, and Battles.
Heroes represent player characters, they can be one of four classes, Warrior, Mage, Cleric, or Rogue. Depinding on a hero's class they will have different stats and abilities, for example a Warrior is stronger and more durable than a mage and therefore they have a higher strength and constitution. These statistics effect how their various abilities work. As heroes defeat monsters they gain xp (experience points) and if they gain enough experience they level up and become stronger.
Monsters are what the heroes fight. They can be classified into monster families (like dragons and undead) as well as individual monster types within those families (like Red Dragons and Vampires). Monsters have their own abilities depending on a combination of their family and type.
Battles This is what brings everything together. Participants will perform actions in order of their speed
attribute - faster units go before slower units. Hero abilities are user determined, monster abilities are used according to their command queue (described below) and target heroes randomly. Battle continues until either all the heroes or all the monsters are wiped out.
Keep in mind when writing your classes/methods on how you can break problems down into smaller tasks/methods to write smaller, more easily maintainable bits of code. Do not hesitate to create helper methods.
Features common to all Heroes: When initialising a Hero you may pass a
level
parameter to specify what level of hero you would like.
- Stats:
strength
constitution
intelligence
speed
hp
- current hit pointsmaxhp
- maximum hit pointsmp
- current magic pointsmaxmp
- maximum magic pointslevel
- current character levelxp
- current experience pointsxp_for_next_level()
- when xp == this it causes the hero to level up (should be equal tolevel * 10
)gain_xp(xp)
- causes the hero to gainxp
experience points, should also handle the process of levelling upfight(target)
- Basic attack dealing damage equal to the hero'sstrength
to thetarget
abilities
- a collection of strings representing the hero's combat abilities. For example aMage
'sabilities
would be something like('fight', 'fireball', 'frostbolt')
take_damage(damage)
- hero takesdamage
amount of damage, having it subtracted from their current hpheal_damage(healing)
- hero healshealing
hit points, but not beyond theirmaxhp
is_dead()
- if the hero's hp have been completely depleted this returnsTrue
It is advised to also make use of a _level_up()
helper method to handle the levelling up process, making other helper methods may also be useful.
Stats Base level stats (strength, constitution, intelligence, speed) for a level 1 Hero start at 6 and may be modified by child classes.
Hit Points Base level hit points for a level 1 Hero start at 100 + 1/2 of the hero's
constitution
, dropping any fractions.
Magic Points Base level magic points for a level 1 Hero start at 50 + 1/2 of the hero's
intelligence
, dropping any fractions.
Levelling Up When a hero has
xp
greater than or equal toxp_for_next_level
they level up. Any excess xp is carried over after their xp is set to 0. Upon level up all stats are increased by one (or more if the hero has a stat modifier, this will be explained later). Theirmaxhp
is increased by 1/2 their newconstitution
and theirhp
is brought back up to full.maxmp
is also increased by 1/2 the newintelligence
, mp also being filled. When increasingmaxhp
ormaxmp
remember to drop any fractions.
Stat Modifiers Subclasses can apply modifiers to the hero's stats, either increasing or decreasing them. Modifiers should be added directly to the base value of the appropriate stat. At level up if the modifier is positive it is added to to the standard +1 received at level up. Example: If a subclass had a +1
strength
modifier it would have 7strength
at level 1 and would gain 2strength
each time it levelled up.
Special Abilities Each subclass has its own unique special abilities. Many of these abilities have an
mp
cost, if the hero does not have enough mp to use the ability anInsufficientMP
error should be raised. When calculating damage, drop fractions from final damage number.
Stat Modifiers
strength
+1intelligence
-2constitution
+2speed
-1
shield_slam(target)
- Cost: 5 MP
- Damage: 1.5x
strength
reckless_charge(target)
- Cost: 4 HP
- Damage: 2x
strength
Stat Modifiers
strength
-2intelligence
+3constitution
-2
fireball(target)
- Cost: 8 MP
- Damage: 6 + 0.5x
intelligence
frostbolt(target)
- Cost: 3 MP
- Damage: 3 +
level
Stat Modifiers
speed
-1constitution
+1
heal(target)
- Cost: 4 MP
- Heals target for 3 *
constitution
smite(target)
- Cost: 7 MP
- Damage: 4 + 0.5x (
intelligence + constitution
)
Stat Modifiers
speed
+2strength
+1intelligence
-1constitution
-2
backstab(target)
- Cost: None
- Special requirement: Target must be undamaged. If target is damaged must raise
InvalidTarget
- Damage: 2x
strength
rapid_strike(target)
- Cost: 5 MP
- Damage: 4 +
speed
Features common to all monsters: When initialising a Monster you may pass a
level
parameter to specify what level of monster you would like.
- Stats:
strength
constitution
intelligence
speed
maxhp
hp
level
xp()
- returns the xp that would be earned if this monster were to be defeatedfight(target)
take_damage(damage)
heal_damage(healing)
is_dead()
attack(target)
- Attacks the target using the next ability in the monster's command queue (explained later) Note: Monsters do not use MP
Stats Monsters start with a base stat level of 8 for all stats except speed versus the 6 that heroes receive, monster speed has a base of 12. Instead of receiving stat modifiers like heroes, monsters may receive stat multiplier instead. These are applied in the following manner: At level 1 a stat is set to
base value
x the stat multiplier. For levelled monsters set the stat tobase value
X multiplier +(level - 1)
x multiplier, dropping fractions. Monster have a base HP of 10 (this may be overidden by monster families and subtypes, more on this later) to calculate their actualmaxhp
use the base hp + (level - 1) x (0.5xconstitution
), dropping fractions as usual.
XP A monster's xp value may be calculated by taking the average of their stats (
strength
,constitution
,intelligence
,speed
) and adding it tomaxhp / 10
. Finally we take this all and multiply it by theXP_MULTIPLIER
.
Command Queue The command queue determines in which order a Monster uses its abilities. When an ability is used it cycles out to the end of the queue. Example: If a monster had a queue of
['fight', 'bash', 'slash']
the first time it attacked it would usefight
, the next time it would usebash
and so on.
Monsters are divided into families before being separated into individual monster types. Monster families can have multipliers, abilities, and other special features that apply to all of their members.
Family features
- Base HP: 100
constitution
multiplier: 2xp
multiplier: 2- Special feature: Dragons have damage reduction, all damage they take is reduced by 5.
tail_swipe(target)
: Deals 1.5 * (strength
+speed
) damage
RedDragon
strength
multiplier: 2intelligence
multiplier: 1.5fire_breath(target)
: deals 3xintelligence
damage
GreenDragon
strength
multiplier: 1.5speed
multiplier: 1.5poison_breath(target)
: deals 1.5x (intelligence
+constitution
) damage
Family Features
constitution
multiplier: 0.25- Special feature: Undead take damage from attempts to heal them. Note: The Undead's own special healing abilities do not damage it
life_drain(target)
: Damages the target for 1.5xintelligence
, healing the Undead for the same amount.
Vampire
intelligence
multiplier: 2- Base HP: 45
bite(target)
: Deals 2xspeed
damage to the target, healing the Vampire for the same amount. Permanently lowers the target'smaxhp
by an amount equal to the damage dealt.
Skeleton
strength
multiplier: 1.25speed
multiplier: 0.5intelligence
multiplier: 0.25- Base HP: 30
bash(target)
: deal 2xstrength
damage
Family Features
slash(target)
: dealstrength + speed
damage
Troll
strength
multiplier: 1.75constitution
multiplier: 1.5- Base HP: 20
regenerate(*args)
: Restoresconstitution
health to the Troll.
Orc
strength
multiplier: 1.75- Base HP: 16
blood_rage(target)
: Deals 2xstrength
damage to the target while dealing 0.5xconstitution
damage to the Orc
Preparing a battle should be as simple as creating a new Battle
while passing in a list of participants. The battle should handle sorting the participants in order of speed
and then cycle through the list, moving units to the end of the line as they take turns performing actions.
A Battle should have the following outward facing interface:
next_turn()
: processes the next unit's turncurrent_attacker()
: returns the unit at the front of the initiative queueis_hero_turn()
: returnsTrue
ifcurrent_attacker
is aHero
is_monster_turn()
: returnsTrue
ifcurrent_attacker
is aMonster
execute_command(command, target)
: uses an ability on the targetraise_for_battle_over()
: raisesVictory
if all monsters are defeated, and raisesDefeat
if all heroes are defeated
The following helper methods are recomended to break the project into smaller, more manageable chunks:
_monster_turn()
: handles a monster's turn_hero_turn()
: announces (adds to the event log) that it is a hero's turn_process_initiative()
: handles rearranging the initiative after a unit's turn_process_dead()
: handles removing dead units from the initiative queue and triggers xp rewards_reward_xp()
: handles rewarding xp to living members of the party_check_victory()
: returnsTrue
if 0 monsters in initiative queue_check_defeat(): returns
True` if 0 heroes in initiative queue
Upon running the next_turn
command the program should check to see if the current attacker is a monster, if it is it will pick a hero target at random and use the monster's next attack in its command queue. It will then repeat the process until it reaches a hero's turn. At this point it returns a multi-line string containing all the events that have happened so far and stating what hero's turn it is. (Details on event formatting below.) It is then up to the user to select a target and action and then execute_command
. This will run the corresponding command (throwing any appropriate errors, including InvalidCommand
if the command doesn't exist) and then resume the cycle of moving through the initiative queue until it reaches a Hero again. At the end of each unit's turn all dead units should be removed from the initiative queue and xp should be rewarded to all heroes for any monsters killed, triggering level ups if appropriate. If at the start of a turn all the monsters are dead the program should raise a Victory, if all the heroes are dead it should raise a Defeat.
Event formatting Each event should be recorded as a new entry in the list that is returned
- *
fight
command used:{monster} attacks {target} for {damage}!
- other attack ability used:
{monster} hits {target} with {ability} for {damage} damage!
- unit dies:
{unit} dies!
- XP is rewarded:
{xp} XP rewarded!
- Hero levels up:
{hero} is now level {level}!
- Unit ability causes damage to self:
{unit} takes {damage} self-inflicted damage!
- Hero's turn:
{hero}'s turn!