Back to index

The copyright situation for this article is unclear. It does not belong to the author of this site. Please see the copyright notice. If you have information about the copyright contact me!

The Aedon Attribute and Rule System

by Federico Di Gregorio

0. Introduction

This document describe some ideas about how to write and extensible and easily maintainable rule set for a mud. A testbed implementation (written in Python) is available as the Aedon mud server. Please contact the author, Federico Di Gregorio (fog@debian.org) for more information, CVS access and the like.

1. Definitions

The term 'instance' is used to refer to an object in the Object Orientated (OO) programming language chosen to write the mud server. Objects in the mud world are referred as 'concretes.' The term 'attribute' is used when talking about concrete attributes, like size, weight, et al. When referring to object attributes the term 'value' is used (yes, this is bad but I am not English and I have a limited vocabulary.) 'Rule' is used to refer to both game rules and server rules.

Wizard

2. Concretes

Every object present in the simulated world is represented by an instance of the Concrete class in the server. Concretes are organized in a hierarchy but this has nothing to do with the OO hierarchy of the programming language used to write the mud server. Every concrete has a *classes* attribute that lists the full number of classes the concrete belongs to. By looking at the classes attribute one can extrapolate the full hierarchy tree, but that is all. There is no inheritance or instance and class methods: nothing of the classical OO programming. A concrete can belong partially to a class. For example, a rotten apple is still edible, just not as edible as a good one. We can express this concept by assigning a value in the range [0,1] for every class the concrete belongs to.

Some examples:

Note 1: I will use the python grammar for representing attribute values (omitting '"' for the strings.)

Note 2: the first name below is the object id.

Note 3: sytara is my favorite human character.

sytara.classes = {living:1.0, human:1.0, edible:0.9}
apple.classes = {vegetable:1.0, fruit:1.0, edible:1.0}
rotten_apple.classes = {vegetable:1.0, fruit:1.0, edible:0.6, poison:0.1}
wolf.classes = {living:1.0, animal:1.0, wolf:1.0, edible:0.9}
sword.classes = {weapon:1.0, sword:1.0, iron:1.0}

The classes attribute is used by the server to choose what rules and constructors to apply. Read on...

3. Attributes

Apart from classes, a concrete can have any number of attributes. An attribute is completely defined by its id (or name), its value (int, float, string, list, map), and tree sorted lists of triggers, get filters and set filters. A concrete is born with a relatively small set of attributes (usually a concrete is created by copying another concrete or an archetype, i.e., a concrete that has been saved to static storage for future use by developers) but gets more while interacting with other concretes.

When a concrete is asked for an attribute it does not have, the system select and applies a *constructor* for the attribute. Constructors are overloaded. That means that there are different constructors for the same attribute and one is selected depending of the concrete class.

A little example: the rule system specify that the attribute used to determine how much food an edible provides is called food_value. The wolf attack and kills sytara (sic!) then eats the corpse. But a human has not the food_value attribute so the system looks up the food_value constructor table:

food_value (DEFAULT) [
return 0>
]

food_value (O.IS_OF(living):1000) [
return O.get('food')
]

The default is to return 0, i.e., the concrete does not provide any food. But, if the concrete is a living (like sytara was) the second constructor is applied and the value of food_value is the current food attribute (sytara had her breakfast just half an hour ago... what a lucky wolf!)

Attributes can also have sorted lists of filters that gets called in order on a get() or set(). That is useful to resolve the effects of temporary changes to the attributes. Moreover the filters get a handle on the concrete requesting the get() or set() and can return different results to different classes of concretes. An example is a spell that makes sytara less tasty to wolves. The spell resolve its effect by placing a timed filter on food_value attribute. If a wolf asks for that attribute (the AI wants to target the concrete yielding the most food) the filter returns a fake value (maybe even 0 if the spell is strong enough.) Another way for the spell to work is to filter the size attribute... maybe the AI does not attack *big* creatures... eheh.

