1. Code structure and Utilities

In this lesson we will set up the file structure for EvAdventure. We will make some utilities that will be useful later. We will also learn how to write tests.

1.1. Folder structure

Create a new folder under your mygame folder, named evadventure. Inside it, create another folder tests/ and make sure to put empty __init__.py files in both. This turns both folders into packages Python understands to import from.

mygame/
   commands/
   evadventure/         <---
      __init__.py       <---
      tests/            <---
          __init__.py   <---
   __init__.py
   README.md
   server/
   typeclasses/
   web/
   world/

Importing anything from inside this folder from anywhere else under mygame will be done by

# from anywhere in mygame/
from evadventure.yourmodulename import whatever 

This is the ‘absolute path` type of import.

Between two modules both in evadventure/, you can use a ‘relative’ import with .:

# from a module inside mygame/evadventure
from .yourmodulename import whatever

From e.g. inside mygame/evadventure/tests/ you can import from one level above using ..:

# from mygame/evadventure/tests/ 
from ..yourmodulename import whatever

1.2. Enums

Create a new file mygame/evadventure/enums.py.

An enum (enumeration) is a way to establish constants in Python. Best is to show an example:

# in a file mygame/evadventure/enums.py

from enum import Enum

class Ability(Enum): 

    STR = "strength"

You access an enum like this:

# from another module in mygame/evadventure

from .enums import Ability 

Ability.STR   # the enum itself 
Ability.STR.value  # this is the string "strength"

Having enums is recommended practice. With them set up, it means we can make sure to refer to the same thing every time. Having all enums in one place also means you have a good overview of the constants you are dealing with.

The alternative would be to for example pass around a string "constitution". If you mis-spell this ("consitution"), you would not necessarily know it right away - the error would happen later when the string is not recognized. If you make a typo getting Ability.COM instead of Ability.CON, Python will immediately raise an error since this enum is not recognized.

With enums you can also do nice direct comparisons like if ability is Ability.WIS: <do stuff>.

Note that the Ability.STR enum does not have the actual value of e.g. your Strength. It’s just a fixed label for the Strength ability.

Here is the enum.py module needed for Knave. It covers the basic aspects of rule systems we need to track (check out the Knave rules. If you use another rule system you’ll likely gradually expand on your enums as you figure out what you’ll need).

# mygame/evadventure/enums.py

class Ability(Enum):
    """
    The six base ability-bonuses and other 
    abilities

    """

    STR = "strength"
    DEX = "dexterity"
    CON = "constitution"
    INT = "intelligence"
    WIS = "wisdom"
    CHA = "charisma"
     
    ARMOR = "armor"
    
    CRITICAL_FAILURE = "critical_failure"
    CRITICAL_SUCCESS = "critical_success"
    
    ALLEGIANCE_HOSTILE = "hostile"
    ALLEGIANCE_NEUTRAL = "neutral"
    ALLEGIANCE_FRIENDLY = "friendly"
    

Here the Ability class holds basic properties of a character sheet.

1.3. Utility module

Create a new module mygame/evadventure/utils.py

This is for general functions we may need from all over. In this case we only picture one utility, a function that produces a pretty display of any object we pass to it.

This is an example of the string we want to see:

Chipped Sword 
Value: ~10 coins [wielded in Weapon hand]
 
A simple sword used by mercenaries all over 
the world.
 
Slots: 1, Used from: weapon hand
Quality: 3, Uses: None
Attacks using strength against armor.
Damage roll: 1d6

Here’s the start of how the function could look:

# in mygame/evadventure/utils.py

_OBJ_STATS = """
|c{key}|n
Value: ~|y{value}|n coins{carried}

{desc}

Slots: |w{size}|n, Used from: |w{use_slot_name}|n
Quality: |w{quality}|n, Uses: |wuses|n
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
Damage roll: |w{damage_roll}|n
""".strip()


def get_obj_stats(obj, owner=None): 
    """ 
    Get a string of stats about the object.
    
    Args:
        obj (Object): The object to get stats for.
        owner (Object): The one currently owning/carrying `obj`, if any. Can be 
            used to show e.g. where they are wielding it.
    Returns:
        str: A nice info string to display about the object.
     
    """
    return _OBJ_STATS.format(
        key=obj.key, 
        value=10, 
        carried="[Not carried]", 
        desc=obj.db.desc, 
        size=1,
        quality=3,
        uses="infinite"
        use_slot_name="backpack",
        attack_type_name="strength"
        defense_type_name="armor"
        damage_roll="1d6"
    )

Here we set up the string template with place holders for where every piece of info should go. Study this string so you understand what it does. The |c, |y, |w and |n markers are Evennia color markup for making the text cyan, yellow, white and neutral-color respectively.

We can guess some things, such that obj.key is the name of the object, and that obj.db.desc will hold its description (this is how it is in default Evennia).

But so far we have not established how to get any of the other properties like size or attack_type. So we just set them to dummy values. We’ll need to get back to this when we have more code in place!

1.4. Testing

Important

It’s useful for any game dev to know how to effectively test their code. So we’ll try to include a Testing section at the end of each of the implementation lessons to follow. Writing tests for your code is optional but highly recommended; it can feel a little cumbersome at first, but you’ll thank yourself later.

create a new module mygame/evadventure/tests/test_utils.py

How do you know if you made a typo in the code above? You could manually test it by reloading your Evennia server and do the following from in-game:

py from evadventure.utils import get_obj_stats;print(get_obj_stats(self))

You should get back a nice string about yourself! If that works, great! But you’ll need to remember doing that test when you change this code later.

A unit test allows you to set up automated testing of code. Once you’ve written your test you can run it over and over and make sure later changes to your code didn’t break things.

In this particular case, we expect to later have to update the test when get_obj_stats becomes more complete and returns more reasonable data.

Evennia comes with extensive functionality to help you test your code. Here’s a module for testing get_obj_stats.

# mygame/evadventure/tests/test_utils.py

from evennia.utils import create 
from evennia.utils.test_resources import BaseEvenniaTest 

from ..import utils

class TestUtils(BaseEvenniaTest):
    def test_get_obj_stats(self):
        # make a simple object to test with 
        obj = create.create_object(
            key="testobj", 
            attributes=(("desc", "A test object"),)
        ) 
        # run it through the function 
        result = utils.get_obj_stats(obj)
        # check that the result is what we expected
        self.assertEqual(
            result, 
            """ 
|ctestobj|n
Value: ~|y10|n coins

A test object

Slots: |w1|n, Used from: |wbackpack|n
Quality: |w3|n, Uses: |winfinite|n
Attacks using |wstrength|n against |warmor|n
Damage roll: |w1d6|n
""".strip()
)

What happens here is that we create a new test-class TestUtils that inherits from BaseEvenniaTest. This inheritance is what makes this a testing class.

We can have any number of methods on this class. To have a method recognized as one containing code to test, its name must start with test_. We have one - test_get_obj_stats.

In this method we create a dummy obj and gives it a key “testobj”. Note how we add the desc Attribute directly in the create_object call by specifying the attribute as a tuple (name, value)!

We then get the result of passing this dummy-object through get_obj_stats we imported earlier.

The assertEqual method is available on all testing classes and checks that the result is equal to the string we specify. If they are the same, the test passes, otherwise it fails and we need to investigate what went wrong.

1.4.1. Running your test

To run your test you need to stand inside your mygame folder and execute the following command:

evennia test --settings settings.py .evadventure.tests

This will run all your evadventure tests (if you had more of them). To only run your utility tests you could do

evennia test --settings settings.py .evadventure.tests.test_utils

If all goes well, you should get an OK back. Otherwise you need to check the failure, maybe your return string doesn’t quite match what you expected.

1.5. Summary

It’s very important to understand how you import code between modules in Python, so if this is still confusing to you, it’s worth to read up on this more.

That said, many newcomers are confused with how to begin, so by creating the folder structure, some small modules and even making your first unit test, you are off to a great start!