1
\$\begingroup\$

This not-so-little Python project I was working on for over 8 hours, is actually the skeleton of my RPG engine.

Basically the test script uses the engine module to load a game world, including entities (which can be of random variants and even amounts), and then prints the name and location of every entity ingame. (Luckily there usually aren't over 25!)

Every entity generated has a completely random name, courtesy of a syllable-based generator, which is an improved version of a piece of code that has actually already been here.

At the moment all it does is:

C:\Users\pc\Documents\SMushesAdv>python test.py
Uweth-Edzi the Medium-sized Dragon from the Draco Mountain
Isp-Ou the Small Dragon from the Draco Mountain
Ppokpoi the Small Dragon from the Draco Mountain
Zippa the Small Dragon from the Draco Mountain
Tim the Medium-sized Dragon from the Draco Mountain
Fomupoewotp the Small Dragon from the Draco Mountain
Proudpezedri the Small Dragon from the Draco Mountain
Aedr the Medium-sized Dragon from the Draco Mountain
Osupzoesp the Medium-sized Dragon from the Draco Mountain
Eponai the Large Dragon from the Draco Mountain
Aoradisou the Baby Sheep from Yohantown's Prairies
Joib the Brown Sheep from Yohantown's Prairies
Atz-Oeyao the Intelligent Sheep from Yohantown's Prairies
Kpilesr the Intelligent Sheep from Yohantown's Prairies
Abulofudr the Intelligent Sheep from Yohantown's Prairies
Haji the White Sheep from Yohantown's Prairies
Vusza the Intelligent Sheep from Yohantown's Prairies
Jifu-Pp the Intelligent Sheep from Yohantown's Prairies
Kpathohe the Naked Summer Sheep from Yohantown's Prairies
Traidh the White Sheep from Yohantown's Prairies
Exif-Eqoinutr the Baby Sheep from Yohantown's Prairies
Anuhi the Intelligent Sheep from Yohantown's Prairies
Srefuppo-Dr the Intelligent Sheep from Yohantown's Prairies
Iji-Muhowase the Intelligent Sheep from Yohantown's Prairies
Ayae-Dho the Intelligent Sheep from Yohantown's Prairies
Adutidpot the Naked Summer Sheep from Yohantown's Prairies
Eka-Kz the Intelligent Sheep from Yohantown's Prairies
Kafoi the Intelligent Sheep from Yohantown's Prairies

But don't go away just yet! It does involve a few good tricks and compact code to be able to load XML entities and worlds. I might have been slightly inspired by Dwarf Fortress in a way.

This is the used example of XML world:

mushworld.xml

<?xml version="1.0"?>
<world>
    <places>
        <place name="the Draco Mountain">
            <flock type="dragon" amount="8-11" variant="small;medium" /> <!-- ';' separates random choices -->
            <entity type="dragon" variant="large" />
        </place>

        <place name="Yohantown's Prairies">
            <flock type="sheep" amount="10-25" variant="*" /> <!-- "*" means random variant -->
        </place>
    </places>

    <paths> 
        <path ends="mountain;prairies" />
    </paths>

    <etypes> <!-- entity dependencies -->
        <etype filename="etypes/dragon.xml" />
        <etype filename="etypes/sheep.xml" />
    </etypes>
</world>

And this is the XML used for the entity types:

etypes/dragon.xml

<?xml version="1.0"?>
<entitytype name="Dragon" id="dragon">
    <functions>
        <function name="tick">
            import random

            def dragon_tick(index, entity, world):
                if random.random() &lt;= 0.1:
                    # wander
                    possib = set()

                    for p in world.paths:
                        if entity.place in p:
                            possib.add(p)

                    if len(possib) &gt; 0:
                        entity.place = random.choice(tuple(possib))
        </function>

        <function name="fly">
            def dragon_fly(index, entity, world):
                if entity.attr['destination'] == entity.place:
                    return False

                i = 2

                while entity.attr['destination'] != entity.place and i > 0:
                    i -= 1
                    world.path_move(entity, entity.attr['destination'])

                return i > 0
        </function> 

        <function name="slash">
            def dragon_slash(index, entity, world):
                if entity.attr['target'] is None:
                    return

                if world.load_entity_index(entity.attr['target']).size &lt; entity.variant['size']:
                    return

                world.damage_entity_index(entity.attr['target'], __import__('random').uniform(30, 50))
        </function>

        <function name="firebreath">
            def dragon_fire(index, entity, world):
                if entity.attr['target'] is None:
                    return

                if world.load_entity_index(entity.attr['target']).size &gt;= entity.variant['size']:
                    return

                for _ in range(6):
                    world.damage_entity_index(entity.attr['target'], __import__('random').uniform(10, 25))
        </function>
    </functions>

    <base>
        <attr name="type" value="creature" />

        <flag name="hasMelee" />
        <flag name="hasRanged" />
        <attr name="meleeAttackFunc" value="slash" /> <!-- function called when attacking a close enemy (larger enemies) -->
        <attr name="rangedAttackFunc" value="firebreath" /> <!-- function called when attacking a far enemy (smaller enemies) -->

        <!-- reaction traits, in a scale from 1 to 10 -->
        <attr name="hostility" value="2.25" /> <!-- negativity of reaction to living entities of other kinds --> 
        <attr name="courage" value="8" /> <!-- likelihood to stay when underwhelmed or outnumbered -->
        <attr name="sociality" value="6" /> <!-- likelihood to form 'friends' (will tend to follow each other and share friends and enemies) -->
        <attr name="leadership" value="7.2" /> <!-- likelihood to become leader (least likely to follow friends, most likely to be followed) -->

        <!-- kinetic (movement & combat) attributes -->
        <attr name="specialMoveFunc" value="fly" /> <!-- special movement is used when hunting, chasing or fleeing -->
        <attr name="speed" value="0.3" /> <!-- likelihood to evade enemy attacks -->
    </base>

    <variants>
        <variant name="Small Dragon" id="small">
            <attr name="size" value="2.5" />
            <attr name="health" value="280" />
            <attr weight="78" /> <!-- in kilograms -->
        </variant>

        <variant name="Medium-sized Dragon" id="medium">
            <attr name="size" value="4" />
            <attr name="health" value="350" />
            <attr weight="138" />
        </variant>

        <variant name="Large Dragon" id="large">
            <attr name="size" value="5" />
            <attr name="health" value="400" />
            <attr weight="210" />
        </variant>
    </variants>
</entitytype>

etypes/sheep.xml

<?xml version="1.0"?>
<entitytype name="Sheep" id="sheep">
    <functions>
        <function name="tick">
            import random

            def sheep_tick(index, entity, world):
                if random.random() &lt;= 0.275:
                    # wander
                    possib = set()

                    for p in world.paths:
                        if entity.place in p:
                            possib.add(p)

                    if len(possib) &gt; 0:
                        entity.place = random.choice(tuple(possib))

                if random.random() &lt;= 0.35:
                    # graze
                    world.heal_entity_index(index, random.uniform(5, 15))
        </function>
    </functions>

    <base>
        <attr name="type" value="creature" />

        <!-- reaction traits, in a scale from 1 to 10 -->
        <attr name="hostility" value="0" /> <!-- negativity of reaction to living entities of other kinds --> 
        <attr name="courage" value="2" /> <!-- likelihood to stay when underwhelmed or outnumbered -->
        <attr name="sociality" value="8.5" /> <!-- likelihood to form 'friends' (will tend to follow each other and share friends and enemies) -->
        <attr name="leadership" value="2" /> <!-- likelihood to become leader (least likely to follow friends, most likely to be followed) -->

        <!-- kinetic (movement & combat) attributes -->
        <attr name="speed" value="0.7125" /> <!-- likelihood to evade enemy attacks -->
    </base>

    <variants>
        <variant name="White Sheep" id="white">
            <attr name="size" value="1.5" />
            <attr name="health" value="120" />
            <attr weight="70" /> <!-- in kilograms -->
        </variant>

        <variant name="Brown Sheep" id="brown">
            <attr name="size" value="1.5" />
            <attr name="health" value="120" />
            <attr weight="70" /> <!-- in kilograms -->
        </variant>

        <variant name="Baby Sheep" id="pup">
            <attr name="size" value="0.8" />
            <attr name="health" value="70" />
            <attr weight="55" /> <!-- in kilograms -->
        </variant>

        <variant name="Naked Summer Sheep" id="summer">
            <attr name="size" value="1.4" />
            <attr name="health" value="115" />
            <attr weight="64" /> <!-- in kilograms -->
        </variant>

        <variant name="Intelligent Sheep" id="mushy">
            <attr name="size" value="1.4" />
            <attr name="health" value="115" />
            <attr weight="64" /> <!-- in kilograms -->
            <attr name="courage" value="3.5" />
            <flag name="mush" />
        </variant>
    </variants>
</entitytype>

The Python code used in this project

engine.py

import json
import lxml
import tempfile
import namegen
import random

# from irc.bot import SimpleIRCBot
from lxml import etree


idnum = 0 # The default ID counter.

class EntityType(object):
    """An entity type.

    Think like this: each kind of entity ingame perform
    a distinct set of actions and obey to a distinct
    set of rules. Each entity is just a string in a
    list, but this string is changed by instances of this
    class, which will perform these actions in place of the
    functionally inane entity string.

    So, if we have a 'dragon#Mountain#large#{""}'"""

    def __init__(self, name, id=None, base_attributes=(), variants=(), functions=()):
        self.name = name
        self.id = id or idnum
        self.base_attributes = dict(base_attributes)
        self.variants = dict(variants)
        self.functions = dict(functions)

        for k, v in variants.items():
            variants[k]['flags'] = set(v['flags'])

        base_attributes['flags'] = set(base_attributes['flags'])

        for k, v in dict(base_attributes['attr']).items():
            for vk, vv in dict(variants).items():
                if k not in vv['attr']:
                    self.variants[vk]['attr'][k] = v

        for f in base_attributes['flags']:
            for vk, vv in dict(variants).items():
                if f not in vv['flags']:
                    self.variants[vk]['flags'].add(f)

        if not id:
            idnum += 1

    def instantiate(self, place, variant):
        """Creates an entity string and returns it."""
        return "{}#{}#{}#{}".format(self.id, "{} the {}".format(namegen.generate_name(random.randint(4, 12)), self.variants[variant]['name']), place, variant)

    def call(self, func, index, entity, world):
        return self.functions[func](index, entity, world)

class LoadedEntity(object):
    def __init__(self, world, s):
        self.type = world.etypes[s.split("#")[0]]
        self.name = s.split('#')[1]
        self.place = s.split("#")[2]
        self.variant = self.type.variants[s.split("#")[3]]

class GameWorld(object):
    def __init__(self, etypes, paths, places, entities):
        self.etypes = dict(etypes)
        self.paths = list(paths)
        self.places = list(places)
        self.entities = list(entities)

    def tick(self):
        for i, e in enumerate(self.entities):
            en = LoadedEntity(self, e)
            et = self.etypes[en.type]

            if 'tick' in self.etypes.functions:
                self.etypes.call('tick', i, e, self)

class GameLoader(object):
    """A class which children will load
    SMA (Sentient Mushes: Adventure) and derivated
    games for you."""

    def __init__(self):
        if type(self) is GameLoader:
            raise TypeError("Do not use GameLoader directly!")

    def load_world(self, filename):
        """Returns a GameWorld instance containing all of the
        Location instances that represent places in
        the game world. This should also call self.load_entity_type
        so that it may load the referenced entity types into the
        game world."""
        return None

    def load_entity_type(self, filename):
        """Returns an EntityType instance.

        The filename is one of the entity type filenames
        described by the world. EntityType must be called
        to create the instance after processing the file's
        content."""
        return None

class XMLGameLoader(object):
    """The default loader. Sentient Mushes: Adventure is XML!
    (I'm tired of people telling me XML sucks, please shut up,
    I use what I want! I'm *indie*!)"""

    def load_world(self, filename):
        """Returns a GameWorld instance containing all of the
        Location instances that represent places in
        the game world."""
        xworld = etree.parse(open(filename))
        etypes = {}
        paths = []
        places = []
        entities = []

        for el in xworld.iter():
            if el.tag == 'etypes':
                for t in el:
                    if t.tag == 'etype':
                        e = self.load_entity_type(t.get('filename'))
                        etypes[e.id] = e

        for el in xworld.iter():
            if el.tag == "places":
                for p in el:
                    for sub in p:
                        if sub.tag == "flock":
                            if sub.get('type') in etypes:
                                amount = sub.get('amount')

                                if '-' in amount:
                                    amount = random.randint(int(amount.split('-')[0]), int(amount.split('-')[1]))

                                else:
                                    amount = int(amount)

                                for _ in range(amount):
                                    entities.append(etypes[sub.get('type')].instantiate(p.get('name'), (random.choice(sub.get('variant').split(';')) if sub.get('variant') != '*' else random.choice(tuple(etypes[sub.get('type')].variants.keys())))))

                        elif sub.tag == "entity":
                            if sub.get('type') in etypes:
                                entities.append(etypes[sub.get('type')].instantiate(p.get('name'), (random.choice(sub.get('variant').split(';')) if sub.get('variant') != '*' else random.choice(tuple(etypes[sub.get('type')].variants.keys())))))

                    places.append(p.get('name'))

            elif el.tag == "paths":
                for p in el:
                    if p.tag == "path":
                        paths.append(set(p.get('ends').split(';')))

        return GameWorld(etypes, paths, places, entities)

    def load_entity_type(self, filename):
        """Returns an EntityType instance.

        The filename is one of the entity type filenames
        described by the world. EntityType must be called
        to create the instance after processing the file's
        content."""

        try:
            etype = etree.parse(open(filename))
            name = etype.getroot().get('name')
            id = etype.getroot().get('id')

            functions = {}
            base = { 'attr': {}, 'flags': set() }
            variants = {}

            funcholder = tempfile.CodeHolder()

            for sub in etype.iter():
                if sub.tag == "functions":
                    for f in sub:
                        fncs = list(funcholder.quick("{}-{}".format(id, f.get('name')), f.text).values())
                        functions[f.get('name')] = fncs[-1]

                elif sub.tag == "base":
                    for a in sub:
                        if a.tag == "attr":
                            base['attr'][a.get('name')] = a.get('value')

                        elif a.tag == "flag":
                            base['flags'].add(a.get('name'))

                elif sub.tag == "variants":
                    for va in sub:
                        v = { 'attr': {}, 'flags': set() }

                        for a in va:
                            if a.tag == "attr":
                                v['attr'][a.get('name')] = a.get('value')

                            elif a.tag == "flag":
                                v['flags'].add(a.get('name'))

                        v['name'] = va.get('name')
                        variants[va.get('id')] = v

        finally:
            funcholder.deinit()

        return EntityType(name, id, base, variants, functions)

tempfile.py

import importlib
import textwrap
import os

class CodeHolder(object):
    def __init__(self):
        self.files = set()

    def __setitem__(self, key, code):
        f = open("tmp_{}.py".format(key), 'wb')
        f.write(textwrap.dedent(code).encode('utf-8'))

        self.files.add(key)

        f.close()

    def __getitem__(self, key):
        mod = importlib.import_module("tmp_" + key)

        funcs = list(filter(lambda x: hasattr(x, '__call__') and hasattr(x, '__name__'), map(lambda x: getattr(mod, x), dir(mod))))
        res = {}

        for f in funcs:
            res[f.__name__] = f

        return res

    def quick(self, key, code):
        self[key] = code
        return self[key]

    def deinit(self):
        for k in self.files:
            os.unlink("tmp_{}.py".format(k))

namegen.py

"""
This name generator is an improved version of das' random syllable-based
name generator.

Original source at: https://codereview.stackexchange.com/q/156903
Improved by: Gustavo R. Rehermann (Gustavo6046)
"""

import random

vowels = "aeiou"
consonants = "bcdfghjklmnpqrstvwxyz"
pre_consonants = "tspdk"
post_consonants = "rhpz"
ditongs = ["ae", "ai", "ou", "ao", "oe", "oi"]

def generate_name(length, digraph_rate=0.3, ditong_rate=0.2, hyphen_rate=0.125):
    if length <= 0:
        return False

    full_syl = ""

    while length > 0:
        if full_syl == '':
            decision = random.choice(('consonant', 'vowel'))

        elif full_syl[-1:].lower() in vowels:
            decision = 'consonant'

        elif full_syl[-1:].lower() in consonants:
            decision = 'vowel'

        if random.random() <= hyphen_rate and len(full_syl) > 0 and len(full_syl.split('-')[-1]) > 2:
            syl_choice = '-'

        elif decision == 'consonant':
            if random.random() <= digraph_rate:
                syl_choice = random.choice(pre_consonants) + random.choice(post_consonants)
                # length -= 1

            else:
                syl_choice = random.choice(consonants)

        else:
            if random.random() <= ditong_rate:
                syl_choice = random.choice(ditongs)
                # length -= 1

            else:
                syl_choice = random.choice(vowels)

        full_syl += syl_choice
        length -= len(syl_choice)

    return '-'.join(map(lambda x: x[0].upper() + x[1:], filter(lambda x: len(x) > 0, full_syl.split('-'))))
\$\endgroup\$
  • \$\begingroup\$ Can you explain why the entities need to be in XML files and cannot just be normal Python classes? \$\endgroup\$ – Graipher May 28 '18 at 15:02
  • \$\begingroup\$ @Grapher it allows for an easier way to extend the game, without the need for dynamic import craziness (which is only used for embedded code). XML is also more versatile. In the newest versions, they contain more than just code and data: they have a peculiar structure, that has to be processed when the world is being loaded, which would be at the least difficult with just Python OOP. \$\endgroup\$ – Gustavo6046 May 28 '18 at 23:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.