set() filters can be used in the same way to reduce damage, etc...

There are also triggers, that are called in prioritized order every time an attribute is accessed until one of them returns a false value and stops the chain. Triggers are useful to implement reactions, and thing like that but this is outside the scope of this paper.

4. Rules

Rules are the core of the system. A rule chain can be started by a player command (eat apple), by a critter action (the AI governing the wolf decides it is time to eat and attacks the nearest living, for example), by a timed event (a spell expires) or by another rule.

A rule has always a *subject* (S) and can have an *object* (O) and a *complement* (C.) In the examples above the subjects are the player's character and the wolf, the objects are the apple and the living (my poor sytara) and the complement the wolf's fangs (the weapon the wolf is using in the attack.)

A rule is composed by a *master* part, a *default* part (also called the master rule and the default rule) and by any number of normal parts (also called simply rules or subrules.) The master part specify how other parts are to be applied (that is the rule policy.) Policies are:

* apply every rule that scores (see below) over a certain value, apply default if no rule scores high enough.

* apply every rule that scores (see below) over a certain value, but do not apply the default.

* apply only the top scoring rule.

* apply the top scoring rules only if it scores above the given value, else apply the default one.

Scoring depends on subject, object and attribute classes and attributes. A rule scores points when S, O or C are of the given class (the score is multiplied for their class value) or have certain attributes. Obviously, when the server is evaluating a rule no new attributes can be created. Here is a little complete example taken from the current server:

eat (MASTER, RULE_CUT_VALUE:750) [
pass
]

eat (DEFAULT) [
return 'Mmm... %s does not seems edible.' % O.short_desc
]

eat (S.IS_OF(living):500, O.IS_OF(edible):500) [
max_food = S.get('food_max', 5)
food = S.get('food') + O.get('food_value')/S.get('size', 1) * f
if food max_food: food = max_food
S.set('food', food)
O.destroy()
return O.get('msg_eat', 'Tastes good.')
]

Some explanations: every rule that scores over 750 is applied. By default an object is not edible, the default rule just print a short message. If a living tries to eat something edible the last rule is applied. It gets the food_value attribute from the object (if the object does not have it, the server invokes the right constructor, read above) and use it to calculate how much nutrition the object provides (f is the factor, i.e., rule score/1000.) After that a custom message (if present) or a default one is returned to the user and the object destroyed.

Now, let us suppose we want to add a poisoned apple to the game. Let us also suppose that the rule for applying venom effects is already in place. We can do it by adding the following rule:

eat (S.IS_OF(living):500, O.IS_OF(food):400, O.SHOULD_BE(poison):100) [
APPLY(poison)
return O.get('msg_eat', 'Tastes strange.')
]

This rules *requires* the object to be of class poison. But that is not sufficient to apply the rule. It still has to score at least 750. If the player tries to eat a poisoned dagger, it gets the "Mmm... the dagger does not seems edible." message, because no rules score high enough and the default one gets applied.

If the wolf tries to eat rabbit killed by poison (classes = {living:1.0, animal:1.0, rabbit:1.0, edible:0.8, poison:0.2}, because the venom makes the rabbit a little bit lesser nutrient and a little bit poisoned) this rule scores 500*1.0 (the wolf is a living) + 400*0.8 (the rabbit is edible) + 100*0.2 (the poison) = 840. The other rule scores 900. So both are applied. The only effect of this rule is to apply the poison rule. The message returned is the one from the other rule (the one who scored higher): poison level is too low to be tasted.

Note that the addition of this rule not only enabled us to add poisoned apples to the world but every kind of poisoned food. It is like adding some specialized methods at the right hierarchy level in an OO paradigm, but the is a fundamental difference. We do not need to care about how the other rules were written (the ancestor methods in the OO world) nor we need to remember to call them.) Rules are independent and is the system that selects the right ones to apply.